"일반 데이터"클래스를 사용해야하는 이유가 있습니까?


43

레거시 코드에서 때때로 데이터 래퍼에 지나지 않는 클래스를 볼 수 있습니다. 같은 :

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

OO에 대한 이해는 클래스는 데이터의 구조 와 해당 데이터에서 작동하는 방법이라는 것입니다. 이것은이 유형의 객체를 배제하는 것처럼 보입니다. 나에게 그들은 structsOO의 목적을 넘어서는 것이 아닙니다 . 코드 냄새 일 수는 있지만 반드시 악하다고 생각하지는 않습니다.

그러한 물건이 필요한 경우가 있습니까? 이것이 자주 사용된다면, 디자인을 의심하게 만드는가?


1
이것은 귀하의 질문에 대한 답변은 아니지만 그럼에도 불구하고 관련이있는 것 같습니다 : stackoverflow.com/questions/36701/struct-like-objects-in-java
Adam Lear

2
나는 당신이 찾고있는 용어가 POD (Plain Old Data)라고 생각합니다.
Gaurav

9
구조화 된 프로그래밍의 전형적인 예입니다. 반드시 나쁘지는 않지만 객체 지향이 아닙니다.
Konrad Rudolph

1
이것이 스택 오버플로에 있지 않아야합니까?
Muad'Dib

7
@ Muad'Dib : 기술적으로 프로그래머에 관한 것 입니다 . 컴파일러는 평범한 오래된 데이터 구조를 사용하는지 신경 쓰지 않습니다. 당신의 CPU는 아마 그것을 즐깁니다 ( "캐시에서 신선한 데이터 냄새가 마음에 듭니다"). 그건 사람 에 너무 신경 "이 내 방법이 덜 순수하게 하는가?" 질문.
Shog9

답변:


67

분명히 악하지 않고 코드도 냄새가 없습니다. 데이터 컨테이너는 유효한 OO 시민입니다. 때로는 관련 정보를 함께 캡슐화하려고합니다. 다음과 같은 방법을 사용하는 것이 훨씬 좋습니다

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

...보다

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

클래스를 사용하면 DoStuffWithBottle의 모든 호출자를 수정하지 않고도 Bottle에 추가 매개 변수를 추가 할 수 있습니다. 또한 Bottle을 서브 클래 싱하고 필요한 경우 코드의 가독성과 구성을 더욱 향상시킬 수 있습니다.

예를 들어 데이터베이스 쿼리의 결과로 반환 될 수있는 일반 데이터 개체도 있습니다. 이 경우 이들의 용어는 "데이터 전송 개체"라고 생각합니다.

일부 언어에서는 다른 고려 사항도 있습니다. 예를 들어 C # 클래스와 구조체는 다르게 동작합니다. 구조체는 값 형식이고 클래스는 참조 형식이기 때문입니다.


25
아냐 DoStuffWith방법이어야한다 Bottle OOP에서 클래스 (그리고 아마도 너무 불변이어야 함). 위에서 작성한 것은 OO 언어에서 좋은 패턴이 아닙니다 (레거시 API와 인터페이스하지 않는 한). 그것은 이다 , 그러나, 비 OO 환경에서 유효한 디자인.
Konrad Rudolph

11
@Javier : 그렇다면 Java도 최고의 대답이 아닙니다. OO에 대한 Java의 강조는 압도적입니다. 언어는 기본적으로 다른 것에 능숙하지 않습니다.
Konrad Rudolph

11
@ JohnL : 물론 단순화했습니다. 그러나 일반적으로 OO의 객체는 데이터가 아닌 state를 캡슐화 합니다. 이것은 훌륭하지만 중요한 차이점입니다. OOP의 요점은 많은 양의 데이터를 가지고 있지 않다는 것입니다. 그것은되는 메시지를 보내는 새로운 상태를 만들 상태 사이. 나는 메쏘드가없는 객체와 메시지를 보내는 방법을 알지 못한다. (그리고 이것은 OOP의 원래 정의이므로 결함이 있다고 생각하지 않습니다.)
Konrad Rudolph

13
@ Konrad Rudolph : 이것이 메서드 내부에 주석을 명시 적으로 작성한 이유입니다. Bottle 인스턴스에 영향을 미치는 메소드가 해당 클래스에 들어가야한다는 데 동의합니다. 그러나 다른 객체가 Bottle의 정보를 기반으로 상태를 수정 해야하는 경우 내 디자인이 상당히 유효하다고 생각합니다.
Adam Lear

