플래그를 확인할 필요가없는 디자인 패턴이 있습니까?


28

데이터베이스에 문자열 페이로드를 저장하겠습니다. 두 가지 전역 구성이 있습니다.

  • 암호화
  • 압축

이들 중 하나만 활성화되거나 둘 다 활성화되거나 비활성화되는 방식으로 구성을 사용하여 활성화 또는 비활성화 할 수 있습니다.

내 현재 구현은 다음과 같습니다.

if (encryptionEnable && !compressEnable) {
    encrypt(data);
} else if (!encryptionEnable && compressEnable) {
    compress(data);
} else if (encryptionEnable && compressEnable) {
    encrypt(compress(data));
} else {
  data;
}

데코레이터 패턴에 대해 생각하고 있습니다. 올바른 선택입니까 아니면 더 나은 대안이 있습니까?


5
현재 가지고있는 문제가 무엇입니까? 이 기능에 대한 요구 사항이 변경 될 가능성이 있습니까? IE, 새로운 if진술 이있을 가능성이 있습니까?
대런 영

아니요, 코드를 개선하기 위해 다른 솔루션을 찾고 있습니다.
Damith Ganegoda

46
당신은 이것에 대해 거꾸로 가고 있습니다. 패턴을 찾은 다음 패턴에 맞는 코드를 작성하십시오. 요구 사항에 맞게 코드를 작성한 다음 선택적으로 패턴을 사용 하여 코드 를 설명 합니다.
Monica와의 가벼움 경주

1
참고 당신이 당신의 질문은 참으로의 중복이라고 생각하면 이 일 후 애 스커 당신이 "무시"하는 옵션이 최근 혼자 힘 가까운 그와 같은 다시와. 나는 내 자신의 질문 중 일부에 그것을했고 그것은 매력처럼 작동합니다. 다음은 내가 그것을 어떻게, 3 단계 - 내 "지시"유일한 차이 미만 3K 담당자가 있기 때문에, 당신은 통과해야한다는 것입니다 플래그 대화 "복제"에 도착 옵션
모기

8
@LightnessRacesinOrbit : 당신이 말하는 것에 약간의 진실이 있지만, 코드를 구성하는 더 좋은 방법이 있는지 묻는 것이 완벽하게 합리적이며 제안 된 더 나은 구조를 설명하기 위해 디자인 패턴을 호출하는 것이 합리적입니다. (그럼에도 불구하고, 나는 디자인을 요구하는 XY 문제의 약간의 동의 패턴 당신이 원하는 것은입니다 디자인 또는 엄격하게 잘 알려진 패턴을 따르지 않을 수있다.) 또한, "패턴"에 대한 합법적이다 잘 알려진 패턴을 사용하는 경우 구성 요소 이름을 적절하게 지정하는 것이 좋습니다.
ruakh

답변:


15

코드를 디자인 할 때는 두 가지 옵션이 있습니다.

  1. 그냥 끝내십시오.이 경우 거의 모든 솔루션이 효과가 있습니다.
  2. 언어 주의적 이념을 활용하는 해결책을 설계하고 해결책을 설계하십시오 (이 경우 OO 언어-다형성을 결정 수단으로 사용)

말할 것도 없기 때문에 나는 두 가지 중 첫 번째에 집중하지 않을 것입니다. 방금 작동 시키려면 코드를 그대로 두십시오.

그러나 만약 당신이 그것을 pedantic 한 방법으로하고 실제로 원하는 방식으로 디자인 패턴의 문제를 해결한다면 어떻게 될까요?

다음 프로세스를 볼 수 있습니다.

OO 코드를 디자인 할 때 코드에있는 대부분의 if코드가있을 필요는 없습니다. 당연히 ints 또는 floats 와 같은 두 스칼라 유형을 비교하려는 경우을 가질 가능성이 if있지만 구성에 따라 프로 시저를 변경하려면 다형성 을 사용 하여 원하는 것을 달성 할 수 있습니다 . if의) 객체가 인스턴스화 장소, 귀하의 비즈니스 로직에서 -에 공장 .

