리팩토링하기 전에 단위 테스트를 작성하는 방법은 무엇입니까?


55

"리팩토링 할 때 단위 테스트를 어떻게 유지합니까?"와 유사한 질문에 대한 질문에 대한 답변을 읽었습니다. 필자의 경우 시나리오는 우리가 가지고있는 일부 표준을 검토하고 따라야 할 프로젝트가 주어 졌다는 점에서 약간 다릅니다. 현재 프로젝트에 대한 테스트는 전혀 없습니다!

서비스 계층에서 DAO 유형 코드를 혼합하지 않는 등 더 잘 수행 할 수 있다고 생각하는 여러 가지 사항을 확인했습니다.

리팩토링하기 전에 기존 코드에 대한 테스트를 작성하는 것이 좋습니다. 문제는 리팩토링 할 때 특정 논리가 수행되는 위치를 변경하면 테스트가 중단되고 이전 구조를 염두에두고 테스트가 작성된다는 것입니다 (모의 적 인 의존성 등).

제 경우에는 가장 좋은 방법은 무엇입니까? 리팩토링 된 코드를 중심으로 테스트를 작성하고 싶지만 원하는 행동을 바꿀 수있는 것을 잘못 리팩터링 할 위험이 있음을 알고 있습니다.

이것이 리 팩터인지 재 설계인지에 상관없이 수정 될 용어에 대한 이해가 행복합니다. 현재 리팩토링에 대한 다음 정의를 작성 중입니다. 그 방법을 바꾸십시오. " 따라서 소프트웨어의 기능을 변경하지 않고 소프트웨어의 방법 / 위치를 변경합니다.

마찬가지로, 나는 재 설계로 간주 될 수있는 메소드의 서명을 변경하면 그 주장을 볼 수 있습니다.

다음은 간단한 예입니다

MyDocumentService.java (흐름)

public class MyDocumentService {
   ...
   public List<Document> findAllDocuments() {
      DataResultSet rs = documentDAO.findAllDocuments();
      List<Document> documents = new ArrayList<>();
      for(DataObject do: rs.getRows()) {
         //get row data create new document add it to 
         //documents list
      }

      return documents;
   }
}

MyDocumentService.java (리팩터링 / 재 설계)

public class MyDocumentService {
   ...
   public List<Document> findAllDocuments() {
      //Code dealing with DataResultSet moved back up to DAO
      //DAO now returns a List<Document> instead of a DataResultSet
      return documentDAO.findAllDocuments();
   }
}

14
그것은 정말 리팩토링 당신이, 또는 계획 재 설계 ? 두 경우에 답이 다를 수 있기 때문입니다.
herby

4
저는 정의에 따라 작업하고 있습니다. "리팩토링을 사용하면 정의에 따라 소프트웨어의 기능을 변경하지 않고 소프트웨어의 작동 방식을 변경할 수 있습니다." 그래서 나는 리팩토링이 경우에, 용어에 대한 이해 수정 주시기 생각
PDStat

21
통합 테스트를 작성하지 마십시오. 계획중인 "리팩토링"이 단위 테스트 수준 이상입니다. 새로운 수업 (또는 당신이 알고있는 오래된 수업) 만 단위 테스트합니다.
OrangeDog

2
리팩토링 정의와 관련하여 소프트웨어가 무엇을 명확하게 정의합니까? 다시 말해, 독립 API가있는 모듈로 이미 "인수 화"되었습니까? 그렇지 않은 경우 아마도 가장 높은 (사용자를 향한) 레벨을 제외하고 리팩토링 할 수 없습니다. 모듈 수준에서는 필연적으로 다시 디자인하게됩니다. 이 경우 단위를 작성하기 전에 단위 테스트 작성에 시간을 낭비하지 마십시오.
Kevin Krumwiede

4
테스트의 안전망없이 테스트 하네스로 가져 오기 위해 약간의 리팩토링을 수행해야 할 가능성이 큽니다. 내가 줄 수있는 최선의 조언은 IDE 또는 리팩토링 도구가 당신을 위해 그것을하지 않을 경우, 직접하지 마십시오. CUT를 사용할 수있을 때까지 자동 리팩토링을 계속 적용하십시오. Michael Feather의 "레거시 코드로 효과적으로 작업하기"의 사본을 선택하고 싶을 것입니다.
RubberDuck

