"메소드가 변경없이 재사용되는 경우 메소드를 기본 클래스에 넣거나 인터페이스를 작성하십시오"라는 것이 좋은 규칙입니까?


10

내 동료가 기본 클래스 또는 인터페이스를 만드는 중 하나를 선택하기위한 규칙을 제시했습니다.

그는 말한다 :

구현하려는 모든 새로운 방법을 상상해보십시오. 그들 각각의 경우,이 고려 : 이 방법은 하나 개 이상의 클래스에 의해 구현 될 것입니다 정확히 없이,이 양식에 어떤 변화? 대답이 "예"이면 기본 클래스를 작성하십시오. 다른 모든 상황에서는 인터페이스를 만듭니다.

예를 들면 다음과 같습니다.

클래스 고려 cat하고 dog클래스를 확장 mammal하고 하나의 방법이를 pet(). 그런 다음 alligator아무 것도 확장하지 않고 단일 메소드를 가진 클래스를 추가합니다 slither().

이제 eat()모든 메소드에 메소드를 추가하려고 합니다.

의 구현 경우 eat()방법은 정확히 동일합니다 cat, dog그리고 alligator, 우리는 기본 클래스 (하자 말, 작성해야 animal하는 구현이 방법을).

그것의 구현의 경우, alligator사소한 방법으로 다르다, 우리가 만들어야 IEat인터페이스와 메이크업을 mammal하고 alligator그것을 구현합니다.

그는이 방법이 모든 경우를 포괄한다고 주장하지만 그것은 지나치게 단순화 된 것처럼 보인다.

이 규칙을 따르는 것이 가치가 있습니까?


27
alligatorS '의 구현 eat이 허용하는지에 당연히 다르다 catdog매개한다.
sbichenko

1
구현을 공유하기 위해 추상 기본 클래스를 원하지만 올바른 확장 성을 위해 인터페이스를 사용해야하는 경우 종종 특성을 대신 사용할 수 있습니다 . 즉, 귀하의 언어가이를 지원하는 경우입니다.
amon

2
구석에 자신을 칠할 때는 문과 가장 가까운 곳에있는 것이 가장 좋습니다.
JeffO

10
사람들이하는 가장 큰 실수는 인터페이스가 단순히 빈 추상 클래스라고 믿는 것입니다. 인터페이스는 프로그래머가 "이 규칙을 따르는 한 나에게 무엇을 주든 상관하지 않는다"고 말하는 방법입니다. 그런 다음 구성을 통해 (이상적으로) 코드 재사용을 수행해야합니다. 동료가 잘못되었습니다.
riwalk

2
광산의 CS의 PROFF는 슈퍼 클래스가되어야한다고 가르쳤다 is a및 인터페이스가 acts likeis. 개 is a포유 동물과 acts like먹는 사람 이요 이것은 포유류가 학급이어야하고 먹는 사람은 인터페이스 여야한다는 것을 말해 줄 것입니다. 항상 매우 유용한 안내서였습니다. (!) 참고 :의 예는 isThe cake is eatable또는 The book is writable.
MirroredFate

답변:


13

나는 이것이 좋은 경험 법칙이라고 생각하지 않습니다. 코드 재사용이 걱정된다면 PetEatingBehavior고양이와 개를위한 식사 기능을 정의하는 역할을 수행 할 수 있습니다 . 그런 다음 IEat코드 재사용을 함께 할 수 있습니다 .

요즘에는 상속을 사용해야하는 이유가 점점 줄어들고 있습니다. 하나의 큰 장점은 사용의 용이성입니다. GUI 프레임 워크를 사용하십시오. 이를 위해 API를 설계하는 일반적인 방법은 거대한 기본 클래스를 노출하고 사용자가 대체 할 수있는 메소드를 문서화하는 것입니다. 따라서 "기본"구현은 기본 클래스에 의해 제공되므로 애플리케이션이 관리해야하는 다른 모든 복잡한 작업은 무시할 수 있습니다. 그러나 필요할 때 올바른 방법을 다시 구현하여 사물을 사용자 정의 할 수 있습니다.

