Java로 프로그램을 만들다 보면 어떤 데이터의 유효성 검증을 어디에서 어떻게 수행하는게 좋을지 고민을 하게 된다.
특히 사용자의 입력값은 어떻게 들어올 지 모르는 데이터이기 때문에 유효성 검증을 어디에서 할 지 정하는게 중요하다.
사용자 입력의 유효성 검증 및 파싱 단계는 다음과 같다.
기본 입력 및 입력 형식 검증 : 빈 문자열이 아닌지, 숫자면 숫자가 맞는지, 입력 형식에 맞는지 검사
ex) 바비큐립-1,초코케이크-2,제로콜라-1
필요 데이터 추출 : 요구사항에 맞게 필요한 데이터로 변환
ex) 바비큐립, 1
초코케이크, 2
제로콜라, 1
비즈니스 로직 유효성 검증 : 비즈니스 로직에 올바른 데이터인지 검증
ex) 개수 총합이 20 이하, 이름에 맞지 않는 메뉴 등
로직에 적용 : 최종적으로, 검증된 데이터를 로직에 활용
ex) Order 객체 생성
이 단계들은 각각 알맞은 계층의 알맞은 클래스에서 수행되어야 코드 유지보수가 용이해지고, 오류 가능성도 줄어들고, 테스트와 디버깅이 편해진다.
MVC 패턴을 기준으로, 위 단계들은 View
, Controller
, Model
계층에서 수행된다.
Controller
: 일반적
View
: 빠른 기본 유효성 확인
기본적인 유효성 검증은 일반적으로 컨트롤러에서 수행되지만 사용자가 입력한 정보가 기본적인 요구사항을 충족하는지 빠르게 확인하기 위해 View 에서 수행해도 된다. (ex 이메일 형식 검증)
Controller
: 대부분의 경우
뷰에서 전달받은 데이터를 기반으로 정보를 추출하고, 모델이 이해할 수 있는 형태로 변환한다. 복잡한 비즈니스 로직을 표함할 경우 다른 레이어에서 수행도 고려할 수도 있다.
Model
비즈니스 규칙 유효성 검증은 모델(도메인) 레이어에서 주로 수행한다. 모델 레이어는 비즈니스 로직과 관련된 데이터의 무결성을 유지하는 데 중요한 역할을 한다.
Service
검증된 데이터를 사용하여 최종 비즈니스 로직을 수행한다.
(ex 도메인 인스턴스 생성, 로직 계산)
각 단계에서의 객체들은 본인이 알아야할 정보를 검증하며 올바르지 않을 경우 예외를 발생시켜야 한다.
즉, 사용자의 입력이 올바르지 않을 경우, 예외를 발생시키고 처리 (ex 재입력 요구) 를 해야 하는데, 검사 단계가 나누어져 있다 보니 이것을 한번에 처리하기 번거롭다.
이것을 수행하려면, 사용자의 입력을 받고, 기본적인 검증을 하고, 비즈니스 로직 유효성 검증까지 모두 한 구간에서 수행한 후, 예외가 발생하면 그 구간의 처음으로 돌아가서 다시 사용자의 입력을 받으면 된다.
아래는 그것의 예시 코드이다.
사용자 입력으로 Order
객체를 만드는 과정을 수행한다.
GameController
public class GameController {
public void run() {
Order order = requestOrder();
}
private Order requestOrder() {
return executeWithRetry(() -> {
String inputOrder = inputView.readOrder(); // 1. 기본 입력 검증 및 입력 형식 검증
Map<String, Integer> parsedOrder = parseOrder(inputOrder); // 2. 필요 데이터 추출
return new Order(parsedOrder); // 4. 로직에 적용
});
}
// 재시작 로직을 재사용 하기 위한 메소드
public <T> T executeWithRetry(Supplier<T> action) {
while (true) {
try {
return action.get();
} catch (Exception e) {
outputView.printErrorMessage(e);
}
}
}
// 필요 데이터 추출 하는 메소드 parseOrder..
}
InputView
클래스public class InputView {
public String readOrder() {
outputView.printReadOrderMessage(); // 입력 안내 출력
String input = Console.readLine()
// 1. 기본 입력 검증 및 입력 형식 검증
if (isInvalidOrder(input)) {
throw new OrderException(); // 사용자 정의 예외 발생
}
return input;
}
// 기본 입력 검증, 형식 검증 메소드 isInvalidOrder ..
}
Order
public class Order {
private final Map<String, Integer> menuQuantities;
public Order(Map<String, Integer> parsedOrder) {
// 3. 비즈니스 로직 유효성 검증
validateOrder(parsedOrder);
this.menuQuantities = new HashMap(parsedOrder);
}
// 유효성 검증 메소드..
}
프로그램의 크기가 크지 않다는 전제하에
├── controller
│ ├── GameController.java
│ └── InputParser.java : 입력 문자열을 모델이 알아듣는 형식으로 변환
├── model
│ ├── Order.java : 클래스에 알맞은 비즈니스 로직 유효성 검증
│ └── OrderService.java : 비즈니스 로직 수행
└── view
└── InputView.java : 기본 입력 및 형식 검증
이런 식으로 구성하면 문제 없다.
만약 InputParser가 비즈니스 로직을 깊게 사용한다면 Service 패키지로,
비즈니스 로직을 사용하지는 않지만 여러 부분에서 재사용 된다면 util 패키지로 이동하면 된다.