현재 귀하의 프로세스는 4 가지 경로를 거칠 수 있습니다.

  1. data암호화되거나 압축되지 않습니다 (아무것도 호출하지 않음, return data)
  2. data압축 compress(data)되어있다
  3. data암호화되어 있습니다 (전화 encrypt(data)를 거십시오)
  4. data압축 및 암호화 (호출 encrypt(compress(data))및 반환)

4 가지 경로를 보면 문제가 있습니다.

데이터를 조작 한 다음 반환하는 다른 메소드를 3 (이것은 아무것도 호출하지 않는 경우 4) 프로세스를 호출하는 하나의 프로세스가 있습니다. 메소드는 이름이 다르고 , 공개 API (메소드가 동작을 전달하는 방법)가 다릅니다 .

어댑터 패턴을 사용하여 발생한 이름 colision (공용 API를 통합 할 수 있음)을 해결할 수 있습니다. 간단히 말해서, 어댑터는 호환되지 않는 두 개의 인터페이스가 함께 작동하도록 도와줍니다. 또한 어댑터는 API 구현을 통합하려는 클래스의 새 어댑터 인터페이스를 정의하여 작동합니다.

이것은 구체적인 언어가 아닙니다. 그것은 일반적인 접근 방식이며, 모든 키워드는 C #과 같은 언어에서 제네릭 ( <T>)으로 바꿀 수 있습니다 .

지금 당장 압축과 암호화를 담당하는 두 개의 클래스를 가질 수 있다고 가정하겠습니다.

class Compression
{
    Compress(data : any) : any { ... }
}

class Encryption
{
    Encrypt(data : any) : any { ... }
}

엔터프라이즈 세계에서는 이러한 특정 클래스조차도 class키워드로 대체 interface되거나 (C #, Java 및 / 또는 PHP와 같은 언어를 처리해야하는 경우) 키워드 와 같은 인터페이스로 대체 될 가능성이 있지만 class키워드는 그대로 있습니다. CompressEncrypt방법은로 정의 될 수 순수 가상 ++ 당신에게, C 코드를해야한다.

어댑터를 만들기 위해 공통 인터페이스를 정의합니다.

interface DataProcessing
{
    Process(data : any) : any;
}

그런 다음 인터페이스를 유용하게 사용하려면 구현을 제공해야합니다.

// when neither encryption nor compression is enabled
class DoNothingAdapter : DataProcessing
{
    public Process(data : any) : any
    {
        return data;
    }
}

// when only compression is enabled
class CompressionAdapter : DataProcessing
{
    private compression : Compression;

    public Process(data : any) : any
    {
        return this.compression.Compress(data);
    }
}

// when only encryption is enabled
class EncryptionAdapter : DataProcessing
{
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(data);
    }
}

// when both, compression and encryption are enabled
class CompressionEncryptionAdapter : DataProcessing
{
    private compression : Compression;
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(
            this.compression.Compress(data)
        );
    }
}

이렇게하면 4 개의 클래스로 끝나게되는데, 각 클래스는 완전히 다른 것을 수행하지만 각 클래스는 동일한 공용 API를 제공합니다. Process방법.

none / encryption / compression / both 결정을 처리하는 비즈니스 로직에서 DataProcessing이전에 디자인 한 인터페이스에 따라 개체를 디자인합니다 .

class DataService
{
    private dataProcessing : DataProcessing;

    public DataService(dataProcessing : DataProcessing)
    {
        this.dataProcessing = dataProcessing;
    }
}

프로세스 자체는 다음과 같이 간단 할 수 있습니다.

public ComplicatedProcess(data : any) : any
{
    data = this.dataProcessing.Process(data);

    // ... perhaps work with the data

    return data;
}

더 이상 조건이 없습니다. 클래스 DataService는 데이터가 dataProcessing멤버 에게 전달 될 때 실제로 어떤 작업을 수행할지 모릅니다. 실제로 데이터에 신경 쓰지 않으며 책임이 없습니다.

이상적으로, 작성한 4 개의 어댑터 클래스를 테스트하여 단위 테스트를 수행하여 작동하는지 테스트하고 테스트에 통과시키는 것이 이상적입니다. 그리고 그들이 통과하면 코드에서 호출하는 위치에 관계없이 작동 할 것입니다.

