아래 내용은 여기 [만들면서 배우는 클린 아키텍쳐]를 참고한 내용입니다.
레이어드 아키텍처 (계층형 아키텍쳐)
- 사용자와의 상호작용을 담당하는 프레젠테이션 계층과 엔티티의 영속성을 처리하는 영속성 레이어를 별개의 계층으로 구분한다.
- 계층형 아키텍쳐에서 프레젠테이션 계층은 하위의 도메인 계층에 의존하고, 도메인 계층은 하위의 영속성 계층에 의존한다. 따라서 도메인 계층 입장에서 의존성은 비대칭적이다.
계층형 아키텍쳐의 문제점은 무엇일까?
- 계층형 아키텍쳐는 데이터베이스 주도 설계를 유도한다.
- 전통적인 계층형 아키텍쳐의 토대는 데이터베이스이다. 비즈니스 관점에서는 도메인 로직을 먼저 만들어야 하고, 이를 기반으로 영속성 계층과 웹 계층을 만들어야 한다.
- 서비스는 영속성 모델을 비즈니스 모델처럼 사용하게 되고 이로 인해 도메인 로직뿐만 아니라 즉시로딩/지연 로딩, 데이터베이스 트랜잭션, 캐시 플러시 등등 영속성 계층과 관련된 작업을 해야 한다.
- 지름길을 택하기 쉬워진다.
- 하위 계층에서 상위 계층을 접근할 수 있다.
- 테스트하기 어려워진다.
- 웹 계층 테스트에서 도메인 계층 뿐만 아니라 영속성 계층도 모킹해야 한다.
- 유스케이스를 숨긴다.
- 넓은 서비스는 코드 상에서 특정 유스케이스를 찾는 것을 어렵게 만든다.
- 좁은 도메인 서비스가 유스케이스 하나씩만 담당하게 한다면 UserService에서 사용자 등록 유스케이스를 찾는 대신 RegisterUserService를 열어서 작업할 수 있다.
- 동시 작업이 어려워진다.
- 영속성 계층을 먼저 개발해야 하고, 그 다음에 도메인 계층을, 그리고 마지막으로 웹 계층을 만들어야 한다. 그렇게 때문에 특정 기능은 동시에 한 명의 개발자만 작업할 수 있다.
순환 의존성
- 계층형 아키텍쳐에서 계층 간 의존성은 항상 다음 계층인 아래 방향을 가리킨다. 그러므로 영속성 계층에 대한 도메인 계층의 의존성 때문에 영속성 계층을 변경할때마다 잠재적으로 도메인 계층도 변경해야 한다.
- 그러나 도메인 코드는 애플리케이션에서 가장 중요한 코드다. 영속성 코드가 바뀐다고 해서 도메인 코드까지 바꾸고 싶지는 않다.
그럼 이 의존성을 어떻게 제거할 수 있을까 ?
엔티티는 도메인 객체를 표현하고 도메인 코드는 이 엔티티들의 상태를 변경하는 일을 중심으로 하기 때문에 먼저 엔티티를 도메인 계층으로 올린다.
그러나 이제는 영속성 계층의 리포지토리가 도메인 계층에 있는 엔티티에 의존하기 때문에 두 계층 사이의 순환 의존성이 생긴다. 이 부분이 바로 DIP를 적용해야 하는 부분이다. 도메인 계층에 리포지토리에 대한 인터페이스를 만들고, 실제 리포지토리는 영속성 계층에서 구현하게 하는 것이다.
헥사고날 아키텍처 (육각형 아키텍쳐)
- 육각형 아키텍쳐에서는 사용자 인터페이스나 데이터베이스 모두 비즈니스 로직으로부터 분리돼야 하는 외부 요소로 취급합니다.
- 여기서 핵심은 의존성의 방향으로 비즈니스 로직이 외부 요소에 의존하지 않고 프렌젠테이션 계층과 데이터 소스 계층이 도메인 계층에 의존하도록 만들어야 한다는 것이다.
- 따라서 도메인 계층의 입장에서 의존성은 대칭적이다
육각형 아키텍쳐에서 애플리케이션은
- 비즈니스 관심사를 다루는 내부(inside)와
- 기술적인 관심사를 다루는 외부(outside)로 분해된다.
- 여기서 외부에 포함된 기술적인 컴포넌트를 어댑터(adapter)라 부르고,
- 어댑터가 내부와 상호작용하는 접점을 포트(port)라고 부른다.
클린 아키텍쳐란 ?
설계가 비즈니스 규칙의 테스트를 용이하게 하고, 비즈니스 규칙은 프레임워크, 데이터베이스, UI 기술, 그 밖의 외부 애플리케이션이나 인터페이스로부터 독립적일 수 있는것.
도메인 코드가 바깥으로 향하는 어떤 의존성도 없어야 하는데, 이는 의존성 역전 원칙의 도움으로 모든 의존성이 도메인 코드를 향하고 있다.
- 육각형 안에는 도메인 엔티티와 이와 상호 작용하는 유스케이스가 있다.
- 육각형 바깥에는 애플리케이션과 상호작용하는 다양한 어뎁터들이 있다. 웹 브라우저와 상호 작용하는 웹 어댑터도 있고, 일부 어댑터는 외부 시스템과 상호 작용하며, 데이터베이스와 상호 작용하는 어댑터도 있다.
- 왼쪽에 있는 어댑터들은 (애플리케이션 코어를 호출하기 때문에) 애플리케이션을 주도하는 어댑터들이다.
- 반면 오른쪽에 있는 어댑터들은 (애플리케이션 코어에 의해 호출되기 때문에) 애플리케이션에 의해 주도되는 어댑터들이다.
애플리케이션 코어와 어댑터들 간의 통신이 가능해지려면 애플리케이션 코어가 각각의 포트를 제공해야 한다. 주도하는 어댑터에게는 그러한 포트카 코어에 있는 유스케이스 클래스들에 의해 구현되고 호출되는 인터페이스가 될 것이고, 주도되는 어댑터에게는 그러한 포트가 어댑터에 의해 구현되고 코어에 의해 호출되는 인터페이스가 될 것이다.
- 데이터 영속성 계체 : 주도되는 어뎁터
- 컨트롤러 : 주도하는 어댑터
이러한 핵심 개념으로 인해 이 아키텍쳐 스타일은 ‘포트와 어댑터’ 아키텍쳐로도 알려져 있다. 클린 아키텍쳐처럼 육각형 아키텍쳐도 계층으로 구성할 수 있다. 가장 바깥쪽에 있는 계층은 애플리케이션과 다른 시스템 간의 번역을 담당하는 어댑터로 구성돼 있다. 다음으로 포트와 유스케이스 구현체를 결함해서 애플리케이션 계층을 구성할 수 있는데, 이 두가지가 애플리케이션 인터페이스를 정의하기 때문이다. 마지막 계층에는 도메인 엔티티가 위치한다.
참고할 수 있는 소스코드 : https://github.com/wikibook/clean-architecture
Controller
@PostMapping(path = "/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}")
void sendMoney(
@PathVariable("sourceAccountId") Long sourceAccountId,
@PathVariable("targetAccountId") Long targetAccountId,
@PathVariable("amount") Long amount) {
SendMoneyCommand command = new SendMoneyCommand(
new AccountId(sourceAccountId),
new AccountId(targetAccountId),
Money.of(amount));
sendMoneyUseCase.sendMoney(command);
}
UseCase (Interface)
public interface SendMoneyUseCase {
boolean sendMoney(SendMoneyCommand command);
}
Service
@Override
public boolean sendMoney(SendMoneyCommand command) {
checkThreshold(command);
LocalDateTime baselineDate = LocalDateTime.now().minusDays(10);
Account sourceAccount = loadAccountPort.loadAccount(
command.getSourceAccountId(),
baselineDate);
Account targetAccount = loadAccountPort.loadAccount(
command.getTargetAccountId(),
baselineDate);
AccountId sourceAccountId = sourceAccount.getId()
.orElseThrow(() -> new IllegalStateException("expected source account ID not to be empty"));
AccountId targetAccountId = targetAccount.getId()
.orElseThrow(() -> new IllegalStateException("expected target account ID not to be empty"));
accountLock.lockAccount(sourceAccountId);
if (!sourceAccount.withdraw(command.getMoney(), targetAccountId)) {
accountLock.releaseAccount(sourceAccountId);
return false;
}
accountLock.lockAccount(targetAccountId);
if (!targetAccount.deposit(command.getMoney(), sourceAccountId)) {
accountLock.releaseAccount(sourceAccountId);
accountLock.releaseAccount(targetAccountId);
return false;
}
updateAccountStatePort.updateActivities(sourceAccount);
updateAccountStatePort.updateActivities(targetAccount);
accountLock.releaseAccount(sourceAccountId);
accountLock.releaseAccount(targetAccountId);
return true;
}
더 참고하면 좋을 사이트 :
- 우아한 형제들 주니어 개발자의 클린 아키텍처 맛보기 https://techblog.woowahan.com/2647/
'개발' 카테고리의 다른 글
git. merge strategy (1) | 2023.10.24 |
---|---|
[chrome plugin] 단축키로 크롬 플러그인 연동 (suggested_key) (0) | 2023.09.30 |
[chrome plugin] 크롬 플러그인 샘플 코드 작성해보기 (feat. development-basics) (0) | 2023.09.22 |
strong consistency vs eventual consistency (0) | 2023.06.18 |
[번역] 엔터프라이즈 어플리케이션에서 파사드 디자인 패턴의 3가지 유스케이스 (1) | 2023.06.04 |