언제 빌더 패턴을 사용 하시겠습니까? [닫은]


531

일부 무엇 일반 , 실제 사례 빌더 패턴을 사용하는가? 무엇을 사나요? 왜 팩토리 패턴을 사용하지 않습니까?


stackoverflow.com/questions/35238292/...는 일부 API를 언급 사용 빌더 패턴이
알리레자 Fattahi

AaronTetha의 답변 은 정말 유익합니다. 다음은 이러한 답변과 관련된 전체 기사 입니다.
디아블로

답변:


262

빌더와 팩토리 IMHO의 주요 차이점은 오브젝트를 빌드하기 위해 많은 작업을 수행해야하는 경우 빌더가 유용하다는 것입니다. 예를 들어 DOM을 상상해보십시오. 최종 객체를 얻으려면 많은 노드와 속성을 만들어야합니다. 팩토리는 팩토리가 하나의 메소드 호출 내에서 전체 오브젝트를 쉽게 작성할 수있는 경우에 사용됩니다.

빌더를 사용하는 한 가지 예는 XML 문서를 작성하는 것입니다. 예를 들어 HTML 조각을 작성할 때이 모델을 사용했습니다. 예를 들어 특정 유형의 테이블을 작성하기위한 빌더가 있고 다음과 같은 메소드가있을 수 있습니다 (매개 변수는 표시되지 않음). :

BuildOrderHeaderRow()
BuildLineItemSubHeaderRow()
BuildOrderRow()
BuildLineItemSubRow()

그런 다음이 빌더는 HTML을 뱉어 냈습니다. 큰 절차 적 방법을 사용하는 것보다 읽기가 훨씬 쉽습니다.

Wikipedia 에서 빌더 패턴을 확인하십시오 .


1021

다음은 Java에서 패턴 및 예제 코드 사용을 주장하는 몇 가지 이유이지만, 디자인 패턴 에서 Gang of Four가 다루는 빌더 패턴의 구현입니다 . Java로 사용하는 이유는 다른 프로그래밍 언어에도 적용됩니다.

Joshua Bloch는 Effective Java, 2nd Edition 에서 다음과 같이 말합니다 .

빌더 패턴은 생성자 또는 정적 팩토리가 소수 이상의 매개 변수를 갖는 클래스를 설계 할 때 적합합니다.

우리는 어느 시점에서 각각의 추가가 새로운 옵션 매개 변수를 추가하는 생성자 목록이있는 클래스를 만났습니다.

Pizza(int size) { ... }        
Pizza(int size, boolean cheese) { ... }    
Pizza(int size, boolean cheese, boolean pepperoni) { ... }    
Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) { ... }

이를 텔레 스코핑 생성자 패턴이라고합니다. 이 패턴의 문제점은 생성자가 4 또는 5 개의 매개 변수 길이이면 지정된 상황에서 원하는 특정 생성자와 매개 변수 의 필수 순서 를 기억하기어렵다는 것입니다.

Telescoping 생성자 패턴에 대한 한 가지 대안 은 필수 매개 변수를 사용하여 생성자를 호출 한 후 다음과 같은 선택적 세터를 호출하는 JavaBean 패턴입니다 .

Pizza pizza = new Pizza(12);
pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);

여기서 문제는 객체가 여러 호출을 통해 생성되기 때문에 구성 과정에서 일관성이없는 상태에있을 수 있다는 것입니다. 또한 스레드 안전을 보장하기 위해 많은 노력이 필요합니다.

더 나은 대안은 빌더 패턴을 사용하는 것입니다.

public class Pizza {
  private int size;
  private boolean cheese;
  private boolean pepperoni;
  private boolean bacon;

  public static class Builder {
    //required
    private final int size;

    //optional
    private boolean cheese = false;
    private boolean pepperoni = false;
    private boolean bacon = false;

    public Builder(int size) {
      this.size = size;
    }

    public Builder cheese(boolean value) {
      cheese = value;
      return this;
    }

    public Builder pepperoni(boolean value) {
      pepperoni = value;
      return this;
    }

    public Builder bacon(boolean value) {
      bacon = value;
      return this;
    }

    public Pizza build() {
      return new Pizza(this);
    }
  }