API에 대한 인터페이스를 고수하면 일반적으로 간단한 예제를 만들기 위해 더 많은 작업을 수행해야합니다. 그러나 인터페이스가있는 API IEatMammal기본 클래스 보다 적은 정보 를 포함 하는 단순한 이유로 연결이 덜 유지되고 유지 보수가 더 쉬운 경우가 많습니다 . 따라서 소비자는 약한 계약에 의존하고 리팩토링 기회 등을 얻습니다.

Rich Hickey를 인용하려면 : Simple! = Easy


기본적인 PetEatingBehavior구현 예를 제공 할 수 있습니까?
sbichenko

1
@exizt 먼저, 나는 이름을 고르는 것이 나쁘다. 그래서 PetEatingBehavior아마 잘못된 것입니다. 이것은 장난감 예제 일뿐이므로 구현을 줄 수 없습니다. 그러나 리팩토링 단계를 설명 할 수 있습니다. 개체를 정의하기 위해 고양이 / 개가 사용하는 모든 정보를 취하는 생성자가 있습니다 eat(치아 인스턴스, 위 인스턴스 등). 그것은 그들의 eat방법에 사용되는 고양이와 개에게 공통적 인 코드만을 포함합니다 . PetEatingBehavior회원 을 작성 하여 dog.eat/cat.eat로 전달하면됩니다.
Simon Bergot

