나중에 필요할 수 있으므로 지금 중복 코드를 추가해야합니까?


114

바르게 또는 잘못,이 중복 코드 / I 수표에 추가하는 것을 의미하는 경우에도, 현재 나는 항상 가능한 내 코드와 같은 강력한 만들려고한다 신념이야 알고 지금 어떤 소용이되지 않습니다,하지만 그들은 줄 아래로 x 년이 될 수 있습니다.

예를 들어, 현재이 코드 조각이있는 모바일 응용 프로그램을 만들고 있습니다.

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
    }

    //2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
    if(rows.Count == 0)
    {
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

섹션 2를 구체적으로 살펴보면 섹션 1이 참이면 섹션 2도 참이라는 것을 알고 있습니다. 왜 섹션 1이 거짓이고 섹션 2가 참인지에 대한 이유를 생각할 수 없으므로 두 번째 if진술이 중복됩니다.

그러나 앞으로이 두 번째 if진술이 실제로 필요한 경우와 알려진 이유가있을 수 있습니다.

어떤 사람들은 처음에 이것을보고 미래를 염두에두고 프로그래밍하고 있다고 생각할 수 있습니다. 그러나 나는 이런 종류의 코드가 나로부터 "숨겨진"버그를 가지고있는 몇 가지 사례를 알고있다. 함수 xyzabc실제로 수행해야 할 때 함수 가 수행 되는 이유를 파악하는 데 시간이 더 오래 걸렸습니다 def.

다른 한편으로, 이러한 종류의 코드로 인해 새로운 동작으로 코드를 훨씬 쉽게 향상시킬 수있는 수많은 사례가 있습니다. 돌아가서 모든 관련 검사를 수행 할 필요가 없기 때문에.

이런 종류의 코드에 대한 일반적인 규칙 규칙 이 있습니까? (이것이 좋거나 나쁜 실천으로 간주되는지 듣고 싶습니다.)

주의 : 이것은 이 질문 과 비슷한 것으로 간주 될 수 있지만 , 그 질문과 달리 마감일이 없다고 가정 한 답변을 원합니다.

TLDR : 향후 잠재적으로 더 강력 해 지도록 중복 코드를 추가해야합니까?



95
소프트웨어 개발에서 결코 일어나지 않아야 할 일이 항상 발생합니다.
로마 라이너

34
당신 if(rows.Count == 0)이 결코 일어나지 않을 것이라는 것을 안다면 , 당신은 예외가 발생할 때 예외를 제기 할 수 있습니다-그리고 당신의 가정이 왜 잘못되었는지 점검하십시오.
knut

9
질문과 관련이 없지만 코드에 버그가 있다고 생각합니다. 행이 null이면 새 목록이 만들어지고 (추측합니다) 버려집니다. 그러나 행이 널이 아닌 경우 기존 목록이 변경됩니다. 더 나은 디자인은 클라이언트가 비어 있거나 비어 있지 않은 목록을 전달하도록하는 것입니다.
Theodore Norvell 2016 년

9
rows널이 아닌가? 컬렉션이 null 인 이유는 적어도 .NET에는 없습니다. 비어 있지만 null은 아닙니다 . rows호출자가 논리에 결함이 있음을 의미하기 때문에 null 인 경우 예외 가 발생 합니다.
Kyralessa

답변:


176

연습으로, 먼저 논리를 확인합시다. 앞으로 살펴 보 겠지만 논리적 문제보다 더 큰 문제가 있습니다.

첫 번째 조건 A와 두 번째 조건 B를 호출하십시오.

먼저 말합니다 :

섹션 2를 구체적으로 살펴보면 섹션 1이 참이면 섹션 2도 참이라는 것을 알고 있습니다.

즉 : A는 B를 의미하거나보다 기본적인 용어를 의미합니다. (NOT A) OR B

그리고:

섹션 1이 거짓이고 섹션 2가 참으로 평가되는 이유에 대해서는 생각할 수 없으므로 두 번째 if 문이 중복됩니다.

즉 : NOT((NOT A) AND B). (NOT B) OR AB가 A를 암시하는 데 데몬의 법칙을 적용하십시오 .

따라서 두 진술이 모두 사실이면 A는 B를 의미하고 B는 A를 의미합니다. 이는 이들이 동일해야 함을 의미합니다.

따라서 검사는 중복됩니다. 프로그램을 통해 네 개의 코드 경로가있는 것처럼 보이지만 실제로는 두 개만 있습니다.

이제 문제는 코드를 작성하는 방법입니다. 실제 질문은 : 방법의 명시된 계약은 무엇입니까? 조건이 중복되는지 여부에 대한 질문은 붉은 청어입니다. 실제 질문은 "현명한 계약을 설계했으며 내 방법이 해당 계약을 명확하게 구현하고 있습니까?"입니다.

선언을 보자.

public static CalendarRow AssignAppointmentToRow(
    Appointment app,    
    List<CalendarRow> rows)

공개이므로 임의 호출자의 잘못된 데이터에 강력해야합니다.

값을 반환하므로 부작용이 아닌 반환 값에 유용해야합니다.

그러나이 방법의 이름은 동사이므로 부작용에 유용합니다.

list 매개 변수의 계약은 다음과 같습니다.

  • 널리스트는 정상입니다
  • 하나 이상의 요소가있는 목록은 괜찮습니다.
  • 요소가없는 목록이 잘못되어 가능하지 않아야합니다.

이 계약은 미쳤다 . 이것에 대한 문서를 작성한다고 상상해보십시오! 테스트 케이스 작성을 상상해보십시오!

내 충고 : 다시 시작하십시오. 이 API에는 사탕 기계 인터페이스가 있습니다. (이 표현은 가격과 선택이 두 자리 숫자 인 Microsoft의 사탕 기계에 대한 오래된 이야기에서 나온 것이며 항목 75의 가격 인 "85"를 입력하는 것이 매우 쉽습니다. 재미있는 사실 : 예, 실제로 Microsoft의 자동 판매기에서 껌을 꺼내려고 할 때 실수로 실수했습니다!)

현명한 계약을 설계하는 방법은 다음과 같습니다.

메서드를 부작용이나 반환 값에 유용하게 사용하십시오.

리스트 같은 가변 타입을 입력으로 받아들이지 마십시오. 일련의 정보가 필요하면 IEnumerable을 사용하십시오. 순서 만 읽으십시오. 아니라면 전달 된 컬렉션에 기록하지 않는 매우 이 방법의 계약 것이 분명. IEnumerable을 사용하면 호출자에게 컬렉션을 변경하지 않겠다는 메시지를 보냅니다.

널을 허용하지 마십시오. null 시퀀스는 혐오입니다. 의미가있는 경우 호출자가 빈 시퀀스를 전달하도록 요구하십시오.

발신자가 계약을 위반하면 즉시 비즈니스를 의미하고 프로덕션이 아닌 테스트에서 버그를 잡을 수 있도록 충돌이 발생합니다.

먼저 가능한 한 합리적으로 계약을 설계 한 다음 계약을 명확하게 구현하십시오. 이것이 미래 설계를 보장하는 방법입니다.

지금은 특정 사례에 대해서만 이야기했으며 일반적인 질문을했습니다. 다음은 일반적인 추가 조언입니다.

  • 개발자로서 추론 할 수 있지만 컴파일러로는 추론 할 수없는 사실이있는 경우 어설 션을 사용하여 해당 사실을 문서화하십시오. 미래의 다른 개발자 나 동료 중 한 사람과 같은 다른 개발자가이 가정을 위반하면 어설 션이 알려줄 것입니다.

  • 테스트 범위 도구를 받으십시오. 테스트가 모든 코드 줄을 포함하는지 확인하십시오. 발견되지 않은 코드가 있으면 테스트가 누락되었거나 불완전한 코드가있는 것입니다. 죽은 코드는 일반적으로 죽지 않기 때문에 놀랍도록 위험합니다! 몇 년 전의 믿을 수 없을 정도로 끔찍한 Apple "고장 실패"보안 결함은 즉시 떠 오릅니다.

  • 정적 분석 도구를 받으십시오. 도대체 몇 가지를 얻으십시오; 모든 도구에는 고유의 전문성이 있으며 다른 도구의 상위 집합은 없습니다. 도달 할 수 없거나 중복 된 코드가 있음을 알려줄 때주의하십시오. 다시 말하지만, 버그 일 가능성이 높습니다.

내가 말하는 것처럼 들리면 : 첫째, 코드를 잘 디자인하고, 둘째로 코드를 테스트하여 오늘날 올바른지 확인하십시오. 이런 일을하면 미래를 훨씬 쉽게 다룰 수 있습니다. 미래에 대한 가장 어려운 부분은 사람들이 과거에 쓴 모든 버그가 많은 사탕 기계 코드를 다루는 것입니다. 지금 바로 구매하면 향후 비용이 절감됩니다.


24
나는 이전에 이와 같은 방법에 대해 실제로 생각한 적이 없으며, 지금 생각하면 항상 모든 사건을 다루려고 노력하는 것처럼 보입니다. 실제로 내 방법을 호출하는 사람 / 방법이 필요한 것을 전달하지 못하면 실수를 해결하려고 시도하지 않습니다 (실제로 그들이 의도 한 바를 모르는 경우). 이것에 감사합니다. 귀중한 교훈을 얻었습니다!
KidCode

4
메소드가 값을 리턴한다는 것이 부작용에도 유용하지 않다는 것을 의미하지는 않습니다. 동시 코드에서 두 함수를 모두 반환하는 함수의 기능은 종종 중요하며 ( CompareExchange그러한 능력없이 상상할 수 있습니다!) 심지어 비 동시 시나리오에서도 "존재하지 않는 경우 레코드 추가 및 전달 된 함수 중 하나를 반환 " 레코드 또는 존재하는 레코드 "는 부작용과 반환 값을 모두 사용하지 않는 접근 방식보다 더 편리 할 수 ​​있습니다.
supercat

5
@KidCode 그래, 에릭은 정말 복잡한 주제까지도 명확하게 설명하는 데 능숙하다 :)
Mason Wheeler

