헥사고날 아키텍처란?

김민범·2025년 8월 14일

Spring

목록 보기
29/29

헥사고날 아키텍처(Hexagonal Architecture)는 Alistair Cockburn이 2005년에 제안한 소프트웨어 아키텍처 패턴으로, "포트와 어댑터 아키텍처(Ports and Adapters Architecture)"라고도 불립니다. 이 아키텍처는 애플리케이션의 핵심 비즈니스 로직을 외부 의존성으로부터 완전히 분리하여 테스트 가능하고 유지보수성이 높은 소프트웨어를 만드는 것을 목표로 합니다.

핵심 개념

1. 헥사곤(육각형) 구조

헥사고날 아키텍처의 핵심은 애플리케이션을 육각형 모양으로 시각화하는 것입니다. 육각형의 중심에는 비즈니스 로직이 있고, 각 변은 외부 세계와의 상호작용 지점을 나타냅니다. 육각형을 선택한 이유는 6개의 면이 있어서가 아니라, 내부와 외부의 명확한 구분을 시각적으로 표현하기 위함입니다.

2. 포트(Ports)

포트는 애플리케이션 내부와 외부 간의 통신을 위한 인터페이스입니다. 포트는 두 가지 유형으로 나뉩니다:

Primary Port (Driver Port)

  • 애플리케이션을 구동하는 외부 액터가 사용하는 포트
  • 웹 컨트롤러, REST API, GraphQL 엔드포인트 등에서 사용
  • 애플리케이션의 Use Case를 정의하는 인터페이스

Secondary Port (Driven Port)

  • 애플리케이션이 외부 서비스나 인프라를 사용하기 위한 포트
  • 데이터베이스, 외부 API, 파일 시스템 등에 대한 인터페이스
  • Repository, Gateway 패턴으로 구현됨

3. 어댑터(Adapters)

어댑터는 포트의 구현체로, 외부 기술과 애플리케이션 내부를 연결하는 역할을 합니다.

Primary Adapter (Driver Adapter)

  • 외부에서 애플리케이션을 호출하는 어댑터
  • REST 컨트롤러, CLI 인터페이스, 이벤트 리스너 등

Secondary Adapter (Driven Adapter)

  • 애플리케이션이 외부 서비스를 호출하기 위한 어댑터
  • JPA 리포지토리, HTTP 클라이언트, 메시지 큐 클라이언트 등

계층 구조

1. Domain Layer (도메인 계층)

- 엔티티 (Entities)
- 값 객체 (Value Objects)
- 도메인 서비스 (Domain Services)
- 비즈니스 규칙과 로직

2. Application Layer (애플리케이션 계층)

- Use Cases / Application Services
- 애플리케이션 로직 조율
- 트랜잭션 관리
- 포트 인터페이스 정의

3. Infrastructure Layer (인프라 계층)

- 어댑터 구현
- 데이터베이스 연결
- 외부 API 호출
- 프레임워크 종속적인 코드

의존성 방향

헥사고날 아키텍처의 핵심 원칙은 의존성 역전(Dependency Inversion)입니다:

외부 (Infrastructure) → 내부 (Application/Domain)
  • 내부 계층은 외부 계층을 알지 못함
  • 외부 계층만이 내부 계층을 알고 있음
  • 인터페이스는 내부에서 정의하고, 구현체는 외부에서 제공

실제 구현 예시

도메인 계층

// 도메인 엔티티
public class User {
    private UserId id;
    private String name;
    private Email email;
    
    public void changeName(String newName) {
        // 비즈니스 규칙 검증
        if (newName == null || newName.trim().isEmpty()) {
            throw new IllegalArgumentException("이름은 비어있을 수 없습니다");
        }
        this.name = newName;
    }
}

애플리케이션 계층

// 포트 인터페이스
public interface UserRepository {
    User findById(UserId id);
    void save(User user);
}

// Use Case
public class ChangeUserNameUseCase {
    private final UserRepository userRepository;
    
    public ChangeUserNameUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public void changeUserName(UserId userId, String newName) {
        User user = userRepository.findById(userId);
        user.changeName(newName);
        userRepository.save(user);
    }
}

