🍀 실전! 멀티 모듈 프로젝트 구조와 설계 - 김대성님

무려 18년된 레거시 프로젝트를 현대화적인 프로젝트로 전환하며 느낀 점과, 여러 차례 멀티 모듈 프로젝트 구성하면서 어떻게 하면 유지보수하기 좋은 멀티모듈 프로젝트를 만드는지 비법을 전수해주신다.


🍀 “WHY” 멀티 모듈 프로젝트는 왜 구조가 중요할까?

아키텍쳐는 프로젝트 초기에 이루어져야하는 일련의 설계 결정이다. 아키텍처는 요소의 구조와 그 관계에 관한 것.”

여기서 아키텍처라는 단어를 멀티 모듈로 바꿔서 생각해보자.

멀티 모듈 프로젝트는 초기에 이루어져야하는 일련의 설계 결정이다. 멀티 모듈 프로젝트는 요소의 구조와 그 관계에 관한 것.”

크게 어색하지 않음을 알 수 있다. 결국 멀티 모듈 프로젝트는 프로젝트의 아키텍쳐와 같이 나중에 변경하기 어렵다. (CI/CD 와 굉장히 밀접하므로, CI/CD 프로세스까지 모두 바꾸어야 한다.)

문장이 크게 와닿지는 않았지만, 멀티 모듈 구조를 처음에 잘 잡지 못하면 계속해서 고생한다는 걸 이야기하는 것 같다.

대부분의 멀티 모듈 프로젝트에서 초기에 core, command, admin, api, batch 구조에서 각 모듈이 적절하게 분배되어 있을 것이다. 그러나 서비스가 길어지면 길어질수록 core와 common이 비대칭적으로 커지고, api, batch 등이 모듈이 계속해서 추가될 것이다.

image

헉!! 완전 우리회사 멀티모듈과 구조가 동일하다!!

이렇게 되는 이유는 ‘중복 코드는 죄악이다. 중복 코드는 제거한다.’는 개발자의 강박과 같은 버릇 때문이다. 우리는 진지하게 고민해보아야 한다. core, common 모듈 속 코드가 정말 모든 곳에서 다 필요한가? 중복을 제거하기 위해 core, common과 같은 모듈에 모든 기능을 구현하면 많은 문제점이 생겨난다. 그 중 대표 4가지.

  1. Too many connections
    • common 모듈을 의존하는 모든 서비스들이 커넥션 풀을 모두 할당 받는다.
  2. NoClassDefFoundError
    • 특정한 모듈이 하위 버전 라이브러리를 참조하는 경우 업그레이드가 난해해진다.
  3. “Copy & Paste”
    • 참조하는 곳이 너무 많아 일단 if.. else 분기처리 copy & paste
    • 즉, 코드를 점점 이상하게 만들어내고, 참조하는 클래스가 많아서 어디서 사이드 이펙트가 발생할지 모르게 된다.
    • 깨진창문 효과 -> 급속도로 나쁜 코드들이 생겨남
  4. All Build & Deploy
    • 특정한 클래스를 수정해서 배포할 때도, 글자하나 바꿔서 배포할 떄도… 무조건 All Build & Deploy

멀티 모듈 프로젝트를 나누는 기준이 적절하지 않다면 시간이 흘러 4가지 사례가 모두 나타나게 된다. 그 때가서 멀티 모듈의 구조를 수정한다면, 그 과정이 굉장히 고통스럽고 많은 비용이 들 것이다.


🍀 “WHAT” 무얼 기준으로 멀티 모듈 프로젝트 구조를 나뉘어야할까?

core, common 모듈의 역할, 책임, 협력 관계가 올바른지 고민하면서 멀티 모듈 구조를 변경해본다. 시간이 지남에 따라 여러가지 시도를 해보다 결국 데이터 접근 방식에 따라(기술 베이스 지향적인) 멀티 모듈을 나누게 될 것이다.

image

아! 이렇게 하니 core, common 모듈의 부담이 줄어들었다! 그런데 이건 도메인 기반 설계가 아니라 기술 기반 설계인데… 이게 올바른건가? 결국 우리는 도메인의 위치를 고민해보아야 한다.

도메인 위치 고민해보기

뮤직 서비스의 간단한 도메인들과 함께, 노래를 하나 추가한다고 가정해보자.

image

아티스트, 앨범, 재생, 가사, 비디오 등의 도메인 정보를 어떻게 관리할까? META(뮤직서비스의 기반이 되는 공통 모듈)라는 멀티 모듈에 모두 모아둘 것이다. 그리고 이 META 모듈은 모든 모듈들에서 필요로 하고 있을 것이다.

