본문 바로가기

개발/Spring

Spring 프로젝트 아키텍처 정하기

새로운 프로젝트를 또 혼자 개발해야하니 아키텍처를 정해야겠다. 기존 프로젝트들은 모두 모놀리식 아키텍처로 계층형도 있고 도메인 중심 패키지 구조로 되어있는 것도 있다. 이번 프로젝트에서는 멀티 모듈도 고려해볼 참이라 또 아키텍처에 대해 찾아보고 정리를 해보았다. 늘 고민이 되는 부분이다. 클로드를 사수 삼아 함께 논의했다.

 

1. 계층형 아키텍처

계층형 아키텍처는 레이어 별로 패키지 관리한다.

com.example
├── ExampleApplication.java
├── config/           # 스프링 설정 클래스
├── domain/           # 도메인 엔티티
├── repository/       # JPA 레포지토리
├── service/          # 비즈니스 로직
├── controller/       # REST API 컨트롤러
├── dto/              # 데이터 전송 객체
├── exception/        # 예외 처리
├── security/         # Spring Security, JWT 설정
├── util/             # 유틸리티 클래스
└── external/         # 외부 API 연동

 

장점

  • 이해하기 쉽고 친숙한 구조
  • 빠르게 개발 시작 가능

 

단점

  • 비즈니스 기능이 여러 계층으로 분산 되어 응집도가 낮음
  • 규모가 커지면 기능 추적이 어려워짐
  • 변경 시 모든 계층 수정 필요
  • 도메인 간 경계가 불분명해 의존성 관리가 어려움

 

생각

매우 익숙한 구조로 어떤 패키지에 넣지?? 에 대한 고민 없이 개발이 가능하다.

변경을 할 때 이 도메인의 컨트롤러가 어디에 있었지.. 하고 한참 찾아야 한다.

검색을 통해 바로 찾을 수 있긴 하지만 클래스명을 모르면 검색하기 어렵고 처음 프로젝트에 대해 파악하기가 어렵다.

같은 패키지에 있으니 서비스 간 의존성에 대한 깊은 고민 없이 마구잡이로 참조하다보면 어느새 스파게티 코드가 되어버린다.

 

 

2. 도메인 중심 패키지 구조

도메인(기능)별로 패키지 분리

com.example
├── ExampleApplication.java
├── global/
│   ├── config/         # 전역 설정 (Security, Web 등)
│   ├── error/          # 예외 처리
│   ├── util/           # 공통 유틸리티
│   └── dto/            # 공유 DTO 클래스
│
├── domain/
│   ├── user/           # 사용자 관련 기능
│   │   ├── controller/
│   │   ├── service/
│   │   ├── repository/
│   │   ├── entity/
│   │   └── dto/
│   │
│   ├── car/            # 차량 관련 기능
│   │   └── ...
│   │
│   ├── favorite/       # 즐겨찾기 기능
│   │   └── ...
│   │
│   └── search/         # 검색 기능
│       └── ...

 

장점

  • 기능별 응집도가 높음
  • 도메인 로직의 경계가 명확
  • 패키지 단위 개발 및 유지보수 용이
  • 확장성이 좋음
  • 협업할 때 작업 분담 용이

 

단점

  • 도메인 간 의존성 관리 필요
  • 공통 코드 관리가 복잡
  • 모듈 간 결합도 관리가 명시적이지 않음

 

생각

이 기능은 어떤 패키지에 넣어야할까..? 에 대한 고민이 끝도 없이 이어질 때가 있다.

도메인 별로 정리되어있어서 역할과 책임에 대한 고민을 하게 된다.

의존성과 모듈 간 결합도를 고려하여 개발을 하다보면 어느새 조금이나마 확장과 변경에 용이한 코드에 대해서 생각하게 된다.

협업을 할 때 한 기능별로 작업을 나눌 수 있어 용이했고, 공통 코드 관리가 좀 어려웠다.

 

 

3. 모듈러 모놀리스(멀티 모듈)

물리적으로 도메인 별 모듈을 분리

example/
├── example-app/
├── example-user-module/
├── example-car-module/
└── example-common/

 

장점

  • 물리적으로 분리된 모듈 구조로 경계가 명확
  • 모듈 간 의존성이 명시적으로 관리
  • 독립적인 테스트와 배포 가능
  • 마이크로서비스로 전환 용이
  • 대규모 팀 작업에 적합

 