인프라 계층

// 어댑터 구현
@Repository
public class JpaUserRepository implements UserRepository {
    private final UserJpaRepository jpaRepository;
    
    @Override
    public User findById(UserId id) {
        return jpaRepository.findById(id.getValue())
            .map(this::toDomainEntity)
            .orElseThrow(() -> new UserNotFoundException(id));
    }
    
    @Override
    public void save(User user) {
        UserEntity entity = toJpaEntity(user);
        jpaRepository.save(entity);
    }
}

// Primary 어댑터
@RestController
public class UserController {
    private final ChangeUserNameUseCase changeUserNameUseCase;
    
    @PutMapping("/users/{id}/name")
    public ResponseEntity<Void> changeUserName(
            @PathVariable String id, 
            @RequestBody ChangeNameRequest request) {
        changeUserNameUseCase.changeUserName(
            new UserId(id), 
            request.getName()
        );
        return ResponseEntity.ok().build();
    }
}

주요 장점

1. 테스트 용이성

// 단위 테스트 - 외부 의존성 없이 테스트 가능
@Test
void should_change_user_name() {
    // given
    UserRepository mockRepository = mock(UserRepository.class);
    User user = new User(new UserId("1"), "원래이름", new Email("test@test.com"));
    when(mockRepository.findById(any())).thenReturn(user);
    
    ChangeUserNameUseCase useCase = new ChangeUserNameUseCase(mockRepository);
    
    // when
    useCase.changeUserName(new UserId("1"), "새이름");
    
    // then
    verify(mockRepository).save(argThat(u -> u.getName().equals("새이름")));
}

2. 기술 독립성

  • 데이터베이스를 MySQL에서 PostgreSQL로 변경 시 어댑터만 교체
  • REST API를 GraphQL로 변경 시 컨트롤러 어댑터만 교체
  • 비즈니스 로직은 변경 없음

3. 유지보수성

  • 관심사의 명확한 분리
  • 각 계층의 책임이 명확함
  • 변경의 영향 범위가 제한적

실제 프로젝트 구조

src/
├── main/
│   ├── java/
│   │   ├── domain/
│   │   │   ├── model/        # 엔티티, 값 객체
│   │   │   └── service/      # 도메인 서비스
│   │   ├── application/
│   │   │   ├── port/
│   │   │   │   ├── in/       # Primary 포트
│   │   │   │   └── out/      # Secondary 포트
│   │   │   └── service/      # Use Case 구현
│   │   └── adapter/
│   │       ├── in/
│   │       │   ├── web/      # REST 컨트롤러
│   │       │   └── event/    # 이벤트 리스너
│   │       └── out/
│   │           ├── persistence/  # 데이터베이스 어댑터
│   │           └── external/     # 외부 API 어댑터

주의사항과 단점

1. 복잡성 증가

  • 간단한 CRUD 애플리케이션에는 과도할 수 있음
  • 인터페이스와 구현체의 수가 많아짐

2. 학습 곡선

  • 팀 전체가 아키텍처를 이해해야 함
  • 초기 개발 속도가 느려질 수 있음

3. 과도한 추상화

  • 불필요한 인터페이스 생성 가능성
  • 성능 오버헤드 발생 가능

언제 사용해야 하는가?

적합한 경우:

  • 복잡한 비즈니스 로직을 가진 애플리케이션
  • 외부 의존성이 자주 변경되는 환경
  • 높은 테스트 커버리지가 필요한 프로젝트
  • 장기간 유지보수가 예상되는 시스템

부적합한 경우:

  • 단순한 CRUD 애플리케이션
  • 프로토타입이나 단기 프로젝트
  • 작은 규모의 팀이나 프로젝트

헥사고날 아키텍처는 소프트웨어의 유연성과 테스트 가능성을 크게 향상시킬 수 있는 강력한 아키텍처 패턴입니다. 하지만 프로젝트의 규모와 복잡성을 고려하여 적절히 적용하는 것이 중요합니다.

0개의 댓글