코딩마을방범대

포트와 어댑터 아키텍처 본문

🎃 기타/상식 ❗

포트와 어댑터 아키텍처

신짱구 5세 2024. 1. 9. 17:55

 

 

포트와 어댑터 아키텍처는 핵사고날 아키텍쳐라고도 불린다.

여기서 포트는, 통신 연결을 위한 인터페이스를 의미한다.

어댑터는, 하나의 장치나 시스템이 다른 장치나 시스템과 상호작용할 수 있도록 인터페이스를 변환하는 역할을 한다.

자세한 내용은 아래에서 확인할 수 있다.

 

 

 

개요

 

Controller에서 사용하는 DTO를 Service 메소드의 파라미터로 사용하게되면 문제가 발생한다.

Controller의 변경이 Service에 영향을 줄 수 있기 때문이다.

 

핵사고날 아키텍쳐는 계층 간의 결합도를 낮추고 응집도를 높이는 것을 목표로 하는 아키텍처이다.

 

 

Layered Pattern (Cake Pattern)

주로 사용되는 패턴으로, 기능을 기반으로 패키지를 구성하는 방법

 

Port And Adapter Pattern

연관성을 기반으로 패키지를 구성하는 방법

 

 


 

 

 

어댑터(Adapter)

포트를 통해 애플리케이션 코어와 외부 세계를 연결한다.

특정 외부 기술이나 프레임워크에 의존적인 로직을 담당하며, 이를 통해 애플리케이션 코어는 외부와의 결합도를 최소화하고, 어댑터를 통한 교환 가능성을 확보한다.

 

유형 설명
인커밍 어댑터(주도하는) 사용자 인터페이스(UI), 테스트 또는 외부 시스템으로부터의 요청을 애플리케이션 코어로 주도하는데 사용
(예. Spring의 Controller)
아웃고잉 어댑터(주도되는) 애플리케이션 코어에서 외부에 데이터를 전달하는 역할을 담당한다. 
예를 들어, 데이터베이스에 데이터를 저장하거나 외부 시스템에 메시지를 전송하는 등의 역할을 한다.
(예. Spring의 Repository)

 

 

 


 

 

포트(Port)

애플리케이션 코어가 제공해야 할 기능을 나타내며 어댑터를 통해 애플리케이션 코어에 접근하는 인터페이스이다.

 

유형 설명
인커밍 포트 (주도하는) 외부 요청이 애플리케이션 코어로 들어오는 경로를 정의한다. 
예를 들어, 웹 요청, GUI 이벤트, 스케쥴링 이벤트 등이 인커밍 포트를 통해 애플리케이션 코어로 들어올 수 있다.
(예. Spring의 Controller와 Service 사이의 인터페이스)
아웃고잉 포트 (주도되는) 애플리케이션 코어가 외부 세계에 서비스를 제공하기 위한 경로를 정의한다. 
예를 들어, 데이터베이스, 메시징 시스템, 웹 서비스 등에 데이터를 전송하거나 요청하는 경우에 사용한다.
(예. Spring의 Repository와 Service 사이의 인터페이스)

 

 

 


 

 

 

💡 핵사고날 아키텍쳐의 유스케이스란?

 

애플리케이션의 비즈니스 로직을 나타내며, 사용자나 시스템이 수행하려는 기능을 표현한다.

이를 도메인 영어로 유스케이스로 표현하며, Service의 구체적인 행위이다.

 

 

 

 

 

 


 

 

 

 

 

 

 

 

인커밍 구성요소

 

인커밍 어댑터

  • controller 에 해당하며, 도메인 로직에서는 외부 세계에 해당한다.
  • 어댑터는 포트를 통해 도메인 로직 내부에 접근할 수 있다.
@WebAdapter
@RestController
@RequiredArgsConstructor
public class TestController {
	~
}

 

 

 


 

 

인커밍 포트

  • 어댑터가 어플리케이션 코어에 접근하는 인터페이스이다.
public interface SendMoneyUseCase {
	boolean sendMoney(SendMoneyCommand command);
}

 

 

 


 

 

 

유스케이스(인커밍 포트의 구현체)

  • 도메인 로직을 실행하는 코드의 집합으로, 어떤 작업을 수행하고 어떤 결과를 반환해야 하는지에 대한 규칙을 정의한다.
@RequiredArgsConstructor
@UseCase
@Transactional
public class SendMoneyService implements SendMoneyUseCase {
	@Override
	public boolean sendMoney(SendMoneyCommand command) {
		//도메인 로직
	}
}

 

 

 

어댑터는 특정 포트를 통해 애플리케이션 코어에 접근하고, 그 포트를 통해 실행하고자 하는 도메인 로직에 해당하는 구현체를 호출하게 된다. 

따라서 이를 호출하는 어댑터는 애플리케이션 코어의 구체적인 사항을 알 필요 없이 사용하는 포트만 알면 된다.

 

이렇게 함으로써 헥사고날 아키텍처는 외부 요구사항(예를 들어 사용자 인터페이스 또는 데이터베이스 접근 방식)의 변경이 애플리케이션 코어 즉, 비즈니스 로직에 영향을 미치는 것을 방지한다.

그 결과, 각 계층은 자신의 책임에만 집중하면 되므로 결합도는 낮아지고 응집도는 높아지게 된다.

 