3
@supercat 물론이지만, 추론하기가 더 어렵습니다. 첫째, 아마도 전역 상태를 수정하지 않는 것을보고 동시성 문제와 상태 손상을 피하는 것이 좋습니다. 이것이 합리적이지 않은 경우에도 두 가지를 분리해야합니다. 즉, 동시성이 문제가되는 곳 (따라서 매우 위험한 것으로 간주 됨)과 처리되는 곳이 분명하게됩니다. 이것은 오리지널 OOP 논문의 핵심 아이디어 중 하나였으며 배우의 유용성의 핵심입니다. 종교적 규칙은 없습니다. 두 가지를 구분하는 것이 좋습니다. 보통 그렇습니다.
Luaan

5
이것은 거의 모든 사람에게 매우 유용한 게시물입니다!
Adrian Buzea

89

위에서 보여준 코드에서 수행하는 작업은 방어 코딩 만큼 미래의 증거가 아닙니다 .

if진술은 다른 것들을 테스트합니다. 둘 다 필요에 따라 적절한 테스트입니다.

섹션 1은 null객체를 테스트하고 수정 합니다. 참고 : 목록을 만들면 하위 항목 (예 :)이 만들어지지 않습니다 CalendarRow.

섹션 2는 사용자 및 / 또는 구현 오류를 테스트하고 수정합니다. 당신이 있기 때문에 그냥은 List<CalendarRow>당신이 목록에있는 항목이 있음을 의미하지 않는다. 사용자와 구현자는 이해하기 쉬운 지 여부에 관계없이 상상할 수없는 것을 수행합니다.


1
실제로 나는 입력을 의미하기 위해 '멍청한 사용자 트릭'을 사용하고 있습니다. 예, 입력을 신뢰해서는 안됩니다. 수업 밖에서도 가능합니다. 확인! 이것이 미래의 문제 일 뿐이라고 생각하면 오늘 해킹 해 드리겠습니다.
candied_orange

1
@CandiedOrange는 그 의도 였지만 표현은 시도 된 유머를 전달하지 못했습니다. 나는 문구를 바꾸었다.
Adam Zuckerman

