-
[Java] 객체 필드 Validation 손쉽게 구현하기 (feat. Bean Validation & @NotNull)스터디 노트 2023. 10. 12. 14:04
가끔 Domain을 구성하다 보면 자연스레 필수적으로 받아야만 하는 데이터들이 있다.
예를 들어 보면 Account라는 계좌 정보를 나타내는 Domain이 있다고 해보겠다.
@Getter @AllArgsConstructor public class Account { private String accountId; private String username; private String phoneNumber; }
당연히 있어야 하는 계좌번호를 간단하게 accountId라고 나타내고 사용자 명을 username, 그리고 핸드폰 번호를 받는 구조로 구성이 되어있다.
너무나도 당연하게 위 필드들은 모두 Null을 허용하지 않을 생각이다.
비즈니스 관점에서 이 Account라는 도메인의 인스턴스를 활용할 때, 각 필드에 null을 수동으로 체크하는 방식으로 해도 사실은 무관하다..
public class AccountService { ... public boolean accountValidation(Account account) { if (account.getAccountId() == null) return false; ...반복 } ... }
쉽지 않은 길이지만 가능은 하다. 서비스에서 도메인 필드의 검증 로직을 비즈니스 로직 중 해당 인스턴스의 필드 사용 시점에 검증을 할 수 있게 만들어 놨다...💩
정상적으로 동작하기에 '됐다'하기엔 마음이 찜찜한 상태이다.
사실 비즈니스와 필드의 validation은 연관을 가질 수도 있긴 하지만, 비즈니스적 관점에서 보면 '굳이' 몰라도 되는 정보다.
애시당초 도메인이란 데이터를 담는 그릇이기에 그 도메인이 담고 있는 데이터를 신뢰할 수 있다는 전제 하에 비즈니스 로직을 구현하는게 조금 더 개발친화적인 방법이라 생각한다.
(오류의 위험도도 더 떨어트릴 수 있는 방법이라 생각하기도 하고..)각설하고 조금 더 멋지게 고칠 수는 없을까?🙄
사실 null을 검증하려면 다양한 방법이 있다.
그 중 여기서 예를 들어볼 것은 생성자에서 검증하는 방식이다.
Objects 클래스의 requireNonNull()함수를 사용하면 간편하게 null을 검증할 수 있다.
public class Account { private String accountId; private String username; private String phoneNumber; public Account(String accountId, String username, String phoneNumber) { Objects.requireNonNull(accountId); Objects.requireNonNull(username); Objects.requireNonNull(phoneNumber); this.accountId = accountId; this.username = username; this.phoneNumber = phoneNumber; } }
이렇게 null을 허용하지 않는 생성자를 만들어 냈다. 클린해보이..는가?🤢
개인적으로 잘 모르겠다. 그리고 가장 큰 문제는 이런 작업을 객체 생성자에 모두 그것도 필드 마다 선언을 해주어야 한다.
이럴꺼면 사실 비즈니스이 도메인에 종속되지 않는다는 장점 이외에 무슨 장점이 있는지 잘 모르겠다.
그냥 권한을 위임했다 정도일 뿐이지..
그럼 조금 더 글뤄벌한 방식은 없을까?
참 다행히도 해당 프로젝트에서 SpringBoot를 활용하고 있다면, 가볍게 다음 의존성을 추가해준다.
멋지게 추가된 의존성을 보며 Gradle을 한 번 볶아주고 다시 Account로 돌아가서 조금 더 멋지게 고쳐볼까 한다.
만약 많은 객체들이 상속을 받는 부모 클래스를 통해 각 객체들의 필드에 대한 데이터 검증을 시행해보면 어떨까?
그렇다면 공통적으로 Validation을 검증해주는 상속 가능한 추상 클래스를 만들어 객체에 상속을 적용해보자.
public abstract class SelfValidation<T> { private Validator validator; public SelfValidation() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); } protected void validate() { Set<ConstraintViolation<T>> validateSet = validator.validate((T) this); if (!validateSet.isEmpty()) { throw new ConstraintViolationException(validateSet); } } }
Jakarta Bean Validation 라이브러리 내 포함되어 있는 Validation 클래스를 통해 Validator를 얻어낸다.
그리고 그 Validator의 validate 메소드를 통해 선언된 객체의 필드들을 검증하는 작업을 진행한다.
이렇게 만들어진 추상 클래스를 Account 클래스에서 상속한다.
그리고 validate() 메소드를 통해 필드 값 검증 작업을 수행하도록 처리한다.
@Getter public class Account extends SelfValidation { @NotNull private String accountId; @NotNull private String username; @NotNull @Pattern(regexp = "^[\\d]{2,3}-[\\d]{3,4}-[\\d]{4}+$") private String phoneNumber; public Account(String accountId, String username, String phoneNumber) { this.accountId = accountId; this.username = username; this.phoneNumber = phoneNumber; this.validate(); } }
@NotNull Annotation이 추가되어 해당 필드의 null 값을 검증하였고, 전화번호의 경우 null 검증과 동시에 패턴 검증의 기능을 추가시켰다.
그리고 강력해진 생성자에 SelfValidation 추상 클래스에서 제공해주는 validate() 함수를 호출하도록 하면 준비는 끝!
테스트를 해보자.
가볍게 메인 메소드를 만들어 테스트를 해보았다.
public static void main(String[] args) { Account account = new Account(null, "deguruv", "101"); }
위 메인 메소드에서는 두 가지 입력값이 잘못 들어갔다.
accountId가 null로 들어갔고, phoneNumber가 형편없이 들어갔다.
개떡같이 던져주니 우리가 구현한 SelfValidation은 똑똑하게 틀린 값들을 잡아내주었다.
그리고 친절하게도 어떠한 필드의 뭐가 잘못 되었는지 상세한 로깅과 함께 Exception을 발생시켜준다. 😚
phoneNumber: "^[\d]{2,3}-[\d]{3,4}-[\d]{4}+$"와 일치해야 합니다, accountId: 널이어서는 안됩니다
우리가 프로젝트를 구성하다보면 수많은 객체와 필드를 생성하게 된다.
그리고 너무나도 당연하게 비즈니스 로직에서 도메인을 사용할 때 필드 값에 대한 신뢰가 동반되어야 할 것이다.
해당 자료는 도서 '만들면서 배우는 클린 아키텍처'의 내용을 참고하여 작성되었습니다.
'스터디 노트' 카테고리의 다른 글
[Java] IntStream 메소드 사용해보기 (0) 2023.10.16 자바 JDK 9 부터 JDK17까지의 주요 코딩 특징 (0) 2023.10.12 자바 21 특징 - SequencedCollection (2) 2023.10.10 자바 21 특징 - 가상 쓰레드 (1) 2023.10.09 자바스크립트 코드 프로처럼 쓰는 팁 (1) 2023.10.08