모델이 데이터의 유효성을 검사하는 경우 잘못된 입력에서 예외를 발생시키지 않아야합니까?


9

SO 질문을 읽으면 사용자 입력 유효성 검사에 대한 예외 예외가 발생하는 것으로 보입니다.

그러나 누가이 데이터를 검증해야합니까? 내 응용 프로그램에서 모든 유효성 검사는 비즈니스 계층에서 수행됩니다. 클래스 자체만으로 각 속성마다 유효한 값을 알고 있기 때문입니다. 속성 유효성 검사 규칙을 컨트롤러에 복사하는 경우 유효성 검사 규칙이 변경 될 수 있으며 이제 수정해야 할 두 곳이 있습니다.

비즈니스 계층에서 유효성 검사를 수행해야한다는 전제입니까?

내가하는 일

따라서 내 코드는 일반적으로 다음과 같이 끝납니다.

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      throw new ValidationException("Name cannot be empty");
    }
    $this->name = $n;
  }

  public function setAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        throw new ValidationException("Age $a is not valid");
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      throw new ValidationException("Age $a is out of bounds");
    }
    $this->age = $a;
  }

  // other getters, setters and methods
}

컨트롤러에서 입력 데이터를 모델에 전달하고 예외를 포착하여 사용자에게 오류를 표시합니다.

<?php
$person = new Person();
$errors = array();

// global try for all exceptions other than ValidationException
try {

  // validation and process (if everything ok)
  try {
    $person->setAge($_POST['age']);
  } catch (ValidationException $e) {
    $errors['age'] = $e->getMessage();
  }

  try {
    $person->setName($_POST['name']);
  } catch (ValidationException $e) {
    $errors['name'] = $e->getMessage();
  }

  ...
} catch (Exception $e) {
  // log the error, send 500 internal server error to the client
  // and finish the request
}

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

이것이 나쁜 방법론입니까?

다른 방법

어쩌면 isValidAge($a)반환 true / false에 대한 메소드를 작성 하고 컨트롤러에서 호출해야합니까?

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if ($this->isValidName($n)) {
      $this->name = $n;
    } else {
      throw new Exception("Invalid name");
    }
  }

  public function setAge($a) {
    if ($this->isValidAge($a)) {
      $this->age = $a;
    } else {
      throw new Exception("Invalid age");
    }
  }

  public function isValidName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      return false;
    }
    return true;
  }

  public function isValidAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        return false;
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      return false;
    }
    return true;
  }

  // other getters, setters and methods
}

그리고 컨트롤러는 기본적으로 동일하지만 try / catch 대신 if / else가 있습니다.

<?php
$person = new Person();
$errors = array();
if ($person->isValidAge($age)) {
  $person->setAge($age);
} catch (Exception $e) {
  $errors['age'] = "Invalid age";
}

if ($person->isValidName($name)) {
  $person->setName($name);
} catch (Exception $e) {
  $errors['name'] = "Invalid name";
}

...

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

그래서 내가 무엇을해야하니?

나는 원래의 방법에 매우 만족하며, 일반적으로 내가 보여준 동료들은 그것을 좋아했습니다. 그럼에도 불구하고 대체 방법으로 변경해야합니까? 아니면이 잘못하고 다른 방법을 찾아야합니까?


"원래"코드를 약간 처리 ValidationException하고 다른 예외를 수정했습니다
Carlos Campderrós

2
최종 사용자에게 예외 메시지를 표시 할 때의 한 가지 문제점은 모델이 사용자가 사용하는 언어를 갑자기 알아야한다는 점이지만 대부분 View에 대한 문제입니다.
Bart van Ingen Schenau 2016 년

@BartvanIngenSchenau 좋은 캐치. 내 응용 프로그램은 항상 단일 언어이지만 모든 구현에서 발생할 수있는 현지화 문제를 생각하는 것이 좋습니다.
Carlos Campderrós

유효성 검사에 대한 예외는 프로세스에 유형을 주입하는 멋진 방법입니다. 와 같은 유효성 검사 인터페이스를 구현하는 객체를 반환하여 동일한 결과를 얻을 수 있습니다 IValidateResults.
Reactgular

답변:


