변경에 대한 온도차 줄이기
일교차
여름이 다가오고 있는 것 같다. 아직은 시기상 초여름으로 분류되지만 꽤나 더운 날씨들이 계속되고 있다.
초여름과 한여름의 차이가 뭘까 한번 생각해봤는데, 일교차가 적다는 것이 특징이 아닐까 싶다. 전자의 날씨는 아침, 저녁에는 비교적 선선한 반면에 여름이 절정에 달하면 뜨거운 날씨가 거의 하루종일 지속된다.
멍멍이도 안걸린다는 여름 감기에 걸리는 시기가 주로 일교차가 큰 초여름이라고 한다. 이런 경험에 기반한 본능인걸까, 우리는 변화무쌍한 것보단 안정적인 것을 선호하는 경향이 있을 것이다.
소프트웨어, 코드도 결국 현실의 물체나 현상을 옮겨온다고 생각하기 때문에 이런 습성이 반영되는 듯 하다. 우리는 다른 클래스의 변화에 민감하지 않고 서로 의존적이지 않은 아키텍쳐를 좋은 구조라고 말한다.
오늘은 운영중인 서비스의 패키지 구조를 어떤 식으로 개선해 나갔는지, 그동안 해왔던 생각들을 정리해보고자 한다. 물론 정답은 없다. 그래도 오답은 피하려고 노력해봤다.
1. 기능
controller, service, repository…
처음엔 기능별로 클래스들을 분류했다.
1
2
3
4
5
6
7
8
9
10
11
12
└── src
└── controller
└── NoticeController
└── MapController
└── ...
└── service
└── NoticeService
└── MapService
└── ...
└── repository
└── NoticeRepository
└── ...
진짜 서비스 초창기에는 컨트롤러 2개 있고, api 서버에서 하는 일이 거의 없다시피 해서 관리에 큰 어려움이 없었다.
그런데 점점 기능에 살이 붙어가고, 서버쪽 코드가 커지면서 뭔가 정신 산만한 구조가 되어가고 있었다. 가장 큰 문제는 기능별로 구별하고자 했던 이 패키지 구조가 점점 의도와는 다르게 작동하는 곳들이 발생했다.
common, global 이라는 짬통
뭔가 애매한 클래스들이 점점 발생하기 시작했다. 주로 특정 작업만 단편적으로 수행하는 유틸성 클래스가 해당이 많이 된 것 같은데, 이런 애들은 어디로 분류할지에 대한 고민이 시작되었다. 그리고 가장 만만한 네이밍을 찾았으니, 대망의 common, global이다.
분류에 대한 고민이 드는 순간 빠져나올 수 없는 잡동사니 모음집 패키지에 클래스를 생성했고, 점점 세력이 커져가는 것을 보면서 이제는 더 나은 방법에 대한 고민을 할 시점이라 생각했다.
2. 속성
map, notice, auth…
이 코드가 속한 속성에 따라 패키지를 구별해보자는 생각을 했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
└── src
└── notice
└── NoticeController
└── NoticeService
└── ...
└── admin
└── AdminController
└── AdminService
└── ...
└── auth
└── AuthFilter
└── EncrpytUtil
└── ...
예를 들면 공지사항 관련 코드들을 같은 패키지에 모으고, 보안이나 권한 관련은 auth 패키지에 다 모아놓는 방식이다.
지금 다시 생각해보면 이 구조 자체가 문제라기보다는 내가 속성들에 대한 분류를 제대로 못한게 가장 큰 패인이 아닌가 싶다. 패키지를 분류하는 속성에 대한 기준이 제대로 없었다.
거미줄은 체계라도 있지
이 때 당시 가장 큰 문제점은 변경사항에 대한 영향이 너무나도 컸다는 것이다.
사실 이때가 어드민 관련 기능들이 생기던 시점인데, 뭐만 바꾸면 어드민쪽에도 변경사항이 생겼다. 가장 큰 원인은 서비스 클래스에서 이곳저곳 속성들의 서비스나 레포지토리를 중구난방으로 참조하고 있었다.
예를 들자면 어드민 기능중에 공지사항을 등록, 수정하는 기능이 있다고 해보자. 이런 기능을 전부 공지사항쪽에 구현해놓으니 어드민 서비스에서 필연적으로 공지사항 서비스를 참조해야 한다. 이를 해결하기 위해 어드민 쪽에서 공지사항 엔티티를 참조, 별도의 레포지토리를 만들고 어드민에 관련된 쿼리들을 작성했으나 결국엔 공지사항에 대한 참조는 없어지지 않은 셈이고, 오히려 속성별로 모아보겠다는 처음의 취지도 무색해지고 있었다.
더 나은 방법이 없을까 계속 고민을 했다.
3. 적절히 섞어보자
클린, 헥사고날 아키텍쳐
다른 아키텍쳐들에 대해 찾아봤는데 헥사고날 이야기가 많이 나왔다. 요즘 핫한가 보다.
포트, 어댑터와 다양한 인터페이스들로 의존성을 끊어내는 방법은 인상적이긴 했으나 내 상황에서는 너무 과한 것 같았다. 오히려 내 눈에 들어온건 4개의 계층으로 나눠진 저런 구조에 대한 그림들이었다.
3계층으로 재해석
코드를 작성하다보니 웬지 난 3개의 계층으로 나누면 코드가 적절히 분류될것 같다는 생각이 들었다. 아래와 같은 구조였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
└── src
└── presentation
└── controller
└── model
└── ...
└── domain
└── notice
└── community
└── post
└── comment
└── ...
└── infra
└── auth
└── config
└── ...
프레젠테이션 -> 도메인 -> 인프라 순으로 참조하는 구조였다.
프레젠테이션은 사용자와 상호작용 하는 부분이다. 주로 컨트롤러와 요청, 응답 모델들이 있다. 도메인은 세부 기능들에 대한 로직들이 존재하고, 인프라는 config같은 전역 설정들이나 필터, 로깅과 같은 기능들에 대한 정의가 있었다.
이 때는 도메인을 잘 구별해보려고 노력했다. 나누는 기준은 ‘나눌 수 있는 서비스의 최소 단위’였다. 예를 들면 공지사항 도메인은 공지사항을 읽어오고, 가공할 수 있는 최소 단위라고 생각했다. 하지만 커뮤니티 도메인은 ‘게시글’과 ‘댓글’로 이루어져 있기 때문에 하위 도메인을 추가적으로 나눌 필요가 있다고 생각했다.
그러나 이 방식도 여전히 어드민에서 참조가 짬뽕이 되는 현상을 막을 수는 없었다. 어드민 도메인에서 공지사항 도메인을 이곳저곳 참조하는 문제는 여전했고, 컨트롤러에서 기능들을 조합하자니 뭔가 어색했다. 컨트롤러쪽의 코드가 비대해지는 문제도 존재했다.
고민을 하다가 계층을 하나 더 추가해보기로 했다.
4계층으로 재해석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
└── src
└── presentation
└── controller
└── model
└── ...
└── application
└── noticeUseCase
└── adminUseCase
└── ...
└── domain
└── notice
└── community
└── post
└── comment
└── ...
└── infra
└── auth
└── config
└── ...
프레젠테이션 -> 어플리케이션 -> 도메인 -> 인프라 순으로 참조하는 구조다.
어플리케이션 계층에 유즈케이스라는 클래스들을 추가했는데, 여기서 각각의 도메인들에 구현된 서비스 코드들을 조합해서 하나의 완성된 기능으로 만들어낸다.
이 방식이 좋았던 점은 도메인의 응집도를 굉장히 높여주었다. 공지사항 도메인에는 공지사항과 관련된 기능들을 전부 작성해주고, 어플리케이션의 유즈케이스 클래스로 각각의 기능들을 분리해주면 된다. 유저와 관련된 유즈케이스에는 공지사항 도메인의 읽기 기능만 가져오면 되고, 어드민과 연결된 유즈케이스에는 수정, 삭제, 등록 기능을 가져온다. 이렇게 작성하니 조금 더 응집도 있는 패키지 구조가 완성될 수 있었고, 계층간 역할도 명확해졌다.
한 가지 고민이 되었던 순간이라면 어플리케이션의 유즈케이스 재활용 문제였다.
뉴스레터를 업데이트하는 기능이었는데, 스케줄러를 통해 자동으로 업데이트 되고 있었다. 그런데 어드민 쪽에서 강제로 갱신하는 기능을 만들었는데, 여기서
- 뉴스 유즈케이스를 참조할 것인지
- 도메인 계층에서 재사용할 수 있는 코드를 작성할지
- 별개의 코드를 다시 작성할지
위와 같은 고민들에 빠졌다.
일단 1번 옵션은 제외했다. 서로간의 온갖 참조 문제 때문에 새로 추가된 계층인데 여기서 다른 유즈케이스를 참조한다면 똑같은 문제가 시작되리라 생각했다. 재사용성에 너무 집중한 나머지 내가 정해놓은 추상화 계층을 깨버리기는 싫었다.
그래서 2번 방식으로 작성을 하려다 다시 생각해보니 어드민 쪽에서 강제 갱신 기능과 스케줄러를 통해 갱신되는 로직이 약간 차이가 생기게 되었다. 그래서 어느정도의 중복이 있긴 하지만 별도의 코드를 다시 작성하는 3번 방식으로 진행했다. 아마 완전히 같은 방식이라면 도메인 계층에서 서로 참조하는 코드를 작성했을것이다.
결론
내 서비스에는 당분간 이러한 구조로 코드를 작성할 것 같다. 물론 작년과 올해의 주변 세상이 다른 것처럼, 더 나은 방식이 생각나면 언제든지 바뀔 수 있다.
참고
- 내 경험