본문 바로가기

프로젝트/트러블슈팅

[트러블 슈팅] 레이어별 멀티 모듈 적용 (과도한 모듈 분리로 실패🤪)

도입

 

원데이 히어로 프로젝트는 멀티 모듈 단일 프로젝트이다. 프로젝트에 멀티 모듈을 적용하면서 트러블슈팅을 겪었다. 이번 글에선 프로젝트 내에서 과도한 모듈 분리로 인한 실패 경험을 소개하고자 한다. 단어 그대로 실패 경험이니 우리처럼 하지 마세요!라는 글이다.

 

먼저 멀티 모듈을 왜 적용했고 어떻게 모듈을 나눴는지 그리고 발생했던 문제와 해결하긴 했지만 왜 잘못된 설계인지 이야기해보겠다.

 

멀티모듈이란

 

멀티 모듈을 왜 사용할까? 멀티 모듈은 역할과 의존성 분리를 통해 시스템의 분리, 통합을 유연하게 만들어 줄 수 있는 좋은 아키텍처를 만들 수 있다. MSA에서 멀티 모듈이 부각되곤 하는데, 멀티 모듈을 활용해 역할과 의존성을 잘 분리한다면 모놀리틱에서 MSA로 용이하게 전환될 수 있다. 

 

잘 설계된 멀티 모듈은 의존성을 끊어주고 격리한 뒤 제약을 걸어 응집도를 높이고 설계 의도를 잘 드러나게 해준다. 하지만 잘 못 설계하면 모듈이 역할에 필요한 최소한의 의존성을 가지는 것이 아닌 넘치는 기술 의존을 가진 모듈이 등장한다. 

 

넘치는 의존을 가진 모듈은 이미 내부가 스파게티처럼 얽혀있다. 따라서 특정 기능을 변경하거나 모듈을 분해하고 싶어도 불가능하다. 의존도가 너무 높아 변경 영향이 시스템 전체로 퍼지기 때문이다.

 

멀티모듈을 잘 못 설계하면 문제가 발생한다는 것은 알았다. 그렇다면 어떻게 모듈을 나눠야 할까? 

 

도메인을 기준으로 혹은 아키텍처를 기준으로 나눌 수 있다. 팀원과 이야기를 나눴을 때, 도메인을 기준으로 나누기에는 각 도메인이 너무 작다고 판단했다. 그래서 아키텍처를 기준으로 나누기로 했다.

 

아키텍처는 레이어드 아키텍처이다. 각 계층이 독립적이고 관심사가 다르므로 계층별 모듈화를 통해 응집도를 높이고자 했다. 그리고 레이어드 아키텍처에서 의존성이 단방향으로 흘러야하고, 상위 계층이 하위 계층을 의존할 수 있지만 그 반대는 안된다.

 

그리고 상위 계층에서 인접한 하위 계층이 아닌 인접하지 않는 하위 계층에 요청하는 것은 피해야한다. 그래서 모듈의 의존 방향을 통해 제약을 걸었다. 상위 계층 모듈은 인접한 하위 계층만 의존하고 하위 계층은 상위 계층을 모른다.

 

아래는 원데이히어로 프로젝트 모듈 구조의 일부이다. 

 

api는 presentation 계층으로 실행 가능한 모듈이다. MVC와 RestDocs를 의존하고 있다.

 

application은 비즈니스 로직이 있는 계층 모듈이고 domain은 데이터를 접근하는 계층 모듈로 entity를 관리한다. 

 

domain은 Spring Data JPA를 의존하고 있다. 이때 application은 @Transaction을 사용하기 위해 JPA가 필요하다. 따라서 domain 모듈에서 JPA를 api 의존성으로 두었다.

 

마지막으로 common은 모든 모듈이 의존하고 있는 공통 모듈로 어떠한 의존성도 가지지 않는다. common 모듈은 프로젝트 전체에서 사용할 커스텀 예외, 유틸 클래스를 관리한다.

 

프로젝트 모듈 구조

 

문제

프로젝트에서 동적 쿼리 혹은 복잡한 쿼리에서 QueryDsl을 사용하고 있다. QueryDsl은 QueryDsl을 위한 gradle 설정도 해줘야한다. 그리고 보통 조회를 위한 복잡한 쿼리를 작성할 때 사용하므로 화면(View)을 위한 읽기 작업을 수행한다. 뷰 로직은 자주 변경되므로 분리하는 것이 좋다.

 

따라서 아래와 같이 QueryDsl을 위한 모듈을 만들어주었고 처음엔 domain 모듈이 직접 의존하도록 했다. 그리고 QueryDsl을 모듈로 분리한 순간부터 우리는 잘못된 길로 들어서기 시작했다..🤪

 

QueryDsl 모듈 추가

 

위의 모듈 구조는 domain이 직접 QueryDsl을 의존하고 있다. QueryDsl은 데이터베이스에 존재하는 영속 데이터로 접근하여 읽는 기술 중 하나이므로 QueryDsl 모듈은 infrastruction 레이어라고 판단했다. 

 

위와 같이 domain 모듈과 같이 고수준 모듈이 infrastructure 모듈(QueryDsl 모듈)과 같이 저수준 모듈에 직접 의존한다면 "테스트 어려움", "확장 어려움"과 같은 문제가 발생한다.

 

그리고 QueryDsl은 Annotation Processor 방식으로 동작한다. 따라서 domain 모듈에 위치한 @Entity을 읽은 뒤 domain 모듈 내에 QClass를 생성한다. QueryDsl 모듈은 domain 모듈에 위치한 QClass를 읽어야했다.

 