답변:


56

회귀 를 검사하는 테스트를 찾고 있습니다. 즉, 기존의 행동을 어기는 것. 나는 그 행동이 어느 수준에서 동일하게 유지되는지, 그리고 그 행동을 주도하는 인터페이스가 동일하게 유지되는 것을 식별하고 그 시점에서 테스트를 시작합니다.

이제이 수준 아래에서 무엇을 하든지 행동은 동일하게 유지되는 몇 가지 테스트가 있습니다 .

테스트와 코드가 어떻게 동기화 될 수 있는지에 대해 의문을 가질 수 있습니다. 구성 요소에 대한 인터페이스 가 동일하게 유지되면이 문제에 대한 테스트를 작성하고 새 구현을 만들 때 두 구현에 대해 동일한 조건을 지정할 수 있습니다. 그렇지 않은 경우 중복 구성 요소에 대한 테스트가 중복 테스트임을 수락해야합니다.


1
Viz, 당신은 단위 테스트보다는 통합 또는 시스템 테스트를하고있을 것입니다. 여전히 "단위 테스트"도구를 사용하지만 각 테스트마다 하나 이상의 코드 단위를 사용하게됩니다.
Móż

예. 매우 그렇습니다. 회귀 테스트 는 서버에 대한 REST 요청 및 후속 데이터베이스 테스트 (예 : 단위 테스트가 아님)와 같이 매우 높은 수준의 작업을 수행 할 수 있습니다 .
Brian Agnew

40

권장되는 방법은 버그를 포함하여 코드의 현재 동작을 테스트하는 "핀 다운 테스트"를 작성하는 것부터 시작하지만 요구 사항 문서를 위반하는 특정 동작이 버그인지 여부를 판단하지 않아도됩니다. 모르는 문제에 대한 해결 방법 또는 문서화되지 않은 요구 사항 변경을 나타냅니다.

이러한 핀 다운 테스트는 단위 테스트보다는 통합 수준이 높아 리팩토링을 시작할 때 계속 작동하도록하는 것이 가장 합리적입니다.

그러나 코드를 테스트 할 수 있도록하기 위해 일부 리팩토링이 필요할 수 있습니다. "안전한"리팩토링에주의하십시오. 예를 들어, 거의 모든 경우에 비공개 인 메소드는 아무 것도 깨지 않고 공개 할 수 있습니다.


통합 테스트의 경우 +1 앱에 따라 실제로 웹 앱에 요청을 보내는 수준에서 시작할 수 있습니다. HTML을 다시 전송하는 경우 리팩토링으로 인해 앱이 다시 전송하는 내용이 변경되어서는 안됩니다.
jpmc26

'핀 다운'테스트 문구가 마음에 듭니다.
Brian Agnew

12

아직 레거시 코드 를 사용한 효과적인 작업리팩토링-기존 코드의 디자인 개선을 모두 읽지 않은 경우 제안 합니다.

[..] 나에게 나타나는 문제는 리팩토링을 할 때 특정 로직이 수행되는 위치를 변경함에 따라 테스트가 중단되고 테스트는 이전 구조를 염두에두고 작성 될 것입니다 (모의 된 의존성 등) [ ..]

필자는 이것이 반드시 문제라고 생각하지는 않습니다. 테스트를 작성하고 코드 구조를 변경 한 다음 테스트 구조 조정하십시오 . 이렇게하면 새로운 구조가 실제로 이전 구조보다 나은지 여부에 대한 직접적인 피드백을 얻을 수 있습니다. 그렇다면 조정 된 테스트 를 작성하는 것이 더 쉬울 것입니다. 버그는 테스트를 통과).

또한 다른 사람들이 이미 쓴 것처럼 : 너무 세밀한 테스트 는 작성하지 마십시오 (최소한 처음 에는 작성하지 마십시오 ). 높은 수준의 추상화를 유지하십시오 (따라서 테스트는 회귀 또는 통합 테스트로 더 잘 특성화 될 수 있습니다).


1
이. 테스트는 끔찍해 보이지만 기존 동작을 다룹니다. 그런 다음 코드가 리팩터링되면 잠금 단계에서 테스트도 수행됩니다. 당신이 자랑스럽게 생각할 때까지 반복하십시오. ++
RubberDuck

