[EduKit] 멀티모듈 도입기

2025. 6. 26. 00:58·Spring

왜 멀티 모듈 구조로 전환을 결심한 계기

처음 우리 팀은 하나의 단일 모듈 안에 모든 기능을 밀어넣는 방식으로 개발을 시작했다. MVP 단계에서는 빠르게 기능을 구현하고 결과를 검증하는 것이 우선이었기 때문에, 모듈을 나누는 일은 우선순위에서 밀릴 수밖에 없었다. 하지만 시간이 지날수록 새로운 기능이 추가되고, 코드 베이스가 커지면서 자연스럽게 구조에 대한 고민이 깊어지기 시작했다. 특히 생기부 AI 기능의 1차 개발이 끝나고, 이어서 커뮤니티 서비스와 교사 인증 배치 기능이 순차적으로 개발될 예정이었기에, 기능 간의 책임을 분리하고 테스트/배포 단위를 나눌 수 있는 아키텍처가 필요하다는 판단이 들었다.

무엇보다 결정적인 이유는, 이 프로젝트 안에 기술적 성격이 완전히 다른 두 기능이 함께 존재했다는 점이다. 하나는 사용자 간 상호작용이 중심이 되는 커뮤니티 기능이었고, 다른 하나는 교사들이 보다 쉽게 학생 생활기록부를 작성할 수 있도록 돕는 AI 기반의 생기부 보조 기능이었다.이 두 기능은 기술적으로나 도메인적으로나 전혀 겹치는 부분이 없었고, 사용하는 자원의 종류도 달랐다. 커뮤니티 기능은 IO 중심의 작업이 대부분이었고, 생기부 AI 기능은 텍스트 분석과 생성이 포함된 CPU 중심의 작업이었다. 이런 상황에서 자연스럽게 “각 기능을 별도의 모듈로 나누는 것이 낫지 않을까?”라는 고민이 생기게 되었고, 실제로 기능별로 API 모듈을 분리하는 방향으로 아키텍처를 리팩토링해볼 생각을 하게 되었다. 이런 기술적인 이유들 외에도, 코드 관리와 운영 측면에서의 부담도 무시할 수 없었다. 커뮤니티 기능이 배포되는 와중에 AI 기능에서 에러가 발생한다면, 전체 서비스가 영향을 받을 수도 있다는 불안이 항상 존재했다. 우리가 처음부터 도입했던 CI/CD 파이프라인은 모든 기능이 하나의 jar로 묶이는 구조였고, 배포 단위를 세분화할 수 없다는 점은 빠르게 반복적으로 배포해야 하는 팀에게는 명백한 리스크였다. 이러한 고민들을 멘토님들과 공유했을 때, 멀티 모듈 구조로 전환해보는 것을 적극적으로 권유받았고, 결국 구조적 리팩토링을 결심하게 되었다.

왜, 어떻게, 무엇을 고민했을까?

기능별 API 모듈 분리를 할 것인가?

처음에는 “기능별 API 모듈 분리”를 통해 MSA처럼 구성할 수 있다고 생각했다. 하나의 API 모듈에서 커뮤니티 기능을, 다른 모듈에서는 AI 기능을 처리하도록 나누면 논리적으로도 깔끔하고 장애 전파도 막을 수 있다고 판단했다. 예를 들어, AI 서버에서 높은 부하로 인해 장애가 발생해도 커뮤니티 기능이 정상 동작할 수 있도록 구성하는 식이다. 실제로 AI 기능은 특정 요청 한 건당 수십 초에 이르는 처리 시간이 필요한 반면, 커뮤니티 기능은 빠른 응답성과 높은 사용자 상호작용 빈도가 요구되었기 때문에, 두 기능은 운영 관점에서도 상당히 다르게 다뤄져야 했다.

또한 각 기능을 모듈로 분리한다면 독립적인 배포와 테스트가 가능해지기 때문에 협업과 책임 구분 측면에서도 유리할 수 있었다. 이 모든 이유를 바탕으로 초반에는 API 레벨에서의 기능 분리를 진지하게 고려했었다.

다시 단일 API 모듈로 통합하다.