  private Pizza(Builder builder) {
    size = builder.size;
    cheese = builder.cheese;
    pepperoni = builder.pepperoni;
    bacon = builder.bacon;
  }
}

참고 피자는 불변이고, 그 파라미터 값이 하나의 위치에있는 . Builder의 setter 메소드는 Builder 오브젝트를 리턴 하므로 체인화 될 수 있습니다 .

Pizza pizza = new Pizza.Builder(12)
                       .cheese(true)
                       .pepperoni(true)
                       .bacon(true)
                       .build();

결과적으로 작성하기 쉽고 읽기 쉽고 이해하기 쉬운 코드가 만들어집니다. 이 예제에서, 빌드 메소드는 매개 변수가 빌더에서 Pizza 오브젝트로 복사 된 후 매개 변수를 점검 하고 올바르지 않은 매개 변수 값이 제공된 경우 IllegalStateException을 발생 시키도록 수정 될 수 있습니다 . 이 패턴은 융통성이 있으며 향후 더 많은 매개 변수를 쉽게 추가 할 수 있습니다. 생성자에 대해 4 개 또는 5 개 이상의 매개 변수가있는 경우에만 유용합니다. 즉 , 앞으로 더 많은 매개 변수를 추가 할 것으로 의심되면 처음에는 가치가있을 수 있습니다.

나는 Joshua Bloch의 Effective Java, 2nd Edition 책에서이 주제에 대해 많은 것을 빌렸다 . 이 패턴과 다른 효과적인 Java 관행에 대해 더 배우려면 강력히 권장합니다.


24
원래 GOF 빌더와 다른가요? 디렉터 클래스가 없기 때문입니다. 나에게 거의 다른 패턴처럼 보이지만 매우 유용하다는 데 동의합니다.
Lino Rosa

194
이 특정 예제의 경우 부울 매개 변수를 제거하고 말할 수있는 것이 더 좋지 않을 것입니다.new Pizza.Builder(12).cheese().pepperoni().bacon().build();
Fabian Steeg

46
이것은 빌더 패턴 보다 Fluent Interface 처럼 보입니다 .
Robert Harvey

21
: @Fabian 슈 테크, 나는 사람들이 부울 세터를 찾고 좋네요에 과민 반응하는 생각 세터 이런 종류의 런타임 변경을 허용하지 않는 것을 명심 Pizza.Builder(12).cheese().pepperoni().bacon().build();, 당신은 당신의 코드를 다시 컴파일해야하거나 일부 페퍼로니 필요하면 불필요한 로직을 피자. 최소한 @Kamikaze 용병과 같이 매개 변수화 된 버전을 제공해야합니다. Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();. 그런 다음 다시 우리는 단위 테스트를하지 않습니다.
egallardo

23
@JasonC 그래, 불변 피자는 어떤 용도로 사용됩니까?
Maarten Bodewes

325

식당을 생각해 보자. "오늘의 식사"를 만드는 것은 공장 패턴입니다. 부엌에 "오늘의 식사를주세요"라고 말하면 부엌 (공장)은 숨겨진 기준에 따라 어떤 객체를 생성할지 결정하기 때문입니다.

사용자 정의 피자를 주문하면 빌더가 나타납니다. 이 경우, 웨이터는 요리사 (빌더)에게 "피자, 치즈, 양파, 베이컨을 넣습니다!"라고 말합니다. 따라서 빌더는 생성 된 오브젝트가 가져야하는 속성을 노출하지만 설정 방법을 숨 깁니다.



19

.NET StringBuilder 클래스는 빌더 패턴의 좋은 예입니다. 주로 일련의 단계에서 문자열을 만드는 데 사용됩니다. ToString ()을 수행하는 최종 결과는 항상 문자열이지만 해당 문자열의 생성은 사용 된 StringBuilder 클래스의 함수에 따라 다릅니다. 요약하자면, 기본 아이디어는 복잡한 객체를 구축하고 구축 방법에 대한 구현 세부 사항을 숨기는 것입니다.


9
나는 이것이 빌더 패턴이라고 생각하지 않습니다. StringBuilder는 문자 배열 클래스 (예 : 문자열)의 또 다른 구현이지만 문자열을 변경할 수 없기 때문에 성능 및 메모리 관리를 고려합니다.
Charles Graham