그래서이 방법으로 if더 이상 내 코드에 s 가 없을 것입니까?

아니요. 비즈니스 로직에 조건이있을 가능성은 적지 만 여전히 어딘가에 있어야합니다. 장소는 당신 공장입니다.

그리고 이것은 좋습니다. 생성과 실제로 코드 사용에 대한 우려를 분리합니다. 팩토리를 안정적으로 만들면 (Java에서 Google 의 Guice 프레임 워크 와 같은 기능을 사용할 수있는 경우도 있음 ) 비즈니스 로직에서 올바른 클래스를 선택하는 것에 대해 걱정하지 않아도됩니다. 당신은 당신 공장이 일하고 알고있는 것을 배달 할 것이라는 것을 알고 있기 때문에.

이러한 모든 클래스, 인터페이스 등이 필요합니까?

이것은 우리를 처음으로 돌아옵니다.

OOP에서 다형성을 사용할 경로를 선택하고 실제로 디자인 패턴을 사용하고 싶거나 언어의 기능을 이용하고 싶거나 모든 것을 따르고 싶다면 모든 것이 객체 이데올로기입니다. 그럼에도 불구하고이 예제는 필요한 모든 팩토리를 보여 주지도 Compression않으며, Encryption클래스 를 리팩토링하고 대신 인터페이스로 만들려면 구현도 포함해야합니다.

결국 당신은 매우 특정한 것들에 초점을 맞춘 수백 개의 작은 클래스와 인터페이스로 끝납니다. 반드시 나쁘지는 않지만 원하는 두 숫자를 추가하는 것만 큼 간단하게 수행하는 것이 가장 좋은 해결책은 아닙니다.

당신이 그것을 완료하고 신속하게 얻을하려는 경우, 당신은 잡을 수 Ixrec의 솔루션 적어도 제거 관리, else if그리고 else내 생각에, 더 일반보다 조금있다, 블록, if.

이것이 좋은 OO 디자인을 만드는 나의 방법임을 고려하십시오 . 구현이 아닌 인터페이스에 코딩하는 것은 지난 몇 년 동안 내가해온 방법이며 가장 편한 방법입니다.

나는 개인적으로 if-less 프로그래밍을 더 좋아하고 5 줄의 코드에 대한 더 긴 솔루션을 훨씬 더 높이 평가할 것입니다. 코드를 디자인하는 데 익숙하고 읽는 것이 매우 편안합니다.


업데이트 2 : 내 솔루션의 첫 번째 버전에 대한 토론이 활발했습니다. 토론은 주로 저에 의한 것이며, 사과드립니다.

나는 해결책을 보는 방법 중 하나이지만 유일한 해결책은 아닌 방식으로 답변을 편집하기로 결정했습니다. 또한 데코레이터 부분을 제거했는데 대신 외관을 의미했습니다. 어댑터는 외관 변형이기 때문에 결국 완전히 빠져 나가기로 결정했습니다.


28
나는 공감하지 않았지만 그 이론적 근거는 원래 코드가 8 줄에서 한 일을 수행하는 어리석은 양의 새로운 클래스 / 인터페이스 일 수 있습니다 (다른 답변은 5 일). 내 의견으로는 그것이 달성하는 유일한 것은 코드의 학습 곡선을 높이는 것입니다.
Maurycy

6
@Maurycy OP가 요청한 것은 그러한 솔루션이 존재하는 경우 일반적인 디자인 패턴을 사용하여 자신의 문제에 대한 솔루션을 찾으려고하는 것이 었습니다. 내 솔루션이 자신 또는 Ixrec의 코드보다 길습니까? 그것은. 나는 인정한다. 내 솔루션은 디자인 패턴을 사용하여 문제를 해결하고 그의 질문에 대답하고 프로세스에서 필요한 모든 경우를 효과적으로 제거합니까? 그렇습니다. Ixrec는 그렇지 않습니다.
Andy

26
명확하고 신뢰할 수 있고 간결하며 성능이 뛰어나고 유지 관리 가 가능한 코드를 작성하는 것이 좋습니다. 누군가 목표와 이론적 근거를 명확하게 밝히지 않고 SOLID를 인용하거나 소프트웨어 패턴을 인용 할 때마다 돈이 있다면 부자 일 것입니다.
Robert Harvey