image

만약 여기서 재생은 MySQL, 가사는 MongoDB에 적재되는 경우 META 도메인은 어떤 모듈에 위치해야할까? 기술 베이스 지향적으로 멀티 모듈을 구성하게 되면 이런식으로 결정을 내리기 어려운 문제가 발생한다. 어려움은 여기서 끝나지 않는다.

image

가사라면 가사 번역, 재생이라면 스트리밍, AI, VOD 등 외부 유관부서 및 업체 연동 모듈이 계속해서 추가되게 된다. 연동 모듈들은 지속적으로 늘어나게 된다. 연동 모듈들은 처음 구현시 큰 문제나 변경사항 없이 얌전하다가… 특정 시점이 오면 버전 업 요청이 오고, 코드 변화가 굉장히 크게 생긴다.

이 파트에서 크게 충격을 받았다. 생각해보면 우리는 아무렇지 않게 외부 서비스가 하위호환성을 유지시켜줄 것이라 믿고 사용하지 않는가? 그러나 하위 호환성을 꼭 지켜주리라는 법은 없다. 그렇게 버전업이 발생할 경우 대량의 코드가 발목을 붙잡힐 것 같다.

도대체 뭘 기준으로 나누어야 이런 복잡도를 잡아낼 수 있을까?

일단 무조건 CORE 와 COMMON을 삭제하고 시작해라.

코드가 일부 중복되는 것보다, core와 common이 잠재적으로 가지고 있는 위험성이 더 큰 문제다. 우리가 겪고 있는 지금 시대는 10년, 20년 전에 비해 엄청나게 복잡하고 고도화되어 있다. 그 때는 맞고, 지금은 틀리다. 중복은 제거되어야 한다고 말을 하지만, 요즘은 중복이 유지되는게 오히려 낫다.

무얼 기준으로 -> DDD에서 경계나누기로 불리우는, 바운디드 컨텍스트를 기준으로 생각해보자. DDD는 큰 모델을 서로 다른 Bounded Context로 나누고 상호관계를 명시하여 처리한다.

여기서 DDD에 등장하는 Bounded Context 와 같은 이야기가 나온다. 음식점 식탁에서 바라보는 피자(토핑, 도우, 치즈)와, 뒷골목 쓰레기통에서 바라보는 피자(크기, 위생, 벌레)의 차이. 우리는 코드 중복으로 인한 성능보다, 불필요한 컨텍스트로 인한 복잡도를 잡아야하는 시대에 개발하고 있다.

image

  • BOOT(SEVER): 서버모듈. batch, admin, api. 코드의 변화가 제일 잦게 일어남. 이걸 하나의 그룹으로 생각한다.
  • DATA(DOMAIN): 데이터(도메인)모듈. domain, meta, user, chart.
  • INFRA(연동모듈): 구현되고 나면 변화는 적지만, 버전업이 되면 아주 큰 변화
  • CLOUD(SYSTEM): 모듈. 변화가 적다. config, gateway, discovery. aws, gcp, azure.

특징에 맞고 성격에 맞고, 생명주기에 맞게 끔. 4개를 기준으로 그룹을 나누고 프로젝트를 구성하면 된다.


🍀 “HOW” 실전에서 멀티모듈 프로젝트를 구현해야 하나요?

4가지를 기준으로 멀티 모듈을 구성해보자.

image

그리고 멀티 그룹의 성격에 맞게 프로젝트를 각각 구성한다.

image

우선 가독성이 좋아지고, gradle과 같은 빌드 도구에서 그룹 폴더명을 기준으로 적용할 기술들을 일괄적으로 설정할 수 있다는 장점이 있다.

image

이렇게 구성하고 나니 모양새도 괜찮고, 가독성도 좋다. 그런데, 운영을 하다보니 계속늘어나는 모듈들 > 복잡도 증가 > 늘어나는 빌드 시간 + ….

또한 멀티 그룹 내의 설정 파일을 바꾸면 깃헙의 웹훅이 반응해서 다른 프로젝트도 이벤트가 전달되어 리플레시가 되는 일이 발생한다. 이런걸 어떻게 해결해야할까?

프로젝트를 조금더, 멀티그룹의 특성에 맞게 한번 더 쪼개자.