3
구현 오류 / 나쁜 데이터 인 경우 빠른 질문은 오류를 해결하는 대신 충돌하지 않아야합니까?
KidCode

4
@KidCode "오류 수정"을 복구하려고 할 때마다 두 가지 작업을 수행하고 알려진 정상 상태로 돌아가고 값 비싼 입력을 조용히 잃지 않아야합니다. 이 경우 그 규칙에 따라 다음과 같은 질문이 제기됩니다.
candied_orange

7
이 답변에 강력하게 동의하지 않습니다. 함수가 유효하지 않은 입력을 받으면 프로그램에 버그가 있음을 의미합니다. 올바른 접근 방식은 유효하지 않은 입력에 대해 예외를 발생시켜 문제를 발견하고 버그를 수정하는 것입니다. 당신이 묘사하는 접근법은 버그를 숨기고 더 교활하고 추적하기 어렵게 만듭니다. 방어 적 코딩은 입력을 자동으로 신뢰하지 않고 유효성을 검사하지만 유효하지 않거나 예기치 않은 입력을 "수정"하기 위해 무작위로 추측해야한다는 의미는 아닙니다.
JacquesB

35

나는이 질문이 기본적으로 맛이 없다고 생각한다. 예, 강력한 코드를 작성하는 것이 좋습니다. 그러나 예제의 코드는 KISS 원칙을 약간 위반하는 것입니다 ( "미래 증명"코드가 많을 수 있음).

나는 개인적으로 미래를 위해 코드를 방탄으로 만들지 않을 것입니다. 나는 미래를 모른다. 그래서 미래에 도착했을 때 그러한 "미래의 방탄"코드는 비참하게 실패 할 운명이다.

대신 다른 접근 방식을 선호합니다 assert(). 매크로 또는 유사한 기능을 사용하여 명시 적으로 가정한다고 가정합니다 . 그렇게하면 미래가 다가올 때 더 이상 가정이없는 곳을 정확하게 알 수 있습니다.


4
나는 미래에 무엇이 있는지 알지 못하는 것에 대한 당신의 요점을 좋아합니다. 지금 내가하고있는 일은 문제가 될 수있는 것을 추측 한 다음 솔루션을 다시 추측하는 것입니다.
KidCode

3
@KidCode : 좋은 관찰. 여기 당신의 생각은 실제로 당신이 받아 들인 것을 포함하여 여기의 많은 대답보다 훨씬 똑똑합니다.
JacquesB

1
나는이 답변을 좋아한다. 향후 독자가 점검이 필요한 이유를 쉽게 이해할 수 있도록 코드를 최소화하십시오. 장래 독자가 불필요하게 보이는 물건에 대한 수표를 보면 수표가 왜 있는지 이해하려고 노력하는 데 시간을 낭비 할 수 있습니다. 미래의 인간은이 클래스를 사용하는 다른 사람이 아니라이 클래스를 수정할 수 있습니다. 또한 디버깅 할 수없는 코드를 작성하지 마십시오 . 현재 발생할 수없는 사례를 처리하려고하는 경우입니다. (주 프로그램에서 제공하지 않는 코드 경로를 사용하는 단위 테스트를 작성하지 않는 한)
Peter Cordes

23

당신이 생각하고 싶은 또 다른 원칙은 빨리 실패 한다는 생각입니다 . 아이디어는 프로그램에서 문제가 발생했을 때, 최소한 개발하기 전에 프로그램을 개발하는 동안 프로그램을 즉시 중지하고 싶다는 것입니다. 이 원칙에 따라 가정을 확실하게 유지하기 위해 많은 검사를 작성하려고하지만 가정을 위반할 때마다 프로그램이 중지되도록하는 것이 중요합니다.

대담하게 말하면, 프로그램에 작은 오류가 있더라도 시청하는 동안 오류가 완전히 발생하기를 바랍니다!

이것은 직관적이지 않은 것처럼 들릴 수 있지만 일상적인 개발 과정에서 가능한 빨리 버그를 발견 할 수 있습니다. 코드를 작성하고 있는데 코드가 완성되었다고 생각하지만 테스트 할 때 충돌이 발생하더라도 아직 완료되지 않았다는 데는 의문의 여지가 없습니다. 또한 대부분의 프로그래밍 언어는 오류 후 최선을 다하지 않고 프로그램이 완전히 충돌 할 때 사용하기 가장 쉬운 뛰어난 디버깅 도구를 제공합니다. 가장 큰 가장 일반적인 예는 처리되지 않은 예외를 발생시켜 프로그램을 중단하면 예외 메시지가 실패한 코드 줄과 프로그램이 수행 한 코드 경로를 포함하여 버그에 대한 엄청난 양의 정보를 알려줍니다. 해당 코드 줄 (스택 추적)으로가는 길.

더 많은 생각을하려면 다음과 같은 짧은 에세이를 읽으십시오 : 프로그램을 올바른 위치에 두지 마십시오 .


때로는 무언가 잘못 된 후에도 프로그램이 계속 실행되기를 원하기 때문에 작성중인 검사가있을 수 있기 때문에 이것은 당신과 관련이 있습니다. 예를 들어, 피보나치 시퀀스의 간단한 구현을 고려하십시오.

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

이것은 작동하지만 누군가 함수에 음수를 전달하면 어떻게됩니까? 그때 작동하지 않습니다! 따라서 함수가 음이 아닌 입력으로 호출되는지 확인하는 것이 좋습니다.

다음과 같이 함수를 작성하는 것이 좋습니다.

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        n = 1; // Replace the negative input with an input that will work
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

그러나 이렇게하면 나중에 실수로 음의 입력으로 피보나치 함수를 호출 할 수 있습니다. 더 나쁜 것은, 프로그램이 계속 실행되지만 문제가 발생한 위치에 대한 단서를 제공하지 않으면 서 무의미한 결과를 생성하기 시작할 것입니다. 가장 어려운 유형의 버그를 수정했습니다.

대신 다음과 같이 수표를 작성하는 것이 좋습니다.

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        throw new ArgumentException("Can't have negative inputs to Fibonacci");
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

