
이 글은 DTO에 관한 소개글이 아닙니다.
최근 DTO에 관해 생각해볼 점이 있어서 글을 적어본다.
회사 내에서 DTO를 사용하는 스타일이 정말 다양하다.
개인적으로는 컨벤션을 규정하여 진행하고 싶지만
프로젝트의 접점이 없기도 하고
결국은 다들 각자의 스타일을 주장하다보니ㅋㅋ
쉽게 통일할 수 없는 상황이다.
그래서 지금까지 내가 생각해보고 정하게 된
DTO 컨벤션에 역사(?)와 이유에 대해서 설명하고 싶다.
말 그대로 취준 시절 개인 프로젝트에선
도메인 하나 당 하나의 DTO를 사용했었다.
BoardDTO, MemberDTO ...
각 DTO에는 도메인에서 사용하는 모든 필드값들을 전부 때려박았다.
이렇게 하나의 DTO를 사용하게 되면 장점은.. 편하다.
후에 소개할테지만 request와 response DTO를 분리하지 않고
하나의 DTO로 모든 것을 관리하니 가독성이고 자시고 그냥 무지성이었다.
물론 내 개인프로젝트에서 모든 코드를 알고 있으니 필드에 null 값이 들어가도
딱히 불편하다고 생각하지는 못했던 것 같다. 그러던 어느날..
취업을 하고 유지보수 프로젝트에 추가개발 건으로 입사한 사람끼리 투입을 하게 되었고
같이 들어간 팀원은 주니어 프론트 개발자였다.(이 친구에게 참 많이 배웠다)
그 친구가 내 첫번째 스타일의 DTO 응답값들을 보면서 한마디 했다.
"형 이렇게 만들면 다른 사람한테 진짜 혼나ㅋㅋ"
null 구멍이 숭숭 뚫린 DTO는 필드값이 20개가 넘어갔고
거기에 List도 집어넣고 하다보니 이건 뭐 네트워크 창에서 읽어낼 수가 없었다.
아차 싶었다.
개발자로 일을 하면서 결국 중요한 건 새로운 기술, 어떤 걸 많이 알고 있는가 이런게 아니었다.
나도 남이 만든 개떡같은 코드를 유지보수한 적이 있다보니
가장 쌍욕 나올때가 '이거 안쓰는데?', '왜 있는거지?' 였다.
그런 사소한 과정들을 내 뒤에 개발자에게 뿌리고 있었던 것이다.
그 이후 DTO 병에 걸렸다고 봐도 좋을 정도로 DTO를 분리했다.
API 당 requestDTO, responseDTO를 만들었고
아무리 중복되더라도 후에 확장성을 생각한다는 명목하에 만들었다.
그렇게 하다보니
@Getter
public class BoardRequest {
private String title;
private String content;
private String name;
private String phone;
private MultipartFile imageFile;
...
}
@Getter
public class BoardUpdateRequest {
private String title;
private String content;
private String name;
private String phone;
private MultipartFile imageFile;
...
}
이런 중복된 필드가 많이 겹친 class 들이 생겨났다. 정말 심했을 땐
@Getter
public class BoardMemberPhoneRequest {
private String phone;
}
요런 유도리 없이 목적에만 충실한 DTO를 만들어내기도 했다.
자 또 한번 생각해보자 과연 위의 DTO들은 올바르다고 할 수 있을까.
이런 과정을 겪다보니 나 스스로의 컨벤션을 정하게 되었다.
1. 각 api에 request, response 를 분리한다.
2. requestDTO에는 엔티티와 같은 필드를 가진 공통 dto를 작성한다.
3. 중복된 필드값이 많아도 목적이 다르다면 분리해서 클래스를 만든다.
4. responseDTO 필드에는 null값을 포함되선 안된다.
5. record class를 사용해서 dto를 작성한다.
이 정도의 기본 골조를 잡아두어 내 나름대로 만족한 dto를 작성하고 있다.
3번의 경우 예시를 들고 싶은 부분이
바로 위에서 소개한 BoardRequest와 BoardUpdateRequest이다.
2개의 클래스는 각각 게시판 작성, 게시판 수정의 목적으로 만든 class이지만
꽤 많은 필드에서 중복된 부분이 존재한다.
하지만 두 클래스의 목적은 엄연히 다르다고 볼 수 있다.
이렇게 되면 각 DTO는 확장성을 생각해보지 않으면 안된다.
BoardUpdateRequest에서는 imageFile을 지워야 할 deleteFile 필드가 존재해야 할 수도 있고
그런 필드들이 산발적으로 들어오게 된다면 단지 중복된 필드가 많다고 하여
BoardRequest가 BoardUpdateRequest의 역할을 할 수는 없게 된다.
블로그에서 꽤나 자주 보이고 회사 동료 개발자분도 사용하고 있는 방법인데
//inner dto class
@Getter
public class TestDTO {
private String name;
@Getter
public static class TestInnerDto {
private String test;
}
}
//controller
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("")
public String test(@RequestBody TestDTO.TestInnerDto dto) {
log.info(dto.getTest());
return "good";
}
}
위와 같이 공통의 dto안에 innerClass를 만들어 관리하는 방법이다.
나의 생각으로는 도메인의 크기가 커지면 커질 수록
가독성과 클래스가 너무나도 커지는 경향이 있어
지양해야 하는 방법이 아닐까 싶다.
그리고 innerClass를 사용하게 되면 나타나는 불편한 진실이 있었는데
@Getter
public class TestDTO {
@NotNull(message = "name is null")
private String name;
@Getter
public static class TestInnerDto {
@NotNull(message = "test is null")
private String test;
}
}
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("")
public String test(@RequestBody @Valid TestDTO.TestInnerDto dto, BindingResult bindingResult) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
log.info(fieldError.getField());
}
log.info(dto.getTest());
return "good";
}
}

바로 Validation이 걸리지 않는다..
분명이 Post요청으로는 아무런 값을 보내지 않고 있지만
BindingResult, advice에서 valid에러가 잡히지 않는다는 것이다.
꽤나 큰 하자가 아닌가?
이게 만약 InnerClass에서 Valid에 걸리는 방법이 있다고 해도
그 방법이 과연 @Valid를 통해서
예외를 잡는 방법보다 쉬울까 싶다.
올바른 DTO 에 대해서 생각해보며 지금까지 내가 사용한 방식과 보았던 방식을 정리해 보았다.
확실한 정답은 없겠지만 속한 소속에 따라 이런 자잘한 부분의 컨벤션을 정해두고
개발을 진행한다면 그것이 진짜 올바른 DTO가 아닐까? 싶다.