나는 이것이 상속으로부터 멀리 떨어져 OP를 조종하는 많은 사람들의 유일한 대답이라고 믿을 수 없다 : (상속은 코드 재사용이 아닙니다!
Danny Tuppeny

1
@DannyTuppeny 왜 안되죠?
sbichenko

4
@exizt 클래스에서 상속받는 것은 큰 문제입니다. 많은 언어에서 하나만 가질 수있는 무언가에 (아마도 영구적 인) 의존성을 취하고 있습니다. 코드 재사용은이 자주 사용되는 기본 클래스를 사용하는 데 매우 사소한 일입니다. 다른 클래스와 공유하고 싶은 메소드로 끝나지만 다른 메소드를 재사용하여 이미 기본 클래스를 가지고 있거나 다형성으로 사용되는 "실제"계층 구조의 일부 (?) 인 경우 어떻게해야합니까? ClassA가 클래스 B의 유형 인 경우 상속을 사용하십시오 (예 : 자동차가 일부 내부 구현 세부 사항을 공유 할 때가 아니라 자동차에서 상속 함)
Danny Tuppeny

11

이 규칙을 따르는 것이 가치가 있습니까?

그것은 괜찮은 경험의 규칙이지만, 내가 위반 한 곳은 꽤 있습니다.

공평하게 말하면, 나는 다른 학생들보다 (추상적 인) 기본 수업을 더 많이 사용합니다. 나는 이것을 방어 프로그래밍으로한다.

저에게 (많은 언어로) 추상 기본 클래스는 기본 클래스가 하나만있을 수 있다는 주요 제약 조건을 추가합니다. 인터페이스가 아닌 기본 클래스를 사용하도록 선택함으로써 "이것은 클래스 (및 상속자)의 핵심 기능이며, 다른 기능과 willy-nilly를 혼합하는 것은 부적절합니다"라고 말합니다. 인터페이스는 반대이다 : "이것은 어떤 핵심적인 책임이 아닌 어떤 대상의 특성이다".

이러한 종류의 디자인 선택은 구현자가 코드를 사용하는 방법에 대한 암시 적 가이드를 작성하고 확장하여 코드를 작성합니다. 코드베이스에 큰 영향을 미치기 때문에 디자인 결정을 내릴 때 이러한 사항을 더 강력하게 고려하는 경향이 있습니다.


1
3 년 후이 답변을 다시 읽어 보면
nilly

9

친구 단순화의 문제점은 방법을 변경해야하는지 여부를 결정하는 것이 매우 어렵다는 것입니다. 수퍼 클래스와 인터페이스의 정신을 말하는 규칙을 사용하는 것이 좋습니다.

기능이 무엇인지 생각하여 무엇을해야하는지 파악하는 대신 만들려는 계층 구조를 살펴보십시오.

여기 내 교수가 차이점을 어떻게 가르쳤습니까?

수퍼 클래스는 있어야 is a하고 인터페이스는 acts like또는 is입니다. 개 is a포유 동물과 acts like먹는 사람 이요 이것은 포유류가 학급이어야하고 먹는 사람은 인터페이스 여야한다는 것을 말해 줄 것입니다. 의 일례는 isThe cake is eatable또는 The book is writable(모두 결정 eatable하고 writable인터페이스).

이와 같은 방법론을 사용하는 것은 상당히 간단하고 쉬우 며, 코드가 생각하는 것보다는 구조와 개념에 코드를 작성하게합니다. 따라서 유지 관리가 쉬워지고 코드를 더 읽기 쉽고 디자인이 더 간단 해집니다.

코드가 실제로 무엇을 말하는지에 관계없이, 이와 같은 방법을 사용하는 경우 지금부터 5 년 후에 다시 같은 방법을 사용하여 단계를 다시 추적하고 프로그램의 설계 방식과 상호 작용 방식을 쉽게 파악할 수 있습니다.

내 두 센트.


6

exizt의 요청에 따라 더 긴 답변으로 의견을 확대하고 있습니다.

사람들이하는 가장 큰 실수는 인터페이스가 단순히 빈 추상 클래스라고 믿는 것입니다. 인터페이스는 프로그래머가 "이 규칙을 따르는 한 나에게 무엇을 주든 상관하지 않는다"고 말하는 방법입니다.

.NET 라이브러리는 훌륭한 예입니다. 예를 들어,을 받아들이는 함수를 작성할 때 IEnumerable<T>" 데이터를 저장 하는 방법에 신경 쓰지 않습니다 . foreach루프를 사용할 수 있다는 것을 알고 싶습니다 .

이것은 매우 유연한 코드로 이어집니다. 갑자기 통합하려면 기존 인터페이스의 규칙에 따라 수행하면됩니다. 인터페이스를 구현하기가 어렵거나 혼란 스러우면 둥근 구멍에 사각형 못을 꽂으려는 힌트 일 수 있습니다.

그러나 CS 코드 교수는 상속이 모든 코드 재사용 문제에 대한 해결책이며 상속을 통해 한 번만 쓰고 어디서나 사용할 수 있으며 고아를 구출하는 과정에서 혈관염을 치료할 수 있다고 말했습니다. 떠오르는 바다에서 더 이상 눈물이 나지 않을 것입니다.

"코드 재사용"이라는 단어의 소리를 좋아하기 때문에 상속을 사용하는 것은 꽤 나쁜 생각입니다. Google코드 스타일 가이드 는이 요점을 상당히 간결하게 만듭니다.

구성은 종종 상속보다 더 적절합니다. ... [B] 서브 클래스를 구현하는 코드가 기본과 서브 클래스 사이에 퍼지므로 구현을 이해하기가 더 어려울 수 있습니다. 서브 클래스는 가상이 아닌 함수를 대체 할 수 없으므로 서브 클래스는 구현을 변경할 수 없습니다.

상속이 항상 답이 아닌 이유를 설명하기 위해 MySpecialFileWriter †라는 클래스를 사용하겠습니다. 맹목적으로 상속이 모든 문제에 대한 해결책이라고 믿는 사람은 코드 FileStream를 복제하지 않기 위해 상속을 시도해야한다고 주장합니다 FileStream. 똑똑한 사람들은 이것이 멍청하다는 것을 인식합니다. FileStream클래스에 객체 (로컬 또는 멤버 변수) 가 있어야하며 그 기능을 사용해야합니다.

FileStream예는 인위적인 것처럼 보일 수 있지만, 그렇지 않다. 동일한 인터페이스를 동일한 방식으로 구현하는 두 개의 클래스가있는 경우 중복 된 작업을 캡슐화하는 세 번째 클래스가 있어야합니다. 당신의 목표는 레고처럼 구성 할 수있는 독립적 인 재사용 가능한 블록 인 클래스를 작성하는 것입니다.

그렇다고해서 모든 비용으로 상속을 피해야한다는 의미는 아닙니다. 고려해야 할 점이 많으며 대부분 "구성 대 상속"이라는 질문을 조사하여 다룰 것입니다. 우리 고유의 Stack Overflow 에는 주제에 대한 몇 가지 좋은 답변이 있습니다.

하루가 끝나면 동료의 정서에 따라 정보에 입각 한 결정을 내리는 데 필요한 깊이 나 이해력이 부족합니다. 주제를 연구하고 스스로 알아 내십시오.

† 상속을 설명 할 때 모두가 동물을 사용합니다. 그것은 쓸모가 없습니다. 11 년간의 개발 과정에서 나는이라는 클래스를 작성하지 않았 Cat으므로 예제로 사용하지 않을 것입니다.


따라서 올바르게 이해하면 종속성 주입은 상속과 동일한 코드 재사용 기능을 제공합니다. 그렇다면 상속을 어떻게 사용 하시겠습니까? 유스 케이스를 제공 하시겠습니까? (정말 감사합니다 FileStream)
sbichenko

1
@exizt, 아니요 동일한 코드 재사용 기능을 제공하지 않습니다. 구현이 잘 포함되어 있기 때문에 더 많은 코드 재사용 기능을 제공합니다 (대부분의 경우). 클래스는 내재적으로 기능을 얻고 기존 함수를 오버로드하여 기능의 절반을 완전히 변경하지 않고 객체와 공개 API를 통해 명시 적으로 상호 작용합니다.
riwalk

4

예를 들어, 특히 자동차 나 동물에 관한 이야기는 아닙니다. 동물의 먹는 방법 (또는 가공 식품)은 실제로 그 유전과 관련이 없으며, 모든 동물에게 적용 할 수있는 단일 먹는 방법은 없습니다. 이것은 물리적 기능 (입력, 처리 및 출력 방법)에 더 의존합니다. 포유 동물은 육식 동물, 초식 동물 또는 잡식 동물 일 수 있지만 조류, 포유류 및 물고기는 곤충을 먹을 수 있습니다. 이 동작 은 특정 패턴으로 캡처 할 수 있습니다.

이 경우 다음과 같이 동작을 추상화하면 더 좋습니다.

public interface IFoodEater
{
    void Eat(IFood food);
}

public class Animal : IFoodEater
{
    private IFoodProcessor _foodProcessor;
    public Animal(IFoodProcessor foodProcessor)
    {
        _foodProcessor = foodProcessor;
    }

    public void Eat(IFood food)
    {
        _foodProcessor.Process(food);
    }
}

또는 FoodProcessor호환 가능한 동물 ( ICarnivore : IFoodEater, MeatProcessingVisitor) 을 먹이려고 하는 방문자 패턴을 구현하십시오 . 살아있는 동물에 대한 생각은 소화 트랙을 추출하여 일반적인 트랙으로 대체하는 데 방해가 될 수 있지만 실제로는 호의를 베풀고 있습니다.

이를 통해 동물을 기본 클래스로 만들 수 있으며 새로운 유형의 음식과 프로세서를 추가 할 때 다시 변경할 필요가 없으므로이 동물 클래스를 만드는 데 집중할 수 있습니다 매우 독창적입니다.

그렇습니다. 기본 수업은 제 자리에 있습니다. 경험의 법칙이 적용됩니다 (때때로 경험의 법칙처럼 항상 그렇지는 않지만).


1
IFoodEater는 일종의 중복입니다. 다른 무엇을 먹을 수있을 것으로 기대하십니까? 예, 동물은 끔찍한 것입니다.
기분 좋은

1
식충 식물은 어떻습니까? :) 진심으로,이 경우에 인터페이스를 사용하지 않을 때의 주요한 문제는 동물과 먹는 개념을 밀접하게 연관 시켰으며 동물 기본 클래스가 적합하지 않은 새로운 추상화를 도입하는 것이 훨씬 더 많은 작업이라는 것입니다.
Julia Hayward