1
나는 두 권의 책 권장 사항을 두 번째로 사용합니다. 테스트 코드를 처리해야 할 때는 항상 가까이에 있습니다.
Toby Speight

5

모든 의존성을 조롱하는 엄격한 단위 테스트를 작성하지 마십시오. 어떤 사람들은 이것이 실제 단위 테스트가 아니라고 말할 것입니다. 그들을 무시하라. 이 테스트는 유용하며 그게 중요합니다.

예를 보자.

public class MyDocumentService {
   ...
   public List<Document> findAllDocuments() {
      DataResultSet rs = documentDAO.findAllDocuments();
      List<Document> documents = new ArrayList<>();
      for(DataObject do: rs.getRows()) {
         //get row data create new document add it to 
         //documents list
      }

      return documents;
   }
}

테스트는 아마도 다음과 같습니다.

DocumentDao documentDao = Mock.create(DocumentDao.class);
Mock.when(documentDao.findAllDocuments())
    .thenReturn(DataResultSet.create(...))
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());

DocumentDao를 조롱하는 대신 종속성을 조롱하십시오.

DocumentDao documentDao = new DocumentDao(db);
Mock.when(db...)
    .thenReturn(...)
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());

이제 테스트를 중단하지 않고도 로직을 MyDocumentService로 이동할 수 있습니다 DocumentDao. 테스트는 기능이 동일하다는 것을 보여줍니다 (테스트 한 한).


DocumentService를 테스트하고 DAO를 조롱하지 않으면 단위 테스트가 아닙니다. 그것은 통합 테스트와 통합 테스트 사이에 있습니다. 안 그래?
Laiv

7
@Laiv, 실제로 사람들이 단위 테스트라는 용어를 사용하는 방법에는 상당한 차이가 있습니다. 일부는 엄격하게 격리 된 테스트만을 의미하기 위해 사용합니다. 다른 테스트로는 빠르게 실행되는 테스트가 있습니다. 일부는 테스트 프레임 워크에서 실행되는 모든 것을 포함합니다. 그러나 궁극적으로 단위 테스트라는 용어를 어떻게 정의하고 싶은지는 중요하지 않습니다. 문제는 어떤 테스트가 유용한 지에 대한 것이므로 단위 테스트를 정확히 정의하는 방법에 산만 해지지 않아야합니다.
Winston Ewert

유용성이 가장 중요한 것임을 보여주는 훌륭한 요점. 단위 테스트를 위해 가장 사소한 알고리즘에 대한 사치스러운 단위 테스트는 막대한 시간 낭비와 귀중한 자원이 아니라면 좋은 것보다 더 해 롭습니다. 이것은 거의 모든 것에 적용될 수 있으며 내 경력의 초기에 알고 싶은 것입니다.
Lee

3

당신이 말했듯이, 당신이 행동을 바꾸면 그것은 리 팩터가 아닌 변형입니다. 어떤 수준에서 행동을 변화 시키는가 차이를 만드는 것입니다.

최상위 수준의 공식 테스트가없는 경우 코드를 다시 디자인 한 후에 코드가 작동하는 것으로 간주되도록 클라이언트 (코드 또는 인간 호출)가 동일하게 유지해야하는 요구 사항을 찾아보십시오. 구현해야하는 테스트 사례 목록입니다.

테스트 케이스 변경이 필요한 변경 구현에 대한 질문을 해결하기 위해 디트로이트 (클래식) vs 런던 (mockist) TDD를 살펴 보는 것이 좋습니다. 마틴 파울러 (Martin Fowler)는 그의 위대한 기사 인 Mocks는 스터브가 아니지만 많은 사람들이 의견을 가지고 있습니다. 외부를 바꿀 수없는 가장 높은 수준에서 시작하여 아래로 내려가는 경우 실제로 변경해야하는 수준에 도달 할 때까지 요구 사항이 상당히 안정적으로 유지되어야합니다.

테스트 없이는이 작업이 어려울 수 있으며 새 코드가 정확히 필요한 작업을 수행 할 수있을 때까지 이중 코드 경로를 통해 클라이언트를 실행하고 차이점을 기록하는 것이 좋습니다.


3

여기 내 접근 방식. 4 단계의 리 팩터 테스트이기 때문에 시간면에서 비용이 듭니다.

