입력값 검증을 InputView에서 진행할까, Domain에서 진행할까?
가장 많이 고민했던 부분이다.
입력값 검증을 input할 때 View에서 해야할지, domain을 생성하는 시점에 해야할지 고민했다.
웹서비스에서의 회원가입 절차에서는
- 올바르지 않은 입력값(ex. 비밀번호 특수문자 미포함, 10자 이하 등)의 경우 주로 웹프론트엔드에서 검증한다.
- 중복된 아이디의 경우 주로 백엔드에서 검증한다.
따라서 빈 입력값 및 글자수 제한 등의 조건은 View에서 처리하고, 중복 등과 같은 조건은 Domain에서 처리하는 방법을 생각해봤다.
하지만 페어와 협의 과정에서 해당 미션에서는 “자동차 이름이 중복될 수 없다.”라는 조건이 없기에 중복된 이름에 대해서는 이름 뒤에 구분자 및 식별숫자를 붙여주기로 결정했다.
따라서 해당 미션의 입력값 검증은 모두 View단에서 처리했다.
그리고.. 내게 돌아온 코드리뷰!
뷰 단에서만 유효성 검사를 진행하고 도메인에서는 유효성 검사를 진행하지 않으면, 언제든 유효하지 않은 값을 가진 도메인 인스턴스가 생성될 수 있다.
도메인이 뷰를 통해서만 생성될 수 있는게 아니기 때문이다.
그리고, 뷰가 깨끗한 데이터만을 보내줄 것이라고 믿지 말자!!
도메인 : 난 누구도 믿지 않는다. 나의 데이터는 내가 검증한다.
View와 Domain 둘 다에서 유효성 검사를 해보자!
Q. View과 Domain 둘 다에서 유효성 검사를 해주기 위해 적합한 방법은 무엇일까?
다음은 내 생각의 흐름이다. (중간 과정에서 지금의 내 생각과 다른 생각들이 존재한다.)
1. Validator의 위치에 대한 고민
동일한 로직이 도메인에서도 쓰이고, 뷰에서도 쓰인다.
따라서 도메인 내 / 뷰 내에 존재시키지 않고 외부에 존재시키는 것이 좋을 것 같다.
2. Validator는 Boolean을 return해줄까, Exception을 throw하고 void를 return해줄까?
class Car {
public Car(String input) {
if (validate(input)) {
throw new IllegalArgumentException();
}
}
...
}
boolean validate(String input) {
return !input.isBlank();
}
void validate(String input) {
if (input.isBlank()) {
throw new IllegalArgumentExcpetion();
}
}
나는 두 번째 방법(validate 함수에서 예외를 던져주는 방법)을 선택했다.
두 번째 방법에서는 검증 규칙이 여러개일때, 어떤 검증 규칙을 어긴것인지에 따라 에러 메시지를 달리 하기 간편할 것 같기 때문이었다!
무슨 말인지 코드로 말해보자면..
void validate(String input) {
validatePositiveInteger(input);
validateRange(Integer.parseInt(input));
}
void validateInteger(String input) {
try {
Integer.parseInt(input);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("입력값은 숫자여야 합니다.");
}
}
void validateRange(int input) {
if (input < MIN_INCLUSISVE || input > MAX_INCLUSIVE) {
throw new IllegalArgumentException("입력값은 3이상 5이하여야합니다.");
}
}
Boolean을 return해주면, 단순히 유효한 입력값인지 아닌지 여부만 확인할 수 있다.
하지만 validate함수에서 직접 예외를 던지면, 어떠한 제약조건을 어겨 예외가 던져진 것인지 구분할 수 있다.
3. 둘 다에게 공통적으로 사용되는 메소드면, Validator를 클래스로 만드는게 어떨까?
그러면 도메인별로 Validator 클래스를 만들어보자!
public abstract class Validator<T> {
private static String ERROR_HEADER = "[ERROR] ";
protected static void throwError(String errorMessage) {
throw new IllegalArgumentException(ERROR_HEADER + errorMessage);
}
abstract void validate(T param);
}
Validator를 추상 클래스로 선언하고, XXXValidator에서 Validator를 상속받아 사용하는 방법을 생각했다.
🤔 Validator를 인터페이스가 아닌 추상클래스로 만든 이유
여기서 Validator를 인터페이스가 아닌 추상클래스로 만든 이유는,
각종 Validator들에서 thorwError라는 함수 구현체를 공통으로 사용하기 위해서이다!
인터페이스는 추상클래스와 달리 구현부가 있는 일반 메서드, 일반 멤버 변수를 가질 수 없다.
( 추후에 안 것인데, 자바8 이후부터는 인터페이스에서도 static, default 메소드를 사용할 수 있다. 따라서 인터페이스로 만들어도 됐다! )
public class ParticipantsValidator extends Validator<List<String>> {
private static final String DUPLICATE_CAR_NAME = "자동차 이름은 중복될 수 없습니다.";
@Override
public void validate(List<String> carNames) {
validateDuplication(carNames);
}
private void validateDuplication(final List<String> carNames) {
if (carNames.stream().distinct().count() != carNames.size()) {
throwError(DUPLICATE_CAR_NAME);
}
}
}
이런식으로 각종 Validator들을 만들어줬다!
4. 도메인을 생성할 때 마다 Validator 인스턴스를 만드는 것은 좋지 않은 방법인 것 같다.
그렇다면 두가지 선택지가 있을 것 같다.
1. static 메서드
validate 함수를 static 메서드로 만드는 방법을 생각해봤다.
하지만 static 함수를 쓴다면 Override하지 못한다.
난 Validator를 Override해서 쓰고싶은걸..
2. Validator 객체들을 싱글턴으로 만들기
public class ParticipantsValidator extends Validator<List<String>> {
public static final ParticipantsValidator PARTICIPANTS_VALIDATOR = new ParticipantsValidator();
private ParticipantsValidator() {
}
@Override
public void validate(List<String> carNames) {
...
}
}
싱글턴으로 객체를 생성해서 도메인과 뷰에서 하나의 Validator 인스턴스를 사용하도록 했다.
5. 시도 횟수의 경우, InputView에서는 String을 검증해야하고 Domain에서는 Int를 검증해야한다.
InputView에서는 사용자에게 입력받은 String 시도 횟수를 검증해야한다.
Domain에서는 파라미터로 전달받은 Int 시도 횟수를 검증해야한다.
같은 CountValidator를 사용하고싶은데.. 어떻게 해야할까?
여기서 자바의 특징중 하나인 오버로딩이 등장한다! (오버라이딩 말고 오버로딩)
public class CountValidator extends Validator<Integer> {
@Override
public void validate(final Integer count) {
validateRange(count);
}
public void validate(final String count) {
validatePositiveNumber(count);
validate(Integer.parseInt(count));
}
private void validatePositiveNumber(final String count) {
final String POSITIVE_NUMBER_REGEX = "^[1-9]\\\\d{0,8}$";
if (!count.matches(POSITIVE_NUMBER_REGEX)) {
throwError(NOT_PROPER_COUNT);
}
}
private void validateRange(final int count) {
final int MIN_COUNT = 1;
final int MAX_COUNT = 999_999_999;
if (count < MIN_COUNT || count > MAX_COUNT) {
throwError(NOT_PROPER_COUNT);
}
}
}
오버라이딩한 메서드를 오버로딩해도 될까?라는 의문을 가졌지만
오버로딩은 객체지향 프로그래밍 특징 중 하나인 다형성(polymorphism)을 구현하는 방법 중 하나로, 자바의 장점이라 생각해 괜찮다는 결론을 내렸다. (물론 나 혼자의 결론임)
6. 그래서, Domain과 InputView에서 Validator 코드를 공유하는게 맞는 판단일까?
처음으로 돌아가보자.
지금까지의 흐름을 세줄요약 해보면
- View와 Domain 둘 다에서 유효성 검사를 진행하기 위해
- Validator 클래스를 생성해
- View와 Domain에서 공유했다.
그런데, View와 Domain에서 Validator를 공유하는게 과연 맞는 판단일까?
MVC 패턴에서 겨우겨우 뷰와 도메인을 분리해놨는데, 이렇게되면 다시 View와 Domain의 연결고리가 생기게된다.
InputView에서 제약조건이 하나 더 생겼다고 가정하자. 그러면 InputView에 대한 입력값 검증이 하나 더 추가되어야한다.
이 때, Validator에서 코드 변화가 생기고, 이 변화는 도메인에도 영향을 끼칠 가능성이 있다.
그러므로, Domain과 InputView에서 따로 유효성 검사를 해주는 게 옳다고 판단했다.
이 때 코드에서 생긴 코드 중복은 살짝 눈 감아주도록 하자.. 😌
7. Domain과 InputView에서 실행할 유효성 검사를 나누어보자
제약조건은 두가지로 나눌 수 있다.
a. 인풋을 오류 없이 잘 받아내기 위해 생긴 제약조건
b. 도메인 규칙
a를 검증하는 것은 InputView의 책임이고, b를 검증하는 것은 Domain의 책임인 것 같다.
자동차 미션 제약조건들을 분류해보면
a. for Input
- 빈 입력값이 입력될 수 없다.
- 시도횟수에 숫자가 아닌 입력값이 들어올 수 없다.
b. for Domain
- 자동차 이름의 길이는 1자 이상 5자 이하이다.
- 자동차 이름은 중복될 수 없다.
와 같은 것들이 있을 것 같다.
a는 InputView에서 검증하고, b는 Domain에서 검증하자!
7. 그러면 Validator 객체를 만들까, 클래스 내 메서드로 정의할까?
메서드로 진행해도 될 것 같다.
사실 이 부분에 대한 확신은 없다.
Domain에서 검증하고자 하는 규칙은 Domain 규칙인데, 이를 Domain 클래스내에 넣으면 Domain 규칙까지 한 곳에서 확인 가능하니 응집도가 높다할 수 있지 않을까?
그래서 나는 Domain내의 메서드로 validate 함수를 선언해 사용했다.
public class CarName {
private static final int MIN_CAR_NAME_LENGTH = 1;
private static final int MAX_CAR_NAME_LENGTH = 5;
private final String name;
public CarName(final String name) {
validateLength(name);
this.name = name;
}
private void validateLength(final String carName) {
if (carName.length() < MIN_CAR_NAME_LENGTH || carName.length() > MAX_CAR_NAME_LENGTH) {
throw new IllegalArgumentException();
}
}
}
나의 Validator에 대한 기나긴 고민의 과정은 여기까지이다.
세줄 요약해보자.
1. View와 Domain 둘 다에서 유효성 검사함.
2. View와 Domain의 유효성 검사는 따로 한다.
3. View와 Domain 클래스 내 메서드로 검증을 수행함.
이게 최고의 방법인지에 대한 확신은 없다. (아마 아닐것이다)
하지만 내가 이렇게 코드를 짬에 있어서, 모두 이유가 동반했기 때문에 아주 만족한다.
이유를 가지고 코드를 작성하는게 가장 중요한 것 아닐까?
'우테코 5기 > 레벨1' 카테고리의 다른 글
[체스 미션] 상속 대신 전략패턴 도입하기 (4) | 2023.03.28 |
---|---|
정적 팩토리 메서드로 생성자 다이어트 시키기 (4) | 2023.03.05 |
전략 패턴을 통한 테스트 (2) | 2023.02.17 |
View단에서 도메인 객체를 생성해 return하는게 좋을까, 아니면 입력값을 그대로 return하는게 좋을까? (3) | 2023.02.17 |
상수를 하나의 클래스에 모으는 것이 좋을까? (0) | 2023.02.17 |