본문 바로가기
Spring/SpringBoot

[SpringBoot] Spring Validation을 이용한 유효성 검증하기

by J4J 2024. 1. 29.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 spring validation을 이용한 유효성 검증하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

Spring Validation이란?

 

spring validation은 spring에서 유효성 검증을 하기 위해 도와줍니다.

 

그리고 여기서 말하는 유효성 검증은 개발된 API의 구조에 맞게 request data가 올바르게 넘어왔는지, 특정 메서드를 호출할 때 사용되어야 하는 paramter 값들이 올바르게 전달되었는지 등을 확인하는 것을 의미합니다.

 

 

 

일반적으로 validation 적용은 controller에서 많이 활용됩니다.

 

client로부터 전달받는 요청은 API 서버 관점에서 예상하지 못한 다양한 케이스가 존재할 수 있기 때문에 유효성 검증이 활발하게 이루어져야 합니다.

 

 

 

그 외에도 service, repository 등에도 사용될 수 있습니다.

 

하지만 해당 비즈니스 로직은 client로부터 전달받는 요청이 아니고 API 서버 개발자들에 의해 호출되기 때문에 controller와 달리 항상 사용하지는 않습니다.

 

그래도 상황에 따라 controller 외의 클래스 파일에도 유효성 검증을 추가하게 된다면 보다 명확한 로직 처리를 수행하는데 도움을 줍니다.

 

 

반응형

 

 

Controller에서 유효성 검증

 

유효성 검증을 하기 위해서는 먼저 다음과 같이 validation dependency 추가가 필요합니다.

 

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-validation'
}

 

 

 

 

추가가 완료되었다면 controller에서 유효성 검증하는 방법에 대해 소개드리겠습니다.

 

먼저 parameter로 넘어오는 값들에 대한 검증 방법입니다.

 

예를 들어 name, age의 parameter 값을 전달받는 API가 있는데 name은 항상 값이 들어있어야 하고, age는 항상 최솟값이 10이어야 한다면 다음과 같이 validation 설정을 해줄 수 있습니다.

 

package com.jforj.validation.controller;

import com.jforj.validation.dto.GetDto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class ValidationController {

    @GetMapping("/get/param")
    public ResponseEntity<GetDto> getParam(
            @NotBlank String name, // null, 빈 문자열, 공백으로만 이루어진 문자열이 들어오는 경우 exception 발생 
            @Min(10) Long age // 10보다 작은 값이 들어오는 경우 exception 발생
    ) {
        log.info("name: {}", name);
        log.info("age: {}", age);

        return ResponseEntity.ok(
                GetDto
                        .builder()
                        .name(name)
                        .age(age)
                        .build()
        );
    }
}

 

 

 

API를 위와 같이 생성한 뒤 유효성 검증에 실패하는 테스트 코드를 작성하여 동작시키면 다음과 같은 결과를 확인할 수 있습니다.

 

package com.jforj.validation.controller;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class ValidationControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void getParamTest() throws Exception {
        mockMvc
                .perform(
                        get("/get/param")
                                // name이 paramter에서 제외됨
                                .param("age", "10")
                )
                .andDo(print())
                .andExpect(status().isBadRequest());
    }
}

 

get param 테스트 동작 결과

 

 

 

 

이번에는 동일하게 paramter 값을 전달받는 경우이지만 객체 형태로 처리되는 코드를 원하는 경우는 다음과 같이 작성해볼 수 있습니다.

 

// dto
package com.jforj.validation.dto;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;

@Builder
public record GetDto(
        @NotBlank // null, 빈 문자열, 공백으로만 이루어진 문자열이 들어오는 경우 exception 발생
        String name,

        @Min(10) // 10보다 작은 값이 들어오는 경우 exception 발생
        Long age
) {
}


// controller
package com.jforj.validation.controller;

import com.jforj.validation.dto.GetDto;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class ValidationController {
    @GetMapping("/get/object")
    public ResponseEntity<GetDto> getObject(
            @Valid GetDto getDto // @Valid를 추가해야 GetDto에 설정된 유효성 검증 수행
    ) {
        log.info("name: {}", getDto.name());
        log.info("age: {}", getDto.age());

        return ResponseEntity.ok(getDto);
    }
}

 

 

 

 

객체로 전달받는 경우 @Valid 어노테이션을 추가해줘야 GetDto 클래스에 정의된 유효성 검증이 수행됩니다.

 

만약 GetDto에 유효성 검증 설정들을 했지만 @Valid를 사용하지 않는다면 유효성 검증이 올바르게 수행되지 않습니다.

 

해당 경우에 대한 것도 테스트 코드를 작성한 뒤 동작시켜보면 다음과 같은 결과를 확인할 수 있습니다.

 

package com.jforj.validation.controller;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class ValidationControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void getObjectTest() throws Exception {
        mockMvc
                .perform(
                        get("/get/object")
                                // name이 paramter에서 제외됨
                                .param("age", "10")
                )
                .andDo(print())
                .andExpect(status().isBadRequest());
    }
}

 

get object 테스트 동작 결과

 

 

 

 

마지막으로 body로 넘어오는 값들에 대한 검증 방법입니다.

 

body로 처리하는 방식은 parameter를 객체로 처리하는 것과 동일합니다.

 

body로 전달받는 객체에 @Valid 처리를 해주고 클래스 내부 변수들에는 유효성 검증을 위한 설정들을 해주면 됩니다.

 

// dto
package com.jforj.validation.dto;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;

@Builder
public record PostDto(
        @NotBlank // null, 빈 문자열, 공백으로만 이루어진 문자열이 들어오는 경우 exception 발생
        String name,

        @Min(10) // 10보다 작은 값이 들어오는 경우 exception 발생
        Long age
) {
}


