Dto와 @Valid

chrkb1569·2022년 7월 31일
0

Spring

목록 보기
4/11
post-thumbnail

스프링 멘토링에서 사용하였던 프로젝트에 @Valid 어노테이션을 통하여 데이터 입력과 관련된 오류 처리 기능을 향상시켰으며, Dto를 활용하여 클라이언트에게 제한된 정보를 제공하도록 만들었습니다.

DTO

일단 먼저 DTO가 무엇인지에 대하여 먼저 설명하자면, Data Transfer Object의 약자로 말 그대로 데이터를 이동할때 사용하게 되는 객체를 나타냅니다.

이때의 데이터 교환은 계층 간 발생하는 데이터의 교환을 의미하며, 보통 Controller와 Service 사이의 데이터 교환에서 주로 사용되는 객체를 의미합니다.

우리가 사용자의 요청으로부터 서비스를 제공하는 경로는 다음과 같습니다.

클라이언트(요청) -> Controller -> Service -> Repository -> Service -> Controller -> 클라이언트(응답)

클라이언트의 요청을 Controller가 매핑함으로써 미리 정의된 기능인 Service를 실행하게 되는데, 해당 Service는 Repository 내에 저장된 데이터를 기반으로 동작하기 때문에 Repository에서 데이터 베이스에 저장된 Entity를 기반으로 작업을 실행하고, 반환값을 Controller에 전달해주며, Controller가 반환값을 클라이언트에게 응답으로 전해주게 됩니다.

우리가 이제까지 만들어보았던 프로젝트들 역시 이러한 과정을 통하여 동작하지만, 우리의 프로젝트들은 클라이언트에게 모든 정보를 제공한다는 치명적인 단점이 존재합니다.

다음은 모든 데이터를 조회하는 기능을 수행하였을 경우의 결과 화면입니다.

앞에서 설명하였던대로 Entity의 모든 정보가 노출됨을 확인할 수 있습니다.

하지만, 만약 Entity의 정보들 중 노출되기 민감한 정보가 포함되어 있다면 아마 큰 문제가 발생할 것입니다.

따라서, 우리는 데이터 출력에 활용되는 객체를 바꿀 필요가 있습니다.

하지만, Entity를 수정하자니 데이터베이스에 저장된 데이터들에 영향을 줄 것 같아서 직접적으로 데이터를 수정하기가 꺼려집니다.

이와 같은 상황에서 활용하는 것이 DTO입니다.

DTO를 활용할 경우, 다음과 같이 사용자가 요청하는 경우, 서버가 응답하는 경우 사용하는 객체의 형태를 변화시킬 수 있습니다.

