기본적인 웹 요청을 처리할 수 있는 프로그램을 페어와 만들며, DTO 사용과 관련한 컨벤션에 대해 논의했다. 크게 1) API마다 서로 다른 DTO를 사용할지, 하나의 DTO를 사용할지와 2) DTO의 네이밍 컨벤션에 대해 논의했다.
Web 계층의 DTO
Spring Boot 프레임워크를 사용해 웹 요청을 받고 응답하도록 했다. 이때, Spring Boot에서 제공하는 @Controller, @RequetsMapping 어노테이션을 사용해 요청을 처리하는 Web 계층이 만들어졌다. 완성된 Web 계층의 코드는 아래와 같다.
@RequestMapping("/reservations")
@RestController
public class ReservationController {
private final ReservationService reservationService;
public ReservationController(ReservationService reservationService) {
this.reservationService = reservationService;
}
@GetMapping
public ResponseEntity<List<ReservationFindAllResponse>> findAllReservation() {
List<ReservationFindAllResponse> response = reservationService.findAllReservation();
return ResponseEntity.ok().body(response);
}
@PostMapping
public ResponseEntity<ReservationFindResponse> saveReservation(@RequestBody ReservationSaveRequest request) {
ReservationFindResponse response = reservationService.saveReservation(request);
return ResponseEntity.ok().body(response);
}
@DeleteMapping("/{reservation_id}")
public ResponseEntity<Void> deleteReservation(@PathVariable(value = "reservation_id") Long id) {
reservationService.deleteReservation(id);
return ResponseEntity.ok().build();
}
}
Java
복사
여기서 웹 요청을 받고 응답할 때 사용하는 매개체인 Json으로 파싱되는 객체가 ~Response, ~Request 로 끝나는 객체들이다. 그리고 이 객체들은 DTO들이다.
API마다 서로 다른 DTO를 사용할지, 하나의 DTO를 사용할지
Json으로 파싱되는 객체를 앞으로 편의를 위해 API의 응답값이라 부르겠다. 실제론 API의 요청값 혹은 응닶값으로 사용된다.
API들마다 서로 다른 DTO를 응답값으로 사용하는게 더 적합한 상황
API의 응답값으로 도메인 객체를 사용하면 어떻게 될까? API 응답값으로 도메인을 사용할 경우, 해당 도메인에 관련된 API들은 전부 도메인에 의존하게 된다. 만일 도메인의 필드가 하나라도 변경되면, 클라이언트한테 제공하는 인터페이스인 API들이 일파만파 영향을 받게 된다.
마찬가지로 여러 API들이 응답값으로 하나의 DTO를 사용할 경우, 도메인을 사용한 경우와 똑같은 사이드 이펙트가 발생한다. 따라서, 각 API들은 응답값으로 서로 다른 DTO를 사용하는 편이 인터페이스(API)의 변경을 최소화할 수 있다.
다만 이 상황은 클라이언트가 도메인 필드들을 관리하고 있지 않을 때 특히 적합하단 점을 알아야 한다. 즉 클라이언트 측에 특정 도메인의 필드들에 대해 일괄적으로 다루는 코드가 존재하지 않고, 아래처럼 각 API마다 필드들을 따로 처리하도록 관리중이라면 API 마다 서로 다른 DTO를 사용하는 편이 낫다.
const row = event.target.parentNode.parentNode;
const nameInput = row.querySelector('input[type="text"]');
const dateInput = row.querySelector('input[type="date"]');
const timeInput = row.querySelector('input[type="time"]');
const reservation = {
name: nameInput.value,
date: dateInput.value,
time: timeInput.value
};
Java
복사
API들이 하나의 DTO를 응답값으로 사용하는 게 더 적합한 상황
만일 클라이언트가 백엔드가 가지고 있는 도메인을 동일하게 코드로 관리하고 있다면, 오히려 같은 도메인에 대한 API들은 하나의 DTO를 사용하는 편이 유리할 수도 있다.
예를 들어 클라이언트가 reservation 도메인에 대한 필드들을 하나의 객체로 정의해 관리한다면, 오히려 Get /reservations 와 Get /reservations/{id} 와 같이 동일한 도메인에 대한 API들을 처리할 때 만들어둔 reservation 객체를 그대로 사용하면 되니, 백엔드에서도 하나의 DTO로 통일해 응답해주길 원할 것이다.
트레이드 오프 상황에서의 절충안 찾기: 도메인 대신 관심사 위주로 바라보기
위에 적은 두 상황에 대해 정리하자면 아래와 같다.
•
API마다 서로 다른 DTO를 사용했을 때
◦
장점: 도메인 변경 시 API 변경을 최소화할 수 있음(FE의 관리포인트 감소)
◦
단점: 도메인 변경 시 여러 DTO가 변경되어야 할 수 있음(BE의 관리포인트 증가)
•
API들이 같은 DTO를 사용했을 때
◦
장점: 도메인 변경 시 DTO 변경을 최소화할 수 있음(BE의 관리포인트 감소)
◦
단점: 도메인 변경 시 여러 API가 변경될 수 있음(FE의 관리포인트 증가)
결국 BE의 관리포인트로 가져갈지 FE의 관리포인트로 가져갈지의 트레이드 오프다. 모델을 BE와 FE가 공통으로 관리한다면 후자가 나을 거 같고, 모델을 BE만 관리한다면 전자가 나을 것이다.
혹은 양쪽을 일부씩 차용할 수도 있다. 도메인 자체보단 관심사 위주로 바라보아, 관심사가 같은 API들은 내부적으로 공통된 DTO를 사용하게 하는 것이다.
DTO의 네이밍 컨벤션
Web 계층에서 사용할 DTO나 메서드의 네이밍 컨벤션에 대해선 아래와 같이 정했다. 내가 이전에 애용하던 방식을 조금더 디테일하게 정리한 것인데, Web 계층의 네이밍 컨벤션에 대해 고민이 되던 분들은 차용해도 좋을 거 같다.
구체적인 메서드 이름은 물론 사람마다 더 좋다 생각하는 게 다를 수 있지만, 한 가지 확실한 사실은 DTO 네이밍 컨벤션은 꼭 정하고 프로젝트를 진행하는 편이 전체 프로젝트의 통일성과 가독성을 유지하는데 확실히 도움이 된단 점이다.
•
조작행위 : save(저장) / update(수정) / delete(삭제) / find(조회)
•
컨트롤러 메서드명 : (조작 행위) - [여러 건] - (자원) - [조건]
◦
저장 - 자원 : saveUser()
◦
수정 - 자원 : updateUser()
◦
삭제 - 자원 : deleteUser()
◦
조회 - 자원 : findUser()
◦
조회 - 여러건 - 자원 : findAllUser()
◦
조회 - 자원 - 이름으로 : findUserByName()
◦
조회 - 여러건 - 자원 - 이름으로 : findAllUserByName()
•
요청 DTO : (자원) - (조작 행위)
◦
자원 - 저장 : UserSaveRequest
◦
자원 - 수정 : UserUpdateRequest
◦
자원 - 삭제 : UserDeleteRequest
•
응답 DTO : (자원) - (조작 행위) - [여러 건] - [조건] (단건은 생략)
◦
자원 - 조회 : UserFindResponse
◦
자원 - 조회 - 여러건 : UserFindAllResponse
◦
자원 - 조회 - 이름으로 : UserFindByNameResponse
◦
자원 - 조회 - 여러건 - 이름으로 : UserFindAllByNameResponse