23
Java의 StringBuilder 클래스와 마찬가지로 절대적으로 빌더 패턴입니다. 이 두 클래스의 append () 메소드가 StringBuilder 자체를 반환하여 b.append(...).append(...)최종적으로 호출하기 전에 연결할 수있는 방법에 주목하십시오 toString(). 인용 : infoq.com/articles/internal-dsls-java
pohl

3
@pohl Ya 나는 이것이 실제로 빌더 패턴이라고 생각하지 않습니다. 더 유창한 인터페이스라고 말할 수 있습니다.
Didier A.

"이 두 클래스의 append () 메소드가 어떻게 StringBuilder 자체를 반환하는지에 주목하십시오."이것은 빌더 패턴이 아니라 유창한 인터페이스입니다. 빌더가 종종 유창한 인터페이스를 사용한다는 것입니다. 빌더는 유창한 인터페이스를 가질 필요가 없습니다.
bytedev

그러나 StringBuilder는 본질적으로 동기화되는 StringBuffer와 달리 동기화되지 않습니다.
Alan Deep

11

다중 스레드 문제의 경우 각 스레드마다 복잡한 객체를 구축해야했습니다. 오브젝트는 처리중인 데이터를 나타내며 사용자 입력에 따라 변경 될 수 있습니다.

대신 공장을 사용할 수 있습니까? 예

왜 안 했어? 빌더는 내가 생각하는 것이 더 이해가됩니다.

팩토리는 동일한 기본 유형 (같은 인터페이스 또는 기본 클래스 구현) 인 다른 유형의 오브젝트를 작성하는 데 사용됩니다.

빌더는 동일한 유형의 오브젝트를 반복해서 빌드하지만 구성은 동적이므로 런타임시 변경할 수 있습니다.


9

처리 할 옵션이 많을 때 사용합니다. jmock과 같은 것을 생각해보십시오.

m.expects(once())
    .method("testMethod")
    .with(eq(1), eq(2))
    .returns("someResponse");

그것은 훨씬 더 자연스럽고 ... 가능합니다.

xml 빌딩, 문자열 빌딩 및 기타 여러 가지가 있습니다. java.util.Map건축업자로 썼다 면 상상해보십시오 . 다음과 같은 작업을 수행 할 수 있습니다.

Map<String, Integer> m = new HashMap<String, Integer>()
    .put("a", 1)
    .put("b", 2)
    .put("c", 3);

3
나는 "if"맵이 빌더 패턴을 구현하는 것을 읽는 것을 잊었고 그곳에서 그 구조를 보려고
놀랐

3
:) 미안합니다. 많은 언어에서 빈 공간 대신 ​​자기 자신을 반환하는 것이 일반적입니다. Java로 할 수는 있지만 그리 일반적이지 않습니다.
더스틴

7
맵 예제는 단순히 메소드 체인의 예제입니다.
nogridbag

@nogridbag 실제로 메소드 캐스케이드에 더 가깝습니다. 연쇄를 시뮬레이트하는 방식으로 연쇄를 사용하고 있지만 연쇄 적으로 연쇄 적이지만 의미 적으로 연쇄 적으로 동작합니다.
Didier A.

9

Microsoft MVC 프레임 워크를 거치면서 빌더 패턴에 대해 생각했습니다. ControllerBuilder 클래스에서 패턴을 발견했습니다. 이 클래스는 컨트롤러 팩토리 클래스를 반환하고 콘크리트 컨트롤러를 만드는 데 사용됩니다.

빌더 패턴을 사용할 때의 이점은 자신의 팩토리를 만들어 프레임 워크에 연결할 수 있다는 것입니다.

@Tetha에는 피자를 제공하는 이탈리아 사람이 운영하는 식당 (프레임 워크)이 있습니다. 피자를 준비하기 위해 이탈리아 사람 (오브젝트 빌더)은 피자베이스 (기본 클래스)와 함께 Owen (공장)을 사용합니다.

이제 인도 사람은 이탈리아 사람에서 식당을 인수합니다. 피자 대신 인도 식당 (프레임 워크) 서버 dosa. 인도 사람을 준비하기 위해 (인물 제작자) Maida (기본 클래스)와 함께 프라이팬 (공장)을 사용합니다.

