한창 면접 준비로 바쁘던 와중, '지속 가능한 개발'에 대한 내용을 강조하는 모집 공고를 보고 지원했었다.
엥 모지
근데 사실 이력서에 적던 프로젝트에서도 Hexagonal 아키텍처를 통해 시도한 내용이 있어서, 이를 강조하면서 내용을 정리해봤다.
특히 실제 서비스하는 기업들의 기술 도입과 실패 사례를 통해서 배우는 부분이 많은거같다.
지속 가능한 시스템
애플리케이션은 분명히 변한다. 반드시 바뀌고 필히 확장된다.
소프트웨어 개발 과정에서 설계는 프로덕트의 품질, 유지보수성, 확장성에 결정적인 영향을 미친다.
복잡해지는 비즈니스 요구사항과 기술 환경의 변화에 유연하게 대응하기 위한 소프트웨어 아키텍처가 필요하다.
지속 가능한 시스템은 시간이 지나도 유지보수, 확장이 용이하며 요구사항의 변화에 유연하게 적응할 수 있는 시스템을 말한다.
지속 가능한 개발을 위한 시도
1. 비즈니스 로직
우리는 Controller-Service-Repository 구조를 띄는 Spring을 사용하면서, 관습적으로 Service Layer에 비즈니스 로직을 작성하고 있다.
계층 아키텍처에서 말하는 Data Access Layer와 소통하는 로직도 말이다!
구현 로직을 몰라도 비즈니스 흐름을 이해 가능한 코드를 목표로 해야한다. 인터페이스를 활용해서 메서드를 기능별로 분리하고, 추상화하면 자세한 구현 로직을 몰라도 코드의 흐름을 읽기 쉽게 만든다.
아래의 코드는 이미 Hexagonal Architecture를 적용해서 직접 프로젝트에서 작성한 코드의 일부이다.
@UseCase
@RequiredArgsConstructor
public class UserDataService implements RegisterUserDataUseCase, FindUserDataUseCase {
private final RegisterUserDataPort registerUserDataPort;
private final FindUserDataPort findUserDataPort;
private final UserDataMapper userDataMapper;
@Override
@Transactional
public UserData registerUserData(RegisterUserDataCommand command) {
UserDataJpaEntity jpaEntity = registerUserDataPort.createUserData(...);
return userDataMapper.mapToDomainEntity(jpaEntity);
}
@Override
@Transactional
public UserData findUserData(FindUserDataCommand command) {
UserDataJpaEntity entity = findUserDataPort.findUserData(command.getUserId());
return userDataMapper.mapToDomainEntity(entity);
}
}
같은 CRUD를 구현한 내용이지만, Service Layer에서는 데이터베이스와 어떤 의존없이 추상화(Port Interface)를 통해서 작성되었다.
어때 각 메서드가 어떤 일을 하는지 한 눈에 들어오지!!!
2. 소프트웨어 레이어
Presentation - Business - (Implement) - Data Access
1. 레이어는 위에서 아래 방향(→)으로만 참조되어야한다.
2. 레이어의 참조는 하위 레이어를 건너 뛰지 않아야한다.
3. 동일한 계층간에는 서로 참조하지 않아야한다.
클린 아키텍처(Clean Architecture)
로버트 C. 마틴의 클린 아키텍처는 소프트웨어 개발의 복잡성을 관리하고, 지속 가능한 시스템을 구축하기 위한 강력한 설계 원칙이다.
변화하는 요구사항과 기술 환경에 유연하게 대응할 수 있으며, 고품질의 소프트웨어를 지속적으로 제공할 수 있다.
지속 가능성을 보장하는 요소
유지보수성, 확장성, 대응력, 테스트 용이성, 독립성
클린 아키텍처의 핵심 원칙
계층 분리, 의존성 규칙, 컴포넌트 분리
Entity : 가장 내부에서 비즈니스 규칙과 애플리케이션의 핵심 로직을 정의한다.
UseCases : 시스템이 사용자에게 제공해야할 요구사항을 구현
Interface Adapters : 데이터 변환 등 외부 시스템과의 통신을 처리
Frameworks and Drivers : 웹 서버, 데이터베이스, UI 등 외부 요소와의 연동
소프트웨어의 독립성을 강조하여 변경에 유연하고 테스트가 용이한 시스템
의존성 역전(Dependency Inversion)
전통적인 설계에서 비즈니스 로직이 데이터베이스나 네트워크 등의 외부 시스템에 직접 의존하는 구조가 많다. 하지만 이런 구조는 외부 시스템이 변경될때마다 비즈니스 로직도 영향을 받기 때문에 유지보수성이 현저히 떨어지게 된다.
그래서 외부 시스템에 의해서 비즈니스 로직이 영향을 받지 않도록 구성하는 것을 핵심 원칙으로 둔다 .
이를 구현하기 위해서 구체적인 구현(Implement)가 아닌 추상화(Interface)에 의존하도록 강제하는 원칙을 말한다.
비즈니스 로직은 MySQL같은 데이터베이스나 HttpClient에 의존해선 안되고, 저장소나 HTTP 요청 인터페이스에 의존해야한다.
이를 통해서 비즈니스 로직(UseCase)는 어떤 외부 시스템에 의존되는지조차 알 수 없으며, 자연스럽게 특정 라이브러리나 프레임워크에 종속되지 않고 핵심 비즈니스를 보호할 수 있다.
각 계층은 서로에게 영향을 받지 않고 독립적으로 변경, 대체될 수 있어서 시스템 전체의 유지보수상과 확장성이 높아진다.
결국 핵심은 '변하기 쉬운 것'과 '변하기 어려운 것'을 분리해서 소프트웨어를 더 유연하고 안정적으로 유지하고자 하는 것이다.
Hexagonal Architecture
비즈니스 로직을 외부 세계와 격리시켜 유연하고 테스트하기 쉬운 구조를 만드는 것을 목표로 한다.
인터페이스나 기반 요소(infrastructure)의 변경에 영향을 받지 않는 핵심 코드를 만들고 이를 견고하게 관리하도록 설계한다.
Adapter는 인터페이스를 다른 인터페이스로 바꿔주는 클래스
Outbound Adapter(내부에서 외부로 나가는 요청)는 Outbound Port의 구현체로, Persistence Adapter라고 한다
Port는 말그대로 계층간을 연결하는 인터페이스
Service는 In Port를 타고 들어온 요청을 처리하기 위해 In Port를 구현한 클래스
중앙의 도메인 영역에 엔티티를 위치하고, 입출력을 포트와 어댑터를 통해 외부와 소통한다.
도메인 로직을 Port를 통해 접근하며, 외부 서비스는 어댑터를 통해 포트와 연결된다.
Service와 Adapter에 접근할 때는 반드시 Port와 Adapter를 통해서만 접근하도록 만들어야 한다
외부 시스템으로 사용하던 Kafka에서 RabbitMQ로 변경한다고 할때. 기존 레이어드 아키텍처는 UserService의 비즈니스 로직을 수정해야한다. 포트 어댑터의 경우는, RabbitMQ에 맞는 어댑터를 작성하고 갈아끼우기만 하면 된다.
포트와 어댑터란?
포트는 바로 인터페이스이다.
어댑터는 클라이언트에 제공할 인터페이스를 따르면서도 내부 구현은 서버의 인터페이스로 위임하는 것이다.
Repository의 구현체인 REdisRepository는 Repository(Port)의 인터페이스를 따르면서도 내부적으로 Redis 프로토콜과 연결하므로 Adapter이다.
클린 아키텍처와 헥사고날 아키텍처간의 차이점
안쪽 레이어가 외부 레이어에 대해 알지 못하게 하는 ‘분리’를 구현하는 방식의 차이가 있을 뿐, 비즈니스 로직을 외부 세계로부터 분리한다는 목표는 동일하다.
헥사고날 아키텍처는 클린 아키텍처의 내부 구조를 구체적으로 구현한 방식 중 하나라 볼 수 있다.
그렇다면 지속 가능한 개발을 위해서 Hexagonal Architecture는 항상 옳은가?
은탄환은 없다.
개발을 업으로 여러 기술들을 공부하다보면 한번씩 나오는 말이다. 항상 들어맞는 정답은 어디에도 존재하지 않는다.
뛰어난 유지보수성으로 지속 가능한 개발에 한발짝 앞선 아키텍처로 보여도, 불필요하거나 효용을 보지 못하는 경우도 물론 존재한다.
카카오페이 기술 블로그에서 이미 서비스에서 Hexagonal Architecture 도입의 실패와 롤백에 대한 사례를 기술한게 있다.
카카오페이에서도 실제 처음 도입 직후까지는 도메인 로직의 재사용성을 높히고, 외부 세계의 변동에 빠르게 대응할 수 있었다고 한다.
하지만 처음 고려할때 도메인에 대한 정의를 크게 고려하지 않았기에 Port, 특히 Out Port의 경계가 모호해지기 시작했다.
비효율적으로 구성된 핵심 로직과 Port 때문에, Port의 인터페이스를 변경해야했고 이는 외부의 변경이 비즈니스 로직에 영향을 주지 않는다는 이점을 누리지 못한 셈이다.
카카오페이 홈 서비스에서는 외부 의존 시스템이 많아서 매번 인터페이스를 변경해야하는 수고에 다시 계층 아키텍처로 돌아왔다고 한다.
실패라고 볼 순 없지만 도입 경험을 통해서 Hexagonal Architecture에 적합한 프로젝트는 다음과 같다고 한다.
1. 도메인 모델을 확실하게 정의할 수 있는 프로젝트
2. 의부 의존성이 많지 않은 서비스
출처 및 인용.
https://alistair.cockburn.us/hexagonal-architecture/
https://tech.kakaopay.com/post/home-hexagonal-architecture/
'backend' 카테고리의 다른 글
Hexagonal Architecture의 테스트코드 작성 (feat. StepVerifier, TestPublisher) (0) | 2025.02.13 |
---|---|
비동기 프로그래밍, 얼마나 알고 있나요? (Advanced Asynchronized) (1) | 2025.02.11 |
Axon Framework를 이용해서 이벤트 소싱을 도입하자 (0) | 2024.12.31 |
DDD - Bounded Context를 정의해보자 (1) | 2024.12.24 |
성능 테스트와 개선을 위한 시도와 실패들 (0) | 2024.12.17 |