[Spring] Validation은 어디서 해야할까?

su_y2on·2022년 5월 25일
3

Spring

목록 보기
27/30

Validation은 어디서 해야할까?

하나의 작은 프로젝트(?)를 만들다 보면 슬슬 고민이 되는 부분 바로 유효성검사이다! 크게 Controller, Service, Repository, Entity(model)로 나눠서 작업하는 경우가 많은데 그렇다면 요청에서 넘어온 값들에 대한 유효성검사는 어디서 해주는 것이 좋을까? 마침 팀원들도 같은 고민이 있었고 다양한 의견이 오갔다. 들어오자마자 바로 해주자! 일관성있게 어느 단계에서 모두 해주자 등등..

물론 정답은 없겠지만 그래도 많은 선배 개발자분들도 같은 고민을 했을 것이고 그래도 어느정도 이런게 좋다라는 convention같은게 있지 않을까? 하는 마음에 조사를 시작하였다🧐🧐🧐

여러자료를 찾아볼 수 있었고 제가 이해한 내용들을 정리보았습니다🔥





먼저! 유효성검사란??

어플리케이션에서 사용자의 입력에 대해 유효한지 검사하는 것

유효성 검사는 구체화와 같은 것이다!

구체화(Specification)과 유효성검사(validation)을 구분하는 경우가 있는데 사실 이는 같은 것이다. 예를들어서 client는 JSON data로 요청해야한다 → JSON data가 아닌 요청은 거부한다이다. 즉 구체화와 유호성은 함께 가는 것이다.





그렇다면.. 어디서 하는게 좋을까??

1. Client

빠르게 검증이 가능하지만 validation을 뚫을 수 있기 때문에 client에서만 검증을 하는 것은 안됩니다

또한 DB를 통해서 검증가능한 것들(중복성 검사..)은 불가능합니다.

2. Server

안정적인 유효성 검사가 가능하고 DB를 통한 검증도 가능합니다. 하지만 Server에서만 유효성검사를 한다면 빠른 검증이 불가능하기때문에 Client에서 할 수 있는 것은 최대한 하고 안전장치로 Server에서 유효성검사를 진행하는 것이 좋습니다

📎 둘중에 하나에서만 해야한다면 server! 둘다하면 제일 best



그렇다면 Server에서는 어디에 유효성검사를 두면 좋을까?

사실 답은 없다. 즉 각각의 layer에 적절한 유효성 검사를 위치시켜야 책임이 분산되는것이고 제대로 유효성 검사를 할 수 있다!



Controller Layer : shapes and types

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으로 검증이 가능하기 때문입니다.



Service Layer : from types to concepts

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);
    }
}


Domain Layer : guarding invariants

여기서는 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를 잘 사용해보시기바랍니다~




참고자료

In Clean Architecture, where to put validation logic?

Where does my validation live?

0개의 댓글