시나리오를 보면 음식이 다르고 음식 준비가 다르지만 동일한 식당 (같은 프레임 워크)에서 다릅니다. 식당은 중식, 멕시칸 또는 모든 요리를 지원할 수있는 방식으로 지어 져야합니다. 프레임 워크 내부의 오브젝트 빌더를 사용하면 원하는 요리를 플러그인 할 수 있습니다. 예를 들어

class RestaurantObjectBuilder
{
   IFactory _factory = new DefaultFoodFactory();

   //This can be used when you want to plugin the 
   public void SetFoodFactory(IFactory customFactory)
   {
        _factory = customFactory;
   }

   public IFactory GetFoodFactory()
   {
      return _factory;
   }
}

7

나는 항상 빌더 패턴을 다루기 힘들고, 눈에 잘 띄지 않으며, 경험이 부족한 프로그래머가 자주 사용하는 것으로 싫어했습니다. 초기화 후 단계 가 필요한 일부 데이터에서 개체를 조립해야하는 경우에만 의미가있는 패턴입니다 (즉, 모든 데이터가 수집되면 무언가를 수행하십시오). 대신, 시간 빌더의 99 %에서 단순히 클래스 멤버를 초기화하는 데 사용됩니다.

이러한 경우 withXyz(...)클래스 내에서 유형 설정자 를 선언 하고 참조를 자신에게 반환하는 것이 훨씬 좋습니다 .

이걸 고려하세요:

public class Complex {

    private String first;
    private String second;
    private String third;

    public String getFirst(){
       return first; 
    }

    public void setFirst(String first){
       this.first=first; 
    }

    ... 

    public Complex withFirst(String first){
       this.first=first;
       return this; 
    }

    public Complex withSecond(String second){
       this.second=second;
       return this; 
    }

    public Complex withThird(String third){
       this.third=third;
       return this; 
    }

}


Complex complex = new Complex()
     .withFirst("first value")
     .withSecond("second value")
     .withThird("third value");

이제 우리는 자체 초기화를 관리하고 훨씬 더 우아하다는 것을 제외하고 빌더와 거의 동일한 작업을 수행하는 깔끔한 단일 클래스를 가지고 있습니다.


방금 복잡한 XML 문서를 JSON으로 작성하기로 결정했습니다. 첫째, 클래스 'Complex'가 XML 가능 제품을 처음에 제공 할 수 있다는 것을 어떻게 알 수 있으며 JSON 가능 객체를 생성하기 위해 어떻게 변경합니까? 빠른 답변 : 빌더를 사용해야하므로 할 수 없습니다. 그리고 우리는 ... 완전한 원을 온
데이비드 바커에게

1
total bs, Builder는 변경 불가능한 오브젝트를 빌드하고 제품 클래스를 건드리지 않고 미래의 건물 방식을 바꿀 수있는 능력을 갖도록 설계되었습니다
Mickey Tin

6
흠? Builder에서 설계된 것이 무엇인지 말하는 대답의 어딘가를 읽었습니까? 이는 "언제 작성기 패턴을 언제 사용 하시겠습니까?"라는 질문에 대한 대안적인 관점입니다.이 패턴은 셀 수없이 많은 패턴이 남용 된 경험을 기반으로하며, 작업이 훨씬 더 간단한 작업입니다. 모든 패턴은 언제, 어떻게 사용하는지 아는 경우에 유용합니다. 처음부터 패턴을 문서화하는 것이 요점입니다! 패턴이 과도하게 사용되거나 잘못 사용되면 (오용) 코드의 맥락에서 반 패턴이됩니다. Alas ...
Pavel Lechev

6

빌더의 또 다른 장점은 팩토리가있는 경우 코드에 여전히 커플 링이 있다는 것 입니다. 팩토리가 작동하려면 작성할 수있는 모든 오브젝트를 알아야하기 때문에 입니다. 만들 수있는 다른 객체를 추가하는 경우 팩토리 클래스를 수정하여 객체를 포함시켜야합니다. 이것은 추상 팩토리에서도 발생합니다.

반면에 빌더를 사용하면이 새 클래스에 대한 새 콘크리트 빌더를 작성하기 만하면됩니다. director 클래스는 생성자에서 빌더를 수신하므로 동일하게 유지됩니다.

또한 많은 풍미가 있습니다. 카미카제 용병이 또 하나를 준다.