이제 실수로 음의 입력으로 피보나치 함수를 호출하면 프로그램이 즉시 중지되고 문제가 있음을 알려줍니다. 또한 스택 추적을 제공하면 프로그램에서 프로그램의 어느 부분이 피보나치 기능을 잘못 실행하려고했는지 알려주므로 문제를 디버깅하기위한 훌륭한 시작점이됩니다.


1
C #에 유효하지 않은 인수 또는 범위를 벗어난 인수를 나타내는 특정 종류의 예외가 있습니까?
JDługosz

@ JDługosz p! C #에는 ArgumentException 이 있고 Java에는 IllegalArgumentException이 있습니다.
Kevin

질문은 C #을 사용하고있었습니다. 여기 C ++ (요약 링크) 완전성에 대해.
JDługosz

3
"가장 가까운 안전 상태로 빠르게 실패"가 더 합리적입니다. 예기치 않은 상황이 발생했을 때 응용 프로그램이 충돌하도록 응용 프로그램을 만들면 사용자 데이터가 손실 될 위험이 있습니다. 사용자 데이터가 위험에 처한 장소는 "최종"예외 처리에있어 중요한 포인트입니다 (항상 아무것도 할 수없는 경우가 있습니다-궁극적 충돌). 디버그에서 충돌을 일으키는 것만으로도 다른 웜 캔을 열 수 있습니다. 어쨌든 사용자에게 배포하는 대상을 테스트해야합니다. 이제 테스트 시간의 절반을 사용자가 절대 볼 수없는 버전으로 보내고 있습니다.
Luaan

2
@Luaan : 예제 함수의 책임은 아닙니다.
whatsisname

11

중복 코드를 추가해야합니까? 아니.

그러나 설명하는 내용은 중복 코드가 아닙니다 .

설명하는 것은 함수의 전제 조건을 위반하는 코드를 호출하지 않도록 방어 적으로 프로그래밍하는 것입니다. 이 작업을 수행하든 아니면 단순히 사용자가 문서를 읽고 이러한 위반을 피할 수 있도록하는 것은 전적으로 주관적입니다.

개인적으로, 나는이 방법론에 대해 큰 신자이지만, 모든 것과 마찬가지로 조심해야합니다. 예를 들어 C ++을 사용하십시오 std::vector::operator[]. VS의 디버그 모드 구현을 잠시 제쳐두고이 함수는 범위 검사를 수행하지 않습니다. 존재하지 않는 요소를 요청하면 결과가 정의되지 않습니다. 유효한 벡터 인덱스를 제공하는 것은 사용자의 몫입니다. 이것은 의도적 인 것입니다. 콜 사이트에 추가하여 범위 검사를 "선택"할 수 있지만 operator[]구현이 수행하는 경우 "선택 해제"할 수 없습니다. 상당히 낮은 수준의 기능으로 이것은 의미가 있습니다.

그러나 AddEmployee(string name)일부 고급 인터페이스에 대한 함수를 작성하는 경우 빈 name선언 을 제공 하고이 사전 조건이 함수 선언 바로 위에 문서화되어 있으면이 함수가 최소한 예외를 throw 할 것으로 기대합니다 . 오늘날이 기능에 대해 비위생 사용자 입력을 제공하지 않을 수도 있지만 이러한 방식으로 "안전"하게하면 향후 발생할 수있는 전제 조건 위반을 쉽게 진단 할 수 있습니다. 버그를 감지합니다. 이것은 중복성이 아닙니다. 부지런합니다.

일반적인 규칙 (일반적으로 규칙을 피하려고 노력하지만)을 생각해 내야한다면 다음 중 하나를 만족시키는 기능이라고 말하고 싶습니다.

  • 최고급 언어 (예 : C가 아닌 JavaScript)로 생활
  • 인터페이스 경계에 앉아
  • 성능이 중요하지 않습니다
  • 사용자 입력을 직접 받아들입니다

… 방어 프로그래밍의 혜택을 누릴 수 있습니다. 다른 경우에도 assert테스트 중에 발생하지만 릴리스 빌드에서 비활성화 된 이온을 작성하여 버그를 찾는 기능을 추가로 향상시킬 수 있습니다.