단점

  • 초기 설정 복잡
  • 빌드 시간 문제
  • 모듈 간 인터페이스 설계에 많은 노력 필요

 


번외

기능 중심 모듈 분리

- 기능별로 인프라, 도메인(데이터), 서버 등으로 모듈을 분리한다.

- 장점: 유사한 변경 주기를 가진 코드를 묶어 배포 관리에 용이하다.

- 단점: 도메인 로직이 여러 모듈에 분산될 가능성이 있다. 

그리고 코어, user api, admin api 등으로 공통 코어 모듈을 의존하여 각 사용자별로 모듈을 구성하는 것도 있었다.

이 구성은 코어 모듈이 비대칭적으로 너무 커지는 문제가 있다고 한다.

 

 

생각

마이크로서비스에서 모듈러 모놀리식으로 회귀하는 사례를 보았다.

https://minforbackup.tistory.com/entry/%EC%9A%B0%EC%95%84%ED%95%9C-%EB%AA%A8%EB%86%80%EB%A6%AC%EC%8A%A4

 

뭐든 어떤 기준으로 모듈을 나눌건지(패키지를 나눌건지)가 가장 고민되는 부분인 거 같다.

 

 

 

4. 마이크로서비스 아키텍처

각 도메인별로 완전히 독립된 서비스로 분리

 

장점

  • 서비스별 독립적 확장 가능
  • 각 서비스별 다른 기술 스택으로 개발 가능
  • 장애 격리
  • 대규모 조직에 적합

 

단점

  • 분산 시스템 복잡성
  • 인프라 관리 부담
  • 개발 및 테스트 복잡성 증가
  • 서비스 간 통신 오버헤드
  • 초기 개발 속도 저하

 

생각

소규모 조직, 작은 프로젝트에서 바로 적용을 하기에는 무리가 있다.

일단 기본에 충실해보자.

추후에 서비스가 잘되고 조직이 커져 MSA로 전환해야하는 때가 오면 그나마 쉽게 할 수 있도록 코드를 짜두고 싶다.

 

 

 

비교

초기 개발 속도 계층형 > 도메인 중심 > 모듈러 > 마이크로서비스
유지보수성 모듈러 > 도메인 중심 > 마이크로서비스 > 계층형
확장성 마이크로서비스 == 모듈러 > 도메인 중심 > 계층형
배포 복잡성 마이크로서비스 > 모듈러 > 도메인 중심 == 계층형
팀 협업 용이성 모듈러 > 마이크로서비스 == 도메인 중심 > 계층형
학습 곡선 마이크로서비스 > 모듈러 > 도메인 중심 > 계층형
도메인 경계 명확성 마이크로서비스 == 모듈러 > 도메인 중심 > 계층형
자원 효율성 도메인 중심 == 계층형 > 모듈러 > 마이크로서비스
테스트 용이성 모듈러 > 도메인 중심 > 마이크로서비스 == 계층형
장애 격리 마이크로서비스 > 모듈러 > 도메인 중심 == 계층형

 

 

최종으로 결정한 아키텍처 - 도메인 중심 패키지 구조

  • 어느정도 익숙한 방식으로 빠르게 구현 가능
  • 소규모 조직
  • 초기 단계라 적은 사용자
  • 이후 모듈러 모놀리스나 마이크로서비스로 전환이 용이
  • 단순하게 시작하여 점진적으로 도메인 경계를 명확히 하며, 의존성을 관리하는 방향으로 발전
  • 도메인 간 의존성에 주의하고 응집도를 높이자

 

 

주의할 점

 

1. 도메인 경계 기준

  • 명확한 도메인 정의 - 초기에는 핵심 도메인으로 시작하고 점진적으로 세분화
  • 요구사항을 기반으로 책임과 기능이 명확한 도메인부터 분리
  • 도메인 간 중복 코드 방지 - 공통 기능은 global 패키지로 분리
  • 예) 파일 업로드, 날짜 처리, 외부 API 공통 로직 등

 

