고품질 소프트웨어를 위한 DDD 도입기 Part 1 — 문제 탐구 및 개선 방향

Taehun
tesser-team
Published in
13 min readNov 10, 2023

--

안녕하세요.
테서의 서비스 개발 팀에서 백엔드 직무를 맡고 있는 김태훈입니다.

지금부터 테서의 메인 서비스인 온톨을 개발하는 과정에서 서비스팀이 고민했던 소프트웨어 아키텍처 관련 내용을 “고품질 소프트웨어를 위한 DDD 도입기” 라는 주제로 소개할 예정입니다.

온톨이란?

온톨은 환자를 위한 의료 서비스 앱입니다. 온톨의 핵심 기능은 다음과 같습니다.

  • 병원에서 받은 각종 검사결과지를 환자들이 이해하기 쉽게 설명
  • 커뮤니티를 통해 AI 온톨이가 고민에 대한 답변을 제공
  • 건강검진 결과를 바탕으로 챗봇 상담 제공
  • 보험 설계사의 무료 보험 상담

처음부터 위의 기능들이 모두 있었던 것은 아닙니다. 검사결과지 수동해석부터 시작해서 커뮤니티에 이르기까지 환자들에게 조금 더 나은 서비스를 제공하고자 온톨은 다음과 같은 과정으로 변화해 왔습니다.

  • 2022년 2월: 닥터온톨 런칭(검사결과지 의료진 수동해석)
  • 2023년 1월: 온톨 PoC 런칭(검사결과지 AI 자동해석, 의료진 상담)
  • 2023년 8월: 온톨 해외 PoC 런칭(검사결과지 자동해석)
  • 2023년 10월: 온톨 1.0 런칭(검사결과지 AI 자동해석, 커뮤니티, 보험 상담, 건강검진 챗봇 상담)

온톨 1.0을 런칭을 준비하면서 더 많은 유저들에게 안정적인 서비스를 제공하고자 한층 더 나은 품질의 소프트웨어를 설계해보기로 했습니다.

1. 소프트웨어 아키텍처에 관하여

소프트웨어 아키텍처(Software Architecture)라는 것은 소프트웨어의 내부 구조 설계를 뜻합니다.
건축물을 설계하는 것으로 비유해보겠습니다. 겉보기에 같은 모습, 같은 기능을 제공하는 건축물도 좋은 자재와 견고한 기반 위에 잘 설계된 건축물수리 비용을 절감하고, 안전 사고 발생 위험을 줄이며, 리모델링이나 증축에도 용이할 것이고, 또 오랜 기간동안 사용될 수 있을 것입니다.

소프트웨어도 마찬가지로, 같은 기능을 하는 소프트웨어일지라도 구조적으로 잘 설계된 소프트웨어는 리펙터링과 유지보수 비용이 상대적으로 적게 들고 예기치 못한 오류를 줄일 수 있으며 요구사항 변화에 유연하게 대처할 수 있으며, 장기적으로 사용할 수 있는 코드베이스가 될 것입니다.

https://www.martinfowler.com/architecture/

A poor architecture is a major contributor to the growth of cruft — elements of the software that impede the ability of developers to understand the software. Software that contains a lot of cruft is much harder to modify, leading to features that arrive more slowly and with more defects.
- Martin Fowler

열악한 아키텍처는 개발자가 소프트웨어를 이해하는 능력을 방해하는 소프트웨어 요소인 난잡함을 증가시키는 주요 원인입니다. 허술한 부분이 많이 포함된 소프트웨어는 수정하기가 훨씬 어렵기 때문에 기능이 더 느리게 제공되고 결함도 더 많이 발생하게 됩니다.
- 마틴 파울러

저희가 시작 단계에서 논의했던 설계 목표에 관한 내용을 정리하면 다음과 같습니다.

  1. 안정성 — 고가용성을 위한 안정적인 리소스 확장 및 최적화
  2. 유연성 — 새로운 기획에 따른 유연한 기능 변경
  3. 직관성 — 협업 효율을 위한 일관되고 합리적인 규칙을 가진 코드베이스