7

과거에 사용한 접근법은 모든 유효성 검사 논리 전용 유효성 검사 클래스를 배치하는 것입니다.

그런 다음 이러한 입력 클래스를 프리젠 테이션 레이어에 삽입하여 조기 입력 확인을 수행 할 수 있습니다. 또한 모델 클래스가 동일한 클래스를 사용하여 데이터 무결성을 강화하는 것을 막을 수있는 것은 없습니다.

이 방법을 사용하면 어떤 계층에서 발생하는지에 따라 유효성 검사 오류를 다르게 처리 할 수 ​​있습니다.

  • 모델에서 데이터 무결성 검증이 실패하면 예외를 처리하십시오.
  • 프리젠 테이션 레이어에서 사용자 입력 확인에 실패하면 유용한 팁을 표시하고 값을 모델로 푸시하는 것을 지연하십시오.

그래서 당신은 PersonValidator의 다른 속성을 검증하는 모든 논리를 가진 클래스 와 이것에 의존 Person하는 Person클래스를 가지고 PersonValidator있습니까? 질문에서 제안한 대체 방법에 비해 제안이 제공하는 이점은 무엇입니까? 에 대한 다른 유효성 검사 클래스를 주입하는 기능 만 Person보지만 이것이 필요한 경우는 생각할 수 없습니다.
Carlos Campderrós

적어도 비교적 간단한 경우에는 유효성 검사를 위해 완전히 새로운 클래스를 추가하는 것이 과도하다는 데 동의합니다. 훨씬 더 복잡한 문제에 유용 할 수 있습니다.

글쎄, 여러 사람 / 회사에 판매하려는 응용 프로그램의 경우 각 회사마다 개인의 연령에 맞는 유효 범위를 확인하는 규칙이 다를 수 있으므로 이해하는 것이 좋습니다. 따라서 유용하지만 실제로는 내 요구에 너무 과도합니다. 어쨌든, 당신도 +1
Carlos Campderrós

1
모델에서 검증을 분리하는 것은 커플 링 및 응집력 관점에서도 의미가 있습니다. 이 간단한 시나리오에서는 과잉이 될 수 있지만 별도의 Validator 클래스를 훨씬 더 매력적으로 만들기 위해 단일 "교차 필드"유효성 검사 규칙 만 사용합니다.
세스 M.

8

나는 원래의 방법에 매우 만족하며, 일반적으로 내가 보여준 동료들은 그것을 좋아했습니다. 그럼에도 불구하고 대체 방법으로 변경해야합니까? 아니면이 잘못하고 다른 방법을 찾아야합니까?

당신과 당신의 동료가 그것에 만족한다면, 나는 더 이상 바꿀 필요가 없다는 것을 알았습니다.

실용적 관점 에서 의문의 여지가있는 Exception것은 더 구체적인 것이 아니라 던지는 것입니다. 문제는을 잡으면 Exception사용자 입력의 유효성 검사와 관련이없는 예외를 잡을 수 있다는 것입니다.


이제 "예외는 예외적 인 경우에만 사용해야하고 XYZ는 예외가 아닙니다"와 같은 말을하는 사람들이 많이 있습니다. (예를 들어, @ dann1111의 답변 ... 사용자 오류를 "완전히 정상"으로 표시합니다.)

이에 대한 나의 대답은 무언가 ( "XY Z")가 예외적인지 아닌지를 결정하기위한 객관적인 기준 이 없다는 입니다. 그것은 주관적인 척도입니다. (모든 프로그램 에서 사용자 입력 오류를 확인 해야 한다는 사실 은 발생 오류를 "정상"으로 만들지 않습니다. 실제로 "정상"은 객관적인 관점에서 크게 의미가 없습니다.)

그 진언에는 진실이 있습니다. 일부 언어 (또는보다 정확하게는 일부 언어 구현 )에서 예외 생성, 던지기 및 / 또는 catch는 간단한 조건부보다 훨씬 비쌉니다. 그러나 이러한 관점에서 보면 예외 사용을 피할 경우 수행해야 할 추가 테스트 비용과 작성 / 투사 / 캐치 비용을 비교해야합니다. 그리고 "방정식"은 예외를 던져야 할 확률 을 고려해야합니다 .