10
@ Konrad, doStuffWithBottle이 병 클래스에 들어가야한다는 것에 동의하지 않습니다. 병이 왜 스스로 물건을 만드는 법을 알아야합니까? doStuffWithBottle은 다른 무언가가 병으로 무언가를 할 것임을 나타냅니다 . 병에 병이 들어 있으면 결합이 빡빡합니다. 그러나 Bottle 클래스에 isFull () 메소드가있는 경우에는 완전히 적합합니다.
Nemi

25

데이터 클래스는 경우에 따라 유효합니다. DTO는 Anna Lear가 언급 한 좋은 예입니다. 그러나 일반적으로 메소드가 아직 발아되지 않은 클래스의 씨앗으로 간주해야합니다. 오래된 코드에서 많은 코드를 사용하는 경우 강력한 코드 냄새로 취급하십시오. 그것들은 종종 OO 프로그래밍으로 전환 한 적이없고 절차 적 프로그래밍의 표시 인 오래된 C / C ++ 프로그래머들에 의해 종종 사용됩니다. 게터와 세터에 항상 의존하는 경우 (또는 개인이 아닌 개인이 직접 액세스하는 경우)는 알기 전에 문제를 일으킬 수 있습니다. 의 정보가 필요한 외부 메소드의 예를 고려하십시오 Bottle.

다음 Bottle은 데이터 클래스입니다.

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

여기에 우리는 Bottle몇 가지 행동을 주었습니다 .)

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

첫 번째 방법은 Tell-Don't-Ask 원칙에 위배되며, Bottle바보 를 유지함으로써 병에 대한 암묵적인 지식 (예 : 하나의 fragle ( Cap)을 Bottle클래스 외부의 논리에 빠지게 함)을 알 수 있습니다. 습관적으로 게터에게 의지 할 때 이런 종류의 '누설'을 막으려면 발가락에 있어야합니다.

두 번째 방법은 Bottle작업을 수행하는 데 필요한 사항 만 묻고 , Bottle깨지기 쉬운 지 또는 주어진 크기보다 큰지를 결정합니다. 결과적으로 메소드와 병 구현 사이의 연결이 훨씬 느슨해집니다. 유쾌한 부작용은이 방법이 더 깨끗하고 표현력이 뛰어나다는 것입니다.

이러한 필드와 함께 클래스에 상주해야 할 논리를 쓰지 않고 객체의 많은 필드를 사용하는 경우는 거의 없습니다. 해당 논리가 무엇인지 파악한 다음 논리를 해당 위치로 이동하십시오.


1
이 답변에 투표가 없다고 믿을 수 없습니다. 이것은 간단한 예일 수 있지만 OO가 충분히 악용되면 서비스 클래스를 클래스에 캡슐화해야하는 수많은 기능을 포함하는 악몽으로 변하게됩니다.
Alb

"OO 로의 전환 (sic)을 한 적이없는 오래된 C / C ++ 프로그래머들에 의해"? C ++ 프로그래머는 보통 OO 언어이므로 꽤 OO입니다. 즉 C 대신 C ++를 사용하는 요점
nappyfalcon

7

이것이 당신이 필요로하는 종류라면 괜찮습니다.하지만 제발 제발 제발

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

같은 대신에

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

부디.


14
이것의 유일한 문제는 검증이 없다는 것입니다. 유효한 병이 무엇인지에 대한 논리는 병 클래스에 있어야합니다. 그러나 제안 된 구현을 사용하면 높이와 직경이 음수 인 병을 가질 수 있습니다. 개체를 사용할 때마다 유효성을 검사하지 않고 객체에 비즈니스 규칙을 적용 할 수있는 방법이 없습니다. 두 번째 방법을 사용하면 Bottle 객체가있는 경우 계약에 따라 유효한 Bottle 객체가되었으며 항상 유효한 Bottle 객체가되도록 보장 할 수 있습니다.
Thomas Owens

6
필드에 액세스하는 것과 동일한 구문을 사용하여 유효성 검사 논리를 사용하여 속성 접근자를 추가 할 수 있기 때문에 .NET이 속성보다 약간 우위에있는 영역 중 하나입니다. 클래스가 속성 값을 얻을 수는 있지만 설정할 수없는 속성을 정의 할 수도 있습니다.
JohnL