기존보다 더욱 안정적이고 유연하면서도, 직관적인 코드베이스를 구축하고자 기존 구조의 문제점을 분석하고, 적용해볼 수 있는 여러 설계 방법을 조사하여 검토하였습니다.

2. 기존 구조의 문제점(Feat. Layered Architecture)

레이어드 아키텍처(Layered Architecture)는 오랜 기간 널리 사용된 전통적인 구조로써, 각 계층은 특정한 역할과 책임을 가지며, 높은 계층은 바로 아래 계층의 기능에만 의존하는 ‘낮은 결합도와 높은 응집력’을 지향합니다.
또한, AOP(Aspect-Oriented Programming)와 같은 기술을 통해 횡단 관심사(Cross-cutting Concerns)를 효과적으로 처리할 수 있는 구조적인 기반이 되기도 합니다.

저희 기존 온톨 PoC 서버 애플리케이션의 코드는 일반적으로 널리 사용되는 레이어드 아키텍처(Layered Architecture)에 가까운 형태로 구축되었습니다. 하지만 개발과 유지보수를 진행하면서 조금씩 문제점들이 보이기 시작했습니다. (특정 아키텍처에 대한 비판이 아닌 기존 구조에서 발견된 문제점에 대한 내용입니다.)

2.1 수직적 의존 구조로 인한 유연성 저하

레이어드 아키텍처의 기본 구조는 유저 인터페이스로부터 컨트롤러(또는 리졸버) — 서비스(비즈니스) — 레포지토리 (퍼시스턴스)— 데이터베이스 순으로 수직적인 의존 방향을 갖습니다.

이러한 구조 속에서 상위 계층은 하위 계층에 의존하므로, 하위 계층의 변경이 상위 계층에 영향을 미치게 됩니다. 이는 비즈니스 로직의 유연성을 저하시키고, 변경에 대한 저항성을 증가시킵니다.

가장 하단에 위치한 레포지토리 코드 변경에 따른 비즈니스 로직 수정이 불가피한 경우가 많았습니다.

2.2 방대하고 모호해지는 서비스 레이어

서비스 레이어는 각 모듈의 비즈니스 로직을 담당합니다. 각 도메인(또는 데이터베이스 테이블)으로 매칭되는 각 서비스는 규모가 작은 구조에서는 직관적이고 이해하기 쉬운 형태라 자주 쓰이는 형태입니다.

하지만 기능이 많아지고 그에 따라 처리해야 할 비즈니스 로직이 많아질 경우, 하나의 서비스 클래스가 광범위한 경계를 다루기 때문에 다음과 같은 문제가 발생합니다.

첫째, 원하는 로직이 어디 있는지 찾기 어렵고, 한 군데의 로직 수정 시에도 다른 파트에 문제가 생길 위험이 있습니다. 어떤 로직이든 서비스라는 이름으로 관련되어있는 모든 로직을 메서드화하여 서로 실타래처럼 얽힌 참조 관계를 만들고, 유지보수에 두려움을 낳게 됩니다.

둘째, 무분별하게 외부 클래스를 주입받아 사용하며 클래스 간 경계가 모호해지며, 클래스 상호 간 의존도가 높아지게 됩니다. 처음에는 한두개의 서비스를 DI하여 사용하다가, 결국에는 하나의 서비스에 수많은 서비스가 주입되어 있어 의존관계를 명확히 파악하기 어려워집니다.

객체지향의 기본 원칙인 SOLID 중 SRP, 단일 책임 원칙은 하나의 클래스가 하나의 역할을 수행해야 한다고 규정합니다. 방대해진 서비스 레이어는 결국 객체지향 관점에서 클래스로서의 본질에서 점점 벗어나게 됩니다.

2.3 테스트 코드의 신뢰도 저하 및 피로도 발생

https://dev.to/flippedcoding/5-reasons-testing-code-is-great-8dj

테스트 코드 작성의 원칙에 따라 단위 테스트는 단위별 로직을 격리된 환경에서 테스트할 때 그 진가를 발휘합니다.