// controller
package com.jforj.validation.controller;

import com.jforj.validation.dto.PostDto;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class ValidationController {
    @PostMapping("/post")
    public ResponseEntity<PostDto> post(@RequestBody @Valid PostDto postDto) {
        log.info("name: {}", postDto.name());
        log.info("age: {}", postDto.age());

        return ResponseEntity.ok(postDto);
    }
}

 

 

 

 

post 처리를 위한 테스트 코드도 다음과 같이 작성해볼 수 있습니다.

 

그리고 테스트 결과도 함께 확인해보시면 될 것 같습니다.

 

package com.jforj.validation.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jforj.validation.dto.PostDto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class ValidationControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void postTest() throws Exception {
        PostDto postDto =
                PostDto
                        .builder()
                        // name이 paramter에서 제외됨
                        .age(10L)
                        .build();

        mockMvc
                .perform(
                        post("/post")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(new ObjectMapper().writeValueAsString(postDto))
                )
                .andDo(print())
                .andExpect(status().isBadRequest());
    }
}

 

post 테스트 동작 결과

 

 

 

 

Controller가 아닌 곳에서 유효성 검증

 

controller와 controller가 아닌 곳에서 유효성 검증을 하는 방법의 차이점은 1가지입니다.

 

위에서 본 것처럼 controller는 유효성 검증을 위한 parameter 및 객체에 대한 설정만 해주면 됩니다.

 

하지만 controller 가 아닌 곳에서는 클래스 파일에 @Validated라는 어노테이션을 추가해줘야 합니다.

 

 

 

 

예시로 service를 다음과 같이 생성하여 유효성 검증을 위한 코드를 작성할 수 있습니다.

 

package com.jforj.validation.service;

import com.jforj.validation.dto.GetDto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

@Service
@Validated // @Validated를 추가해야 클래스 내부에서 설정된 유효성 검증 수행
@Slf4j
public class ValidationService {

    public GetDto getParam(
            @NotBlank String name, // null, 빈 문자열, 공백으로만 이루어진 문자열이 들어오는 경우 exception 발생
            @Min(10) Long age // 10보다 작은 값이 들어오는 경우 exception 발생
    ) {
        log.info("name: {}", name);
        log.info("age: {}", age);

        return GetDto
                .builder()
                .name(name)
                .age(age)
                .build();
    }

    public GetDto getObject(
            @Valid GetDto getDto // @Valid를 추가해야 GetDto에 설정된 유효성 검증 수행
    ) {
        log.info("name: {}", getDto.name());
        log.info("age: {}", getDto.age());

        return getDto;
    }
}

 

 

 

 

controller와 달리 클래스 파일에 @Validated 라는 어노테이션이 추가되어 있는 것을 볼 수 있습니다.

 

만약 어노테이션을 추가하지 않는다면 controller와 동일하게 설정만 하는 것이고, 이런 경우에 올바르게 유효성 검증이 수행되지 않는 것을 볼 수 있습니다.

 

위의 코드 또한 테스트 코드를 작성하여 동작해보면 다음과 같은 결과를 확인할 수 있습니다.

 

package com.jforj.validation.service;

import com.jforj.validation.dto.GetDto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ValidationServiceTest {

    @Autowired
    private ValidationService validationService;

    @Test
    void getParam() {
        validationService.getParam(
                null, // name은 null 처리
                10L
        );
    }

    @Test
    void getObject() {
        GetDto getDto =
                GetDto
                        .builder()
                        // name은 제외됨
                        .age(10L)
                        .build();

        validationService.getObject(getDto);
    }
}

 

service 테스트 동작 결과

 

 

 

 

상황 별 발생되는 Exception

 

테스트 코드에 대한 결과를 확인해보시면 아시겠지만 상황 별 발생되는 exception 처리가 모두 상이합니다.

 

발생되었던 exception들을 다음과 같이 정리해 볼 수 있습니다.

 

테스트 exception http 상태 코드
get parameter 테스트 HandlerMethodValidationException 400
get object 테스트 MethodArgumentNotValidException 400
post 테스트 MethodArgumentNotValidException 400
controller 외 테스트 ConstraintViolationException 500

 

 

 

만약 GlobalExceptionHandler 등을 활용하여 exception 핸들링을 수행하시는 분들이라면 표를 참고하여 exception 처리에 맞는 로직 처리를 추가해 주시면 됩니다.

 

 

 

 

Constraint 종류

 

위에서 작성되었던 @NotBlank, @Min 등과 같은 contraint 종류에 대해 정리해보겠습니다.

 

다음과 같이 상황별 사용되는 다양한 것들이 존재합니다.

 

아래 표는 자주 사용될 수 있는 constraint annotation이며 이 외에도 더 다양한 것들이 존재하니 필요하신 부분에 맞게 활용해 보시면 됩니다.

 

constraint annotation 설명
@NotBlank null / 빈 문자열 / 공백으로만 이루어진 문자열 예외 처리
@NotEmpty null / 빈 문자열 예외 처리
@NotNull null 예외 처리
@Null null이 아닌 경우 예외 처리
@Size(min=, max=) min과 max 사이의 문자열 길이가 아닌 경우 예외 처리
@Email email form의 문자열이 아닌 경우 예외 처리
@Pattern(regexp=) regexp에 해당하는 정규식이 아닌 경우 예외 처리
@Min( {number} ) {number}보다 작은 경우 예외 처리
@Max( {number} ) {number}보다 큰 경우 예외 처리

 

 

 

 

 

 

 

 

이상으로 spring validation을 이용한 유효성 검증하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글