예외에 대한 다른 주장은 코드를 이해하기 어렵게 만들 있다는 것입니다. 그러나 단점은 적절하게 사용하면 코드 를 이해 하기 쉽게 만들 수 있다는 것입니다.


요컨대-예외를 사용하거나 사용하지 않기로 결정하는 것은 몇 가지 단순한 교리를 기준으로하지 말고 장점을 평가 한 후에 이루어져야합니다.


일반 Exception이 던져 지거나 잡히는 것에 대한 좋은 지적 . 나는 정말로 자신의 하위 클래스를 던지고 Exception세터의 코드는 일반적으로 다른 예외를 던질 수있는 아무것도 수행하지 않습니다.
Carlos Campderrós 2016 년

"원본"코드를 ValidationException 및 기타 예외를 처리하기 위해 약간 수정했습니다. / cc @ dan1111
Carlos Campderrós

1
+1, 모든 메소드 호출의 반환 값을 확인 해야하는 암흑 시대로 돌아가는 것보다 설명 ValidationException이 훨씬 많습니다. 간단한 코드 = 잠재적으로 적은 오류.
Heinzi 2016 년

2
@ dan1111-귀하의 의견을 가질 권리를 존중하지만 귀하의 의견에는 의견 이외의 다른 내용이 없습니다 . 유효성 검사의 "정상 성"과 유효성 검사 오류 처리 메커니즘 간에는 논리적으로 연결되어 있지 않습니다. 당신이하는 일은 교리를 외우는 것뿐입니다.
Stephen C

@StephenC, 나는 성찰을 통해 사건을 너무 강력하게 언급했다고 생각합니다. 나는 그것이 개인적인 취향에 더 가깝다는 것에 동의합니다.

6

제 생각에는 응용 프로그램 오류사용자 오류 를 구별 하고 전자에 대한 예외 만 사용 하는 것이 유용합니다 .

  • 프로그램이 제대로 실행되지 못하게하는 사항은 예외 입니다.

    예기치 않은 상황이 발생하여 계속 진행할 수 없으며 설계에 다음 사항이 반영됩니다. 정상적인 실행을 중단하고 오류 처리가 가능한 위치로 이동합니다.

  • 유효하지 않은 입력과 같은 사용자 오류는 (프로그램의 관점에서) 완벽하게 정상이며 응용 프로그램에서 예기치 않은 것으로 취급해서는 안됩니다 .

    사용자가 잘못된 값을 입력하고 오류 메시지가 표시되면 프로그램이 "실패"했거나 어떤 방식으로 오류가 발생 했습니까? 아닙니다. 응용 프로그램이 성공적으로 완료되었습니다. 특정 유형의 입력이 제공된 경우 해당 상황에서 올바른 출력을 생성했습니다.

    정상적인 실행의 일부이기 때문에 사용자 오류를 처리하는 것은 예외없이 점프하여 처리하는 것이 아니라 정상적인 프로그램 흐름의 일부 여야합니다.

물론 의도 된 목적 이외의 목적으로 예외를 사용하는 것이 가능하지만, 그렇게하면 패러다임을 혼동하고 이러한 오류가 발생할 때 부정확 한 동작의 위험이 있습니다.

원래 코드에 문제가 있습니다.

  • setAge()메소드 의 호출자는 메소드의 내부 오류 처리에 대해 너무 많이 알고 있어야합니다. 호출자는 나이가 유효하지 않을 때 예외가 발생 하고 메소드 내에서 다른 예외가 발생하지 않음 을 알아야합니다 . 에 추가 기능을 추가하면 나중에이 가정이 깨질 수 있습니다 setAge().
  • 호출자가 예외를 포착하지 않으면 유효하지 않은 연령 예외는 나중에 다른 가장 불투명 한 방식으로 처리됩니다. 또는 처리되지 않은 예외 충돌이 발생할 수도 있습니다. 유효하지 않은 데이터를 입력 할 때는 좋지 않은 동작입니다.

