![[Develop] 모델 검증 로직은 어디에 위치하는게 좋을까](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Flgtez%2FbtsO2W8oI9h%2FAAAAAAAAAAAAAAAAAAAAABzZ1785hWm17GWfX2YCYWG70PaMf7GMbZbKXgxeFljj%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1753973999%26allow_ip%3D%26allow_referer%3D%26signature%3D%252BcTlewv1U%252FW3NXrn6C4JNMtnYfI%253D)
개요
프로그래밍을 하다 보면 반드시 한 번쯤은 "모델 검증 코드를 어디에 둘 것인가?"라는 질문을 마주하게 된다. 처음에는 대수롭지 않아 보이지만, 프로젝트 규모가 조금만 커져도 코드의 유지보수와 팀 내부의 합의에 직접적인 영향을 주는 논쟁거리로 떠오른다. 현업 개발을 시작한 지 얼마 되지 않았지만, 각종 프로젝트들을 경험해보며 여러 아키텍처 글과 실전 코드들을 뒤적이다가 "내가 정말 제대로 이해하고 있나?", "지금 선택이 과연 최선일까?"라는 의문을 반복해서 던지곤 한다.
그래서 이번 글에서는 모델 검증 코드를 모델 내부에 둘지, 아니면 외부의 별도 Validator로 뺄지를 결론부터 단순히 내려버리기보다, 흐름의 주권, 즉 책임이 어디에 머무르고 언제 이동하는가라는 관점에서 보려고 한다.
그전에... 검증 코드, 왜 고민해야 할까?
모델 검증이란 결국 이메일 형식이 올바른지, 금액이 음수가 아닌지, 상태값이 허용된 범위에 있는지 등을 확인하는 일이다. 겉보기에 단순한 작업 같지만 이 검증을 모델 안에 묻어둘지, 아니면 호출부에서 수행할 지에 따라 객체 수명 주기가 달라질 수 있다. 즉, 누가 언제 책임을 가질 지가 코드 구조 전반을 바꿀 수 있게 된다.
모델 내부 검증
모델 내부 검증을 선택하면, 객체는 생성될 때부터 유효성을 스스로 증명해야 한다. 예컨대 아래와 같이 `User` 클래스를 작성하면, 잘못된 이메일 값이 유입되는 순간 예외가 바로 발생하기 때문에 `User`는 결코 유효하지 않은 상태로 존재하지 못한다. ▼
class User {
constructor(email) {
if (!email || !email.includes("@")) {
throw new Error("invalid email");
}
this.email = email;
}
}
이 방식의 핵심은 불변성에 대한 강제력이다. 디버깅을 할 때 문제가 객체 내부에서 즉시 드러나므로 책임 소재가 모호해질 틈이 없다. 다만 검증 로직이 복잡해질수록 모델이 비대해져 Fat Model이 되기 쉽고, 여러 객체나 외부 시스템과 엮인 복합 규칙을 한 클래스 안에 모두 담기는 벅찰 수 있다. 게다가 일부 프레임워크는 생성자나 세터 단계에서의 예외를 제한하므로, 기술적 한계에 부딪히는 순간도 적지 않다.
외부 Validator
반대로 모델을 단순 데이터 컨테이너로 보고 검증 책임을 외부로 옮기면 다음과 같은 구조가 된다. ▼
class User {
email: string;
constructor(email: string) {
this.email = email;
}
}
class UserValidator {
static validate(user: User): void {
if (!user.email || !user.email.includes("@")) {
throw new Error("invalid email");
}
}
}
이 패턴은 모델을 가볍게 유지할 수 있고, 여러 도메인이나 여러 가지의 맥락에서 동일한 Validator를 공유함으로써 검증 규칙을 재사용하거나 조합하기도 쉽다. 하지만 User 객체가 유효하지 않은 상태로 얼마든지 프로그램 내부를 돌아다닐 수 있다는 위험을 감수해야 한다. 실수로 Validator 호출이 누락되면 버그는 조용히 숨어들고, 불변 조건은 깨진다. 결국 검증을 언제, 어디서, 반드시 호출할 것인가를 테스트와 코드 리뷰, 정책으로 강제해야 한다.
책임 주권의 이전과 회수
동기, 비동기 프로그래밍에서 제어 흐름이 함수 호출과 콜백 사이를 오가는 시점을 따졌듯, 검증 책임도 객체 내부에서 외부로, 혹은 그 반대로 이동하며 주권을 교환한다. 내부 검증은 객체가 생성, 변경되는 매 순간 스스로 주권을 지키고, 외부 Validator 모델은 필요한 시점마다 주권을 받아와 검사를 수행한다. 문제는 두 호출 사이 공백기에 객체가 무주권 상태로 방치될 가능성이다.
주권 상태
주권이 객체 내부에 고정되면 불변성이 강하게 보장되고, 문제의 원인을 추적하기도 수월하다. 반면 검증 규칙이 복잡해질수록 클래스가 무거워지고, 단일 객체의 시야로는 해결하기 힘든 도메인 간 교차 규칙을 품기가 어렵다.
주권이 고정되지 않고 퍼지는 경우, 외부 Validator는 규칙을 모듈화해 다양한 문맥에서 재활용할 수 있지만, 호출 누락 위험을 끊임없이 경계해야 하며, 여러 팀 또는 마이크로서비스가 같은 객체를 각자의 시각으로 검증할 때 서로 다른 결과가 나올 수도 있다. 주권이 흩어지면서 각 객체의 방식으로 해석할 수 있기 때문이다. 그렇게 되면 결국 검증 결과를 다시 동기화하는 추가 계층이 필요해지는데, 이는 내부 검증으로 얻는 단순성과 크게 다르지 않은 복잡도를 낳는다.
책임 분산이 가져다주는 유연성과 그 한계
현실 세계의 연필, 가위, 칼 같은 도구들이 스스로 안정성을 검사하지 않고 사용자가 용도에 맞게 점검하듯, 외부 Validator가 때로는 훨씬 직관적으로 느껴진다. 특히 대규모 시스템이나 여러 팀이 협업하는 환경에서 한 객체가 다양한 컨텍스트를 거칠 때, 호출부에서만 필요한 규칙을 적용하면 확실히 유연하다.
그러나 모든 팀이 객체의 의미를 동일하게 해석하지 않을 가능성도 커진다. 어떤 도메인은 객체를 A'라 정의하고, 다른 도메인은 같은 객체를 B로 정의한다면, 결국 그 차이를 조율할 검증 계층을 또 마련해야 한다.
하이브리드 방식 : 불변성은 포기할 수 없다
다수의 소프트웨어 버그는 "기술적으로는 통과했지만 의미론적으로는 잘못된 상태"에서 출발한다. 내부 검증은 이 상태를 구조적으로 차단해 버린다. 그래서 외부 Validator를 쓰더라도, 적어도 절대 깨지면 안 되는 최소 불변 조건만큼은 엔티티 내부에 남겨 두는 하이브리드 설계가 자주 권장된다. (이럴거면 처음부터 글을 하이브리드 설계가 좋다고 썼어야 했는데 독자를 낚은 느낌이다.) 예를 들어, 금융 거래 객체라면 통화 단위나 금액 부호, 허용 범위 같은 필수 제약은 내부에 두고, 변동이 생기는 지역별 세율과 같은 변동성이 큰 조건은 Validator로 빼는 식이다.
현실적 제약과 타협 지점
실제 프로젝트에서는 팀 합의, 프레임워크 한계, 성능, 레거시 API 연동, 테스트 전략 등 현실적 요소가 설계를 흔든다. 대규모 배치 작업에서 수 백만개의 아이템들을 한 번에 갱신해야 한다면, 내부 검증이 오히려 병목이 될 수도 있다. (물론 프론트엔드에서는 그정도의 작업이 수행되면 렌더링단에서 막히겠지만서도 가능성이란 존재하니 말이다.) 또한 서로 다른 언어 스택이 섞인 프로젝트에서는 REST를 이용한 공유 Validator를 구축해 중앙 집중형 검증을 선택하기도 한다. 결국 "객체가 일시적으로라도 무효 상태로 존재할 수 있게 만들었을 때 얻는 이득이, 그로 인해 생길 수 있는 오류 위험보다 큰가?"라는 질문에 프로젝트 팀이 어떻게 답하느냐에 따라 전략이 바뀌게 된다.
구조적 가이드라인
검증 책임을 설계 초기부터 시각화하면 프로젝트가 한참 진행되고 나서 뒤엎어야하는 그런 후폭풍을 줄일 수 있다. UML 클래스, 시퀀스 다이어그램으로 객체 생성, 변형, 소멸 과정을 그려서 검증 호출이 빠질 가능성을 확인하고, 단위, 통합, E2E 테스트에 검증 누락 시나리오를 포함해 런타임에서도 책임 이동이 지켜지는지 검증한다. 코드 리뷰 체크리스트에는 Validator 호출 선행 여부를 명시적으로 넣어 실수를 방지한다. 이런 과정을 통해 선택한 검증 로직을 더 안전하게 사용할 수 있다.
마치며
이분법적으로 선택하는 것이 불가능하기에 하이브리드라는 선택지도 나온 마당에 선택에 절대적인 정답은 없다. 중요한 것은 문제가 터졌을 때 즉시 책임질 주체가 보이는 구조를 마련하는 일이다. 내부 검증은 강력한 불변성을, 외부 Validator는 유연성을 제공하지만, 각각 Fat Model과 호출, 주권 누락 가능성이라는 대가를 요구한다. 그렇기에 프로젝트 특성과 팀 역량, 조직 구조까지 고려해 어디서 책임을 넘기고 회수할지 정교하게 설계해야 한다.
'Develop > Develop' 카테고리의 다른 글
[Develop] 제어 주도에 따른 동기와 비동기 (0) | 2025.05.12 |
---|---|
[Develop] svg vs png : 뭐가 더 좋을까? (0) | 2025.02.07 |