본문 바로가기
Spring 프레임워크/이론

[ 스프링 프레임워크 ] Validation / Data Binding

by Hwanii_ 2023. 10. 20.
728x90

Validation 이란 ?

>> 유효성 검증을 뜻하는 단어.

주로 사용자 또는 서버의 요청 (http request) 내용에서 잘못된 내용이 있는지를 꼼꼼하게 확인하는 단계를 의미 한다.

 

 

 

Validation 종류

학문적으로 여러 세부적인 단계들이 존재 한다.

>> 실제로 개발자가 주로 가지고 가야 하는 검증은 크게 두 종류로 나뉜다.

1)
데이터 검증

- 필수 데이터의 존재 유무

- 문자열의 길이 또는 숫자형 데이터의 경우 값의 범위

- email, 신용카드 번호 등 특정 형식에 맞춘 데이터



2)
비즈니스 검증

- 서비스에 정책에 따라 데이터를 확인 하여 검증
(배달앱에서 배달 요청을 할 때, 해당 주문건이 결제 완료 상태인지 확인 하기 등)

- 경우에 따라 외부 API를 호출하거나 또는 DB의 데이터 조회까지 하는 검증도 존재 한다.

 

 

 

Spring의 Validation

>> 스프링의 웹 레이어에 종속적이지 않는 방법으로 밸리데이션을 하려고 한다.

 

웹 레이어에 종속적이지 않다 라는 말은 다음과 같다.

스프링 프레임워크의 특정 기능에 의존 하지 않고, 별도로 데이터 유효성 검사를 진행 한다는 것이다.

 

이는, 스프링 프레임워크에 유효성 검사 로직을 완전히 종속 시키지 않는 것을 의미 하게 된다.

 

그러면,

스프링 프레임워크에 종속 되지 않으므로, 다른 프레임워크나 웹 플랫폼과 동작 할 수 있다는 것을 의미하게 된다.

결국 유연함과 확장성의 장점을 가지게 된다.

 

예를들어서,

아래의 Java Bean Validation은 스프링 에서 지원하는 내부 라이브러리가 아닌, 외부 자바 유효성 검사 라이브러리 이다.

 

주로 두가지 방법을 활용 하여 밸리데이션을 진행 한다.
(비즈니스 검증은 아니고, 둘다 데이터 검증 이다)

 

1)
Java Bean Validation

 

JavaBean 기반으로 간편하게 개별 데이터를 검증.

JavaBean 이란 ?

데이터를 쉽게 저장하고 꺼내기 위한 프로퍼티 + getter / setter 의 단순한 구조로
이루어져 있는 자바빈 규약으로 만들어진 클래스 이다.

가장 많이 사용하는 방법중 하나로, JavaBean 내에 어노테이션을 명시하여 검증 한다.

 

public class MemberCreationRequest {

    @NotBlank(message="이름을 입력해주세요.")
    @Size(max=64, message="이름의 최대 길이는 64자 입니다.")
    private String name;
    
    @Min(0, "나이는 0보다 커야 합니다.")
    private int age;

    @Email("이메일 형식이 잘못되었습니다.")
    private int email;

    // the usual getters and setters...
    
}


이렇게 DTO 클래스에 어노테이션을 명시 하고,
컨트롤러 에서 @Valid 어노테이션을 명시 하여 사용 한다.

메서드를 수행 하기 전에 JavaBean Validation을 수행 하고,
문제가 없을 경우에만 메서드 내부로 진입 하게 되는 구조 이다.

 

@PostMapping(value = "/member")
public MemeberCreationResponse createMember(

	@Valid @RequestBody final MemeberCreationRequest memeberCreationRequest) {
	// member creation logics here...
    
}


만약 밸리데이션이 실패 하게 되면,
MethodArgumentNotValidException 예외가 발생 하게 된다.

 

 

 

2)
Spring Validator 인터페이스 구현을 통한 Validation

 

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
    
}

 

위와 같이 Person 이라는 DTO 클래스 (Java Bean) 객체가 있을 때, 아래는 해당 인스턴스에만 활용 되는 validator 이다.

 