1
나는 여기서 "먹는 것"이 ​​잘못된 생각이라고 믿는다. IConsumer인터페이스 는 어떻습니까 ? 육식 동물은 육류, 초식 동물은 식물, 식물은 햇빛과 물을 소비 할 수 있습니다.

2

인터페이스는 실제로 계약입니다. 기능이 없습니다. 구현하는 것은 구현 자에게 달려 있습니다. 계약을 이행해야하므로 선택의 여지가 없습니다.

추상 클래스는 구현 자에게 선택 사항을 제공합니다. 모든 사람이 구현해야하는 메소드를 갖거나, 대체 할 수있는 기본 기능을 메소드에 제공하거나, 모든 상속자가 사용할 수있는 일부 기본 기능을 제공하도록 선택할 수 있습니다.

예를 들면 다음과 같습니다.

public abstract class Person
    {
        /// <summary>
        /// Inheritors must implement a hello
        /// </summary>
        /// <returns>Hello</returns>
        public abstract string SayHello();
        /// <summary>
        /// Inheritors can use base functionality, or override
        /// </summary>
        /// <returns></returns>
        public virtual string SayGoodBye()
        {
            return "Good Bye";
        }
        /// <summary>
        /// Base Functionality that is inherited 
        /// </summary>
        public void DoSomething()
        {
            //Do Something Here
        }
    }