12
여기에 두 가지 문제가 있다고 생각합니다. 먼저 Compressionand Encryption인터페이스가 완전히 불필요한 것처럼 보입니다. 장식 과정에 어떻게 든 필요하거나 추출 된 개념을 나타내는 것을 암시하는지 확실하지 않습니다. 두 번째 문제는 같은 클래스를 만드는 것이 CompressionEncryptionDecoratorOP의 조건부와 같은 종류의 조합 폭발로 이어진다는 것입니다. 또한 제안 된 코드에서 데코레이터 패턴이 명확하게 보이지 않습니다.
cbojar

5
SOLID와 simple에 대한 논쟁은 요점을 놓치고 있습니다.이 코드는 둘 다 아니며 데코레이터 패턴도 사용하지 않습니다. 코드는 많은 인터페이스를 사용하기 때문에 자동으로 SOLID가 아닙니다. DataProcessing 인터페이스의 의존성 주입은 다소 훌륭합니다. 다른 모든 것은 불필요합니다. SOLID는 변경 사항을 잘 처리하기위한 아키텍처 수준의 관심사입니다. OP는 자신의 아키텍처 나 코드가 어떻게 변경 될지에 대한 정보를 제공하지 않았기 때문에 답변에서 SOLID에 대해 논의조차 할 수 없습니다.
Carl Leth

120

현재 코드에서 볼 수있는 유일한 문제는 더 많은 설정을 추가 할 때 조합 폭발의 위험이 있다는 것입니다.이 설정은 코드를 다음과 같이 구성하면 쉽게 완화 될 수 있습니다.

if(compressEnable){
  data = compress(data);
}
if(encryptionEnable) {
  data = encrypt(data);
}
return data;

나는 이것이 예라고 여겨 질 수있는 "디자인 패턴"또는 "이디엄"을 모른다.


18
@DamithGanegoda Nope, 내 코드를주의 깊게 읽으면 그 경우에도 똑같은 일을 볼 수 있습니다. 이것이 else두 if 문 사이에 없는 이유와 data매번 할당하는 이유 입니다. 두 플래그가 모두 true이면 compress ()가 실행 된 다음 원하는대로 compress ()의 결과에서 encrypt ()가 실행됩니다.
Ixrec

14
@DavidPacker 기술적으로는 모든 프로그래밍 언어의 모든 if 문도 마찬가지입니다. 나는 매우 간단한 대답이 적절한 문제처럼 보였기 때문에 단순함을 추구했습니다. 귀하의 솔루션도 유효하지만 개인적으로 걱정할 부울 플래그가 두 개 이상인 경우 저장합니다.
Ixrec

15
@DavidPacker : 일부 프로그래밍 이념에 대한 일부 작성자의 코드가 가이드 라인을 얼마나 잘 준수하는지에 따라 올바름이 정의되지 않습니다. 올바른 것은 "코드가해야 할 일을하고 적절한 시간 내에 구현 되었는가"입니다. "잘못된 길"을하는 것이 합리적이라면, 시간은 돈이기 때문에 잘못된 길은 올바른 길입니다.
whatsisname

9
@DavidPacker : 내가 OP의 입장에 있고 그 질문을한다면, Orbit의 의견에 Lightness Race가 정말로 필요한 것입니다. "디자인 패턴을 사용하여 솔루션 찾기"는 이미 잘못된 발에서 시작되었습니다.
whatsisname

6
@DavidPacker 실제로 질문을 더 자세히 읽으면 패턴을 고집하지 않습니다. 그것은 상태 "가 올바른 선택입니다. 내가 데코레이터 패턴에 대해 생각하고, 혹은 더 나은 대안이?" . 내 인용문에서 첫 번째 문장을 다루었지만 두 번째 문장은 다루지 않았습니다. 다른 사람들은 아니오, 올바른 선택이 아니라는 접근 방식을 취했습니다. 그런 다음 자신의 질문에만 답변한다고 주장 할 수 없습니다.
Jon Bentley

12