하지만 아키텍처 설계를 고민하던 도중 시청했던 한 컨퍼런스 영상에서, 인상 깊은 문장을 보았다.

A good architecture allows you to defer critical decisions.
- Robert C. Martin -

api 모듈들을 독립적인 서비스로 나누는 것은 확실히 많은 장점이 있다. 하지만, 실제 서비스를 운영하는 관점에서 본다면 운영 비용이 상당히 증가하게 된다. 팀원과도 이에 대해 지속적으로 대화를 나눠보았고 API 모듈을 쪼갠다면 현재 단일 서버에서도 충분히 감당 가능한 트래픽 수준에서 불필요한 구조적 분리로 인해 더 복잡한 배포, 더 많은 CI 설정, 더 높은 진입 장벽이 발생할 수 있다는 쪽으로 의견이 모였다.

때문에 초반에는 단일 API 모듈로 구성하되 나중에 만약 서비스가 대박이나서 생존을 위해 서비스를 분리하지 않으면 안되겠다 싶을 때 분리하기로 결심했다. 다만, 논리적인 분리와 물리적인 분리를 구분하는 방향을 선택했다. 즉, 단일 API 애플리케이션으로 유지하되, 그 내부에서 기능별, 계층별 분리를 통해 후에 모듈로 분리가 쉬워지도록 구성하기로 하였다.

배치성 로직은 별도 모듈로 처리하자

단일 API 모듈을 사용하더라도, 그 안에서 완전히 다른 실행 방식이 요구되는 기능은 분리해야 했다. 대표적인 것이 교사 인증을 위한 배치 작업이었다. 이 작업은 사용자 요청에 의해 즉시 실행되는 것이 아니라, 매 학기 정기적으로 실행되어야 하며 외부 인증 시스템과 연동해야 한다. 또한 이 작업은 비동기적으로 실행되고, 사용자와 직접적인 연관이 없는 백엔드 로직이기 때문에 서빙 애플리케이션과는 성격이 다르다고 판단했다.

그래서 우리는 이 부분을 batch라는 별도 모듈로 분리했다. 이 모듈은 Runnable 클래스를 통해 주기적으로 실행되며, main 메서드를 갖는 독립 실행 가능 구조로 설계했다. 이렇게 하면 배치 작업은 운영 중인 API 애플리케이션에 영향을 주지 않으면서, 별도로 스케줄링하거나 관리할 수 있게 된다. 

외부 시스템 연동은 별도의 external 모듈로 분리하자

우리는 외부 API(OpenAI, AWS SES 등)와의 연동 로직을 모두 external이라는 별도 모듈에 위치시켰다. 이 결정의 배경은 "격벽(Isolation Barrier)"라는 개념이다. 외부 시스템은 우리가 통제할 수 없는 환경이다. 언제든 장애가 발생할 수 있고, 요청 포맷이나 응답 스펙이 변경될 수 있으며, 호출 횟수 제한이나 요금 정책이 바뀔 수도 있다.

이런 외부 시스템과 직접적으로 연결되는 코드를 도메인 계층이나 서비스 계층 안에 포함시킨다면, 외부 변화가 내부 로직에 즉각적으로 영향을 미치게 된다. 이를 방지하기 위해 외부 연동을 은닉하고, 모든 상호작용을 한 모듈 안에 가둬서 관리하는 것이 더 안전하고 유지보수하기에도 용이하다고 판단하여 별도의 모듈로 분리하였다. 이렇게 1차로 완성된 모듈 초안은 아래 사진과 같았다.

 

끊임 없는 모듈 변경

멀티 모듈 프로젝트는 이번이 처음이었기에, 정말 수많은 블로그 글을 찾아 읽고, 관련된 영상도 꾸준히 찾아봤다. 하지만 구조 설계에는 정해진 정답이 없다는 사실을 곧 깨달았다. 사람마다, 회사마다 추구하는 패턴과 구조가 전부 달랐고, 각자의 상황과 필요에 따라 선택한 방식이 모두 조금씩 달랐다. 그런 다양한 예제와 조언들 속에서 나 역시 어떤 구조가 우리 팀에 가장 적합할지를 계속해서 고민하게 되었고, 그 과정에서 모듈 구조도 몇 번씩 바뀌게 되었다.