image

  • 변화가 제일 잦은 BOOT(SERVER), DATA(DOMAIN)는 그대로 둔다. 이미 잘 되어있다.
  • CLOUD(SYSTEM) 멀티 그룹을 별도의 2개 저장소로 다시 나눈다.
    • 시스템적인 프로젝트
    • 웹훅 이벤트를 필요로 하는 remote config 서버 프로젝트로 분리
  • INFRA(연동모듈) 멀티 그룹을 INFRA-LIBRARY 라는 저장소로 나눈다.

이렇게 각자 특성에 맞게 프로젝트를 분리하게 되면 빌드 시간이 줄어드는 이점도 있지만, 프로젝트의 경계가 명확해짐으로서 실제 인터페이스 구현 설계도 어떤 내용을 주고 받아야하는지 확연히 눈에 띄게 된다.

분리되어 있는 각 저장소 별로 향후 구현을 어떻게 해야할까?

DATA <-> INFRA 멀티 그룹간 관계구현

image

DATA 모듈에서 디비 접근까지 다 관리하고, INFRA 모듈은 INFRA 책임만 지도록. INFRA 모듈에서 디비에 접근하지 않도록 해야한다. 여기서 INFRA 모듈은 AOD 서버를 통해 데이터를 가져온다고 생각하자.

image image

위와 같이 infra 모듈에서 데이터를 조회하는 방법에 대한 구현체를 가지고 사용할 경우, batch-job agent 수가 많아지면 too many connections 문제를 마주하게 된다.

image

그래서 다시 구성해보면 data 모듈에서 Playback에 대한 조회를 모두 마치고, infra 모듈에서는 조회가 이미 끝난 Playback을 요청해서 응답을 받아 사용하기만 하면 된다.

결국 infra 모듈(프로젝트)는 db나 프로젝에 관계없이, AOD 서버를 올바르게 호출하기 위해 구현이 되어야 한다.

BOOT(SERVER) <-> DATA 멀티 그룹간 관계구현

image

둘 간의 관계를 구현하다보면 서비스(@Service) 구현체를 어디에 두어야 하는가에 대한 고민이 깊어진다.

image

결론은 둘 다 있어야 한다. 가사를 생성하는 기능은 가사 생성 요청을 받고 만드는 쪽에, 생성된 가사를 이벤트로 전달 받고 저장하는 기능은 이벤트를 전달 받고 저장하는 쪽에. 각각 책임과 역할에 맞게 구현되어 서로 협력해야한다.

대신 규칙이 하나 필요한데, boot 모듈에서 data 모듈로 절대 ServletRequest 관련 객체가 내려가면 안된다. DATA 모듈은 모든 모듈에서 참조하기 때이다. 모든 모듈은 웹 서버 방식이 아니다. 웹과 의존적인걸 DATA 모듈이 알아선 안된다. 만약 웹 기반 의존성이 DATA 모듈에 주입되면 테스트 코드를 작성할 때 웹서버 관련된 라이브러리가 전부 필요해지고, 웹 서버와 관련된 라이브러리의 의존성을 갖게 되는 순간 Mock객체를 만들어내는 사람도 생기고, DATA 모듈이라는 의미가 상실된다.

CLOUD <-> BOOT 멀티 그룹간 관계구현

둘 간의 관계에서 로직적으로는 문제가 없지만, discovery와 같은 제품을 사용할 때 한가지 의문이 생긴다. discovery의 버전업이 발생하게 되면 모든 서버 프로젝트가 함께 의존성을 갖기 때문에, 다시 빌드 한 후 배포되어야 하는 불편함을 겪는다.

웹 애플리케이션의 변화가 없었음에도 외부 제품의 버전업으로 다시 배포가 되어야하는 것은, 어쩌면 의존성을 더욱 더 명확하게 격리시켜야하지 않을까 생각을 하게 만든다. 이걸 한방에 해결해준게 Istio.


🍀 정리 & QnA

왜 멀티모듈 프로젝트 구조가 중요한가?

  • 잘뭇 구성되면 나중에 변경하기 고통스럽다.
  • 프로젝트 초기에 이루어져야 하는 일련의 설계 과정이다.
  • 개발 생산성에 많은 영향을 미친다.

무엇을 기준으로 멀티 모듈 프로젝트 구조를 나뉘어야 할까요?

  • 경계 안에서 의미를 갖을 수 있는 그룹을 정의하는(나누는)것이 가장 중요하다 (바운디드 컨텍스트)
  • 역할, 책임, 협력 관계가 올바른지 다시 한번 생각한다.
  • BOOT(server), INFRA, DATA(DOMAIN), SYSTEM(Cloud)