귀하의 질문은 실용성이 아닌 것으로 생각됩니다.이 경우 lxrec의 답변이 옳은 것이 아니라 디자인 패턴에 대해 배우는 것입니다.

분명히 명령 패턴 은 당신이 제안하는 것과 같은 사소한 문제에 대해서는 지나치지 만 여기에서는 설명을 위해 다음과 같이 진행됩니다.

public interface Command {
    public String transform(String s);
}

public class CompressCommand implements Command {
    @Override
    public String transform(String s) {
        String compressedString=null;
        //Compression code here
        return compressedString;
    }
}

public class EncryptCommand implements Command {
    @Override
    public String transform(String s) {
        String EncrytedString=null;
        // Encryption code goes here
        return null;
    }

}

public class Test {
    public static void main(String[] args) {
        List<Command> commands = new ArrayList<Command>();
        commands.add(new CompressCommand());
        commands.add(new EncryptCommand()); 
        String myString="Test String";
        for (Command c: commands){
            myString = c.transform(myString);
        }
        // now myString can be stored in the database
    }
}

보시다시피 명령 / 변환을 목록에 넣으면 순차적으로 실행할 수 있습니다. 분명히 두 조건을 모두 실행하거나 if 조건없이 목록에 넣은 내용에 따라 둘 중 하나만 실행합니다.

분명히 조건은 명령 목록을 구성하는 일종의 공장에서 끝날 것입니다.

@texacre의 의견 편집 :

솔루션의 작성 부분에서 if 조건을 피하는 방법에는 여러 가지가 있습니다 . 예를 들어 데스크탑 GUI 앱을 예로 들어 보겠습니다 . 압축 및 암호화 옵션에 대한 확인란을 사용할 수 있습니다. 에서 on clic그 체크 박스의 경우에는 해당 명령을 인스턴스화하고 목록에 추가하거나이 옵션을 선택 해제하는 경우 목록에서 제거합니다.


본질적으로 Ixrec의 답변처럼 보이는 코드없이 "명령 목록을 구성하는 일종의 팩토리"의 예를 제공 할 수 없다면 IMO는 질문에 대답하지 않습니다. 이것은 압축 및 암호화 기능을 구현하는 더 좋은 방법을 제공하지만 플래그를 피하는 방법은 아닙니다.
thexacre

@thexacre 예제를 추가했습니다.
Tulains Córdova

확인란 이벤트 리스너에서 "checkbox.ticked이면 명령 추가"가 있습니까? 마치 주위에 진술이 있다면 깃발을
섞는 것처럼 보입니다

@thexacre 아니요, 각 확인란마다 하나의 리스너입니다. 클릭 이벤트에서 각각 commands.add(new EncryptCommand()); 또는 commands.add(new CompressCommand());각각.
Tulains Córdova

박스의 체크를 해제하면 어떻게됩니까? 내가 만난 거의 모든 언어 / UI 툴킷에서 여전히 이벤트 리스너에서 확인란의 상태를 확인해야합니다. 나는 이것이 더 나은 패턴이라는 데 동의하지만, 플래그가 어딘가에 무언가를한다면 기본적으로 필요하지 않습니다.
thexacre

7

나는 "디자인 패턴"이 "oo 패턴"을 향하여 불필요하게 맞춰지고 훨씬 간단한 아이디어를 완전히 피한다고 생각합니다. 여기서 말하는 것은 (간단한) 데이터 파이프 라인입니다.

클로저에서 시도하려고합니다. 함수가 일류 인 다른 언어도 괜찮습니다. 어쩌면 나중에 C # 예제를 사용할 수는 있지만 좋지 않습니다. 이것을 해결하는 나의 방법은 비 클로저 인에 대한 설명과 함께 다음 단계입니다.

1. 일련의 변환을 나타냅니다.

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

이것은 키워드부터 함수까지의 룩업 테이블 / 사전 / 무엇이든입니다. 다른 예 (키워드를 문자열로) :

(def employees { :A1 "Alice" 
                 :X9 "Bob"})

(employees :A1) ; => "Alice"
(:A1 employees) ; => "Alice"