웹 계층과 도메인 계층의 분리

그다음 고민은 웹 계층과 도메인 계층의 분리였다. 이 부분은 흔히들 이야기하는 “계층형 아키텍처”의 관점에서 생각했다. 우리는 사용자 요청을 직접 처리하는 컨트롤러나 DTO는 api 모듈에 두고, 실제 비즈니스 로직과 엔티티는 domain 모듈에 두는 구조로 분리했다.

영상들을 계속해서 보다보니 멀티모듈 구조에서 시작되었던 고민이 클린 아키텍쳐 쪽으로 빠지게 되었다.

좀 더 욕심을 부려, 클린 아키텍처처럼 domain 모듈은 JPA 등 외부 기술에 의존하지 않고 순수한 POJO 객체로만 구성하려고도 했다. 실제로 한동안은 위의 사진처럼 엔티티와 도메인 모델을 분리해놓고, 양방향 변환을 위한 Mapper 클래스를 작성해가며 적용해봤다. 하지만 현실은 이상과 달랐다. 매 도메인마다 변환 로직이 반복되었고, 프로젝트 규모에 비해 오히려 복잡성만 증가했다. 특히 우리는 대부분의 로직이 JPA 기반에서 실행되기 때문에, 굳이 도메인 객체를 POJO로 유지하면서 JPA와의 변환 비용을 감수할 실익이 없다고 판단했다.

결국 domain 모듈에 JPA 엔티티와 서비스 클래스를 함께 두는 현실적인 구조로 결정하게 되었다. 지금은 이 결정 덕분에 코드 간소화와 유지보수의 편의성을 얻고 있다. 좋은 선택이었던 것 같다, 굿굿~!

Service 계층은 어디에 둘 것인가?

또 하나의 주요 고민은 Service 클래스의 위치였다. 일부 의견은 서비스 계층은 영속성과도 다르고 API와도 거리를 둬야 하니, application이라는 별도 모듈로 분리하자고 제안했다.

하지만 실제 논의와 실험을 거치면서, 그렇게 분리했을 때 얻는 이점은 거의 없다는 결론에 이르렀다. 현재 구조에서는 대부분의 서비스가 하나의 JPA 트랜잭션 단위에서 엔티티를 조회하고, 변경하고, 응답을 조합하는 역할이었기 때문에 굳이 별도의 application 모듈로 분리하는 것이 오히려 중복된 의존성과 모듈 경계를 증가시키는 결과만 낳게 되었다. 그래서 우리는 서비스 계층도 결국 domain 모듈에 포함시키기로 결정했다. 컨트롤러는 api, 서비스와 엔티티는 domain, 배치성 작업은 batch, 외부 연동은 external로 나누는 단순하면서도 효과적인 구조가 현재 프로젝트 상황에 가장 잘 맞는다는 판단이었다.

최종적인 모듈 구조

이렇게 멀티 모듈 구조에 대한 고민과 조사를 반복하다 보니, 구조를 변경하기로 결심한 후 어느덧 1주일이 지나 있었다. 그동안 계속해서 모듈 설계를 고민하느라 정작 개발은 단 한 줄도 진행하지 못했다. 이게 과연 맞는 방향인가 하는 의문이 들기 시작했다. 분석만 하다가 프로젝트의 개발을 놓치고 있는 건 아닐까 하는 불안감도 들었다.

이때, 마지막으로 봤던 컨퍼런스 영상에서 아래와 같은 문장을 보게 되었다.

분석, 설계에 매몰되기 보다는 지금 할 수 있는 최소한의 구현을 즉시 하자. 개념+격벽을 활용해서 구현을 하고, 테스트코드를 통해 증명 + 피드백을 받으면서 구현을 계속하다 보면 결국 설계가 된다