이 주제는 Wikipedia ( https://en.wikipedia.org/wiki/Defensive_programming ) 에서 자세히 설명합니다 .


9

10 가지 프로그래밍 계명 중 두 가지가 여기에 관련됩니다.

  • 입력이 정확하다고 가정하지 않아야한다

  • 나중에 사용하기 위해 코드를 작성하지 마십시오

여기서 널 확인은 "나중에 사용하기위한 코드 작성"이 아닙니다. 나중에 사용할 수 있도록 코드를 만드는 것은 인터페이스가 "언젠가"유용 할 것이라고 생각하기 때문에 인터페이스를 추가하는 것과 같습니다. 다시 말해, 계명은 지금 당장 필요하지 않은 한 추상화 계층을 추가하지 않는 것입니다.

null 검사는 향후 사용과 아무 관련이 없습니다. 그것은 계명 # 1과 관련이 있습니다 : 입력이 정확하다고 가정하지 마십시오. 함수가 입력의 일부 하위 집합을 수신한다고 가정하지 마십시오. 함수는 입력이 얼마나 허풍스럽고 혼란스러워도 논리적으로 반응해야합니다.


5
이 프로그래밍 계명은 어디에 있습니까? 당신은 링크가 있습니까? 나는 그 계명 중 두 번째 계명을 구독하는 여러 프로그램에서 일하지 않았고 그렇지 않은 사람들도 있었기 때문에 궁금합니다. 계명에 가입 한 사람들은 계명에 대한 직관적 인 논리에도 불구하고 Thou shall not make code for future use유지 보수성 문제에 더 빨리 빠졌다. 실제 코딩에서 기능은 기능 목록과 마감일을 제어하는 ​​코드에서만 효과적이며, 미래에 보이는 코드가 필요하지 않은지 확인하십시오.
Cort Ammon

1
사소하게 입증 가능 : 향후 사용 확률과 "미래 사용"코드의 예상 값을 추정 할 수 있고이 두 제품의 결과가 "미래 사용"코드 추가 비용보다 큰 경우 통계적으로 최적입니다. 코드를 추가하십시오. 개발자 (또는 관리자)가 자신의 평가 기술이 원하는만큼 신뢰할 수 없음을 인정해야하는 상황에서 계명이 나타날 것이라고 생각합니다. 따라서 방어 조치는 미래의 과제를 전혀 평가하지 않기로 결정하는 것입니다.
Cort Ammon

2
@CortAmmon 프로그래밍에는 종교적인 계명이 없습니다. "모범 사례"는 문맥 상 이해가 가능하며 추론없이 "모범 사례"를 배우면 적응할 수 없습니다. 나는 YAGNI가 매우 유용하다는 것을 알았지 만 나중에 확장 점을 추가하는 것이 비싼 장소에 대해 생각하지 않는다는 것을 의미하지는 않습니다. 단지 간단한 경우에 대해 미리 생각할 필요가 없다는 것을 의미합니다. 물론, 코드에 점점 더 많은 가정이 가해 져서 코드의 인터페이스가 효과적으로 증가함에 따라 시간이 지남에 따라 변경되기도합니다.
Luaan

2
@CortAmmon "사실적으로 증명할 수있는"사례는 두 가지 매우 중요한 비용, 즉 추정 비용과 (확장 할 수없는) 확장 점의 유지 보수 비용을 무시합니다. 사람들이 추정치를 과소 평가하여 매우 신뢰할 수없는 추정치를 얻는 곳입니다. 매우 간단한 기능의 경우 몇 초 동안 생각하면 충분하지만 처음에는 "간단한"기능을 따르는 웜이 많이있을 가능성이 큽니다. 커뮤니케이션이 핵심입니다. 일이 커질수록 리더 / 고객과 대화해야합니다.
Luaan

1
@Luaan 나는 당신의 요점을 주장하려고했지만, 프로그래밍에 종교적 계명은 없습니다. 확장 점의 추정 및 유지 비용이 충분히 제한된 비즈니스 사례가 존재하는 한, 상기 "명령"에 의문이있는 경우가있다. 코드에 대한 나의 경험에서, 그러한 확장 점을 떠날 것인지 아닌지에 대한 질문은 한 줄 또는 다른 방법으로 한 줄 명령에 잘 맞지 않았습니다.
Cort Ammon

7

'중복 코드'와 'YAGNI'의 정의는 종종 얼마나 멀리 나아갈 지에 달려 있습니다.

문제가 발생하면 해당 문제를 피하는 방식으로 미래 코드를 작성하는 경향이 있습니다. 이 특정 문제를 경험하지 않은 다른 프로그래머는 코드 중복 잉여 문제를 고려할 수 있습니다.

내 제안은 부하와 동료가 당신보다 빨리 기능을 잃어 버린 경우 '아직 실수하지 않은 물건'에 소비하는 시간을 추적하는 것입니다.

그러나 당신이 나와 같다면, 나는 당신이 그것을 '기본적으로'모두 입력했을 것으로 예상하고 실제로 더 이상 당신을 데려 가지 않았습니다.


6

매개 변수에 대한 모든 가정을 문서화하는 것이 좋습니다. 또한 클라이언트 코드가 이러한 가정을 위반하지 않는지 확인하는 것이 좋습니다. 나는 이것을 할 것이다 :

/** ...
*   Precondition: rows is null or nonempty
*/
public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    Assert( rows==null || rows.Count > 0 )
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

[C #이라고 가정하면 Assert는 릴리스 된 코드로 컴파일되지 않으므로 작업에 가장 적합한 도구가 아닐 수 있습니다. 그러나 그것은 또 다른 날에 대한 논쟁입니다.]

왜 이것이 당신이 쓴 것보다 낫습니까? 미래에 클라이언트가 변경된 곳에서 클라이언트가 빈 목록을 전달할 때 첫 번째 행을 추가하고 약속에 앱을 추가하는 것이 옳은 일이라면 코드가 의미가 있습니다. 그러나 그것이 사실 일 것이라는 것을 어떻게 알 수 있습니까? 미래에 대해 더 적은 가정을하는 것이 좋습니다.


5

지금 해당 코드를 추가하는 비용을 추정하십시오 . 마음이 신선하기 때문에 상대적으로 저렴하므로 빨리 할 수 ​​있습니다. 단위 테스트를 추가해야합니다 .1 년 후 일부 방법을 사용하는 것보다 나쁘지 않은데 작동하지 않으며 실제로 시작되지 않았으며 실제로 작동하지 않았 음을 알 수 있습니다.

필요할 때 해당 코드를 추가하는 비용을 추정하십시오. 코드로 돌아가서 모든 것을 기억해야하기 때문에 더 비쌀 것입니다.

추가 코드가 실제로 필요할 확률을 추정하십시오. 그런 다음 수학을 수행하십시오.

다른 한편으로, "X는 결코 일어나지 않을 것이다"라는 가정으로 가득 찬 코드는 디버깅에 끔찍하다. 의도 한대로 작동하지 않으면 어리석은 실수 또는 잘못된 가정을 의미합니다. 당신의 "X는 결코 일어나지 않을 것"은 가정이며, 버그가있을 때는 의심 스럽다. 다음 개발자가 시간을 낭비하게 만듭니다. 일반적으로 그러한 가정에 의존하지 않는 것이 좋습니다.


4
첫 번째 단락에서는 실제로 필요한 기능이 불필요하게 추가 된 기능과 상호 배타적 이라는 것이 밝혀 질 때 시간이 지남에 따라 해당 코드를 유지 관리하는 비용을 언급하는 것을 잊었습니다 . . .
ruakh

또한 잘못된 입력으로 실패하지 않기 때문에 프로그램에 영향을 줄 수있는 버그 비용을 추정해야합니다. 그러나 버그의 정의에 의하면 예상치 못한 버그의 비용을 추정 할 수는 없습니다. 따라서 "수학"전체가 분리됩니다.
JacquesB

3

여기서 가장 중요한 질문은 "하지 않으면 어떻게 될까요?"입니다.

다른 사람들이 지적했듯이 이러한 종류의 방어 프로그래밍은 좋지만 때로는 위험합니다.

예를 들어, 기본값을 제공하면 프로그램을 유지하는 것입니다. 그러나 이제 프로그램이 원하는 작업을 수행하지 않을 수 있습니다. 예를 들어, 빈 배열을 파일에 쓰는 경우 버그를 "실수로 null을 제공했기 때문에 충돌"에서 "실수로 null을 제공했기 때문에 달력 행을 지 웁니다"로 변경했을 수 있습니다. (예를 들어 "// blah"라고 표시된 해당 부분의 목록에없는 것을 제거하기 시작한 경우 )

나를위한 열쇠는 절대로 데이터 손상없다는 것입니다 . 다시 반복하겠습니다. 못. 부정한. 데이터. 프로그램에서 예외가 발생하면 패치 할 수있는 버그 보고서가 나타납니다. 나중에 나쁜 데이터를 파일에 쓰면 소금으로 땅을 뿌려야합니다.

모든 "불필요한"결정은 그 전제를 염두에두고 내려야합니다.


2

여기서 다루는 것은 기본적으로 인터페이스입니다. "input is null, input initialize "동작을 추가하여 메소드 인터페이스를 효과적으로 확장했습니다. 이제 항상 유효한 목록에서 작동하는 대신 입력을 "수정"했습니다. 이것이 인터페이스의 공식적이든 비공식적이든, 누군가 (대부분 당신을 포함하여) 가이 행동을 사용할 것이라고 내기 할 수 있습니다.

인터페이스는 단순하게 유지되어야하며 특히 public static메서드 와 같은 경우 비교적 안정적이어야합니다 . 개인 메서드, 특히 개인 인스턴스 메서드에는 약간의 여유가 있습니다. 인터페이스를 암시 적으로 확장하면 실제로 코드를 더욱 복잡하게 만들 수 있습니다. 이제 실제로 해당 코드 경로를 사용 하고 싶지 않다고 상상해보십시오 . 피하십시오. 이제 메소드 동작의 일부인 것처럼 가장 한 테스트되지 않은 코드가 있습니다. 그리고 나는 아마도 버그가 있다고 말할 수 있습니다 : 당신이리스트를 전달할 때, 그리스트는 메소드에 의해 변경됩니다. 그러나 그렇지 않으면 로컬 을 만듭니다.목록을 작성하고 나중에 버립니다. 이것은 모호한 버그를 추적하려고 할 때 반년 안에 울게하는 일관되지 않은 행동입니다.

일반적으로 방어 프로그래밍은 매우 유용한 것입니다. 그러나 방어 검사를위한 코드 경로는 다른 코드와 마찬가지로 테스트 해야합니다 . 이런 경우에는 이유없이 코드를 복잡하게 만들고 대신 다음과 같은 대안을 선택합니다.

if (rows == null) throw new ArgumentNullException(nameof(rows));

null 인 입력을 원하지 않으며 가능한 한 빨리rows 모든 발신자에게 오류를 알리고 싶습니다 .

소프트웨어를 개발할 때 다루어야 할 많은 가치가 있습니다. 견고성 자체도 매우 복잡한 품질입니다. 예를 들어, 나는 당신의 방어 점검이 예외를 던지는 것보다 더 견고하다고 생각하지 않을 것입니다. 예외는 안전한 장소에서 다시 시도 할 수있는 안전한 장소를 제공하는 데 매우 편리합니다. 데이터 손상 문제는 일반적으로 문제를 조기에 인식하고 안전하게 처리하는 것보다 추적하기가 훨씬 어렵습니다. 결국, 그들은 단지 당신에게 견고성의 환상을주는 경향이 있으며, 한 달 후에 다른 목록이 업데이트 된 것을 보지 못했기 때문에 약속의 10 분의 1이 사라진 것을 알 수 있습니다. 아야.

둘을 구별해야합니다. 방어 프로그래밍 은 오류가 가장 관련이있는 곳에서 오류를 포착하여 디버깅 작업을 크게 지원하고 예외적 인 처리를 통해 "몰래 손상"을 방지하는 유용한 기술입니다. 일찍 실패하고 빨리 실패하십시오. 반면에, 당신이하고있는 것은 "오류 은폐"와 비슷합니다. 입력을 저글링하고 발신자가 무엇을 의미하는지 가정합니다. 이는 사용자가 직면 한 코드 (예 : 철자 검사)에 매우 중요하지만 개발자가 직면 한 코드에서이 코드를 볼 때는주의해야합니다.

가장 큰 문제는 추상화가 무엇이든간에 유출 될 것입니다 ( "전혀가 아닌 형식을 원했습니다! 유지하고 이해하고 테스트해야하는 코드가 필요합니다. 1 년 후 생산 과정에서 버그를 수정하여 널이 아닌 목록을 통과시키는 노력을 비교해보십시오. 이상적인 세계에서는 모든 메소드가 자체 입력으로 만 작동하여 결과를 리턴하고 전역 상태를 수정하지 않기를 원합니다. 물론 현실 세계에서는 그렇지 않은 경우가 많이 있습니다.가장 간단하고 명확한 솔루션 (예 : 파일 저장시)이지만 전역 상태를 읽거나 조작 할 이유가없는 경우 메소드를 "순수"하게 유지하면 코드를 쉽게 추론 할 수 있습니다. 또한 방법을 나누는 데 더 자연스러운 포인트를주는 경향이 있습니다. :)

그렇다고해서 예상치 못한 모든 것이 응용 프로그램 충돌을 일으키는 것은 아닙니다. 예외를 잘 사용하면 자연스럽게 안전한 오류 처리 지점을 형성하여 안정적인 응용 프로그램 상태를 복원하고 사용자가 수행중인 작업을 계속할 수있게합니다 (이상적으로 사용자의 데이터 손실을 피하면서). 이러한 처리 지점에서 문제를 해결하거나 ( "주문 번호 2212를 찾을 수 없습니다. 2212b를 의미 했습니까?") 사용자에게 제어 권한을 부여 할 수있는 기회 ( "데이터베이스 연결 오류. 다시 시도 하시겠습니까?")가 표시됩니다. 이러한 옵션을 사용할 수없는 경우에도, 적어도 그것은 당신에게 아무것도 손상되지있어하는 기회를 줄 것이다 - 내가 사용하는 코드 감상 시작했습니다 usingtry... finally보다 훨씬 더 try...catch예외적 인 조건에서도 불변성을 유지할 수있는 많은 기회를 제공합니다.

사용자는 데이터와 작업을 잃어 버리지 않아야합니다. 이것은 여전히 ​​개발 비용 등과 균형을 이루어야하지만, 꽤 좋은 일반적인 지침입니다 (사용자가 소프트웨어를 구입할지 여부를 결정하는 경우-내부 소프트웨어에는 일반적으로 사치품이 없습니다). 사용자가 다시 시작한 후 다시하고 있던 작업으로 돌아 가면 전체 응용 프로그램 충돌조차 문제가 훨씬 줄어 듭니다. 이것은 정말 견고합니다. Word 는 디스크의 문서를 손상 시키지 않고 작업을 항상 저장 하고 옵션을 제공합니다.충돌 후 Word를 다시 시작한 후 이러한 변경 내용을 복원합니다. 처음에는 버그가없는 것보다 낫습니까? 아마도 그렇지는 않지만 희귀 한 버그를 잡는 데 드는 작업이 모든 곳에서 더 잘 사용될 수 있음을 잊지 마십시오. 그러나 대체 방법보다 훨씬 낫습니다. 예를 들어 디스크의 손상된 문서, 마지막 저장 이후의 모든 작업이 손실되고 충돌 전에 변경 사항이 자동으로 대체 된 문서는 Ctrl + A 및 삭제였습니다.


1

강력한 코드가 지금부터 "년간"혜택을받을 것이라는 가정을 바탕으로이 답변을 드리겠습니다. 장기적인 이점이 목표라면 견고성보다 설계 및 유지 관리 가능성을 우선시합니다.

디자인과 견고 함의 균형은 시간과 초점입니다. 대부분의 개발자는 문제 지점을 통과하고 추가 조건 또는 오류 처리를 수행하는 경우에도 잘 설계된 코드 세트를 사용합니다. 몇 년 동안 사용한 후에 실제로 필요한 장소는 사용자가 식별했을 수 있습니다.

디자인의 품질이 같다고 가정하면 코드 유지가 더 쉽습니다. 그렇다고 알려진 문제가 몇 년 동안 지속되었다고해서 더 나아지는 것은 아니지만, 필요하지 않은 것을 추가하면 문제가 발생합니다. 우리는 모두 레거시 코드를 살펴보고 불필요한 부분을 발견했습니다. 몇 년 동안 작동 한 높은 수준의 신뢰 변경 코드가 있어야합니다.

따라서 앱이 최대한 설계되고 유지 관리가 쉽고 버그가 없다고 생각되면 필요하지 않은 코드를 추가하는 것보다 더 나은 것을 찾으십시오. 무의미한 기능에 대해 오랜 시간을 일하는 다른 모든 개발자들에 대해 존중할 수있는 것이 가장 적습니다.


1

아니 당신은해야하지. 그리고 이러한 코딩 방식으로 버그를 숨길 수 있다고 말할 때 실제로 자신의 질문에 대답하고 있습니다 . 이렇게하면 코드가 더 강력 해지지 않고 오히려 버그가 발생하기 쉽고 디버깅이 더 어려워집니다.

rows인수 에 대한 현재 기대치는 다음과 같습니다. null이거나 그렇지 않으면 하나 이상의 항목이 있습니다. 따라서 질문은 : rows항목이없는 예기치 않은 세 번째 경우를 추가로 처리하기 위해 코드를 작성하는 것이 좋습니다 ?

내 대답은 아니오 야. 예기치 않은 입력의 경우 항상 예외를 발생시켜야합니다. 이것을 고려하십시오 : 코드의 다른 부분이 메소드의 기대 (예 : 계약)를 위반 하면 버그가 있음을 의미 합니다. 버그가 있으면 가능한 한 빨리 알고 싶어서 수정하면 예외가 도움이됩니다.

현재 코드에서하는 일은 코드에 존재하거나 존재하지 않을 수있는 버그를 복구하는 방법을 추측 하는 것입니다. 그러나 버그가 있더라도 완전히 복구하는 방법을 알 수 없습니다. 정의상 버그는 알려지지 않은 결과를 초래합니다. 어쩌면 일부 초기화 코드가 예상대로 실행되지 않았을 때 행이 누락 된 것보다 많은 다른 결과가있을 수 있습니다.

따라서 코드는 다음과 같아야합니다.

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    if (rows != null && rows.Count == 0) throw new ArgumentException("Rows is empty."); 

    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

참고 : 일부 있습니다 특정 는 "추측"하는 것이 합리적 경우이 방법 만 예외를 던지기보다는 잘못된 입력을 처리합니다. 예를 들어 외부 입력을 처리하면 제어 할 수 없습니다. 웹 브라우저는 잘못된 유형의 잘못된 입력을 정상적으로 처리하려고하기 때문에 악명 높은 예입니다. 그러나 이것은 프로그램의 다른 부분에서 오는 호출이 아닌 외부 입력에서만 의미가 있습니다.


편집 : 다른 답변은 방어 프로그래밍을 하고 있다고 말합니다 . 동의하지 않습니다. 방어 프로그래밍은 입력이 유효하다고 자동으로 신뢰하지 않는다는 것을 의미합니다. 따라서 매개 변수의 유효성 검사 (위와 같이)는 방어적인 프로그래밍 기술이지만, 추측하여 예기치 않은 입력 또는 잘못된 입력을 변경해야한다는 의미는 아닙니다. 강력한 방어 접근 방식은 입력의 유효성을 검사하는 것입니다 다음 예기치 않은 또는 잘못된 입력의 경우 예외를 throw합니다.


1

나중에 필요할 수 있으므로 지금 중복 코드를 추가해야합니까?

중복 코드는 언제든지 추가해서는 안됩니다.

미래에만 필요한 코드를 추가해서는 안됩니다.

어떤 일이 있어도 코드가 제대로 작동하는지 확인해야합니다.

"잘 작동"의 정의는 당신의 요구에 달려 있습니다. 내가 사용하고 싶은 기술 중 하나는 "편집증"예외입니다. 특정 사례가 발생하지 않을 것이라고 100 % 확신하는 경우에도 여전히 예외를 프로그래밍하지만 a) 모든 사람에게 이러한 상황이 발생하지 않을 것이라고 명확하게 알리고 b)가 명확하게 표시되고 기록되는 방식으로 처리합니다. 따라서 나중에 크립 손상이 발생하지 않습니다.

의사 코드 예 :

file = File.open(">", "bla")  or raise "Paranoia: cannot open file 'bla'"

file.write("xytz") or raise "Paranoia: disk full?"

file.close()  or raise "Paranoia: huh?!?!?"

이것은 내가 항상 파일을 열거 나 쓰거나 닫을 수 있다는 것을 100 % 확신한다는 것을 분명히 말해줍니다. 즉, 정교한 오류 처리를 만드는 정도까지 가지 않습니다. 그러나 (아니요 : 언제) 파일을 열 수 없으면 프로그램이 여전히 제어 된 방식으로 실패합니다.

사용자 인터페이스는 물론 이러한 메시지를 사용자에게 표시하지 않으며 스택 추적과 함께 내부적으로 로그됩니다. 다시 말하지만, 이것은 예기치 않은 일이 발생했을 때 코드가 "중지"되도록하는 내부 "Paranoia"예외입니다. 이 예제는 실제로 약간 수정되었습니다. 실제로 파일을 여는 동안 오류에 대한 실제 오류 처리를 구현합니다. 이는 정기적으로 발생합니다 (잘못된 파일 이름, USB 스틱 마운트 읽기 전용 등).

다른 답변에서 언급했듯이 매우 중요한 관련 검색어는 "실패"이며 강력한 소프트웨어를 만드는 데 매우 유용합니다.


-1

여기에는 너무 복잡한 답변이 많이 있습니다. 이 질문에 해당 조각 코드에 대해 옳지 않은 느낌을 주었지만 왜 또는 어떻게 수정해야하는지 잘 모르겠습니다. 그래서 내 대답은 문제가 코드 구조에 항상있을 가능성이 높다는 것입니다.

먼저 메소드 헤더 :

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)

