[Spring Boot]DTO Validation 구축

한상욱·2024년 5월 23일
0

Spring Boot

목록 보기
11/17
post-thumbnail

들어가며

이 글은 Spring Boot를 공부하며 정리한 글입니다.

Validation

우리가 생성한 모든 Api들은 DTO를 통해서 클라이언트에게 입력을 받게됩니다. 이러한 과정에서 DTO에 담기는 프로퍼티가 사용자 요구에 따라 검증이 필요할 때가 있습니다.

여기 기본적인 게시글 생성 Api가 존재합니다. 게시글에는 ID, 제목, 내용, 작성자 ID, 공개여부가 필요합니다.

Entity

import ...

@Entity
class Post (
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val id : Long?,

    @Column(nullable = false, length = 100)
    var title : String,

    @Column(nullable = false, length = 2000)
    var post : String,

    @Column(nullable = false, length = 50)
    var userId : Long,

    @Column(nullable = false, length = 10)
    var isPublic : Boolean = true
) {

    fun toResponse() : PostResponseDto = PostResponseDto(
        id = id,
        title = title,
        post = post,
        userId = userId,
        isPublic = isPublic,
    )
}

DTO

이를 위해서 우리가 데이터를 전달받는 DTO는 아래와 같습니다.

import ...

data class PostRequestDto(
    var id : Long?,
    
    var title : String,

    var post : String,

    var userId : Long,

    var isPublic : Boolean
) {
    fun toEntity() : Post = Post(
        id = id,
        title = title,
        post = post,
        userId = userId,
        isPublic = isPublic
    )
}


data class PostResponseDto(
    var id : Long?,
    var title : String,
    var post : String,
    var userId : Long,
    var isPublic : Boolean
)

RequestDto는 클라이언트에게 제공받는 데이터를 담을 오브젝트입니다. 그리고 ResponseDto는 클라이언트에게 되돌려줄 데이터를 담을 오브젝트입니다.

Controller

@RestController
@RequestMapping("/api/posts")
class PostController {

    @Autowired
    private lateinit var postService: PostService

	...

    /**
     * 게시글을 생성하는 Api
     */
    @PostMapping
    private fun postPost(@Valid @RequestBody postRequestDto: PostRequestDto) :
            ResponseEntity<BaseResponse<PostResponseDto>> {
        val result = postService.postPosts(postRequestDto)
        return ResponseEntity.status(HttpStatus.CREATED).body(BaseResponse(data = result))
    }
	
    ...
}

Controller에는 여러 Api가 존재할 수 있지만, POST API만 작성해 주었습니다. 현재는 속성값을 정확하게 기입하면 간단하게 생성이 가능합니다. 여기서, 제목, 내용, 작성자, 공개여부가 반드시 필요하다는 Validation을 구축하겠습니다.

dependency

dependencies {
	...
    // validation 의존성 추가
	implementation("org.springframework.boot:spring-boot-starter-validation")
	...
}

Validation을 사용하기 위해서는 DTO에서 어노테이션을 통해서 간단하게 사용할 수 있습니다. DTO에 아래와 같은 Validation 어노테이션을 추가해보겠습니다.

import ...

data class PostRequestDto(
    var id : Long?,

    @field:NotBlank
    var title : String,

    @field:NotBlank
    var post : String,

    @field:NotNull
    var userId : Long,

    @field:NotNull
    var isPublic : Boolean
) {
    fun toEntity() : Post = Post(
        id = id,
        title = title,
        post = post,
        userId = userId,
        isPublic = isPublic
    )
}

현재 @field:NotBlank와 @field:NotNull 어노테이션을 추가했습니다.
각각, 여러가지 Validation을 담고 있습니다.

  • @NotNull: 해당 값이 null이 아닌지 검증함
  • @NotEmpty: 해당 값이 null이 아니고, 빈 스트링("") 아닌지 검증함 (" "은 허용됨)
  • @NotBlank: 해당 값이 null이 아니고, 공백(""과 " " 모두 포함)이 아닌지 검증함
  • @AssertTrue: 해당 값이 true인지 검증함
  • @Size: 해당 값이 주어진 값 사이에 해당하는지 검증함(String, Collection, Map, Array에도 적용 가능)
  • @Min: 해당 값이 주어진 값보다 작지 않은지 검증함
  • @Max: 해당 값이 주어진 값보다 크지 않은지 검증함
  • @Pattern: 해당 값이 주어진 패턴과 일치하는지 검증함
    출처: https://mangkyu.tistory.com/174 [MangKyu's Diary:티스토리]

만약 데이터 타입이 문자형이 아닐경우 NotBlank를 사용하게 되면 에러가 발생하니 주의해주어야 합니다. 이제 Controller에 Validation을 적용시켜줘야 합니다.
그리고, 코틀린에서는 @field 뒤에 해당 어노테이션을 명시해주어야 호환됩니다. 안하면 적용이 안됩니다.

    @PostMapping
    private fun postPost(@Valid @RequestBody postRequestDto: PostRequestDto) :
            ResponseEntity<BaseResponse<PostResponseDto>> {
        val result = postService.postPosts(postRequestDto)
        return ResponseEntity.status(HttpStatus.CREATED).body(BaseResponse(data = result))
    }

입력 파라미터에 @Valid 어노테이션을 붙여주면 해당 검증을 파라미터에 적용시킬 수 있습니다.

이제, Validation을 통과하지 못한 데이터들은 에러를 반환하게 됩니다.

Exception Handling

이렇게 반환된 에러를 직접 핸들링 할 수 있는데요. 반환 에러도 요구사항에 따라서 통일시켜야 될 수도 있습니다.

이렇게 그냥 반환된 에러는 어째서 발생하였는지 사용자가 알 수 없습니다. 그렇기에 기본응답모델을 이용하여 해당 에러를 핸들링 하도록 하겠습니다.

data class PostRequestDto(
    var id : Long?,

    @field:NotBlank(message = "제목은 반드시 입력해야 합니다!")
    var title : String,

    @field:NotBlank(message = "내용은 반드시 입력해야 합니다!")
    var post : String,

    @field:NotNull(message = "작성자가 없을 수 없습니다!")
    var userId : Long,

    @field:NotNull(message = "공개 여부를 선택해야 합니다!")
    var isPublic : Boolean
) {
    fun toEntity() : Post = Post(
        id = id,
        title = title,
        post = post,
        userId = userId,
        isPublic = isPublic
    )
}

우선, DTO에 해당 Validation을 통과하지 못하면 발생시키는 에러메시지를 적용시켜주었습니다.

@RestControllerAdvice
class CommonExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException::class)
    protected fun methodArgumentNotValidException(
        exception: MethodArgumentNotValidException
    ): ResponseEntity<BaseResponse<Map<String, String>>> {
        val errors = mutableMapOf<String, String>()
        exception.bindingResult.allErrors.forEach { error ->
            val fieldName = (error as FieldError).field
            val errorMessage = error.defaultMessage
            errors[fieldName] = errorMessage ?: "Not Exception Message"

        }
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(BaseResponse(status = ResultStatus.ERROR.name, data = errors ,resultMsg = ResultStatus.ERROR.msg))
    }
    ...

기본적으로 검증을 통과하지 못해 발생하는 에러는 MethodArgumentNotValidException입니다. 해당 에러에서 발생하는 필드의 에러값을 추출한 후 해당 에러를 기본응답에 담아 사용자에게 반환해 줍니다.

이렇게 Validation을 사용하는 방법에 대해서 알아보았습니다.

profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해

0개의 댓글