지금 돌아보면, 그 말이 정말 맞았던 것 같다. 구현을 시작하고 실제 문제에 부딪히다 보니, 자연스럽게 어떤 구조가 우리 팀에 더 적합한지 감이 잡히기 시작했다. 막연한 설계보다는 현실 속 제약과 요구 사항 속에서 도출된 구조가 훨씬 더 단단한 설계가 되는 것 같다. 결국 더 이상의 고민은 멈추고, 우리 프로젝트에 가장 적합한 구조를 아래와 같이 고정하기로 최종 결심하게 되었다.

초기 모듈 구조와 비교했을 때, 우선 큰 변화 중 하나는 domain 모듈의 명칭을 core로 바꾼 것이다. 이 모듈은 단순히 도메인 객체만을 담는 것이 아니라, 영속성 계층부터 도메인 서비스, 비즈니스 로직까지 모두 포함하고 있었기 때문에, 보다 포괄적인 의미의 core라는 이름이 적합하다고 판단했다. 팀원과의 논의 끝에 이 명칭을 최종적으로 채택하게 되었다.

또한 초반 설계에서는 api 모듈이 외부 시스템과의 연동을 담당하는 external 모듈을 컴파일 타임에 직접 참조하도록 되어 있었지만, 이 방식은 모듈 간 결합도를 높이는 문제가 있었다. 외부 연동은 어디까지나 비즈니스 로직의 일부로서 core가 이를 의존하고, api는 오직 core만을 바라보는 구조로 정리하는 것이 더 바람직하다고 판단했다. 이에 따라 의존성 방향을 재조정하여, api → core → external 순서로만 흐르도록 재설계하였다.

설계는 책상 위에서 탄생하기보다는, 구현 속에서 현실을 마주하며 다듬어지는 것임을 다시 한번 경험하게 되었다.

구현을 하면서 부딪혔던 문제

멀티 모듈 구조에서 발생한 의존성 순환 문제

멀티 모듈 구조로 전환하기로 결심하고 본격적으로 구현을 시작했을 때, 처음에는 기존의 레포지토리나 서비스 코드를 그대로 옮겨오면 금방 마이그레이션이 끝날 줄 알았다. 하지만 막상 코드를 나누고 모듈 간의 의존성을 분리하려고 하다 보니, 생각보다 훨씬 많은 문제가 숨어 있었음을 깨달았다.

그중에서도 가장 자주 마주했던 문제는 바로 의존성 순환 문제였다. 멀티 모듈 구조는 기본적으로 모듈 간 독립성과 은닉성, 그리고 명확한 의존성 방향을 보장해야 한다. 모듈이 서로를 직접 참조하는 일이 잦아질수록 구조는 점점 더 단단함을 잃고, 결국 유지보수와 확장성 측면에서 일관성을 해치게 된다. 그런데 기존의 구조에서는 무심코 구현해두었던 코드들이, 모듈로 분리하려고 하자 여기저기서 순환 의존이 발생하며 발목을 잡았다.

Spring Security 연동에서 발생한 순환 의존 문제

그중 하나의 구체적인 예를 들자면, UserDetailsService 구현과 관련된 문제가 있었다. 현재 시스템은 아래와 같은 구조를 가지고 있었다.

모듈 주요 책임 주요 의존성
Core 비즈니스 로직, 영속성 계층 Spring-data-jpa
Api API 요청 처리, 인증/인가 설정 Spring-security

인증/인가 과정을 구현하면서 UserDetailsService를 커스터마이징해야 했는데, 이 구현체 내부에서는 JPA를 통해 DB에서 사용자를 조회해야 했다. 처음에는 당연히 해당 로직을 도메인 계층에 위치시키려 했다. 사용자 조회는 도메인 로직이고, JPA도 core 모듈에 있기 때문이다. 하지만 문제는 UserDetailsService 자체가 spring-security에 포함된 인터페이스라는 점이었다. 만약 core 모듈에 이 구현체를 두게 되면, core가 spring-security에 의존해야 하고 이는 모듈 간 역할 분리를 명확히 하겠다는 원칙에 어긋나게 된다. 결국 도메인 모듈이 프레임워크 설정까지 알게 되는 구조가 되는 셈이었다.