기존 로직에서 구현되었던 형태는 서비스에 레포지토리 구현체를 주입하여 사용하는 것입니다. 추상화가 아닌 구현체에 서비스 레이어가 의존하기 때문에, 데이터베이스 처리 로직이 변경된다면 비즈니스 로직 단위 테스트도 모두 수정되어야 하는 번거로움이 발생했습니다. 데이터베이스 관련 처리 로직에서 발생하는 수많은 변수들을 제어하는 것에 너무 많은 노력이 들어가게 됩니다.

결국, 코드 의존 관계부터 폴더 구조까지, 규칙성이 불분명해 특정 로직을 수정해야 할 때 찾기도 어려웠습니다. 새로운 무언가를 추가해야 할 때 고민하는 시간이 점점 길어졌습니다. 데드 코드도 늘어났고 기능 수정과 커밋에 두려움이 앞섰습니다.

3. 다양한 설계 방법론과 도메인 주도 설계

소프트웨어 구조와 구성요소를 결정하는 것에 있어 다양한 아키텍처와 디자인 패턴이 존재합니다. 저희는 저희에게 적합한 방법을 찾아서 적용하기 위해, 아래 제시한 세 가지를 기준으로 고민하였습니다.

  1. 팀이 목표하는 기간 내 목표치를 달성할 수 있는지 여부
  2. 비즈니스 로직 기능 및 외부 리소스 변경이나 확장에 용이한지 여부
  3. 유지 관리 비용이 불필요하도록 과하게 발생하지 않는지 여부

아래는 저희 팀이 조사하고 적용을 검토했던 아키텍처 및 디자인 패턴입니다.

3.1 Micro Service Architecture

https://www.sharedit.co.kr/posts/12547

마이크로 서비스 아키텍처, 일명 MSA로 불리는 설계 방법론입니다. 일반적으로 모놀리식 아키텍처와 대비되는 개념으로, 하나로 보이는 서비스에 포함되는 각 기능을 독립적인 서비스로 분리하는 것입니다. 모놀리식 아키텍처는 모든 기능이 하나의 큰 애플리케이션 내에 포함되어 있어 상호 간 데이터를 공유하고 호출하는 등의 작업이 쉽습니다. 반면 마이크로 서비스는 각 서비스가 분리되어 있기 때문에 서비스를 별도로 배포 및 유지관리 해주어야 하며, 서비스 간 통신에 관한 처리 및 관리가 필요합니다.

리소스 확장(Scale-out)과 기능별 유지보수 및 배포 유연성 측면의 강점 등을 갖고 있어 대규모 서비스에서 대부분 사용하는 형태이지만, 개발 복잡도와 관리 비용을 고려하여 아직은 시기상조라는 결론이 내려져 조금 더 서비스가 성장했을 때 적용하기로 했습니다.

3.2 CQRS Pattern

https://medium.com/design-microservices-architecture-with-patterns/cqrs-design-pattern-in-microservices-architectures-5d41e359768c

CQRS (Command Query Responsibility Segregation)애플리케이션의 커맨드와 쿼리를 담당하는 부분을 분리하는 소프트웨어 디자인 패턴입니다. 커맨드는 애플리케이션 로직 중 데이터의 상태 변경 작업을 담당하는 부분이며, 쿼리는 상태를 읽어오는 작업을 담당합니다. 이를 적용하면 비즈니스 로직에 맞게 시스템 작업 부하를 개별 관리하여 성능을 향상시킬 수 있고, 명확하게 역할과 책임을 분리하여 유지보수성을 향상시킬 수 있습니다.

CQRS 패턴을 적용하기 앞서 트래픽을 모니터링 하면서 해당 패턴을 적용하는 것이 비용적으로 합리적인지 검토할 필요가 있었습니다. 모니터링 적용 후 우선적으로 리소스를 분리하지 않고 코드 상에서 커맨드와 쿼리를 점진적으로 분리하여 구현하는 것을 고려해보기로 했습니다.

3.3 도메인 주도 설계

https://github.com/stemmlerjs/ddd-forum