먼저, 데이터가 생성되는 경우 사용되는 DTO를 살펴본다면, 다음과 같습니다.

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class BoardCreateRequestDto {
    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotBlank(message = "내용을 입력해주세요.")
    private String content;

    @NotBlank(message = "작성자를 입력해주세요.")
    private String writer;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import mju_spring.study.entity.Board;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class BoardCreateResponseDto {
    private String title;
    private String content;
    private String writer;

    public BoardCreateResponseDto toDto(Board board) {
        return new BoardCreateResponseDto(
        board.getTitle(), board.getContent(), board.getWriter()
        );
    }
}

일단 어노테이션들은 추후에 설명하고, 흐름에 대해서 대략적으로 설명하자면, 클라이언트로부터 데이터를 요청받을 경우에는 BoardCreateRequestDto로 데이터를 받고, 서버로부터 클라이언트에게 데이터를 보내줄 경우에는 BoardCreateResponseDto로 데이터를 반환함으로써, 우리에게 민감한 정보를 가려서 사용자에게 제공해줄 수 있습니다.

    @ResponseStatus(HttpStatus.OK)
    @PostMapping("/boards")
    public Response save(@RequestBody @Valid
    BoardCreateRequestDto requestDto) {
        return Response.success(boardService.save(requestDto));
    }
    @Transactional
    public BoardCreateResponseDto save(
    BoardCreateRequestDto requestDto) {
        Board board = new Board(requestDto.getTitle(),
        						requestDto.getContent(),
        						requestDto.getWriter()
                                						);
        
        boardRepository.save(board);

        return new BoardCreateResponseDto().toDto(board);
    }

이처럼 DTO를 활용하여 데이터를 조회해보면, 다음과 같이 사용자에게 민감한 정보를 제거하여 데이터를 조회할 수 있습니다.

저의 경우에는 id값을 민감한 정보로 지정하여 클라이언트에게 제공하지 않도록 하였습니다.

다음과 같이 id 컬럼이 제공되지 않는 모습을 볼 수 있습니다.

@Valid

@Valid 어노테이션의 경우에는 이전에 제가 만들어보았던 예외 처리의 상위버전입니다.

이전에 특정 컬럼을 입력하지 않았을 경우 발생하는 오류를 지정해주었는데, 솔직히 오류 처리라고 보기에는 좀 미흡한 점이 있었습니다.


바로 다음처럼 컬럼이 존재하는 상태에서 ""과 같이 데이터를 입력하지 않아야만 오류 처리가 동작하고, 특정 컬럼을 제거하여 2개의 컬럼 데이터만 입력하는 경우에는 오류 처리가 동작하지 않았습니다.

이를 보완하기 위하여 사용한 방식이 바로 @Valid 어노테이션입니다.

@Valid 어노테이션을 활용하기 위해서는 먼저, build.gardle에 의존 관계를 형성해주어야합니다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

다음과 같이 입력해주어 의존 관계를 형성해 준다면,

그 이후로부터는 @Valid 어노테이션을 사용할 수 있는데,

사용하기전에 ExceptionAdvice에 Exception을 추가해주어야합니다.


    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Response methodArgumentNotValidException(MethodArgumentNotValidException e) {
        return Response.failure(400, e.getBindingResult().getFieldError().getDefaultMessage());
    }

@Valid 어노테이션을 선언한 뒤에 조건을 충족시키지 못하면 오류가 발생하는데, 이때 발생하는 오류가 바로 MethodArgumentNotValidException 입니다.

해당 메소드가 발생할 경우 getBindingResult().getFieldError().getDefaultMessage()를 통해서 Failure로 Result 인터페이스를 구현해주고 Response를 반환해줍니다.

getBindingResult() -> 매개 변수를 Bean에 Binding 해줄 때, 발생하는 오류를 받기 위함

getFieldError() -> 필드에서 발생하는 오류들을 모두 가져옴

getDefaultMessage() -> message 가져옴

이와 같이 오류 처리를 설정해주었다면, Controller를 다음과 같이 수정해줄 수 있습니다.

    @ResponseStatus(HttpStatus.OK)
    @PostMapping("/boards")
    public Response save(@RequestBody @Valid BoardCreateRequestDto requestDto) {
        return Response.success(boardService.save(requestDto));
    }
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class BoardCreateRequestDto {
    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotBlank(message = "내용을 입력해주세요.")
    private String content;

    @NotBlank(message = "작성자를 입력해주세요.")
    private String writer;
}

아까 DTO를 공부하면서 보았던 코드입니다.

게시글 생성에 필요한 매개변수를 입력 받을 때, 클래스를 통하여 매개변수들을 입력받는데, DTO를 통하여 클래스를 생성한 뒤에 @NotBlank() 어노테이션을 필드에 설정하게 될 경우, 해당 필드가 입력되지 않았다면 아까 설정해주었던 MethodArgumentNotValidException이 발생하게됩니다.

다음과 같이 오류가 잘 처리됨을 확인할 수 있었습니다.

뭐 이렇게 DTO와 @Valid 어노테이션을 통해서 기존의 프로젝트를 수정해보았는데, 기존에 부족하다고 생각했던 부분들을 해결하게 되어 만족스럽습니다ㅋㅋ

특히 오류 처리 부분은 좀 어거지로 하는 느낌이 나서 가장 먼저 해결하고 싶었는데, 어노테이션 설정 해둠으로써 해결할 수 있었네요.

0개의 댓글