이는 모듈 간 역할을 명확히 분리하겠다는 멀티 모듈 구조의 설계 원칙에 정면으로 위배되는 일이었다. 이전까지는 아무런 경계 없이 한 프로젝트 안에서 자유롭게 의존할 수 있었기 때문에 크게 의식하지 못했지만, 모듈을 나누는 순간 얼마나 강하게 결합된 코드였는지를 온몸으로 체감할 수 있었다. 그만큼 기존 코드는 레이어 간 추상화가 부족했고, 인프라나 프레임워크에 지나치게 의존적인 구조였던 것이다.

마무리하며..

멀티 모듈 구조를 적용하면서 가장 크게 느낀 점은 모듈 분리는 곧 추상화와 책임 분리의 문제라는 것이다. 처음에는 단순히 "코드를 나눈다"는 개념으로 접근했지만, 막상 나누고 보니 의존과 결합의 문제들이 생각보다 훨씬 더 깊이 얽혀 있었다. 그동안 아무 제약 없이 한 프로젝트 안에서 쌓아온 코드들이 얼마나 긴밀히 엮여 있었는지를 새삼 깨달을 수 있었고, 분리를 통해 그 복잡도가 명확히 드러났다.

무엇보다 중요한 것은, 각 모듈이 가져야 할 존재 이유와 역할의 경계였다. 단순히 모듈을 나누는 것이 아니라, 왜 이 책임이 이곳에 있어야 하고, 어디까지를 알아야 하며, 무엇은 몰라야 하는지를 명확히 하는 것. 그것이야말로 멀티 모듈 구조의 핵심이자, 유지보수가 가능한 설계의 첫걸음이라고 생각한다.

이번 경험을 통해 얻은 가장 큰 수확은, 완벽한 설계는 절대 부딪히지 않고는 할 수 없다는 사실을 직접 경험한 것이라 생각한다. 구조 설계는 처음부터 완성된 형태로 주어지는 것이 아니라, 개념을 정의하고, 실제 개발 과정에서 직접 부딪히며 문제를 경험하고, 이를 바탕으로 리팩토링을 반복하는 것 통해 점점 더 완성도 높은 구조로 다듬어지는 작업이었다. 결국 좋은 설계란 책에서 배운 이론을 그대로 적용하는 것이 아니라, 현실의 문제 속에서 개선점을 발견하고 스스로 설계 기준을 만들어가는 과정에 가깝다고 생각한다. 

 

여전히 멀티 모듈로의 기존 코드 마이그레이션은 끝나지 않은 상태다. 앞으로도 좋은 코드와 구조에 대해 끊임없이 고민하고, 발전시켜 나가며 이 과정을 통해 더 견고하고 유지보수하기 쉬운 아키텍처를 만들어 가야겠다.💪🏻

'Spring' 카테고리의 다른 글

MDC를 활용한 로깅 개선기  (0) 2025.08.03
효율적인 로그 관리를 위한 Spring Boot + Logback 설정  (1) 2025.07.05
이벤트 기반 처리로 구현하는 회원가입 및 이메일 전송 (feat. Thread pool)  (0) 2025.06.11
재시도 폭풍에서 서버를 지키는 방법 (feat. 동시성 문제)  (2) 2025.05.21
로그인 코드 리팩토링 대장정  (0) 2025.03.19
'Spring' 카테고리의 다른 글
  • MDC를 활용한 로깅 개선기
  • 효율적인 로그 관리를 위한 Spring Boot + Logback 설정
  • 이벤트 기반 처리로 구현하는 회원가입 및 이메일 전송 (feat. Thread pool)
  • 재시도 폭풍에서 서버를 지키는 방법 (feat. 동시성 문제)
wing1008
wing1008
휘발을 막기 위한 기록
  • wing1008
    차곡차곡
    wing1008
  • 전체
    오늘
    어제
    • 분류 전체보기 (68) N
      • Spring (26)
      • JPA (11)
      • JAVA (2)
      • 데이터베이스 (9)
      • 운영체제 (2)
      • 네트워크 (3)
      • 자료구조&알고리즘 (5) N
      • AI (0)
      • Etc (5)
      • 끄적끄적 (5)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
wing1008
[EduKit] 멀티모듈 도입기
상단으로

티스토리툴바