어떤 행에 약속을 지정 하시겠습니까? 파라미터 목록에서 즉시 명확해야합니다. 더 이상의 지식이 없으면 메소드 매개 변수가 다음과 같이 보일 것으로 기대합니다 (Appointment app, CalendarRow row).

다음으로 "입력 검사":

//1. Is rows equal to null? - This will be the case if this is the first appointment.
if (rows == null) {
    rows = new List<CalendarRow> ();
}

//2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
if(rows.Count == 0)
{
    rows.Add (new CalendarRow (0));
    rows [0].Appointments.Add (app);
}

이것은 헛소리입니다.

  1. 확인) 메소드 호출자는 메소드 내부에서 초기화되지 않은 값을 전달하지 않아야합니다. 멍청하지 않은 프로그래머의 책임입니다.
  2. 확인) rows메소드에 전달 하는 것이 잘못되었을 가능성 을 고려하지 않은 경우 (위의 주석 참조) AssignAppointmentToRow약속을 어딘가에 할당하는 것 이외의 방법으로 행을 조작하도록 호출 된 메소드에 대한 책임을지지 않아야합니다 .

그러나 약속을 어딘가에 할당하는 전체 개념은 이상합니다 (코드의 GUI 부분이 아닌 한). 코드에 달력을 나타내는 명시 적 데이터 구조가 포함 된 것 (또는 적어도 시도) ( List<CalendarRows><- Calendar이 방법으로 가고 싶다면 어딘가에 정의되어야하는 경우 Calendar calendar메서드에 전달 됩니다). 이 방법으로 가면 calendar나중에 약속을 배치 (할당)하는 슬롯이 미리 ​​채워질 것으로 예상 됩니다 (예 : calendar[month][day] = appointment적절한 코드). 그러나 메인 로직에서 캘린더 구조를 모두 버리고 객체에 속성이 포함 된 List<Appointment>위치를 갖도록 선택할 수도 있습니다Appointmentdate. 그런 다음 GUI 어딘가에 캘린더를 렌더링해야하는 경우 렌더링 직전에이 '명확한 캘린더'구조를 작성할 수 있습니다.