Validator 인터페이스 내부에 존재하는 두개의 메서드는 아래와 같은 역할을 한다.

 

- supports : 이 validator 가 동작할 조건을 정의 한다. 주로 class 타입을 비교.

 

- validate : 원하는 검증을 진행 한다.

 

예시 코드

더보기

 

public class PersonValidator implements Validator {

    /**
     * This Validator validates only Person instances
     */
     
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
    
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
        
    }
    
}

 

요새는 이 방법 보다 1)의 Java Bean Validation 방식을 선호 한다.

Spring Validator 인터페이스의 장단점

장점 : 
Java Bean Validation에 비해 조금 더 복잡한 검증이 가능 하다.
(두개의 데이터를 비교 하는 식으로)

단점 :

1)
Validation을 수행 하는 코드를 찾기가 상대적으로 어렵다.
(Java Bean Validation 방법은 가독성이 좋다)

2)
완전한 데이터 검증이 아니라 일부 비즈니스 검증이 들어가게 되는 경우가 생기게 된다.
이러한 경우에는 비즈니스 검증 로직이 여러 군데로 흩어질 수 있기 때문에,
잘못된 검증 (중복 검증 / 정책이 변경 됬는데 업데이트 되지 않는 유효성 검사 라던지)
을 수행할 가능성이 높아지게 될 수 있다.

 

>> 코드의 유지 보수 불리 / 가독성 저하

 

 

 

Validation 수행 시 주의 사항 및 패턴

 

[ 주의사항 ]


Validation을 하다 보면 계속 유효성 검사를 하고 싶어지는 욕구가 생기게 된다.

그래서, 반복적으로 유효성 검사를 계속 추가 하게 될 수 있는데,

나도 모르는 사이에 중복된 검증을 하게 될 수도 있다.

이런 경우에 만약에 유효성 검증 정책이 변경 될 때,
모든 중복 코드를 수정 해야 한다는 단점이 발생 하게 된다.

따라서, Validation은 어떠한 로직 초기에 수행 하는것으로 하고,
유효성 검사가 실패 하게 되면 재빠르게 exception 예외를 던지게 하는게 편리 하다.

 

 

 

[ 사용 패턴 예시 ]

 

1)
요청 DTO에서 Java Bean Validation을 사용 하여 단순한 데이터를 1차로 검증 한다.
(데이터 유무 / 범위 / 형식 등)

2)
실제 서비스 로직 초반에 2차적으로 비즈니스 검증을 수행 하게 하고,
실패하게 되면 Custom Exception (ErrorCode, ErrorMessage 등을 입력) 으로
예외를 던지도록 해서 예외를 처리 하는 패턴이 있다.
(Exception Handler 으로 예외를 처리)



사용 패턴은 어디까지나 예시일 뿐 이다.

실제로 프로젝트 또는 팀 에서 사용하는 검증 패턴에 따라 유효성 검사를 수행 하면 된다.

 

 

Data Binding 이란 ?

서로 다른 타입의 데이터를 함께 묶어서 동기화 하는 기법 이다.

 

이 글에서는, 유효성 검사 수행 후, 데이터가 잘 밸리데이션이 되면,

Request DTO 프로퍼티에 다른 유형의 데이터가 담아서 들어 오게 되는 경우라고 생각 하자.

 

스프링에서 데이터를 바인딩 하는 방법은 다양 하다.

 

PropertyEditor, Converter, Formatter 이 있는데,

 

이 글에서는 Converter 와 Formatter 만 간단히 정리 하도록 한다.

 

 

 

Converter<S, T> Interface

 

S(Source)라는 타입을 받아서 T(Target)이라는 타입으로 변환해주는 Interface.

(예시 - json 데이터 형식 이라는 문자열 데이터 타입을 받아서 XxxDTO 라는 사용자 정의 타입으로 변환)

 

인터페이스의 모양은 아래와 같다.

 

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);
    
}

 

활용 예시 :

메서드의 인자 (파라미터) 에 json 데이터 형식으로 문자열이 담겨 오는 경우,

