헥사고날 아키텍처(Hexagonal Architecture)는 Alistair Cockburn이 2005년에 제안한 소프트웨어 아키텍처 패턴으로, "포트와 어댑터 아키텍처(Ports and Adapters Architecture)"라고도 불립니다. 이 아키텍처는 애플리케이션의 핵심 비즈니스 로직을 외부 의존성으로부터 완전히 분리하여 테스트 가능하고 유지보수성이 높은 소프트웨어를 만드는 것을 목표로 합니다.
헥사고날 아키텍처의 핵심은 애플리케이션을 육각형 모양으로 시각화하는 것입니다. 육각형의 중심에는 비즈니스 로직이 있고, 각 변은 외부 세계와의 상호작용 지점을 나타냅니다. 육각형을 선택한 이유는 6개의 면이 있어서가 아니라, 내부와 외부의 명확한 구분을 시각적으로 표현하기 위함입니다.
포트는 애플리케이션 내부와 외부 간의 통신을 위한 인터페이스입니다. 포트는 두 가지 유형으로 나뉩니다:
Primary Port (Driver Port)
Secondary Port (Driven Port)
어댑터는 포트의 구현체로, 외부 기술과 애플리케이션 내부를 연결하는 역할을 합니다.
Primary Adapter (Driver Adapter)
Secondary Adapter (Driven Adapter)
- 엔티티 (Entities)
- 값 객체 (Value Objects)
- 도메인 서비스 (Domain Services)
- 비즈니스 규칙과 로직
- Use Cases / Application Services
- 애플리케이션 로직 조율
- 트랜잭션 관리
- 포트 인터페이스 정의
- 어댑터 구현
- 데이터베이스 연결
- 외부 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();
}
}
// 단위 테스트 - 외부 의존성 없이 테스트 가능
@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("새이름")));
}
src/
├── main/
│ ├── java/
│ │ ├── domain/
│ │ │ ├── model/ # 엔티티, 값 객체
│ │ │ └── service/ # 도메인 서비스
│ │ ├── application/
│ │ │ ├── port/
│ │ │ │ ├── in/ # Primary 포트
│ │ │ │ └── out/ # Secondary 포트
│ │ │ └── service/ # Use Case 구현
│ │ └── adapter/
│ │ ├── in/
│ │ │ ├── web/ # REST 컨트롤러
│ │ │ └── event/ # 이벤트 리스너
│ │ └── out/
│ │ ├── persistence/ # 데이터베이스 어댑터
│ │ └── external/ # 외부 API 어댑터
적합한 경우:
부적합한 경우:
헥사고날 아키텍처는 소프트웨어의 유연성과 테스트 가능성을 크게 향상시킬 수 있는 강력한 아키텍처 패턴입니다. 하지만 프로젝트의 규모와 복잡성을 고려하여 적절히 적용하는 것이 중요합니다.