6
/// <summary>
/// Builder
/// </summary>
public interface IWebRequestBuilder
{
    IWebRequestBuilder BuildHost(string host);

    IWebRequestBuilder BuildPort(int port);

    IWebRequestBuilder BuildPath(string path);

    IWebRequestBuilder BuildQuery(string query);

    IWebRequestBuilder BuildScheme(string scheme);

    IWebRequestBuilder BuildTimeout(int timeout);

    WebRequest Build();
}

/// <summary>
/// ConcreteBuilder #1
/// </summary>
public class HttpWebRequestBuilder : IWebRequestBuilder
{
    private string _host;

    private string _path = string.Empty;

    private string _query = string.Empty;

    private string _scheme = "http";

    private int _port = 80;

    private int _timeout = -1;

    public IWebRequestBuilder BuildHost(string host)
    {
        _host = host;
        return this;
    }

    public IWebRequestBuilder BuildPort(int port)
    {
        _port = port;
        return this;
    }

    public IWebRequestBuilder BuildPath(string path)
    {
        _path = path;
        return this;
    }

    public IWebRequestBuilder BuildQuery(string query)
    {
        _query = query;
        return this;
    }

    public IWebRequestBuilder BuildScheme(string scheme)
    {
        _scheme = scheme;
        return this;
    }

    public IWebRequestBuilder BuildTimeout(int timeout)
    {
        _timeout = timeout;
        return this;
    }

    protected virtual void BeforeBuild(HttpWebRequest httpWebRequest) {
    }

    public WebRequest Build()
    {
        var uri = _scheme + "://" + _host + ":" + _port + "/" + _path + "?" + _query;

        var httpWebRequest = WebRequest.CreateHttp(uri);

        httpWebRequest.Timeout = _timeout;

        BeforeBuild(httpWebRequest);

        return httpWebRequest;
    }
}

/// <summary>
/// ConcreteBuilder #2
/// </summary>
public class ProxyHttpWebRequestBuilder : HttpWebRequestBuilder
{
    private string _proxy = null;

    public ProxyHttpWebRequestBuilder(string proxy)
    {
        _proxy = proxy;
    }

    protected override void BeforeBuild(HttpWebRequest httpWebRequest)
    {
        httpWebRequest.Proxy = new WebProxy(_proxy);
    }
}

/// <summary>
/// Director
/// </summary>
public class SearchRequest
{

    private IWebRequestBuilder _requestBuilder;

    public SearchRequest(IWebRequestBuilder requestBuilder)
    {
        _requestBuilder = requestBuilder;
    }

    public WebRequest Construct(string searchQuery)
    {
        return _requestBuilder
        .BuildHost("ajax.googleapis.com")
        .BuildPort(80)
        .BuildPath("ajax/services/search/web")
        .BuildQuery("v=1.0&q=" + HttpUtility.UrlEncode(searchQuery))
        .BuildScheme("http")
        .BuildTimeout(-1)
        .Build();
    }

    public string GetResults(string searchQuery) {
        var request = Construct(searchQuery);
        var resp = request.GetResponse();

        using (StreamReader stream = new StreamReader(resp.GetResponseStream()))
        {
            return stream.ReadToEnd();
        }
    }
}

class Program
{
    /// <summary>
    /// Inside both requests the same SearchRequest.Construct(string) method is used.
    /// But finally different HttpWebRequest objects are built.
    /// </summary>
    static void Main(string[] args)
    {
        var request1 = new SearchRequest(new HttpWebRequestBuilder());
        var results1 = request1.GetResults("IBM");
        Console.WriteLine(results1);

        var request2 = new SearchRequest(new ProxyHttpWebRequestBuilder("localhost:80"));
        var results2 = request2.GetResults("IBM");
        Console.WriteLine(results2);
    }
}

1
두 가지 방법으로 답을 향상시킬 수 있습니다. 1) SSCCE로 만듭니다. 2) 이것이 어떻게 질문에 대답하는지 설명하십시오.
james.garriss


3

집에서 만든 메시징 라이브러리에서 빌더를 사용했습니다. 라이브러리 코어는 유선에서 데이터를 수신하여 Builder 인스턴스로 수집 한 다음 Builder가 Message 인스턴스를 작성하는 데 필요한 모든 것을 얻었 으면 Builder.GetMessage ()는 데이터를 수집하여 데이터 인스턴스를 작성했습니다. 철사.