해당 문자열을 곧바로 특정 DTO 객체에 담고 싶을 때 사용 할 수 있다.

 

// 요청
GET /user-info
x-auth-user : {"id":123, "name":"Paul"}

// View 에서 클라이언트가 GET 방식으로 user-info 라는 요청값을 보냈다고 가정.
// x-auth-user 이라는 json 데이터 형식으로 데이터를 전송.

// 유저 객체 (DTO)
public class XAuthUser {
    private int id;
    private String name;

    // the usual getters and setters...
}

// Controller 코드
@GetMapping("/user-info")
public UserInfoResponse getUserInfo(@RequestHeader("x-auth-user") XAuthUser xAuthUser) {
 
	// get User Info logic here...
    
}

// @RequestHeader 어노테이션으로 json 데이터 형식의 데이터명을 명시 하고,
// 해당 데이터를 특정 DTO (객체) 에 바로 담고 싶은 경우 위와 같이 작성 하면 된다.


위와 같이 헤더에 담긴 json 데이터 형식의 문자열을 XAuthUser 클래스 (DTO) 에 바로 담고 싶으면,

아래와 같이 Converter를 Bean 으로 등록 하여 사용 하면 된다.

 

@Component
public class XAuthUserConverter implements Converter<String, XAuthUser> {

	@Override
	public XAuthUser convert(String source) {
		return objectMapper.readValue(source, XAuthUser.class);
	}
    
}

 

이와 비슷하게 PathParameter나 기타 특수한 경우의 데이터를 특정 객체에 담고 싶은 경우.

- Converter를 만들어서 Spring에 Bean으로 등록.


- 스프링 내에 ConversionService라는 내장된 서비스에서 Converter 구현체 Bean들을 Converter 리스트에 등록.


- 외부데이터가 들어오고,

Source Class Type → Target Class Type이 Converter에 등록된 형식과 일치하면 해당 Converter가 동작하는 원리

 

 

 

Formatter

 

특정 객체 ↔ String 간의 변환을 담당.

아래 샘플 코드는 Date ↔ String 간의 변환을 수행하는 Formatter 이다.

- print : API 요청에 대한 응답을 줄 때, Date형식으로 된 데이터를 특정 locale에 맞춘 String으로 변환.


- parse : API 요청을 받아올 때, String으로 된 "2021-01-01 13:15:00" 같은 날짜 형식의 데이터를 Date로 변환하도록 함.

 

- 문자열을 Locale 에 따라 다국화 하는 기능도 제공 할 수 있다.

 

package org.springframework.format.datetime;

public final class DateFormatter implements Formatter<Date> {

    public String print(Date date, Locale locale) {
        return getDateFormat(locale).format(date);
    }

    public Date parse(String formatted, Locale locale) throws ParseException {
        return getDateFormat(locale).parse(formatted);
    }
		// getDateFormat 등 일부 구현은 핵심에 집중하기 위해 생략... 
        
}

 

Formatter도 Converter와 마찬가지로,

Spring Bean 으로 등록하면 자동으로 ConversionService에 등록 시켜 주기 때문에,

필요 (요청 / 응답 시 해당 데이터 타입이 있는 경우) 에 따라 자동으로 동작 하게 된다.

 

 

 

[ 정리 ]


이러한 컨버터와 포매터는 빈으로 등록을 해주기만 하면 스프링에 내장 되어 있는
컨버터 서비스 에서 자동으로 인식 하고,
컨버터 리스트 또는 포매터 리스트에 등록이 되어 있기 때문에,
해당되는 타입에 맞는 데이터를 발견 했을 때,
해당 컨버터 또는 포매터가 동작 하게 되는 원리 이다.

 

 

 

Reference

 

Validation, Data Binding, and Type Conversion :: Spring Framework

 

Validation, Data Binding, and Type Conversion :: Spring Framework

There are pros and cons for considering validation as business logic, and Spring offers a design for validation and data binding that does not exclude either one of them. Specifically, validation should not be tied to the web tier and should be easy to loc

docs.spring.io

반응형