3
@ user9521 코드가 "나쁜"값으로 치명적인 오류를 일으키지 않을 것이라고 확신하는 경우 분석법을 따르십시오. 그러나 추가 유효성 검사가 필요하거나 지연로드를 사용하는 기능이나 데이터를 읽거나 쓸 때 다른 검사를 수행하는 경우 명시 적 게터 및 설정자를 사용하십시오. 개인적으로 변수를 비공개로 유지하고 일관성을 위해 게터와 세터를 사용하는 경향이 있습니다. 이런 식으로 유효성 검사 및 / 또는 다른 "고급"기술에 관계없이 모든 변수가 동일하게 취급됩니다.
Jonathan

2
생성자를 사용하면 클래스를 변경할 수 없게 만드는 것이 훨씬 쉽다는 장점이 있습니다. 다중 스레드 코드를 작성하려는 경우에 필수적입니다.
Fortyrunner

7
나는 가능할 때마다 필드를 최종적으로 만들 것입니다. IMHO 나는 기본적으로 기본 필드를 최종적으로 선호하고 변경 가능한 필드에 대한 키워드를 갖습니다. 예 : var
Peter Lawrey

6

@Anna가 말했듯이 분명히 악하지는 않습니다. 물론 작업 (메소드)을 클래스에 넣을 수는 있지만 원하는 경우에만 가능합니다 . 당신은하지 않습니다 에.

클래스가 추상화라는 아이디어와 함께 클래스에 연산을 넣어야한다는 아이디어에 대해 조금 이해하십시오. 실제로 이것은 프로그래머가

  1. 필요한 것보다 많은 클래스를 만듭니다 (중복 데이터 구조). 데이터 구조에 최소한 필요한 것보다 많은 구성 요소가 포함되어 있으면 정규화되지 않으므로 상태가 일치하지 않습니다. 즉, 변경 될 때 일관성을 유지하려면 둘 이상의 위치에서 변경해야합니다. 모든 조정 된 변경을 수행하지 않으면 일관성이없고 버그입니다.

  2. 파트 A가 수정되면 파트 B 및 C에 필요한 변경 사항을 전파하려고 시도 할 수 있도록 통지 메소드를 삽입 하여 문제점 1을 해결하십시오 . 이것이 get-and-set 액세서 메소드를 갖는 것이 권장되는 주요 이유입니다. 이 방법을 사용하는 것이 권장되므로 문제 1을 실례로하여 문제 1과 해결 방법 2를 더 많이 발생시키는 것으로 보입니다. 이로 인해 알림이 불완전하게 구현되어 버그가 발생할뿐만 아니라 런 어웨이 알림의 성능 저하 문제가 발생합니다. 이것들은 무한한 계산이 아니라 매우 긴 계산입니다.

이러한 개념은 일반적으로 이러한 문제로 가득 찬 수백만 라인의 몬스터 응용 프로그램 내에서 일할 필요가 없었던 교사가 좋은 것으로 가르칩니다.

내가하려고하는 것은 다음과 같습니다.

  1. 데이터가 변경 될 때 일관성이없는 상태에 들어갈 가능성을 최소화하기 위해 가능한 한 적은 코드 포인트에서 데이터가 수행되도록 데이터를 가능한 한 정규화 상태로 유지하십시오.

  2. 데이터를 정규화하지 않아야하고 중복성을 피할 수없는 경우 일관성을 유지하기 위해 알림을 사용하지 마십시오. 오히려 일시적인 불일치를 용납하십시오. 이를 수행하는 프로세스에 의해 데이터를 주기적으로 스윕하여 불일치를 해결하십시오. 이렇게하면 알림을 받기 쉬운 성능 및 정확성 문제를 피하면서 일관성을 유지해야 할 책임이 집중됩니다. 그 결과 코드가 훨씬 작고 오류가 없으며 효율적입니다.


3

이러한 종류의 클래스는 다음과 같은 이유로 중간 규모 / 큰 응용 프로그램을 처리 할 때 매우 유용합니다.

  • 일부 테스트 사례를 작성하고 데이터가 일관성을 유지하는 것은 매우 쉽습니다.
  • 해당 정보와 관련된 모든 종류의 동작을 보유하므로 데이터 버그 추적 시간이 단축됩니다.
  • 그것들을 사용하면 메소드 인수를 가볍게 유지해야합니다.
  • ORM을 사용할 때이 클래스는 유연성과 일관성을 제공합니다. 이미 클래스에있는 간단한 정보를 기반으로 계산 된 복잡한 속성을 추가하면 간단한 방법 하나를 작성하는 것이 좋습니다. 이는 데이터베이스를 확인하고 모든 데이터베이스가 새로운 수정 사항으로 패치되는지 확인해야하는 매우 민첩하고 생산적입니다.

요약하면, 내 경험상 그들은 일반적으로 성가신 것보다 더 유용합니다.


3