따라서 암호화 기능을 작성 (transformations :encrypt)하거나 (:encrypt transformations)반환합니다. ( (fn [data] ... )단지 람다 함수입니다.)

2. 일련의 키워드로 옵션을 가져옵니다.

(defn do-processing [options data] ;function definition
  ...)

(do-processing [:encrypt :compress] data) ;call to function

3. 제공된 옵션을 사용하여 모든 변환을 필터링하십시오.

(let [ transformations-to-run (map transformations options)] ... )

예:

(map employees [:A1]) ; => ["Alice"]
(map employees [:A1 :X9]) ; => ["Alice", "Bob"]

4. 기능을 하나로 결합하십시오.

(apply comp transformations-to-run)

예:

(comp f g h) ;=> f(g(h()))
(apply comp [f g h]) ;=> f(g(h()))

5. 그리고 함께 :

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress])

"debug-print"와 같은 새로운 기능을 추가하려는 경우에만 다음이 변경됩니다.

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )
                       :debug-print (fn [data] ...) }) ;<--- here to add as option

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress :debug-print]) ;<-- here to use it
(do-processing [:compress :debug-print]) ;or like this
(do-processing [:encrypt]) ;or like this

어떤 식 으로든 일련의 if 문을 본질적으로 사용하지 않고 적용해야 할 함수 만 포함하도록 func에 어떻게 채워 집니까?
thexacre

funcs-to-run-here (map options funcs)이 필터링을 수행하여 적용 할 함수 세트를 선택합니다. 어쩌면 나는 대답을 업데이트하고 좀 더 자세히 설명해야합니다.
NiklasJ

5

[본질적으로, 내 대답은 위의 @Ixrec대답 에 대한 후속 조치 입니다. ]

중요한 질문 : 당신이 다루어야 할 뚜렷한 조합의 수가 증가 할 것입니까? 대상 도메인을 더 잘 알고 있습니다. 이것은 당신의 판단입니다.
변형의 수가 증가 할 수 있습니까? 글쎄, 그건 상상할 수 없습니다. 예를 들어, 더 다른 암호화 알고리즘을 수용해야 할 수도 있습니다.

별개의 조합 수가 증가 할 것으로 예상되면 전략 패턴 이 도움이 될 수 있습니다. 알고리즘을 캡슐화하고 호출 코드에 상호 교환 가능한 인터페이스를 제공하도록 설계되었습니다. 각 특정 문자열에 대한 적절한 전략을 생성 (인스턴스화) 할 때 여전히 소량의 로직이 있습니다.

요구 사항이 변경되지 않을 것이라고 위에서 언급 했습니다. 변형의 수가 늘어날 것으로 기대하지 않거나 (이 리팩토링을 연기 할 수있는 경우) 논리를 그대로 유지하십시오. 현재 작고 관리 가능한 논리가 있습니다. (전략 패턴에 대한 리팩토링 가능성에 대한 의견에 자기 자신에 대한 메모를 작성하십시오.)


1

스칼라에서이를 수행하는 한 가지 방법은 다음과 같습니다.

val handleCompression: AnyRef => AnyRef = data => if (compressEnable) compress(data) else data
val handleEncryption: AnyRef => AnyRef = data => if (encryptionEnable) encrypt(data) else data
val handleData = handleCompression andThen handleEncryption
handleData(data)

위의 목표를 달성하기 위해 데코레이터 패턴을 사용하는 것 (개별 처리 로직과 분리 방법)은 너무 장황합니다.

OO 프로그래밍 패러다임에서 이러한 디자인 목표를 달성하기 위해 디자인 패턴이 필요한 경우 기능 언어는 함수를 일급 시민 (코드의 1 행 및 2 행) 및 기능 구성 (3 행)으로 사용하여 기본 지원을 제공합니다.


이것이 왜 OP의 접근법보다 낫거나 더 나쁩니 까? 그리고 / 또는 데코레이터 패턴을 사용하려는 OP의 아이디어에 대해 어떻게 생각하십니까?
카스퍼 반 덴 버그

이 코드 스 니펫이 더 좋고 순서 (암호화 전 압축)에 대해 명시 적입니다. 원하지 않는 인터페이스를 피하십시오
Rag
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.