Validation은 어디서 해야할까?
하나의 작은 프로젝트(?)를 만들다 보면 슬슬 고민이 되는 부분 바로 유효성검사이다! 크게 Controller, Service, Repository, Entity(model)로 나눠서 작업하는 경우가 많은데 그렇다면 요청에서 넘어온 값들에 대한 유효성검사는 어디서 해주는 것이 좋을까? 마침 팀원들도 같은 고민이 있었고 다양한 의견이 오갔다. 들어오자마자 바로 해주자! 일관성있게 어느 단계에서 모두 해주자 등등..
물론 정답은 없겠지만 그래도 많은 선배 개발자분들도 같은 고민을 했을 것이고 그래도 어느정도 이런게 좋다라는 convention같은게 있지 않을까? 하는 마음에 조사를 시작하였다🧐🧐🧐
여러자료를 찾아볼 수 있었고 제가 이해한 내용들을 정리보았습니다🔥
어플리케이션에서 사용자의 입력에 대해 유효한지 검사하는 것
구체화(Specification)과 유효성검사(validation)을 구분하는 경우가 있는데 사실 이는 같은 것이다. 예를들어서 client는 JSON data로 요청
해야한다 → JSON data가 아닌 요청은 거부
한다이다. 즉 구체화와 유호성은 함께 가는 것이다.
빠르게 검증이 가능하지만 validation을 뚫을 수 있기 때문에 client에서만 검증을 하는 것은 안됩니다
또한 DB를 통해서 검증가능한 것들(중복성 검사..)은 불가능합니다.
안정적인 유효성 검사가 가능하고 DB를 통한 검증도 가능합니다. 하지만 Server에서만 유효성검사를 한다면 빠른 검증이 불가능하기때문에 Client에서 할 수 있는 것은 최대한 하고 안전장치로 Server에서 유효성검사를 진행하는 것이 좋습니다
📎 둘중에 하나에서만 해야한다면 server! 둘다하면 제일 best사실 답은 없다. 즉 각각의 layer에 적절한 유효성 검사를 위치시켜야 책임이 분산되는것이고 제대로 유효성 검사를 할 수 있다!
user가 보낸 data의 형식이 예상했던 것과 같은지 검사해야한다. 예를들어 아래와 같은 data를 기대했다면
let body = {
"currency": "EUR",
"amount": 1000,
"options": {
"locale": "nl_NL"
}
}
1) payload
에는 유효한 JSON data형식
이어야한다.
2) currency는 string
이어야한다.
3) amount는 integer
이어야한다.
4) option은 object
이며 그 안에 localed은 string
이어야한다.
따라서 아래와 같은 유효성 검사가 이뤄져야합니다.
if ( ! is_string(request.currency)
|| ! is_integer(request.amount)
|| ! is_valid_options(request.options)) {
throw BadRequestException("This is a bad request! No bueno!");
}
이때 적극적으로 권하는 방식은 DTO
를 이용하는 것입니다. 왜냐하면 DTO는 java같은 경우 language-level
에서 type으로 검증이 가능하기 때문입니다.
Domain Layer에 갈 수 있는 상태로 만들어주어야 합니다. 예를 들어 PaymentMethod라는 string변수는 여러 string값을 갖을 수 있었지만 PaymentMethod는 비즈니스 적으로 'ideal', 'creditcard', 'banktransfer'
중에 하나여야 한다면 Service Layer에서 잡아줘야합니다. validation은 아래와 같이 할 수 있습니다. 이때 아래와 같이 VO(value object)
를 활용하면 좋습니다.
class PaymentMethod {
private constructor(readonly name: string) {}
public static fromString(name: string): PaymentMethod {
const valid = ['ideal', 'creditcard', 'banktransfer'];
if ( ! valid.includes(name)) {
throw new SorryPaymentMethodIsNotValid(name);
}
return new PaymentMethod(name);
}
}
여기서는 invariants(항상 사실이어야하는 것)를 체크해줘야합니다. 예를 들어 은행시스템에서 자신이 가지고 있는 돈보다 더 많이 인출하는 것은 불가능하겠죠? 아래와 같은 계좌를 체크하는 VO가 있다고 해봅시다. 여기서 value는 number인지만 잡아줄 뿐입니다.
class Amount
{
constructor(
readonly value: number,
readonly currency: Currency,
) {}
}
따라서 BankAccount라는 domain에 아래와 같이 spend함수에서 validation을 넣어줍니다
class BankAccount
{
public spend(amount: Amount): bool
{
if (this.balance < amount.value) {
return false;
}
this.balance -= amount.value;
return true;
}
}
이때 추천하는 방식은 함수로 따로 빼서 가독성과 재사용성을 높이는 것입니다.
class BankAccount
{
public spend(amount: Amount): bool
{
// guard first
this.guardAgainstOverspending(amount);
// change data later
this.balance -= amount.value;
}
private guardAgainstOverspending(amount: Amount): void
{
if (this.balance < amount.value) {
throw new SorryNotEnoughBalanceForSpending(amount);
}
}
}
칼같은 기준은 없습니다. 위에서 제시한 기준들을 따라도 애매한 케이스가 생길 수 있습니다. 그럴때는 그때그때 선호와 상황에 따라 판단해야합니다. DTO와 VO를 잘 사용해보시기바랍니다~