도메인 로직
특정 비즈니스 영역의 규칙을 나타낸다.
이 규칙들은 주로 도메인 모델을 통해 구현된다.

예를 들어 은행 잔고가 양수일 때만 돈을 뽑을 수 있는 등의 규칙을 말한다.

 

 

 

 

 

 


 

 

 

 

 

 

 

아웃커밍 구성요소

 

 

아웃고잉 포트

  • 애플리케이션 코어에서 외부 세계로 향하는 통로다.
  • 이 포트를 통해 애플리케이션은 자신의 비즈니스 로직 결과를 전달한다.
  • 아웃고잉 포트는 주로 인프라스트럭처 레이어인 외부 세계(데이터베이스, 외부 서비스 등)와의 커뮤니케이션을 담당한다.
  • 아웃고잉 포트는 인터페이스로 정의되고, 실제 구현은 해당 포트를 이용하는 특정 플랫폼이나 기술 스택에 의존적이다.
public interface LoadAccountPort {
	Account loadAccount(AccountId accountId, LocalDateTime baselineDate);
}

 

 

 

 


 

 

 

아웃고잉 어댑터

@RequiredArgsConstructor
@PersistenceAdapter
class AccountPersistenceAdapter implements LoadAccountPort{
	private final SpringDataAccountRepository accountRepository;
    
	@Override
	public Account loadAccount(AccountId accountId,LocalDateTime baselineDate) {
		AccountJpaEntity account =
			accountRepository.findById(accountId.getValue())
			.orElseThrow(EntityNotFoundException::new);

		return accountMapper.mapToDomainEntity(
			account,
			activities,
			withdrawalBalance,
			depositBalance);

	}
}

 

 

 

아웃고잉 포트는 애플리케이션 코어가 외부 세계에 데이터를 전달하거나 명령을 내리는 방법을 정의한다. 

여기에는 데이터베이스로의 데이터 저장, 외부 API 호출 등이 포함된다. 

포트는 이러한 연산을 추상화한 인터페이스로, 애플리케이션 코어는 이 인터페이스를 통해 필요한 기능을 사용한다.

 

어댑터와 애플리케이션 코어 사이에 포트를 두는 것으로, 애플리케이션 코어는 외부 세계의 변화에 영향을 받지 않게 되고, 어댑터 역시 포트 인터페이스의 정의를 따르므로 결합도를 낮출 수 있다. 

 

이러한 방식으로 헥사고날 아키텍처는 외부와의 통신 방법이 변경되어도 애플리케이션 코어의 코드는 변하지 않으며, 

따라서 책임 분리와 응집도 높이는데 도움이 된다.

 

비즈니스 로직
애플리케이션의 비즈니스 규칙을 구현한다.
예컨대 사용자의 인출 요청을 받아 도메인 로직으로 유효성을 체크하고,
결과를 데이터베이스에 저장하고 사용자에게 반환하는 전체 과정을 다룬다.

 

 

 

 

 

 


 

 

 

 

 

 

 

객체지향적인 관점

 

SOLID

 

S (SRP; Single Responsibility Principle, 단일 책임의 원칙)

  • 한 기능에 대한 책임은 한곳에서 책임 져야 한다.
  • 모든 기능이 port와 그것에 맞는 adapter를 만들기 때문에 오직 하나의 메시지에 대해서 응답하고 처리하는 클래스, 객체를 만들 수 있다.

O (OCP; Open-Closed Principle, 개방 폐쇄 원칙)

  • 확장에 대해 열려있고 수정에 대해서는 닫혀있어야 한다는 원칙
    • 확장에 대해 열려 있다: 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
    • 수정에 대해 닫혀 있다: 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.
  • interface 기반으로 새로운 adapter 변경에 대해서는 자유롭고 수정에 대해서는 어떤 변경 없이도 구현이 가능하다.

L (LSP; Liskov Substitution Principle, 리스코프 치환 원칙) - 핵사고날에선 불가

  • 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

I (ISP; Interface segregation principle, 인터페이스 분리 원칙)

  • 클라이언트가 오직 필요로하는 인터페이스(메시지)에 대해서만 의존한다.
  • 모든 기능에 대해서 Port를 만들고 그것에 대한 adapter를 만들기 때문에 가능하다.

D (DIP; Dependency Inversion Princip, 의존성 역전 원칙)

  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 되며, 저수준 모듈이 고수준 모듈에 의존해야 한다는 것이다.
    • 고수준 모듈: 입력과 출력으로부터 먼(비즈니스와 관련된) 추상화된 모듈
    • 저수준 모듈: 입력과 출력으로부터 가까운(HTTP, 데이터베이스, 캐시 등과 관련된) 구현 모듈
  • port와 adapter 관계로 인해서 다형성이 보장이 되며 그것으로 인해서 interface 구현에 대해서 구현하는 사람, 혹은 모듈에 따라서 그것에 상위 클래스 혹은 객체들에 의존하게 됨.

 

 

 

 

 

 


참고사이트

헥사고날 아키텍처: 어댑터와 포트! 결합도를 낮춰보자

헥사고날 설계 (Port And Adapter Architecture)에 대해 알아보자

[OOP] 객체지향 프로그래밍의 5가지 설계 원칙, 실무 코드로 살펴보는 SOLID

 

 

SMALL