내가 노출하려고하는 것은 질문의 예에서 노출 된 것보다 복잡한 구성 요소에 더 적합 할 수 있습니다.

어쨌든 전략은 모든 구성 요소 후보가 인터페이스 (DAO, 서비스, 컨트롤러 등)에 의해 정규화되는 데 유효합니다.

1. 인터페이스

MyDocumentService 에서 모든 공용 메소드를 수집하고 이를 모두 인터페이스에 통합 할 수 있습니다. 예를 들어. 이미 존재하는 경우 새로 설정하는 대신 해당 것을 사용하십시오 .

public interface DocumentService {

   List<Document> getAllDocuments();

   //more methods here...
}

그런 다음 MyDocumentService 가이 새 인터페이스를 구현하도록합니다.

여태까지는 그런대로 잘됐다. 주요 변경 사항은 없었으며 현재 계약을 존중했으며 비 헤이 비는 그대로 남아 있습니다.

public class MyDocumentService implements DocumentService {

 @Override
 public List<Document> getAllDocuments(){
         //legacy code here as it is.
        // with no changes ...
  }
}

2. 레거시 코드의 단위 테스트

여기에 노력이 있습니다. 테스트 스위트를 설정합니다. 성공적인 사례와 오류 사례를 가능한 한 많이 설정해야합니다. 이것들은 결과의 질을 좋게하기위한 것입니다.

이제 MyDocumentService 를 테스트하는 대신 인터페이스를 테스트 할 계약으로 사용합니다.

세부 사항에 들어 가지 않을 것이므로 용서하십시오. 내 코드가 너무 단순하거나 너무 무관 해 보인다면

public class DocumentServiceTestSuite {

   @Mock
   MyDependencyA mockDepA;

   @Mock
   MyDependencyB mockDepB;

    //... More mocks

   DocumentService service;

  @Before
   public void initService(){
       service = MyDocumentService(mockDepA, mockDepB);
      //this is purposed way to inject 
      //dependencies. Replace it with one you like more.  
   }

   @Test
   public void getAllDocumentsOK(){
         // here I mock depA and depB
         // wanted behaivors...

         List<Document> result = service.getAllDocuments();

          Assert.assertX(result);
          Assert.assertY(result);
           //... As many you think appropiate
    } 
 }

이 단계는이 방법에서 다른 것보다 시간이 오래 걸립니다. 미래 비교를위한 참조 지점을 설정하기 때문에 가장 중요합니다.

참고 : 주요 변경 사항이 없었으므로 behaivor는 그대로 유지됩니다. 여기 SCM에 태그를 추가하는 것이 좋습니다. 태그 나 지점은 중요하지 않습니다. 그냥 버전을 만드십시오.

롤백, 버전 비교를 위해 이전 코드와 새 코드를 병렬로 실행할 수 있습니다.

3. 리팩토링

리 팩터는 새로운 컴포넌트로 구현 될 것입니다. 기존 코드는 변경하지 않습니다. 첫 번째 단계는 MyDocumentService를 복사하여 붙여 넣기 하고 이름을 CustomDocumentService로 바꾸는 것만 큼 쉽습니다 (예 :) .

새 클래스는 DocumentService를 계속 구현 합니다. 그런 다음 getAllDocuments ()를 리팩토링 하십시오 . (하나부터 시작하자. 핀 리 팩터)

DAO의 인터페이스 / 방법에 약간의 변화가 필요할 수 있습니다. 그렇다면 기존 코드를 변경하지 마십시오. DAO 인터페이스에서 고유 한 메소드를 구현하십시오. 오래된 코드에 Deprecated 로 주석을 달면 제거해야 할 사항에 대해 나중에 알게됩니다.

기존 구현을 중단 / 변경하지 않는 것이 중요합니다. 두 서비스 를 동시에 실행 한 다음 결과를 비교 하려고합니다 .

public class CustomDocumentService implements DocumentService {

 @Override
 public List<Document> getAllDocuments(){
         //new code here ...
         //due to im refactoring service 
         //I do the less changes possible on its dependencies (DAO).
         //these changes will come later 
         //and they will have their own tests
  }
 }

4. DocumentServiceTestSuite 업데이트

좋아, 이제 더 쉬운 부분. 새 구성 요소의 테스트를 추가합니다.