게임 디자인을 사용하면 1000의 함수 호출 오버 헤드와 이벤트 리스너는 때때로 데이터 만 저장하는 클래스를 보유하고 로직을 수행하기 위해 모든 데이터 만 클래스를 반복하는 다른 클래스를 가질 수 있습니다.


3

안나 리어와

분명히 악하지 않고 코드도 냄새가 없습니다. 데이터 컨테이너는 유효한 OO 시민입니다. 때로는 관련 정보를 함께 캡슐화하려고합니다. 다음과 같은 방법을 사용하는 것이 훨씬 좋습니다.

때때로 사람들은 이런 종류의 프로그래밍이 완벽하다는 것을 명백하게하는 1999 Java 코딩 규칙을 읽는 것을 잊어 버립니다. 실제로 피하지 않으면 코드에서 냄새가납니다! (너무 많은 게터 / 세터)

Java Code Conventions 1999 : 적절한 퍼블릭 인스턴스 변수의 한 예는 클래스가 기본적으로 데이터 구조이며 동작이없는 경우입니다. 다시 말해, 클래스 대신 구조체를 사용했다면 (Java가 구조체를 지원하는 경우), 클래스의 인스턴스 변수를 공개하는 것이 적절합니다. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177

올바르게 사용하면 POJ (일반적인 데이터 구조)가 POJO보다 낫습니다. POJO가 EJB보다 낫습니다.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures


3

Structs는 Java에서도 사용할 수 있습니다. 다음 두 가지에 해당하는 경우에만 사용해야합니다.

  • 동작이없는 데이터를 집계하면됩니다 (예 : 매개 변수로 전달).
  • 집계 데이터에 어떤 종류의 값이 있는지는 중요하지 않습니다.

이 경우 필드를 공개하고 게터 / 세터를 건너 뛰어야합니다. 어쨌든 게터와 세터는 어수선하고 자바는 유용한 언어와 같은 속성을 가지고 있지 않기 때문에 바보입니다. 구조체와 같은 객체에는 어쨌든 메서드가 없어야하므로 공공 필드가 가장 적합합니다.

그러나 그 중 하나가 적용되지 않으면 실제 수업을 처리하는 것입니다. 즉, 모든 필드는 비공개이어야합니다. (더 접근하기 쉬운 범위의 필드가 절대적으로 필요한 경우 게터 / 세터를 사용하십시오.)

가정 구조에 동작이 있는지 확인하려면 필드가 사용되는시기를 확인하십시오. 그것을 위반하는 것 같으면 묻지 말고 그 행동을 수업으로 옮겨야합니다.

일부 데이터가 변경되지 않아야하는 경우 모든 필드를 최종적으로 만들어야합니다. 클래스를 불변으로 만드는 것을 고려할 수 있습니다 . 데이터의 유효성을 검증해야하는 경우 setter 및 생성자에서 유효성 검증을 제공하십시오. 유용한 유용한 방법은 개인 설정자를 정의하고 해당 설정 자만 사용하여 클래스 내에서 필드를 수정하는 것입니다.

병 예제는 두 테스트에 모두 실패합니다. 다음과 같은 코드를 만들 수 있습니다.

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

대신에

double volume = bottle.calculateVolumeAsCylinder();

높이와 직경을 변경 한 경우 동일한 병입니까? 아마 아닙니다. 그것들은 최종적이어야합니다. 직경이 음수 값입니까? 병이 너비보다 커야합니까? 캡이 null 일 수 있습니까? 아니? 이것을 어떻게 확인합니까? 내담자가 멍청하거나 사악하다고 가정하자. ( 차이를 말하는 것은 불가능합니다. )이 값을 확인해야합니다.

새 병 클래스는 다음과 같습니다.

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}

0

IMHO는 종종 객체 지향 시스템에 이와 같은 클래스 가 충분 하지 않습니다 . 신중하게 자격을 갖추어야합니다.

물론 데이터 필드의 범위와 가시성이 넓은 경우 코드베이스에 이러한 데이터를 조작하는 수백 또는 수천 개의 장소가있는 경우에는 바람직하지 않습니다. 그것은 불변을 유지하는 데 어려움과 어려움을 요구하고 있습니다. 그러나 동시에 이것이 전체 코드베이스의 모든 단일 클래스가 정보 숨기기로부터 혜택을 얻는다는 것을 의미하지는 않습니다.

