본문 바로가기
Spring/boot

[Spring Boot] Rest Controller DTO 의 필요성

by 코딩균 2022. 2. 15.

Entity를 Client에 노출하지 말자

API 스펙과 엔티티가 깊이 연관

엔티티를 아래와 같이 직접 노출할 경우, 추후 entity를 수정시 API 스펙 자체가 바뀌어버리는 상황이 온다

클라이언트 쪽과 공유하는 부분이라 큰 혼란이 초래됨

@GetMapping("/api/users")
public List<User> userList(){
	return userService.findUsers();
    // User entity 리스트가 반환됨
}

 

반환값에 특정 필드 추가 불가

클라이언트에서 특정 계산된 값이 필요하다고 할 때,

예를 들어 user의 총 인원 수 

JSON의 기본틀이 깨지게 된다

{
	"count" : 100,
    
    "users" : [
    	{
        	"id" : 1
            "name" : "hj"
            "email" : "email@gamil.com"
         },
      	{
        	"id" : 2
            "name" : "ky"
            "email" : "email2@gmail.com"
         },
         ...(생략)...
    ]
}

이러한 형태가 올바른 형태인데

entity 자체를 반환하고 있으니

count 값을 넣을수가 없다 

 

과도한 정보 노출

불필요한 정보들이 노출되는 문제도 있다. 

비즈니스 로직상 name이 필요하지 않아도 entity 자체이 있기 때문에 어쩔 수 없이 넘어간다

 

 

DTO를 사용하여 Client에 전달

Data Transfer Object 

  • 로직을 가지지 않는 순수히 데이터 교환을 하기 위해 사용하는 객체 
  • getter와 setter만 가지고 있다 (어노테이션 @data 를 통해 만들어줌)

 

아래의 controller를 보면 DTO를 두개 만들어서 데이터를 client에 전달한다

  • ApiResponse : 전달되는 표준 형식을 정의 (status, data)
  • UserDto : 해당 컨트롤러에서 필요한 api 스펙만 정의해놓은 dto 객체

 

User 엔티티들을 UserDto가 한번 감싸고

UserDto 리스트들을 ApiResponse가 한번더 감싸서 client로 내보내 준다

@GetMapping("/api/users")
public ApiResponse userList(){
	List<User> users = userService.findUsers();
    
    List<UserDto> userDtos = users.stream()
    	.map(u -> new UserDto(m.getEmail())
        .collect(Collectors.toList());
    
    return new ApiResponse(200, userDtos);
 }

DTO들은 아래와 같이 정의할 수 있다

@Data
@AllArgsConstructor
static class ApiResponse<T>{
	private int status;
    private T data;
}

@Data
@AllArgsConstructor
static class UserDto{
	private String email;
}

client에 전달되는 JSON 형식은

{
	"status" : 200,
    "data" : [
    	{ 
        	"email" : "email@gmail.com"
        },
        {
        	"email" : "email2@gmail.com"
        }
    ]
}

이런식으로 ApiResponse DTO 객체 형식을 Jackson이 JSON 형식으로 변환해준다

 

DTO를 사용하여 Server에 전달

client에서 data를 서버로 보낼 때에도 마찬가지로 DTO를 이용하여 전달 받는다

  • entity와 api 스펙을 명확하게 분리 가능
  • entity가 변경되어도 api 스펙에 영향을 주지 않는다
@PutMapping("/api/v2/members/{id}")
public Result updateUser(@PathVariable("id") Long id, @RequestBody @Valid UpdateUserRequest request, Errors e){
	
    if(e.hasErrors()){
   		// validation 에러시 처리해 줄 코드
    }
    
    userService.update(id, request.getName());
    User findUser = userService.findUser(id);
    return new Result(200, new UpdateUserResponse(findUser.getId(), findUser.getName()));

}

@Data
static class UpdateMemberRequest{
	
    @NotEmpty(message = "사용자 이름은 필수값입니다")
	private String name;
 }
 
 @Data
 static class UpdateUserResponse{
 	private Long id;
    private String name;
 }

@Valid

해당 어노테이션을 통해 DTO를 통해 넘어오는 데이터에 대해 검증할 수 있다

@NotEmpty등의 validation 어노테이션을 통해 오류가 있다면 Errors 객체를 통해 Error여부를 전달 받을 수 있다

 

 

번외 - PUT? PATCH?

PUT은 data 필드 전체 업데이트시 사용

PATCH는 data의 일부 필드를 업데이트시 사용

 

번외 - 성능을 위해 DTO repository 단에서 아예 패키징 해오면 안될까?

1. 먼저 sevice 단에서 DTO로 변환해 보고 성능 체크

2. 만약 join 걸리는 entity가 많은 쿼리라면 fetch join을 한번 적용

3. repository 단에서 fetch join 을 해와도 너무 많은 데이터를 가져오는 것 같으면 DTO로 패키징 하는 방법 선택

4. 그래도 성능이 별로면 직접 SQL 날린다