public class DocumentServiceTestSuite {

   @Mock
   MyDependencyA mockDepA;

   @Mock
   MyDependencyB mockDepB;

   DocumentService service;
   DocumentService customService;

  @Before
   public void initService(){
       service = MyDocumentService(mockDepA, mockDepB);
        customService = CustomDocumentService(mockDepA, mockDepB);
       // this is purposed way to inject 
       //dependencies. Replace it with the one you like more
   }

   @Test
   public void getAllDocumentsOK(){
         // here I mock depA and depB
         // wanted behaivors...

         List<Document> oldResult = service.getAllDocuments();

          Assert.assertX(oldResult);
          Assert.assertY(oldResult);
           //... As many you think appropiate

          List<Document> newResult = customService.getAllDocuments();

          Assert.assertX(newResult);
          Assert.assertY(newResult);
           //... The very same made to oldResult

          //this is optional
Assert.assertEquals(oldResult,newResult);
    } 
 }

이제 oldResult와 newResult가 독립적으로 검증되었지만 서로 비교할 수도 있습니다. 이 마지막 유효성 검사는 선택 사항이며 결과에 따라 다릅니다. 비교할 수 없을 수도 있습니다.

이 방법으로 두 컬렉션을 비교하기에는 너무 많은 내용이 없지만 다른 종류의 개체 (pojo, 데이터 모델 엔터티, DTO, 랩퍼, 네이티브 형식)에는 유효합니다.

노트

단위 테스트를 수행하는 방법이나 모의 라이브러리를 사용하는 방법을 감히 말하지 않을 것입니다. 나는 당신이 리팩터링을 어떻게해야하는지 말할 것도 없다. 내가하고 싶은 것은 글로벌 전략을 제안하는 것입니다. 앞으로 나아가는 방법은 귀하에게 달려 있습니다. 코드의 복잡성, 복잡성 및 그러한 전략을 시도해 볼 가치가 있는지 정확하게 알 수 있습니다. 여기에는 시간과 자원과 같은 사실이 중요합니다. 또한 향후 이러한 테스트에서 기대할 사항이 중요합니다.

서비스로 예제를 시작했으며 DAO 등을 따릅니다. 의존성 수준에 깊이 들어가기. 다소 위아래 전략 이라고 할 수 있습니다 . 그러나 약간의 변경 / 리 팩터 ( 예 : 투어 예제에서 노출 된 것과 같은 )의 경우 상향식으로 작업이 더 쉬워집니다. 변경의 범위가 적기 때문입니다.

더 이상 사용되지 않는 코드를 제거하고 이전 종속성을 새 코드로 리디렉션하는 것은 사용자의 몫입니다.

더 이상 사용되지 않는 테스트를 제거하면 작업이 완료됩니다. 이전 솔루션의 테스트 버전을 테스트 한 경우 언제든지 서로를 확인하고 비교할 수 있습니다.

수많은 작업의 결과로 레거시 코드를 테스트, 검증 및 버전 화했습니다. 새로운 코드, 테스트, 검증 및 버전 관리가 가능합니다.


3

tl; dr 단위 테스트를 작성하지 마십시오. 보다 적절한 수준에서 테스트를 작성하십시오.


리팩토링에 대한 작업 정의를 고려할 때 :

소프트웨어의 기능을 변경하지 않고 소프트웨어의 작동 방식을 변경합니다

매우 넓은 스펙트럼. 한쪽 끝에는 더 효율적인 알고리즘을 사용하여 특정 방법에 대한 자체 포함 된 변경이 있습니다. 다른 쪽 끝에는 다른 언어로 포팅하고 있습니다.

어떤 수준의 리팩토링 / 재 설계가 수행 되든지간에 해당 레벨 이상에서 작동하는 테스트를 수행하는 것이 중요합니다.

자동화 된 테스트는 종종 레벨별로 다음과 같이 분류됩니다.

  • 단위 테스트 -개별 구성 요소 (클래스, 메소드)

  • 통합 테스트 -구성 요소 간의 상호 작용

  • 시스템 테스트 -완전한 응용 프로그램

본질적으로 손대지 않은 리팩토링을 견딜 수있는 테스트 레벨을 작성하십시오.

생각한다:

