Spring: 백엔드 아키텍처에 대한 고찰

목차

서론#

백엔드 아키텍처에 대해 고민은 오래전부터 했었다.
초기에는 단순한 계층구조(Layered Architecture)를 배우고 시작했으나, 시스템이 커질 수록 결합도가 높아지는 문제를 확인하고
이를 해결하기 위해 멀티 모듈 구조를 도입했다. 그리고 최근에는 도메인 중심의 개발을 위해 클린 아키텍처를 학습하여 새 시스템에 적용하기 시작했다.
아래는 학습한 내용을 토대로 구성한 아키텍처의 모습이다.


설계한 아키텍처 구조#

src
├── core          # 공통 도메인 로직 및 전역 설정
├── service       # 개별 비즈니스 서비스 영역
│   ├── api                 # Presentation: 요청 접수 및 응답 반환
│   ├── api.scheduler       # Entry Point: 스케줄링 기반 작업 진입점
│   ├── application         # Application: 유스케이스 흐름 제어 및 트랜잭션 관리
│   ├── domain              # Domain: 핵심 비즈니스 객체(Entity) 및 정책 정의
│   ├── infrastructure.*    # Infrastructure: DB, Cache, Storage 등 기술적 구현체
│   └── root                # Boot: 의존성 주입(DI) 및 애플리케이션 실행 환경 구성
└── util          # 기술적 보조 도구 (Common, Encryption 등)
  1. api
    • 클라이언트의 요청을 받고 처리 결과를 반환하는 계층으로, Controller와 Request/Response DTO의 매핑을 담당
  2. application
    • 정의된 도메인 객체들의 비즈니스 흐름을 구체화하고, 트랜잭션을 관리하며 외부 시스템과의 상호작용을 조율하는 계층
  3. domain
    • 핵심 비즈니스 규칙과 엔티티를 정의하며 외부 프레임워크나 라이브러리에 의존하지 않는 순수 자바 코드로 작성되어 높은 테스트 용이성과 독립성을 가짐
  4. infrastructure
    • 도메인 계층에서 정의한 인터페이스를 실제 기술(JPA, Redis 등)로 구현. 세부 구현 기술이 바뀌어도 비즈니스 로직은 영향을 받지 않도록 격리
  5. root
    • 각 계층을 하나로 묶어 실행 가능한 애플리케이션으로 조립하는 역할을 함

적용을 통해 느낀점#

도메인 중심의 아키텍처를 실제 시스템에 적용하면서 가장 크게 느낀 변화는 비즈니스 로직의 격리와 테스트의 용이성이라고 생각한다.

  • 순수 자바 기반의 단위 테스트: domain 계층이 특정 프레임워크나 외부 라이브러리에 의존하지 않으므로, 복잡한 설정 없이도 모킹(Mocking) 을 통해 핵심 로직을 검증하는 단위 테스트를 빠르고 정교하게 수행할 수 있었다.
  • 인프라 검증 효율화: infrastructure 계층의 경우, 인터페이스와 구현체가 분리되어 있어 Testcontainers를 활용한 테스트 환경 구축이 매우 용이했다. 이를 통해 실제 외부 시스템과 연결하지 않고도 상호작용을 안전하게 검증할 수 있었다.

문제 인지#

아키텍처를 기반으로 실제 시스템을 운영하면서, 신규 서비스를 확장하려는 시점에 몇 가지 구조적인 문제점이 확인되었다. 문제가 되는 부분은 아래와 같다.

├── service
│   ├── application    # Domain에서 정의한 Business Service의 구현체
│   └── domain         # 핵심 비즈니스 로직의 명세(Interface)가 모여있는 곳
│        └── business
│             └── user
│                  ├── repository
│                  └── service    # 문제의 지점: Application 유스케이스의 추상화

도메인 중심 설계의 이해가 부족하여 application 계층의 추상클래스를 domain 계층에 작성한 것이 문제였다.
같은 domain을 사용하는 새로운 서비스를 개발하고자 했더니, 관계없는 기존 서비스의 비즈니스 로직의 명세까지 강제로 공유되는 상황이 발생했다.


개선 방향: 도메인과 유스케이스의 분리#

먼저 도메인에 대한 정의를 다시 정확히 파악하는게 중요하다고 생각했다.
기존에는 "명세(Interface)는 변하지 않으므로 도메인이다"라고 막연하게 생각했으나, DDD(도메인 주도 설계) 를 통해 그 경계를 명확히 할 수 있었다.

핵심 로직의 재정의

  • 도메인 규칙 (Domain): 데이터 그 자체의 생명주기와 제약사항. (예: "작업 완료 상태에서만 파일 추출 가능")
  • 유스케이스 (Application): 비즈니스 절차와 흐름. (예: "파일을 찾아 압축한 뒤 바이트 배열로 변환하여 다운로드 제공")

이 기준을 바탕으로, domain 계층에 존재하던 유스케이스 성격의 service 들을 application 계층으로 이동시켰다.

재설계된 멀티 모듈 구조

공통 모델은 공유하되, 각 서비스의 실행 로직은 철저히 격리하는 구조로 재편성했다.

src
├── core                 # 시스템 전역 공통 모듈
├── service.core         # [Shared] 모든 서비스가 공유하는 핵심 영역
│   ├── domain           # 순수 엔티티 및 핵심 정책
│   └── infrastructure.* # DB, Cache 등 공통 인프라 구현체
├── service.web          # [Module] 기존 웹 API 서비스
│   ├── api              # Web Controllers
│   ├── application      # 웹 서비스 전용 유스케이스 및 인터페이스
│   └── root             # Web Boot Configuration
├── service.work         # [Module] 신규 백엔드 Worker 서비스
│   ├── application      # Worker 전용 비즈니스 로직 (배치, 처리 등)
│   └── root             # Worker Boot Configuration
└── util                 # 공통 유틸리티

마무리#

이번 재설계를 통해 얻은 기대 효과는 다음과 같다.

  • 지식의 오염 방지: service.work 모듈은 service.web에만 특화된 비즈니스 명세를 더 이상 알 필요가 없다.
  • 응집도 향상: 각 서비스 모듈은 자신에게 필요한 application 유스케이스만 보유하므로, 변경 사항이 발생해도 다른 모듈에 미치는 영향이 매우 적어 진다.
  • 유연한 확장: 향후 새로운 성격의 서비스가 추가되더라도, service.core를 기반으로 독립적인 모듈을 빠르게 붙여 나갈 수 있다.

다행히 기존 시스템의 계층간 분리가 이미 어느정도 잘 되어있어 아키텍처를 재설계하는 과정에 큰 문제가 없었다.
예상하지 못한 부분에서 문제를 인지하고 이를 해결하는 과정에서, 아키텍처에 대한 이해에 한층 더 깊어진 것 같다.

지금 형태가 최선은 아니겠지만, 계속해서 개선해가는 과정을 통해 아키텍처를 보는 나의 시야가 더 유연해졌으면 좋겠다.