그러나 이러한 데이터 필드의 범위가 매우 좁은 경우가 많이 있습니다. 매우 간단한 예는 Node데이터 구조 의 개인 클래스입니다. Node단순히 원시 데이터로 구성 될 수있는 경우 진행되는 객체 상호 작용의 수를 줄임으로써 종종 코드를 크게 단순화 할 수 있습니다 . 대체 버전은 단순히 Tree->NodeNode->Tree는 반대로 양방향 커플 링이 필요할 수 있기 때문에 디커플링 메커니즘으로 사용됩니다 Tree->Node Data.

더 복잡한 예는 게임 엔진에서 자주 사용되는 엔터티 구성 요소 시스템입니다. 이 경우 구성 요소는 종종 사용자가 표시 한 것과 같은 원시 데이터 및 클래스 일뿐입니다. 그러나 일반적으로 특정 유형의 구성 요소에 액세스 할 수있는 시스템이 하나 또는 두 개뿐이므로 범위 / 가시성이 제한되는 경향이 있습니다. 결과적으로 이러한 시스템에서 불변량을 유지하는 것이 여전히 쉬운 경향이 있으며, 이러한 시스템은 object->object상호 작용 이 거의 없으므로 조류의 눈높이에서 일어나는 일을 이해하기가 매우 쉽습니다.

이러한 경우 상호 작용이 진행되는 한 다음과 같은 결과를 얻을 수 있습니다 (이 다이어그램은 커플 링 다이어그램이 아래 두 번째 이미지에 대한 추상 인터페이스를 포함 할 수 있으므로 커플 링이 아닌 상호 작용을 나타냅니다).

여기에 이미지 설명을 입력하십시오

... 이것과 반대로 :

여기에 이미지 설명을 입력하십시오

... 이전 유형의 시스템은 종속성이 실제로 데이터를 향하고 있음에도 불구하고 유지 관리가 훨씬 쉽고 정확성 측면에서 추론하는 경향이 있습니다. 많은 것들이 서로 상호 작용하는 객체 대신 원시 데이터로 변환되어 매우 복잡한 상호 작용 그래프를 형성하기 때문에 주로 결합이 훨씬 적습니다.


-1

OOP 세계에는 훨씬 더 이상한 생물이 존재합니다. 나는 한 번 추상적 인 클래스를 만들었고 아무것도 포함하지 않았습니다. 다른 두 클래스의 공통 부모가되기 위해 포트 클래스입니다.

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

따라서 SceneMember는 기본 클래스이며 장면에 나타나야하는 모든 작업 (추가, 삭제, 가져 오기 ID 등)을 수행합니다. 메시지는 포트 간 연결이며 자체 수명이 있습니다. InputPort 및 OutputPort에는 고유 한 i / o 특정 기능이 있습니다.

포트가 비어 있습니다. 예를 들어, 포트 목록을 위해 InputPort와 OutputPort를 그룹화하는 데 사용됩니다. SceneMember에 멤버 역할을하는 모든 항목이 포함되어 있고 InputPort 및 OutputPort에 포트 지정 태스크가 포함되어 있으므로 비어 있습니다. InputPort와 OutputPort는 서로 다르기 때문에 공통적 인 기능 (SceneMember 제외)이 없습니다. 포트가 비어 있습니다. 그러나 합법적입니다.

어쩌면 반 패턴 일 수도 있지만 좋아합니다.

( "아내"와 "남편"모두에 사용되는 "mate"라는 단어와 같습니다. 구체적인 사람에게 "mate"라는 단어를 사용하지 마십시오. 결혼 여부와 관계없이 "누군가"를 대신 사용하지만 여전히 존재하며 법률 텍스트와 같이 드문 추상적 인 상황에서 사용됩니다.)


1
포트가 왜 SceneMember를 확장해야합니까? SceneMembers에서 작동 할 포트 클래스를 작성해보십시오.
Michael K

1
표준 마커 인터페이스 패턴을 사용하지 않는 이유는 무엇입니까? 빈 추상 기본 클래스와 기본적으로 동일하지만 훨씬 일반적인 관용구입니다.
TMN

1
@Michael : 이론적 인 이유만으로 향후 사용을 위해 Port를 예약했지만이 미래는 아직 도착하지 않았으며 어쩌면 결코 없을 것입니다. 나는 그들의 이름과 달리 그들이 공통점이 없다는 것을 몰랐다. 나는 빈 클래스로 인해 손실을 입은 모든 사람들에 대해 보상을 할 것입니다 ...
ern0

1
@TMN : SceneMember (파생) 유형에는 getMemberType () 메소드가 있으며 SceneMember 객체의 하위 세트에 대해 Scene을 스캔하는 경우가 몇 가지 있습니다.
ern0

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