응용 프로그램은 모두 무엇 필수, 공개적으로 보이는 행동을해야합니다 이전이후에 리팩토링? 그 일이 여전히 똑같이 작동하는지 어떻게 테스트 할 수 있습니까?


2

인터페이스가 사소한 방식으로 변경 될 것으로 예상되는 지점에서 연결되는 테스트 작성에 시간을 낭비하지 마십시오. 이것은 종종 '협업 적'인 클래스 테스트를 시도한다는 표시입니다. 그 가치는 스스로하는 것이 아니라 가치있는 행동을 만들기 위해 밀접하게 관련된 여러 클래스와 상호 작용하는 방식입니다. . 그것은이다 당신이 높은 수준에서 테스트되고 싶지 즉, 테스트 할 행동. 이 수준 이하의 테스트는 종종 추악한 조롱이 필요하며, 결과적인 테스트는 행동을 방어하는 데 도움이되는 것보다 개발에 더 많은 드래그가 될 수 있습니다.

리팩터링, 재 설계 등 무엇이든간에 너무 매달리지 마십시오. 하위 수준에서 여러 구성 요소의 재 설계를 구성하는 변경을 수행 할 수 있지만 통합 수준이 높을수록 단순히 리 팩터에 해당합니다. 요점은 당신에게 어떤 행동이 가치가 있는지 명확하게하고 그 행동을 변호하는 것입니다.

테스트를 작성할 때 고려하는 것이 유용 할 수 있습니다. QA, 제품 소유자 또는 사용자에게이 테스트가 실제로 테스트하는 내용을 쉽게 설명 할 수 있습니까? 테스트를 설명하는 것이 너무 난해하고 기술적 인 것 같으면 잘못된 수준에서 테스트하는 것입니다. '말이되는'포인트 / 레벨에서 테스트하고 모든 레벨에서 테스트로 코드를 작성하지 마십시오.


downvotes에 대한 이유에 항상 관심이 있습니다!
topo morto

1

첫 번째 작업은 테스트에 "이상적인 메소드 서명"을 작성하는 것입니다. 그것을 순수한 기능 으로 만들기 위해 노력하십시오 . 실제로 테스트중인 코드와는 독립적이어야합니다. 작은 어댑터 계층입니다. 이 어댑터 계층에 코드를 작성하십시오. 이제 코드를 리팩터링 할 때 어댑터 계층 만 변경하면됩니다. 다음은 간단한 예입니다.

[TestMethod]
public void simple_addition()
{
    Assert.AreEqual(7, Eval("3 + 4"));
}

[TestMethod]
public void order_of_operations()
{
    Assert.AreEqual(52, Eval("2 + 5 * 10"));
}

[TestMethod]
public void absolute_value()
{
    Assert.AreEqual(9, Eval("abs(-9)"));
    Assert.AreEqual(5, Eval("abs(5)"));
    Assert.AreEqual(0, Eval("abs(0)"));
}

static object Eval(string expression)
{
    // This is the code under test.
    // I can refactor this as much as I want without changing the tests.
    var settings = new EvaluatorSettings();
    Evaluator.Settings = settings;
    Evaluator.Evaluate(expression);
    return Evaluator.LastResult;
}

테스트는 좋지만 테스트중인 코드에 잘못된 API가 있습니다. 어댑터 계층을 업데이트하여 테스트를 변경하지 않고 리팩터링 할 수 있습니다.

static object Eval(string expression)
{
    // After refactoring...
    var settings = new EvaluatorSettings();
    var evaluator = new Evaluator(settings);
    return evaluator.Evaluate(expression);
}

이 예는 Do n't Repeat Yourself 원칙에 따라 수행해야 할 명백한 일이지만 다른 경우에는 그렇게 명확하지 않을 수 있습니다. 장점은 DRY를 뛰어 넘습니다. 실제 이점은 테스트중인 코드에서 테스트를 분리하는 것입니다.

물론,이 기술이 모든 상황에서 권장되는 것은 아닙니다. 예를 들어, POCO / POJO 용 어댑터는 실제로 테스트 코드와 독립적으로 변경 될 수있는 API가 없기 때문에 어댑터를 작성할 이유가 없습니다. 또한 적은 수의 테스트를 작성하는 경우 비교적 큰 어댑터 계층에 노력이 낭비 될 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.