나는 당신의 경험치가 기본 클래스에 의해서도 처리 될 수 있다고 말할 것입니다. 특히 많은 포유류가 아마도 같은 것을 "먹는"것이므로 기본 클래스를 사용하는 것이 인터페이스보다 낫습니다. 왜냐하면 대부분의 상속자들이 사용할 수있는 가상 먹는 방법을 구현할 수 있고 예외적 인 경우는 무시할 수 있기 때문입니다.

"먹는"의 정의가 동물마다 다르면 아마도 인터페이스가 더 좋을 것입니다. 이것은 때때로 회색 영역이며 개별 상황에 따라 다릅니다.


1

아니.

인터페이스는 메소드 구현 방법에 관한 것입니다. 메소드를 구현하는 방법에 대한 인터페이스를 사용할 때마다 결정을 내린다면 뭔가 잘못한 것입니다.

나를 위해 인터페이스와 기본 클래스의 차이점은 요구 사항이 어디에 있는지에 관한 것입니다. 코드 일부에 특정 API가 포함 된 클래스가 필요하고이 API에서 발생하는 묵시적인 동작이 필요한 경우 인터페이스를 만듭니다. 실제로 호출 할 코드가 없으면 인터페이스를 작성할 수 없습니다.

반면에 기본 클래스는 일반적으로 코드를 재사용하는 방법으로 사용되므로이 기본 클래스를 호출하고이 기본 클래스가 속한 객체의 계층 만 고려하는 코드를 거의 신경 쓰지 않습니다.


잠깐, 왜 모든 동물 eat()에서 재 구현 합니까? 우리는 그것을 구현할 수 있습니다. 그래도 요구 사항에 대한 귀하의 요점을 이해합니다. mammal
sbichenko

@exizt : 죄송합니다. 질문을 잘못 읽었습니다.
Euphoric

1

다른 구현을 원한다면 항상 추상 메소드를 사용하여 추상 기본 클래스를 가질 수 있기 때문에 실제로 좋은 규칙은 아닙니다. 모든 상속자에게는 해당 메소드가 있어야하지만 각각 고유 한 구현을 정의합니다. 기술적으로 추상 메소드 집합을 가진 추상 클래스를 가질 수 있으며 기본적으로 인터페이스와 동일합니다.

기본 클래스는 여러 클래스에서 사용할 수있는 일부 상태 또는 동작의 실제 구현을 원할 때 유용합니다. 동일한 구현을 가진 동일한 필드와 메소드를 가진 여러 클래스가있는 경우이를 기본 클래스로 리팩토링 할 수 있습니다. 당신은 당신이 상속하는 방법에주의해야합니다. 기본 클래스의 모든 기존 필드 또는 메서드는 상속자가 특히 기본 클래스로 캐스팅 된 경우 의미가 있어야합니다.

인터페이스는 동일한 방식으로 일련의 클래스와 상호 작용해야 할 때 유용하지만 실제 구현을 예측하거나 신경 쓰지 않습니다. 예를 들어 Collection 인터페이스와 같은 것을 생각해보십시오. 주님은 누군가가 물건을 모으기로 결정할 수있는 무수한 방법들을 모두 알고 있습니다. 연결된 목록? 배열 목록? 스택? 열? 이 SearchAndReplace 메소드는 중요하지 않으며 Add, Remove 및 GetEnumerator를 호출 할 수있는 항목 만 전달하면됩니다.


1

