마틴 파울러(Martin Fowler)가 ‘Patterns of Enterprise Application Architecture’라는 책에서 처음 소개한 엔터프라이즈 애플리케이션 아키텍처 패턴의 하나이다.
클라이언트에서 서버 쪽으로 전송하는 요청 데이터, 서버에서 클라이언트 쪽으로 전송하는 응답 데이터의 형식으로 클라이언트와 서버 간에 데이터 전송이 이루어지고, 이 구간에서 DTO를 사용할 수 있다.
public ResponseEntity postMember(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone) {
.
.
.
여기서는 요청 데이터가 세 개밖에 없지만 실제로는 회원의 주소 정보, 로그인 패스워드, 패스워드 확인 정보 등 더 많은 정보들이 회원 정보에 포함되어 있을 수 있다.
따라서 postMember()에 파라미터로 추가되는 @RequestParam의 개수는 계속 늘어날 수밖에 없다.
그러나 클라이언트의 요청 데이터를 하나의 객체로 모두 전달받을 수 있다면 코드 자체가 굉장히 간결해질 것 같다.
DTO 클래스가 바로 요청 데이터를 하나의 객체로 전달받는 역할을 해준다.
public ResponseEntity postMember(MemberDto memberDto) {
return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
}
@RequestParam을 통해 전달받은 요청 데이터들을 Map에 추가하는 로직이 사라지고, MemberDto 객체를 ResponseEntity 클래스의 생성자 파라미터로 전달하도록 변경되었다.
결과적으로 DTO 클래스를 사용하니 코드 자체가 매우 간결해졌다.
사실 DTO 클래스를 사용하는 가장 중요한 목적은 비용이 많이 드는 작업인 HTTP 요청의 수를 줄이기 위함이라고 할 수 있다.
또 도메인 객체와의 분리라는 목적도 있다.
유효한 데이터를 전달받기 위해 데이터를 검증하는 것을 유효성(Validation) 검증이라고 한다.
DTO 클래스를 사용하면 유효성 검증 로직을 DTO 클래스로 빼내어 핸들러 메서드의 간결함을 유지할 수 있다.
public class MemberDto {
@Email
private String email;
private String name
.
.
.
@Email 애너테이션을 추가하면 클라이언트의 요청 데이터에 유효한 이메일 주소가 포함되어 있지 않을 경우 유효성 검증에 실패하기 때문에 클라이언트의 요청은 거부(reject)된다.
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@Valid MemberDto memberDto) {
return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
}
MemberDto 클래스에서 이메일에 대한 유효성 검증을 진행하므로 코드가 간결하다.
@Valid 애너테이션은 MemberDto 객체에 유효성 검증을 적용하게 해주는 애너테이션이다.
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@RequestBody MemberPostDto memberPostDto) {
return new ResponseEntity<>(memberPostDto, HttpStatus.CREATED);
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(@PathVariable("member-id") long memberId,
@RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
memberPatchDto.setName("홍길동");
// No need Business logic
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
@RequestBody 애너테이션은 JSON 형식의 Request Body를 MemberPostDto 클래스의 객체로 변환을 시켜주는 역할을 한다.
즉, 클라이언트 쪽에서 전송하는 Request Body가 JSON 형식이어야 한다.
만일 JSON 형식이 아닌 다른 형식의 데이터를 전송한다면, Spring 내부에서 ‘Unsupported Media Type’과 같은 에러 메시지를 포함한 응답을 전달한다.
@ResponseBody는 JSON 형식의 Response Body를 클라이언트에게 전달하기 위해 DTO 클래스의 객체를 Response Body로 변환하는 역할을 한다.
그러나 위의 코드상에 @ResponseBody는 없다.
이유는 바로 핸들러 메서드의 리턴 값이 ResponseEntity 클래스의 객체이기 때문이다.
Spring MVC에서는 핸들러 메서드에 @ResponseBody 애너테이션이 붙거나 핸들러 메서드의 리턴 값이 ResponseEntity일 경우, 내부적으로 HttpMessageConverter가 동작하게 되어 응답 객체(여기서는 DTO 클래스의 객체)를 JSON 형식으로 바꿔준다.
클라이언트 쪽에서 JSON 형식의 데이터를 서버 쪽으로 전송하면 서버 쪽의 웹 애플리케이션은 전달받은 JSON 형식의 데이터를 DTO 같은 Java의 객체로 변환하는데 이를 역직렬화(Deserialization)이라고 한다.
반면에 서버 쪽에서 클라이언트에게 응답 데이터를 전송하기 위해서 DTO 같은 Java의 객체를 JSON 형식으로 변환하는 것을 직렬화(Serialization)라고 한다.
JSON 직렬화(Serialization): Java 객체 → JSON
JSON 역직렬화(Deserialization): JSON → Java 객체
Controller 클래스가 늘어남에 따라 DTO 클래스가 두 배(ex. xxxxPostDto + xxxxPatchDto)씩 늘어나는 것을 볼 수 있다.
-> 이 부분은 공통된 멤버 변수의 추출 및 내부 클래스를 이용해서 어느 정도 개선이 가능하다.