2

Java에서 DateTime의 마샬링을 반대하기 위해 XML에 표준 XMLGregorianCalendar를 사용하고 싶었을 때 그것을 사용하는 것이 얼마나 무겁고 번거로운 지에 대한 많은 의견을 들었습니다. 시간대, 밀리 초 등을 관리하기 위해 xs : datetime 구조체의 XML 필드를 분석하려고했습니다.

그래서 GregorianCalendar 또는 java.util.Date에서 XMLGregorian 캘린더를 작성하는 유틸리티를 설계했습니다.

내가 일하는 곳 때문에 법적으로 온라인 공유가 허용되지 않지만 클라이언트가이를 사용하는 방법의 예는 다음과 같습니다. 세부 정보를 추상화하고 xs : datetime에 덜 사용되는 XMLGregorianCalendar의 일부 구현을 필터링합니다.

XMLGregorianCalendarBuilder builder = XMLGregorianCalendarBuilder.newInstance(jdkDate);
XMLGregorianCalendar xmlCalendar = builder.excludeMillis().excludeOffset().build();

이 패턴은 xmlCalendar의 필드를 정의되지 않은 필드로 설정하여 제외되므로 여전히 "빌드"하므로 필터에 더 가깝습니다. xs : date 및 xs : time 구조체를 작성하고 필요할 때 시간대 오프셋을 조작하기 위해 빌더에 다른 옵션을 쉽게 추가했습니다.

XMLGregorianCalendar를 작성하고 사용하는 코드를 본 적이 있다면 이것이 조작하기가 훨씬 쉬워 졌음을 알게 될 것입니다.


0

실제 사례는 클래스를 테스트 할 때 사용하는 것입니다. sut (테스트 대상 시스템) 빌더를 사용합니다.

예:

수업:

public class CustomAuthenticationService
{
    private ICloudService _cloudService;
    private IDatabaseService _databaseService;

    public CustomAuthenticationService(ICloudService cloudService, IDatabaseService databaseService)
    {
        _cloudService = cloudService;
        _databaseService = databaseService;
    }

    public bool IsAuthorized(User user)
    {            
        //Implementation Details
        return true;

}

테스트:

    [Test]
    public void Given_a_User_With_Permission_When_Verifying_If_Authorized_Then_Authorize_It_Returning_True()
    {
        CustomAuthenticationService sut = new CustomAuthenticationServiceBuilder();
        User userWithAuthorization = null;

        var result = sut.IsAuthorized(userWithAuthorization);

        Assert.That(result, Is.True);
    }

sut Builder :

public class CustomAuthenticationServiceBuilder
{
    private ICloudService _cloudService;
    private IDatabaseService _databaseService;

    public CustomAuthenticationServiceBuilder()
    {
        _cloudService = new AwsService();
        _databaseService = new SqlServerService();
    }

    public CustomAuthenticationServiceBuilder WithAzureService(AzureService azureService)
    {
        _cloudService = azureService;

        return this;
    }

    public CustomAuthenticationServiceBuilder WithOracleService(OracleService oracleService)
    {
        _databaseService = oracleService;

        return this;
    }

    public CustomAuthenticationService Build()
    {
        return new CustomAuthenticationService(_cloudService, _databaseService);
    }

    public static implicit operator CustomAuthenticationService (CustomAuthenticationServiceBuilder builder)
    {
        return builder.Build();
    }
}

이 경우 CustomAuthenticationService클래스에 세터를 추가하는 대신 빌더가 필요한 이유는 무엇 입니까?
Laur Ivan

좋은 질문입니다 @LaurIvan! 어쩌면 내 예제가 약간 좋지 않았지만 CustomAuthenticationService 클래스를 변경할 수 없다고 상상해보십시오. 빌더는 단위 테스트 읽기를 향상시키는 매력적인 방법입니다. 그리고 getter와 setter를 만들면 필드가 노출되고 테스트에만 사용됩니다. Sut Builder에 대해 더 자세히 알고 싶다면 Test Data Builder 에 대해 읽을 수 있습니다 .
라파엘 미 셀리
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.