2. 도메인 간 의존성 관리

  • 순환 의존성 방지- 의존성 방향을 단방향으로
  • 도메인 간 통신 시 인터페이스 활용
  • 공통 모듈에 의존하되, 공통 모듈이 도메인에 의존하지 않도록
  • 의존성 최소화 - 도메인 간 지나친 결합 주의
  • 최소 지식 원칙(Law of Demeter) 적용
  • 필요한 정보만 DTO로 주고 받기
  • 특정 도메인에만 필요한 메서드는 내부용으로 제한

 

3. 일관된 패키지 구조 유지

  • 도메인 내부 표준화 - 모든 도메인 패키지에 일관된 하위 구조 적용
domain.user/
  ├── controller/   // API 엔드포인트
  ├── service/      // 비즈니스 로직
  │   ├── api/      // 외부 도메인용 인터페이스
  │   └── impl/     // 구현체
  ├── repository/   // 데이터 액세스
  ├── entity/       // JPA 엔티티
  └── dto/          // 데이터 전송 객체
  • 명확한 네이밍 컨벤션 - 일관된 네이밍 컨벤션 수립 및 적용
  • 예) XxxController, XxxService, XxxRepository 등

 

4. 도메인 내부 응집도 강화

  • 도메인 로직 캡슐화 - 핵심 비즈니스 로직은 도메인 모델(엔티티)에 캡슐화
// 나쁜 예: 비즈니스 로직이 서비스에 있음
@Service
public class CarServiceImpl {
    public void updateCarPrice(Long carId, int newPrice) {
        Car car = carRepository.findById(carId).orElseThrow();
        // 검증 로직이 서비스에 존재
        if (newPrice < 100000) {
            throw new InvalidPriceException("가격이 너무 낮습니다");
        }
        car.setPrice(newPrice);
        carRepository.save(car);
    }
}

// 좋은 예: 비즈니스 로직이 엔티티에 캡슐화됨
@Entity
public class Car {
    private int price;
    
    public void updatePrice(int newPrice) {
        validatePrice(newPrice);
        this.price = newPrice;
    }
    
    private void validatePrice(int price) {
        if (price < 100000) {
            throw new InvalidPriceException("가격이 너무 낮습니다");
        }
    }
}
  • 서비스 계층 책임 명확화 - 서비스를 목적에 따라 분리

 

5. 공통 컴포넌트 관리

  • global 패키지 구성
  • 공통 컴포넌트가 무분별하게 증가하면 관리 어려움 - 기능별로 구분된 global 하위 패키지 구성
global/
  ├── config/       // 스프링 설정
  ├── security/     // 보안 관련
  ├── error/        // 예외 처리
  ├── util/         // 유틸리티
  ├── external/     // 외부 API 연동
  └── dto/          // 공통 DTO
  • 공통 예외 처리 - 글로벌 예외 핸들러와 도메인별 커스텀 예외 정의
// global/error/GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(CarNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleCarNotFound(CarNotFoundException e) {
        // 처리 로직
    }
}

// domain/car/exception/CarNotFoundException.java
public class CarNotFoundException extends RuntimeException {
    // 구현
}

 

6. 테스트 전략

  • 도메인 별 테스트 구성 - 도메인 단위로 테스트 격리, 의존 도메인은 Mock 처리
  • 통합 테스트 전략 - 도메인 간 상호작용 테스트 필요 → 주요 시나리오에 대한 통합 테스트 구성

 

7. 프로젝트 진행 시 주의점

  • 처음부터 완벽한 구조를 만들려고 하지 말고 핵심 도메인부터 시작하여 점진적으로 구조 개선
  • 필요에 따라 리팩토링을 통해 도메인 경계 조정
  • 도메인 모델 및 의존성 다이어그램 문서화

 

 

+ 추가

구현하면서 도메인 경계는 어떻게 설정하고 의존성을 최소화할지, 서비스의 역할과 트랜잭션 처리는 어떻게 해야할지에 대해 고민해봐야겠다. 그리고 문서화!

 

++

헥사고날 아키텍처에 대해서

모듈러 모놀리스는 물리적으로 프로젝트를 나누는 것 - 멀티 모듈

도메인 중심 패키지는 논리적으로 어떻게 구성할 것인가 - 패키지 구조

헥사고날 아키텍처는 의존성 방향을 어떻게 관리할 것인가 - 계층 구조

 

이렇게 각 다른 레벨에서 동작한다.