나는 당신의 응용 프로그램의 세부 사항을 알지 못하기 때문에이 중 일부는 아마도 당신에게 적용되지 않을 수 있지만 두 가지 검사 (주로 두 번째 검사)는 코드에서 우려의 분리와 관련 하여 어딘가에 문제가 있다고 말합니다 .


-2

간단히하기 위해 결국 N 일 안에 (이후 또는 이전에)이 코드 조각이 필요하거나 전혀 필요하지 않다고 가정 해 봅시다.

의사 코드 :

let C_now   = cost of implementing the piece of code now
let C_later = ... later
let C_maint = cost of maintaining the piece of code one day
              (note that it can be negative)
let P_need  = probability that you will need the code after N days

if C_now + C_maint * N < P_need*C_later then implement the code else omit it.

요인 C_maint:

  • 일반적으로 코드를 향상 시켜서보다 자체 문서화하고 테스트하기 쉽게합니까? 그렇다면 부정적인 C_maint기대
  • 코드를 더 크게 만들 수 있습니까 (따라서 읽기 어렵고, 컴파일하기 더 길고, 테스트를 구현하는 등)?
  • 리팩토링 / 재 설계가 보류 중입니까? 그렇다면 범프 C_maint. 이 경우에는 더 많은 변수가있는보다 복잡한 수식이 필요합니다 N.

코드를 가중시키고 가능성이 낮은 2 년 만에 필요할 수있는 큰 것은 제외해야하지만, 유용한 주장을 제시하고 50 %는 3 개월 내에 필요할 것으로 예상되는 작은 것을 구현해야합니다. .


또한 잘못된 입력을 거부하지 않기 때문에 프로그램에 영향을 줄 수있는 버그 비용을 고려해야합니다. 찾기 어려운 버그의 비용을 어떻게 추정합니까?
JacquesB
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.