NDC Oslo 2025 - 최고의 아키텍처라는 TDD, DDD, 헥사고날, 정말 그럴까요?
여기 아주 좋은 유튜브 강연이 있는데요, 이 강연의 핵심만 전체적으로 살펴볼까 합니다.
시니어 개발자라면 누구나 한 번쯤 겪어봤을 법한 상황으로 이야기가 시작되거든요.
새로 온 주니어 개발자가 "선배님, 근데 이 복잡한 구조가 정말 필요한가요?"라고 물을 때, 우리는 보통 확장성, 유지보수성, 품질 같은 멋진 단어들로 그 질문을 막아서곤 하죠.
그런데 발표자는 바로 그날 밤, 잠자리에 누워 스스로에게 똑같은 질문을 던지게 되었다고 하더라고요.
'우리가 신성시하는 이 모든 베스트 프랙티스, 이 모든 프로세스가 정말로 항상 옳은 걸까?' 하고 말입니다.
이 강연은 바로 그 의심에서 출발한 실제 프로젝트 경험담인데요.
TDD, DDD, 헥사고날 아키텍처라는 '좋은 것'들을 모두 적용해 보면서, 오히려 그 원칙들 자체를 비판적으로 되돌아보는 여정이라고 할 수 있습니다.
도메인 주도 설계(DDD), 어디까지 해야 할까?
이야기는 한 고객사로부터 프로젝트를 의뢰받으면서 시작되는데요.
요구사항은 '비상사태 시 기존 통신망이 마비될 경우를 대비해, 직원들에게 SMS 문자를 보낼 수 있는 시스템을 만들어달라'는 것이었습니다.
HR 시스템에서 직원 정보를 가져와 SMS 게이트웨이를 통해 문자를 보내는, 비교적 간단해 보이는 프로젝트였죠.
DDD에 열정적인 개발자라면 당연히 이때부터 'DDD 블루북'을 펼쳐 들고 도메인 분석에 들어가기 마련인데요.
이해관계자들과 깊은 대화를 나누고, 유비쿼터스 언어를 정의하고, 여러 가지 모델을 탐색하는 과정을 거치는 게 정석이죠.
하지만 발표자와 동료는 이번 프로젝트에서 바로 그 '정석'에 질문을 던지기로 했습니다.
"과연 이 문제에 그 정도의 심도 깊은 분석이 정말 필요할까?" 하는 의문이었어요.
그래서 그들은 딱 한 번의 추가 미팅만 진행했는데요.
놀랍게도 그 대화 과정에서 '비상사태'라는 복잡한 시나리오는 사실 핵심이 아니라는 걸 알게 되었습니다.
진짜 요구사항은 그저 '정보를 배포할 안정적인 채널이 하나 더 필요한 것'이었죠.
단 한 번의 대화로 문제의 복잡도가 확 낮아진 겁니다.
여기서 발표자는 두 가지 대안 모델을 고민하는데요.
첫 번째 모델은 아주 직관적입니다.
우리가 만들 애플리케이션이 직접 HR 시스템에서 직원 정보를 가져와서, 직접 SMS를 보내는 방식이죠.
아마 대부분 이렇게 생각했을 거예요.
그런데 '첫 번째 모델에 안주하지 말라'는 DDD의 조언을 따라 두 번째 모델을 탐색해 보는데요.
이미 시중에는 기업용 대량 문자 발송 서비스가 잘 되어 있다는 점에 착안한 겁니다.
그렇다면 우리 애플리케이션의 역할은 직접 문자를 보내는 게 아니라, HR 시스템의 직원 정보를 주기적으로 그 외부 서비스에 동기화해주는 '배치 잡(Batch Job)'으로 바뀌게 되죠.
바로 이 두 번째 모델을 고민하는 과정에서, 처음에는 전혀 드러나지 않았던 진짜 중요한 도메인 지식이 수면 위로 떠오르더라고요.
바로 '직원 그룹(Employee Groups)'이라는 개념이었습니다.
알고 보니 고객사 내부에 여러 하위 조직이 있고, 직원들은 여러 조직에 걸쳐 계약되는 등 복잡한 조직 구조를 가지고 있었거든요.
이 구조를 외부 SMS 서비스의 '직원 그룹'에 정확히 반영하는 것이 이 프로젝트의 진짜 핵심 과제였던 겁니다.
만약 첫 번째 모델만 생각하고 바로 개발에 들어갔다면, 이 중요한 복잡성을 프로젝트 후반에 가서야 발견하고 큰 혼란을 겪었을지도 모를 일이죠.
여기서 발표자는 또 한 번의 중요한 결정을 내리는데요.
세 번째, 네 번째 모델을 더 탐색하며 분석에 시간을 쏟는 대신, 두 번째 모델을 '최대한 빨리 구현해서 고객에게 전달'하기로 한 겁니다.
구체적인 결과물을 놓고 피드백을 받으며 반복 개선하는 것이, 완벽한 설계를 위해 끝없이 회의하는 것보다 훨씬 더 효율적으로 진짜 문제를 드러낼 거라고 판단한 거죠.
결국 DDD에 대한 첫 번째 결론은 이것인데요.
모델링에 '얼마나 많은 시간을 쏟느냐'가 중요한 게 아니라, '어떻게 우리의 모델을 테스트하느냐'가 핵심이라는 겁니다.
그리고 때로는 '빠른 배포'가 그 모델을 검증하는 가장 효과적인 방법일 수 있다는 거죠.
헥사고날 아키텍처, 정말 정답일까?
자, 이제 아키텍처를 선택할 차례인데요.
발표자 팀은 이전부터 즐겨 사용하던 '헥사고날 아키텍처(Hexagonal Architecture)'를 적용하기로 했습니다.
'포트와 어댑터 아키텍처'라고도 불리는 이 구조의 핵심은 도메인 로직을 외부 의존성(DB, API 등)으로부터 철저히 '격리'하는 데 있죠.
이를 통해 도메인 로직을 독립적으로 개발하고 테스트할 수 있게 만드는 것이 주된 목적입니다.
그들은 자바의 네임스페이스(패키지)를 사용해 계층을 분리했는데요.
* domain
: 순수한 비즈니스 로직만 담겨있는 핵심 영역.
* application
: 도메인 로직을 실행시키는 서비스 계층.
* external
: HR 시스템 API 연동이나 SMS 서비스 API 연동처럼 외부 시스템과 통신하는 어댑터.
그리고 아주 엄격한 의존성 규칙을 적용했습니다.
모든 의존성은 오직 domain
을 향해야 하고, domain
은 다른 어떤 계층에도 의존해서는 안 된다는 원칙이죠.
이건 헥사고날 아키텍처의 정석적인 접근법입니다.
하지만 여기서 또다시 스스로에게 질문을 던지는데요.
"이런 엄격한 격리와 추상화가 과연 이 프로젝트의 복잡도를 해결하는 데 정말 필요한가?" 하는 것이었습니다.
이런 아키텍처는 분명 장점이 있지만, 여러 추상화 계층을 만들고 개발자에게 더 많은 규율을 요구하는 비용이 따르기 때문이죠.
아키텍처 선택에 대한 피드백은 결국 '도메인 복잡도'로부터 얻어야 하는데요.
이 프로젝트에서 가장 복잡한 부분은 바로 앞서 발견했던 '직원 그룹 동기화' 로직이었습니다.
발표자는 "시스템에서 우리가 아직 이해하지 못하는 부분이 있다면, 그것을 한 번에 하나씩 추상화하라"는 데이브 팔리(Dave Farley)의 조언을 인용하더라고요.
그래서 그들은 이 복잡한 동기화 로직을 별도의 클래스로 분리해 추상화했습니다.
여기까지는 아주 좋은 판단이죠.
TDD와 '코드 가독성'의 진짜 의미
이렇게 복잡하고 불확실한 로직을 다룰 때 가장 강력한 무기가 바로 '테스트 주도 개발(TDD)'인데요.
규칙을 먼저 생각하는 대신, 구체적인 '예시'를 통해 복잡성을 하나씩 정복해 나가는 방식입니다.
발표자 팀은 다양한 엣지 케이스들을 테스트 케이스로 만들며 동기화 로직을 구체화해 나갔습니다.
* 직원이 같은 그룹에 중복으로 존재하고 전화번호도 같다면?
* 전화번호가 등록되지 않은 직원은 어떻게 처리할까?
* 두 명의 다른 직원이 같은 전화번호를 사용한다면?
이런 테스트 케이스를 작성하는 과정 자체가 도메인을 더 깊이 이해하는 학습 과정이 된 거죠.
흥미로운 점은, 이렇게 작성된 테스트 코드가 거의 비즈니스 요구사항처럼 읽힌다는 점인데요.
결국 실용적인 TDD가 다시 DDD의 영역으로 돌아와 이해관계자와의 소통을 돕는 멋진 그림이 만들어진 겁니다.
이제 이 강연의 가장 핵심적인 부분 중 하나가 나오는데요.
바로 '추상화 우선 접근법'에 대한 비판입니다.
일반적으로 동기화 로직을 만든다고 하면, 많은 개발자들이 역할에 따라 여러 클래스와 인터페이스로 코드를 분리하려고 할 거예요.SynchronizationService
, InconsistencyReporter
, EmployeeContractResolver
등등 멋진 이름의 추상화 계층을 먼저 설계하는 거죠.
하지만 발표자가 실제로 보여준 코드는 달랐습니다.
// This is a simplified representation of the logic shown in the talk.
public class EmployeeGroupSynchronizer {
public SyncReport synchronize(List<Employee> fromHR, List<Group> fromSmsService) {
// ...
// ... A relatively long method with all the logic inside ...
// ...
for (Employee employee : fromHR) {
// Find the corresponding group in the SMS service
// Check for duplicates
// Check for missing phone numbers
// If inconsistent, add to a report list
// ...
}
for (Group group : fromSmsService) {
// Check for employees that no longer exist in HR
// ...
}
// Return a report of all actions and inconsistencies.
return report;
}
}
그는 다소 길어 보이는 단일 메서드 안에 동기화에 필요한 모든 로직을 담았는데요.
왜 그랬을까요?
바로 '코드를 읽는 사람의 인지 부하(Cognitive Load)'를 고려했기 때문입니다.
여러 개의 작은 클래스로 잘게 쪼개진 코드는 당장은 깔끔해 보일지 몰라도, 전체 로직을 이해하기 위해 수많은 파일을 넘나들어야 하는 정신적 비용을 발생시키죠.
반면, 모든 로직이 한곳에 모여있으면 비록 메서드가 길어지더라도 전체적인 그림을 파악하기는 오히려 더 쉬울 수 있다는 겁니다.
'추상화'가 항상 복잡도를 낮춰주는 마법의 총알은 아니라는 아주 중요한 통찰이죠.
이 경험을 통해 발표자는 아키텍처에 대한 최종 결론을 내리는데요.
프로젝트 전체에 헥사고날 아키텍처의 엄격한 규칙을 동일하게 적용하는 것은 과했다는 겁니다.
더 나은 접근법은 '하이브리드' 방식이었을 거라고 회고하더라고요.
즉, 정말 복잡하고 중요한 핵심 도메인 로직(domain
네임스페이스)에만 엄격한 격리 원칙을 적용하고, 나머지 부분(everything else
네임스페이스)은 좀 더 느슨하게 제약을 풀어주는 거죠.
이것이야말로 "시스템의 모든 부분이 잘 설계될 필요는 없다"고 말했던 에릭 에반스의 원래 DDD 사상과도 맞닿아 있는 지점입니다.
마지막으로 TDD에 대해서도 같은 질문을 던지는데요.
"TDD를 모든 코드에 적용해야 하는가?"
그는 동기화 과정에서 발생한 불일치 내역을 슬랙(Slack)으로 보고하는 간단한 코드를 예시로 보여주었습니다.
누가 봐도 명확하고 단순한 I/O 코드였죠.
이런 코드의 경우, 굳이 테스트 코드를 작성하는 것은 투자하는 노력에 비해 얻는 가치가 적을 수 있습니다.
코드를 읽는 것만으로도 충분히 이해되고 신뢰할 수 있다면, 테스트 없이 넘어가는 것도 합리적인 선택이 될 수 있다는 거죠.
결론 우리가 던져야 할 진짜 질문들
이 강연은 우리에게 정답을 알려주지 않는데요.
대신, 우리가 어떤 기술이나 방법론을 적용하기 전에 스스로에게 던져봐야 할 아주 중요한 질문들을 남겨줍니다.
1. 우리가 정말로 알아야 할 도메인 지식은 얼마나 될까? 때로는 빠른 출시가 끝없는 분석보다 더 많은 것을 알려줄 수 있습니다.
2. 우리의 아키텍처 선택을 정당화하는 것은 무엇인가? 아키텍처는 도메인의 실제 복잡도에 맞춰 진화해야 합니다.
3. 코드를 읽는 사람의 '인지 부하'는 어느 정도일까? 더 많은 추상화가 항상 더 나은 코드를 의미하지는 않습니다.
결국 중요한 것은 어떤 원칙을 맹목적으로 따르는 것이 아니라, 우리가 마주한 문제의 '맥락'을 이해하고, 그에 맞는 가장 실용적인 해법을 찾아 나가는 유연한 태도일 겁니다.
마지막에 발표자가 인용한 그루초 막스의 명언이 모든 것을 말해주는데요.
"이것이 저의 원칙입니다. 만약 마음에 들지 않으신다면, 저에겐 다른 원칙들도 있죠."