나는이 질문에 대한 답을 직접보고 다른 사람들이 무엇을 말해야하는지 알지만, 이것에 대해 생각하는 경향이있는 세 가지를 말할 것입니다.

  1. 사람들은 인터페이스에 너무 많은 믿음을, 수업에 너무 작은 믿음을 두었습니다. 인터페이스는 기본적으로 기본 클래스에 물을 많이 주며 경량을 사용하면 과도한 상용구 코드와 같은 문제가 발생할 수 있습니다.

  2. 메서드를 재정의하고, 메서드를 호출 super.method()하고, 나머지 본문이 기본 클래스를 방해하지 않는 작업을 수행하도록하는 것은 전혀 문제가 없습니다. 이것은 Liskov 대체 원칙을 위반하지 않습니다 .

  3. 경험적으로 볼 때, 소프트웨어 엔지니어링에서이 견고하고 독단적 인 것은 일반적으로 어떤 것이 최선의 방법 일지라도 나쁜 생각입니다.

그래서 나는 소금 한알로 말한 것을 취할 것입니다.


5
People put way too much faith into interfaces, and too little faith into classes.이상한. 나는 그 반대가 사실이라고 생각하는 경향이 있습니다.
Simon Bergot

1
"이는 Liskov 대체 원칙을 위반하지 않습니다." 그러나 LSP를 위반하는 것은 매우 가깝습니다. 미안보다 안전합니다.
행복감

1

나는 이것을 향한 언어적이고 의미론적인 접근을 원합니다. 인터페이스가 없습니다 DRY. 그것을 구현하는 추상 클래스를 가지고 중앙 집중화의 메커니즘으로 사용될 수 있지만 DRY에는 적합하지 않습니다.

인터페이스는 계약입니다.

IAnimal인터페이스는 악어, 고양이, 개와 동물의 개념 사이에 전혀 또는 전혀없는 계약입니다. 본질적으로 말하는 것은 "동물이되고 싶다면 이러한 모든 속성과 특성을 가지고 있거나 더 이상 동물이 아닙니다"입니다.

다른 쪽의 상속은 재사용 메커니즘입니다. 이 경우 좋은 의미 론적 추론이 있으면 어떤 방법을 어디로 가야할지 결정하는 데 도움이 될 수 있다고 생각합니다. 예를 들어, 모든 동물이 잠을 자고 똑같이 잠을 자지 만 기본 클래스는 사용하지 않습니다. 먼저 인터페이스에 Sleep()메소드를 IAnimal동물이되는 것에 대한 강조 (의미 적)로 넣은 다음 고양이, 개, 악어에 대한 기본 추상 클래스를 프로그래밍 기술로 사용하여 스스로 반복하지 않습니다.


0

이것이 이것이 최고의 경험 법칙인지 잘 모르겠습니다. abstract기본 클래스에 메소드를 넣고 각 클래스가이를 구현하도록 할 수 있습니다. 모든 사람 mammal이 먹어야하므로 그렇게하는 것이 합리적입니다. 그러면 각각 mammal하나의 eat방법을 사용할 수 있습니다 . 나는 일반적으로 객체 간의 통신을 위해 인터페이스를 사용하거나 클래스를 무언가로 표시하는 마커로만 사용합니다. 인터페이스를 과도하게 사용하면 코드가 느슨해집니다.

또한 @Panzercrisis에 동의하면 일반적으로 일반적인 지침이되어야합니다. 돌로 써야 할 것이 아닙니다. 틀림없이 작동하지 않는 시간이있을 것입니다.


-1

인터페이스는 유형입니다. 그들은 구현을 제공하지 않습니다. 해당 유형을 처리하기위한 계약을 간단하게 정의합니다. 이 계약은 인터페이스를 구현하는 모든 클래스에서 충족해야합니다.

인터페이스와 추상 / 기본 클래스의 사용은 디자인 요구 사항에 따라 결정되어야합니다.

단일 클래스에는 인터페이스가 필요하지 않습니다.

함수 내에서 두 클래스가 상호 교환 가능한 경우 공통 인터페이스를 구현해야합니다. 동일하게 구현 된 모든 기능은 인터페이스를 구현하는 추상 클래스로 리팩토링 될 수 있습니다. 구별되는 기능이없는 경우 해당 시점에서 인터페이스가 중복 된 것으로 간주 될 수 있습니다. 그렇다면 두 클래스의 차이점은 무엇입니까?

더 많은 기능이 추가되고 계층 구조가 확장됨에 따라 추상 클래스를 유형으로 사용하는 것은 일반적으로 복잡합니다.

상속보다 구성을 선호하십시오-이 모든 것이 사라집니다.

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