그리고 JPARepository를 상속한 Repository가 QueryDsl 기능도 가질 수 있도록 QueryDsl 용 인터페이스를 만들고 이를 상속하도록 했다. QueryDsl 용 인터페이스는 domain 모듈에 위치해있고 QueryDsl 모듈은 이 인터페이스의 구현체를 관리한다.

 

결국 QueryDsl 모듈은 domain 모듈의 QClass, 인터페이스를 알아햐 한다.

public interface Repository extends JpaRepository<Entity, Id>, QueryRepository {
}

public interface QueryRepository { // QueryDsl를 위한 Repository
}

 

이 모든 문제를 해결하기 위해 모듈에 DIP를 적용하기로 했다. 고수준 모듈과 저수준 모듈 사이에 인터페이스라는 추상체를 두어 의존 방향을 역전시키는 것처럼 모듈에도 적용하고자 했다.

 

아래와 같은 모듈 구조가 되었는데, 하나씩 살펴보자.

 

infra-mapper 모듈은 infrastructure layer의 추상체를 관리하는 모듈로 Repository가 위치한다. infra-mapper 모듈은 doamain을 api로 의존하는데 domain에 위치한 QClass를 QueryDsl 모듈이 알도록 하기 위함이다.

 

application 모듈은 해당 모듈 안에 위치한 Service가 Repository를 사용하기 위해 infra-mapper 모듈을 의존한다.

 

마지막으로 QueryDsl 모듈은 infra-mapper에 위치한 추상체의 구현체를 관리한다.

 

모듈에 DIP 적용

 

하지만 application이 QueryDsl을 모르다 보니 QueryDsl에 위치한 구현체 빈을 찾을 수 없다는 에러가 발생했다. 

 

그리고 멘토님께 모듈 구조를 보여드렸더니 구조가 너무 복잡하다고 말씀해주셨다. 프로젝트에 새로운 인원이 들어온다면 모듈 구조를 이해하지 못할 것 같다는 답변을 주셨다. (지금 글을 작성하면서도 이렇게 복잡해서야 독자들이 이해할 수 있을까, 의문만 든다🧐)

 

빈을 찾을 수 없다는 에러

 

결국 QueryDsl을 모듈로 분리하지 않고 domain에 위치하도록 변경했다. 그리고 개발이 진행되면서 채팅과 알림 도메인이 추가되었고 다시 멀티 모듈 지옥에 빠지기 시작했다🌋

 

MongoDB, Redis, WebSocket을 사용해야 하는 채팅 도메인은 기존 도메인하고 의존성이 다르므로 채팅 모듈로 분리했다. 그리고 알림 도메인에서도 MongoDB를 사용해야 하므로 채팅 모듈에 Spring Data Mongo를 둘 수 없었다. 따라서 mongo를 하나의 모듈로 분리했다. 

 

채팅과 MongoDB를 모듈로 분리하다 보니 다시 모듈 구조가 복잡해졌다. common은 모든 모듈이 의존하고 있고 common 모듈은 그림에서 생략했다.

 

먼저 application 모듈은 mongo 모듈의 MongoRepository를 사용해 알림 서비스를 구현했다.

 

다음으로 채팅 모듈의 의존 방향을 살펴보겠다. 채팅방은 RDBMS에 채팅 내역은 Mongo에 위치하다 보니 채팅 모듈(chat)은 domain과 mongo 모듈을 의존해야 했다. 그리고 채팅방을 읽고 쓰는 작업은 WebSocket 통신이 아닌 HTTP 통신을 하다보니 api 모듈과 application 모듈이 chat 모듈을 의존한다. 

 

최종 모듈 구조

무엇이 문제였을까?

우리는 왜 다시 복잡한 모듈 구조를 가지게 되었을까?

 

첫 번째, 성급한 모듈화를 진행했다. 

 

구현이 먼저가 아닌 모듈화가 먼저 진행되었다. 구현의 성숙도가 낮을 때 모듈화를 진행시켰다.

 

두 번째, 과도한 추상화로 복잡한 모듈 구조가 탄생했다.

 

첫 번째와 이어지는데, 구현의 성숙도가 낮은 상태에서 모듈화가 진행되어 모듈이 과도하게 분리되었고 적절성을 판단하기 어려웠다.

 

[Server] 멀티 모듈을 설계하는 관점과 고려사항 with Spring & Gradle, 2023 인프콘에서 멀티모듈에 대해서 발표한 내용을 살펴보니, 모듈 구조는 최대한 단순하게 가져가야 한다고 한다. 모듈을 지나치게 세분화한다면 모듈 구조를 처음 접하는 사람은 인지 부하를 줄 수 있다고 한다. 그리고 모듈의 수가 증가할 수록 그만큼 관리 포인트가 늘어나므로 비용적으로 손해라고 한다.

 

세 번째, 아키텍처를 모듈과 일치시키려는 강박이 있었다. 

 

아키텍처와 모듈은 서로 다른 개념임에도 DIP을 모듈에 나타내려고 했다. 아키텍처는 시스템의 전체적인 구현의 형태인 반면, 모듈은 지켜야할 부분에 제약을 거는 것이다. 아키텍처를 투영시키기 위해 모듈을 사용하는 것은 좋지 않다. 따라서 아키텍처에서 제약을 걸고 격리를 시켜 설계 의도를 보여주고 싶은 부분만 모듈화하는 것은 좋다.

 

참고