대체 코드에도 문제가 있습니다.

  • 추가 가능하고 불필요한 방법 isValidAge()이 도입되었습니다.
  • 이제 setAge()메소드는 호출자가 이미 끔찍한 가정을 확인하거나 연령을 다시 확인 한다고 가정 해야 isValidAge()합니다. 나이를 다시 확인하면 setAge() 여전히 일종의 오류 처리를 제공해야하며 다시 제곱으로 돌아갑니다.

제안 된 디자인

  • 만들기 setAge()성공하면 true, 실패하면 false를 돌려줍니다.

  • 반환 값을 확인하고 setAge()실패한 경우 예외가 아니라 사용자에게 오류를 표시하는 정상적인 기능 으로 유효 기간이 잘못되었음을 사용자에게 알립니다 .


그럼 어떻게해야합니까? 대체 방법을 사용하거나 내가 생각하지 않은 완전히 다른 것을 제안 했습니까? 또한 "비즈니스 계층에서 유효성 검사를 수행해야합니다"라는 전제가 잘못된 것입니까?
Carlos Campderrós 2016 년

@ CarlosCampderrós, 업데이트 참조; 당신이 논평 한대로 그 정보를 추가하고있었습니다. 원래 디자인의 위치가 올바른지 확인했지만 예외를 사용하여 해당 유효성 검사를 수행하는 것은 실수였습니다.

대체 방법을 사용하면 setAge다시 유효성을 검사하지만 논리가 기본적으로 "유효한 경우 연령을 설정하고 예외를 throw하십시오"라는 식으로 다시 제곱으로 돌아 가지 않습니다.
Carlos Campderrós 2016 년

2
대체 방법과 제안 된 디자인 모두에서 볼 수있는 한 가지 문제는 연령이 유효하지 않은 이유를 구별 할 수없는 능력입니다. true 또는 오류 문자열을 반환하도록 만들 수 있지만 (예, php는 soooo dirty) "The entered age is out of bounds" == true사람들이 항상 사용해야 하기 때문에 ===많은 문제가 발생할 수 있으므로이 방법은 시도하는 문제보다 문제가 많습니다. 해결
Carlos Campderrós

2
그러나 응용 프로그램을 코딩하는 것은 정말 번거로운 setAge()일입니다. 어디에서나 만들 때마다 실제로 작동하는지 확인해야하기 때문입니다. 예외를 던지는 것은 모든 것이 잘되었는지 확인하는 것을 잊지 않아도된다는 것을 의미합니다. 내가 볼 수 있듯이, 속성 / 속성에 유효하지 않은 값을 설정하는 것은 예외적이며를 던질 가치가 Exception있습니다. 모델은 데이터베이스 또는 사용자로부터 입력을 받는지 상관하지 않습니다. 입력이 잘못되어서는 안되므로 예외를 던지는 것이 합법적입니다.
Carlos Campderrós

4

내 관점에서 (나는 자바 사람이다) 그것은 첫 번째 방법으로 어떻게 구현했는지 완전히 유효합니다.

일부 전제 조건이 충족되지 않으면 (예 : 빈 문자열) 객체가 예외를 발생시키는 것이 유효합니다. Java에서 확인 된 예외 개념은 그러한 목적에 부합합니다. 서명에서 선언해야 예외가 발생하면 호출자가 명시 적으로 예외를 잡아야합니다. 대조적으로, 검사되지 않은 예외 (일명 RuntimeExceptions)는 코드에서 catch-clause를 정의 할 필요없이 언제든지 발생할 수 있습니다. 첫 번째는 복구 가능한 경우 (예 : 잘못된 사용자 입력, 파일 이름이 존재하지 않음)에 사용되지만 후자는 사용자 / 프로그래머가 수행 할 수없는 경우 (예 : 메모리 부족)에 사용됩니다.

그러나 @Stephen C에서 이미 언급했듯이 자신의 예외를 정의하고 실수로 다른 사람을 잡지 않도록 예외를 잡아야합니다.

