DTO(Data Transfer Object)란 데이터 전송 객체를 의미하는 패턴으로 계층 간 데이터 전송을 위해 도메인 모델 대신 사용되는 객체입니다.
❓ 데이터를 교환할 때 Entity 대신 DTO를 이용하는 이유는 뭘까?
데이터 보호 : DTO는 민감한 정보의 노출을 방지할 수 있습니다. 예를 들어, 사용자의 모든 정보를 담고 있는 객체 대신에, 필요한 정보만을 담고 있는 DTO를 통해 데이터를 전송하면, 민감한 정보가 노출되는 것을 방지할 수 있습니다.
유연한 처리 : 엔티티는 일반적으로 DB 구조와 밀접하게 연관되어 있지만 DTO는 비즈니스 요구사항에 맞게 구성될 수 있어 유연합니다.
요구사항 반영 : View와 통신하는 DTO 클래스, 예를 들어 RequestDTO, ResponseDTO는 요구사항에 따라 자주 변경될 수 있습니다.
따라서 Entity 클래스와 분리하여 관리해야 합니다.
데이터 무결성 유지 : Entity 객체를 직접 사용하면 데이터 무결성을 위협받을 수 있습니다.
특히, 여러 스레드가 동시에 같은 Entity 객체를 참조하거나, 클라이언트가 Entity 객체를 자유롭게 변경할 수 있을 때 문제가 될 수 있습니다.
이런 상황에서 DTO를 사용하면, 원본 Entity 객체를 보호하고 데이터의 일관성과 무결성을 유지할 수 있습니다
@Entity
@Getter
@Setter
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String email;
private String address;
private String phone;
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok().body(user);
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.saveUser(user));
}
다음과 같이 Entity를 사용 할 경우, 클라이언트에게 노출되어서는 안되는 정보인 비밀번호와 개인 연락처 정보까지 넘어가게되고, 이와 같이 클라이언트가 User 객체를 직접 전송하면, 클라이언트는 필드 값을 자유롭게 변경할 수 있습니다.
이는 데이터의 무결성을 해칠 수 있습니다.
따라서, 클라이언트에게 전달할 정보를 담는 UserDTO를 아래와 같이 만들 수 있습니다.
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
private String username;
private String email;
}
@GetMapping("/users/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
User user = userService.findUserById(id);
UserDTO userDTO = UserDTO.builder()
.username(user.getUsername())
.email(user.getEmail())
.build();
return ResponseEntity.ok(userDTO);
}
@PostMapping("/users")
public ResponseEntity<UserDTO> createUser(@RequestBody UserDTO userDTO) {
User user = User.builder()
.username(userDTO.getUsername())
.email(userDTO.getEmail())
.build();
user = userService.saveUser(user);
return ResponseEntity.ok(userDTO);
}
위 Controller의 조회 메소드에서는 UserDTO를 반환합니다. 이렇게 DTO를 사용하면, 클라이언트와의 통신에서 민감한 정보를 보호하고, 필요한 데이터만을 송수신할 수 있습니다.
Validation이란 검증이란 의미로, DTO Validation은 DTO 값이 우리가 원하는 형태로 잘 들어왔는지 확인하는 것을 의미합니다.
내부에서 서버의 예기치 않은 동작을 제어하기 위해서 데이터를 전송할 때, 아무런 값이나 가져와서 저장하지 않고 입력 값이 유효한 값인지 확인후 전송을 해야합니다.
위 작업을 DTO 객체에 대한 Validation을 통해 수행 할 수 있습니다.
이제 DTO Validation을 적용하는 방법에 대해서 설명하도록 하겠습니다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
@Getter
@Setter
public class UserDTO {
@NotEmpty
private String username;
@NotEmpty
private String email;
}
@PostMapping("/users")
public ResponseEntity<UserDTO> createUser(@RequestBody @Valid UserDTO userDTO) {
User user = User.builder()
.username(userDTO.getUsername())
.email(userDTO.getEmail())
.build();
user = userService.saveUser(user);
return ResponseEntity.ok(userDTO);
}
해당 DTO를 사용하는 Controller에서 @Valid를 사용하겠습니다.
Controller에서 DTO를 그냥 매개값으로 사용한다고 해서, 값의 검증이 이루어지는 것은 아닙니다. 값의 검증이 이루어지기 위해서는 @Valid 어노테이션을 이용해서 값을 검증하겠다고 미리 알려주어야 합니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> dtoValidation(final MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(errors);
}
}
해당 유효성 검사 실패 시, 검증 실패를 처리할 수 있도록 예외를 처리해야 합니다.
해당 예외처리 로직을 구현하는 GlobalExceptionHandler를 다음과 같이 작성하였습니다.