기존에는 단순히 service/novel, service/episode, controller/novel 처럼 레이어에 더 중점을 두고 패키지를 설계했었다. 이유는 일단 지금까진 이렇게 패키지를 구성하기도 했고 솔직히 초반에는 어떻게 구조를 잡으면 좋을 지 감이 안잡혀서 일단 이렇게 시작했었다.
하지만 이후 Novel, Episode 까지 기능을 구현하면서 아직 그렇게 복잡한 애플리케이션이 아닌데도 생각보다 레이어 모듈 중심의 패키지 구조가 불편하게 다가왔다.
특히 특정 도메인의 기능을 수정하려고 할 때 클래스를 찾기가 불편했다. 나는 특정 도메인의 Service를 수정한다면 Service가 먼저 머리에 떠오른다고 생각해 레이어를 더 상위 레벨로 두었던건데 오히려 개발할때는 역효과가 나는 것이었다.
그래서 일단 Episode 기능까지 빠르게 만들고 패키지 구조를 재설계 하기로 결정했고, 계획대로 Episode 기능을 만든 다음날 바로 구조를 리펙토링 했다.
일단 최종 패키지 구조는 다음과 같다.
src/main
├── java
│ └── com
│ └── iucyh
│ └── novelservice
│ ├── common
│ │ ├── config
│ │ ├── converter
│ │ ├── domain
│ │ ├── dto
│ │ │ ├── apiresponse
│ │ │ │ └── information
│ │ │ └── response
│ │ ├── enumtype
│ │ │ └── valuedenum
│ │ ├── exception
│ │ │ └── errorcode
│ │ ├── repository
│ │ ├── util
│ │ └── validator
│ │ ├── enumfield
│ │ └── notblank
│ ├── episode
│ │ ├── constant
│ │ ├── domain
│ │ ├── exception
│ │ ├── repository
│ │ │ ├── projection
│ │ │ └── query
│ │ │ ├── condition
│ │ │ └── dto
│ │ ├── service
│ │ └── web
│ │ └── dto
│ │ ├── mapper
│ │ ├── request
│ │ └── response
│ └── novel
│ ├── constant
│ ├── domain
│ ├── enumtype
│ ├── exception
│ ├── repository
│ │ └── query
│ │ ├── condition
│ │ ├── cursor
│ │ ├── dto
│ │ └── pagingquery
│ ├── service
│ │ └── codec
│ └── web
│ ├── controller
│ └── dto
│ ├── mapper
│ ├── request
│ └── response
└── resources
├── static
└── templates
이 구조를 통해 특정 기능을 수정할 때 아래와 같은 개선을 얻을 수 있었다.
이 개선의 핵심은 레이어 중심이 아니라 도메인 중심으로 구조를 설계해 novel, episode, common 같은 도메인들이 제일 상위에 위치하도록 변경했다는 것이다. 또한 common 을 제외한 나머지 novel, episode 같은 도메인들은 동일한 패키지 구조로 설계해 일관성을 유지했다. (enumtype의 경우에는 episode에는 아직 필요없는 패키지라서 굳이 만들지 않았다.)
각 도메인별로 controller, dto는 web 패키지 하위로 두었는데, 이유는 다음과 같다.
api 쪽 DTO 들은 원래 NovelDto, CreateNovelDto 처럼 Dto 라는 접미어를 붙이고 dto/ 패키지에 전부 모아두었는데, 이 부분은 확실히 어떤게 요청에 사용되고 어떤게 응답에 사용되는 지 알아보기 힘들다고 생각해 요청 dto는 Request, 응답 dto 는 Response 접미어를 붙이는 것으로 통일했다.
그리고 패키지도 세분화 해 dto/request, dto/response 로 분리하였다. 또 mapper 는 NovelRequestMapper, NovelResponseMapper 두개를 같은 레벨에 두었는데, 어짜피 mapper 는 이 두개 이상으로 늘어날수도 없어서 굳이 더 나누지는 않았다.
또 API DTO 들은 전부 Record 타입으로 변경하였다. 여러 자료를 찾아본 결과 API DTO 가 주로 쓰이는 RequestBody, ModelAttribute, Jackson 모두 All Args 생성자나 Record를 지원하고, DTO 를 봤을 때도 Record 가 좀 더 깔끔하고 DTO를 표현하기 적합하다고 판단해 적극적으로 도입하였다.
하지만 FailResponse, SuccessResponse 이 두개는 공통 응답 포맷인데, 일부러 Record 를 쓰지 않았다. 왜냐하면 일단 생성자에서 모든 값을 받지도 않고, isSuccess 라는 필드에 기본값을 넣어주고 있었는데 이걸 컴팩트 생성자를 활용하게 되면 괜히 복잡해지는 것 같아 더 단순하게 class 로 두었다.
참고로 이번에 패키지 구조를 개선하면서 FailResponse, SuccessResponse 두 공통 응답 포맷도 mapper 를 따로 두었는데, 이유는 모든 곳에서 단순히 new 연산자를 사용해 생성하게되면 나중에 DTO 가 변경되었을 때 수정해야 할 지점이 너무 늘어난다고 생각했기 때문이다.