도메인 주도 설계(DDD)는 복잡한 비즈니스 로직과 문제 영역을 체계적으로 분석하고 모델링하는 설계 패러다임입니다. 이 방법론은 단순히 코드를 작성하는 것을 넘어, 전문가의 도메인 지식을 소프트웨어에 직접 반영하여 가독성 높고 유지보수가 쉬운 시스템을 구축하는 것을 목표로 합니다.

DDD는 복잡한 도메인 로직을 표현력 있는 객체와 서비스로 추상화하고, 이를 바탕으로 협력적인 마이크로 서비스 혹은 모듈화된 아키텍처를 설계할 수 있게 도와줍니다. 이러한 접근 방식은 비즈니스 요구사항의 변화에 빠르게 대응할 수 있게 하며, 도메인 전문가와 개발자 간의 의사소통을 원활하게 합니다.

조사한 바에 따르면 도메인 주도 설계는 비즈니스 요구사항을 체계적으로 모델링하고 이를 소프트웨어에 접목시키는 과정이기 때문에 MSA(Micro Service Architecture)나 Event-Driven Architecture와 같은 설계를 적용하기 앞서 더 큰 아키텍처를 설계할 수 있는 기반이 될 수 있다고 판단하였습니다.

4.1 도메인 주도 설계 이해하기

DDD(도메인 주도 설계)는 단순한 소프트웨어 설계가 아닌, 함께 해결해나가야 할 문제를 정의하고 모델링을 해 나가는 과정입니다. 개발자 뿐만 아니라 팀 내 모든 구성원들(기획자, 디자이너, 연구원, 도메인 전문가 등)이 함께 참여하여 설계합니다. 문제 영역에 대한 공통된 이해도를 바탕으로 협업 생산성을 극대화하는 것을 목표로 합니다. 이것이 충족 되었을 때 좋은 소프트웨어를 설계할 수 있는 조건이 형성되는 것입니다.

4.1 DDD의 장점1 — 비즈니스 요구사항 반영

저희가 개발하는 소프트웨어의 목적은 현실세계의 프로세스를 자동화하고, 비즈니스 문제를 해결하는 것입니다. 그리고 도메인은 그 대상이 됩니다. 즉, 도메인은 소프트웨어가 해결하고자 하는 비즈니스의 영역으로 볼 수 있습니다.
데이터베이스가 아닌 현실세계 문제점을 세분화된 도메인으로 정의하는 데 초점을 맞추어 설계하고 개발한 소프트웨어이기 때문에, 요구사항에 대해 현실적으로 접근하고, 명확하게 정의하게 됩니다.

4.2 DDD의 장점2 — 협업 효율성 향상

모든 구성원들(기획자, 디자이너, 연구원, 도메인 전문가 등)이 함께 참여하여 설계하기 때문에 공통으로 협의된 언어와 개념을 사용하게 됩니다.
다음 편에서 설명할 유비쿼터스 언어부터 도메인 모델, 컨텍스트 맵과 같은 요소들을 통해 요구사항을 실체화하는 과정을 거치게 되는데, 이것은 의사소통이나 개발 방식에서의 통일성을 향상시키는데 큰 도움을 줍니다.

4.3 DDD의 장점3 — 불필요한 복잡성 제거

https://buildplease.com/pages/separation/

처음에는 생소한 개념을 이해하고 적용하는 데 시간이 걸릴 수 있지만, 비즈니스 요구사항을 더 작은 조각으로 나누고 이를 모델링함으로써 시스템이 효율적인 구조를 갖추게 됩니다. 시간이 지남에 따라 불필요한 복잡성이 늘어나는 것을 방지하고 모든 팀원이 이해하기 쉽고 유지보수하기 쉬운 시스템을 구축할 수 있습니다.

지금까지 기존 구조에 문제점을 파악하고, 고품질 소프트웨어 설계를 위한 여러 디자인 패턴 및 아키텍처에 대해 조사한 내용을 작성해보았습니다.

본 시리즈의 다음 포스팅에서는 DDD에 대한 개념과 저희가 적용한 내용들을 소개 하겠습니다.

Part2 보러가기

--

--