그러나 다른 방법 은 로직이없는 데이터 컨테이너 인 데이터 전송 객체 를 사용하는 것 입니다. 그런 다음 유효성 검사를 위해 이러한 DTO 를 유효성 검사기 또는 모델 개체 자체로 전달하고 성공한 경우에만 모델 개체에서 업데이트합니다. 이 접근법은 프리젠 테이션 로직과 애플리케이션 로직이 분리 된 계층 (프레젠테이션은 웹 페이지, 웹 서비스 적용) 인 경우에 종종 사용됩니다. 이러한 방식으로 물리적으로 분리되어 있지만 예와 같이 한 계층에 둘 다있는 경우 유효성 검사없이 값을 설정하는 해결 방법이 없는지 확인해야합니다.


4

Haskell 모자를 착용하면 두 가지 접근 방식이 모두 잘못되었습니다.

개념적으로 발생하는 것은 먼저 많은 바이트가 있고 구문 분석 및 유효성 검사 후에 Person을 구성 할 수 있다는 것입니다.

개인은 이름의 향과 나이와 같은 일정한 불변성을 가지고 있습니다.

이름 만 있지만 나이가없는 사람을 대표 할 수 있다는 것은 모든 비용을 피하고 싶은 것이지, 그것이 완전성을 만드는 것이기 때문입니다. 엄격한 불변은 예를 들어 나중에 나이의 존재를 확인할 필요가 없음을 의미합니다.

내 세계에서 Person은 단일 생성자 또는 함수를 사용하여 원자 적으로 생성됩니다. 해당 생성자 또는 함수는 매개 변수의 유효성을 다시 확인할 수 있지만 반인을 구성해서는 안됩니다.

불행히도 Java, PHP 및 기타 OO 언어는 올바른 옵션을 매우 장황하게 만듭니다. 적절한 Java API에서 빌더 오브젝트가 종종 사용됩니다. 이러한 API에서 사람을 만드는 것은 다음과 같습니다.

Person p = new Person.Builder().setName(name).setAge(age).build();

또는 더 장황한 :

Person.Builder builder = new Person.Builder();
builder.setName(name);
builder.setAge(age);
Person p = builder.build();
// Person object must have name and age here

이 경우 예외가 발생하거나 유효성이 검사되는 위치에 관계없이 잘못된 Person 인스턴스를 수신 할 수 없습니다.


여기서 당신이 한 일은 문제를 Builder 클래스로 옮기는 것인데, 당신은 실제로 대답하지 않았습니다.
Cypher

2
원자 적으로 실행되는 builder.build () 함수에 문제를 현지화했습니다. 이 기능은 모든 검증 단계의 목록입니다. 이 접근 방식과 임시 접근 방식에는 큰 차이가 있습니다. Builder 클래스에는 단순 유형 이외의 변이가없는 반면 Person 클래스에는 강한 변이가 없습니다. 올바른 프로그램을 구축하는 것은 데이터에 강력한 불변성을 적용하는 것입니다.
user239558

여전히 질문에 대답하지 않습니다 (적어도 완전히는 아닙니다). 개별 오류 메시지가 Builder 클래스에서 호출 스택까지 View로 전달되는 방법에 대해 자세히 설명해 주시겠습니까?
Cypher

세 가지 가능성 : build ()는 OP의 첫 번째 예와 같이 특정 예외를 발생시킬 수 있습니다. 사람이 읽을 수있는 오류 세트를 리턴하는 공용 Set <String> validate ()가있을 수 있습니다. i18n-ready 오류에 대한 공개 Set <Error> validate ()가 있습니다. 요점은 이것이 Person 객체로 변환하는 동안 발생한다는 것입니다.
user239558

2

평신도의 말로 :

첫 번째 방법은 올바른 방법입니다.

두 번째 방법은 해당 비즈니스 클래스가 해당 컨트롤러에 의해서만 호출되고 다른 컨텍스트에서는 호출되지 않는다고 가정합니다.

비즈니스 클래스는 비즈니스 규칙을 위반할 때마다 예외를 발생시켜야합니다.

컨트롤러 또는 프리젠 테이션 레이어는 예외 발생을 막기 위해 던질 것인지 또는 자체 검증을 수행해야하는지 결정해야합니다.

주의 사항 : 수업은 잠재적으로 서로 다른 상황에서 서로 다른 통합에 의해 사용됩니다. 따라서 입력이 잘못되면 예외를 처리 할 수있을 정도로 똑똑해야합니다.

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