어떻게 실전 멀티 모듈 프로젝트 구현을 해야할까요?

  • 프로젝트가 커지고 있다면 다시 경계를 나누고 그 기준으로 소스 저장소를 분리한다.
  • INFRA(외부) 라이브러리에너는 DATA 관련 구현을 지향한다. (Anticouruption Layer)
  • 서비스 구현은 각자 역할에 맞게 각각 구현될 수 있다. (공통으로 한쪽에 구현하지 않는다.)
  • 시스템 레벨 구현이 실제 서비스 애플리케이션과 밀접하게 연관되지 않게 격리하거나 전환(Istio)한다.

잘 설계된 멀티모듈 프로젝트는 하나로 합쳐지거나 잘 나뉠 수 있도록 설계된게 잘 설계된 멀티모듈 프로젝트다!

Q. 결국 수 많은 양의 중복코드가 생길텐데, 어떻게 관리하나요?
A. core, common 모듈을 안만들고, 만약에 만들어질 조짐이 보이면 저장소를 완전히 분리해버려요. 저장소가 분리되면 손이 잘 안가기 때문에, 진짜 커먼한게 아니면 코드를 작성할 의욕이 안생깁니다.

이거 완전 꿀팁인듯..

Q. 여러 개의 모듈을 의존해야하는 요구사항도 있고, 의존성이 복잡해지게 될 수도 있는데… 그런 경우에는 어떻게 구성해야하나요?
A. 멀티 모듈보단 DDD의 영역에 가까운거 질문 같아요. 바운디드 컨텍스트라는 개념을 보면 ‘고객’이라는게 상품을 주문할 때의 고객일수도 있고, CX 문의를 넣는 고객일 수도 있어요. 상황에 따라 고객을 다르게 바라볼 수도 있기 때문에, 각 상황에 필요한 정보만을 사용하는게 중요합니다. 결국 질문 주신 복잡도를 풀어내는 건 DDD 를 이용하는게 좋겠습니다.

이 질문과 답변을 들었을 때 ‘또 DDD 야?’ 라는 생각이 들었다. 그런데 지금 생각해보면 참 적절한 답변이 아니었나…


🍀 후기

DDD 를 공부해야겠다고 느낀 첫 번째 세션. (두번째 세션은 권용근님의 레거시 시스템 개편의 기술) ‘그 놈의 DDD가 뭐길래 의존성 복잡도를 어떻게 해결하냐는 질문에 DDD 3글자로 대답이 가능한걸까?’ 생각이 들었고, 권용근님 세션에서도 DDD 이야기가 나오니까 ‘내가 더럽고 치사해서 DDD 공부한다.’ 라는 생각이 들었다. 운도 좋지. 마침 제이슨이 운영하시는 DDD 세레나데 강의가 시작되었고 강의를 들으면서 ‘아 이래서 DDD를 적용하라 하셨구나…’ 이해를 할 수 있었다. (DDD 답변 적절함 👍 제이슨 강의력 👍) DDD가 만능은 아니지만 다른 개발자들이 왜 DDD 라는 용어를 사용하는지, 언제 사용해야 좋은지는 알아두면 좋을거 같다.

멀티 모듈 프로젝트 설계의 경우 우리 회사 프로젝트와 일치하는 내용이 많았다. 비정상적으로 커져있는 core 모듈, 자잘하게 엔드포인트만 추가되는 admin, api 모듈… ‘나중에 바꾸려면 큰 고통이 따른다.’ 라는 말이 쓰라렸다. 당장 바꾸는 건 불가능할 뿐더러, 바꾸게 된다면 어디부터 바꿔야할지도 모르겠고, 바꾸면서 모든 팀원들과 동일한 컨텍스트를 공유하기도 어려울거 같고… 그래서 다들 MSA로 넘어가나보다.

작년에 객체의 역할과 책임 분리, 의존관계 역전과 같은 개념들을 기본기라고 배웠지만 정말 어려웠었다. 두리뭉술 구름 떠다니는 하늘을 보며 하는 이야기 같기도 하고… 그러다가 ‘아 이럴 때 의존관계 역전을 사용하는구나!’, ‘이래서 역할과 책임을 분리해야하는거구나!’ 찌릿하게 자극을 주는 발표들을 본 적이 있다. 권용근님의 우아한멀티모듈, 조용호님의 우아한객체지향. 김대성님의 실전! 멀티 모듈 프로젝트 구조와 설계 역시 그런 발표였던거 같다.

댓글남기기