아키텍처

[클린 아키텍처] 웹 어댑터 구현하기 & 영속성 어댑터 구현하기

icecupregular 2025. 3. 28. 04:03

의존성 역전

adapter.in.web || application.port.in || application.service

컨트롤러 → 포트 ← 서비스

컨트롤러 → 포트 ← 서비스

웹 어댑터는 ‘주도하는’ 혹은 ‘인커밍’ 어댑터다. 외부로부터 요청을 받아 애플리케이션 코어를 호출하고 무슨 일을 해야 할지 알려준다. 이때 제어 흐름은 웹 어댑터에 있는 컨트롤러에서 애플리케이션 계층에 있는 서비스로 흐른다.

애플리케이션 계층은 웹 어댑터가 통신할 수 있는 특정 포트를 제공한다. 서비스는 이 포트를 구현하고, 웹 어댑터는 이 포트를 호출할 수 있다.

왜 어댑터와 유스케이스 사이에 또 다른 간접 계층을 넣어야할까? 애플리케이션 코어가 외부 세계와 통신할 수 있는 곳에 대한 명세가 포트이기 때문이다. 포트를 적절한 곳에 위치시키면 외부와 어떤 통신이 일어나고 있는지 정확히 알 수 있고, 이는 레거시 코드를 다루는 유지보수 엔지니어에게는 무척 소중한 정보다.

웹 어댑터의 책임

웹 어댑터는 일반적으로 다음과 같은 일을 한다.

  • HTTP 요청을 자바 객체로 매핑
  • 권한 검사
  • 입력 유효성 검증
  • 입력을 유스케이스의 입력 모델로 매핑
  • 유스케이스 호출
  • 유스케이스의 출력을 HTTP로 매핑
  • HTTP 응답을 반환

컨트롤러 나누기

  • AccountController를 하나 만들어서 계좌와 관련된 모든 요청을 받는 방법
    • 단점
      • 클래스 마다 코드는 적을수록 좋다.
      • 아무리 메소드로 깔끔하게 분리돼 있어도 파악하기 쉽지 않다
      • 테스트 코드 양도 그만큼 많아진다.

가급적 메소드와 클래스명은 유스케이스를 최대한 반영해서 지어야한다.

유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?

애플리케이션의 웹 어댑터를 구현할 때는 HTTP 요청을 애플리케이션의 유스케이스에 대한 메서드 호출로 변환하고 결과를 다시 HTTP로 변환하고 어떤 도메인 로직도 수행하지 않는 어댑터를 만들고 있다는 점을 염두에 둬야 한다.

영속성 어댑터에서 의존성 역전

application.service || application.port.out || adapter.out.persistence

 서비스                 →             포트               ←              영속성 어댑터

 서비스                →              포트               ←                          “

애플리케이션 서비스에서는 영속성 기능을 사용하기 위해 포트 인터페이스를 호출한다.

이 포트는 실제로 영속성 작업을 수행하고 데이터베이스와 통신할 책임을 가진 영속성 어댑터 클래스에 의해 구현된다.

육각형 아키텍처에서 영속성 어댑터는 ‘주도되는’ 혹은 ‘아웃고잉’ 어댑터다. 애플리케이션에 의해 호출될 뿐, 애플리케이션을 호출하지는 않기 떄문이다.

영속성 계층에 대한 코드 의존성을 없애기 위해 이러한 간접 계층을 추가하고 있다는 사실을 잊지 말자.

영속성 어댑터의 책임

영속성 어댑터가 하는 일을 살펴보자

  • 입력을 받는다
  • 입력을 데이터베이스 포맷으로 매핑한다
  • 입력을 데이터베이스로 보낸다
  • 데이터베이스 출력을 애플리케이션 포맷으로 매핑한다
  • 출력을 반환한다

포트 인터페이스 나누기

특정 엔티티가 필요로 하는 모든 데이터베이스 연산을 하나의 리포지토리 인터페이스에 넣어 두는게 일반적인 방법이다.

인터페이스 분리 원칙은 “ 필요없는 화물을 운반하는 무언가에 의존하고 있으면 예상치 못했던 문제가 생길 수 있다”의 문제에 답을 제시한다. 이 원칙은 클라이언트가 오로지 자신이 필요로 하는 메서드만 알면 되도록 넓은 인터페이스를 특화된 인터페이스로 분리해야 한다고 설명한다.

application.service || application.port.out || adapter.out.persistence

SendMoneyService→ AccountRepository ← 영속성어댑터

RegisterAccountService→ “ ← “

application.service || application.port.out || adapter.out.persistence

SendMoneyService→LoadAccountPort ← 영속성 어댑터

SendMoneyService→UpdateAccountStatePort← “

RegisterAccountService→CreateAccountPort ← “

위 구조를 아래 구조로 변경하면, 테스트에서는 어떤 메서드를 모킹할지 고민할 필요가 없다.

왜냐하면 대부분의 경우 포트당 하나의 메서드만 있을 것이기 떄문이다.

  • 이렇게 매우 좁은 포트를 만드는 것은 코딩을 플러그 앤드 플레이로 만든다.
    • 운반할 다른 화물이 없는 것이다.

영속성 어댑터 나누기

영속성 연산이 필요한 도메인 클래스(애그리거트) 하나당 하나의 영속성 어댑터를 구현하는 방식을 선택할 수 있다.

이렇게 하면 영속성 어댑터들은 각 영속성 기능을 이용하는 도메인 경계를 따라 자동으로 나눠진다.

‘애그리거트 당 하나의 영속성 어댑터’ 접근 방식 또한 나중에 여러 개의 바운디드 컨텍스트의 영속성 요구사항을 분리하기 위한 좋은 토대가 된다.

스프링 데이터 JPA 예제

AccountPersistenceAdapter를 구현한 코드를 보자.

이 어댑터는 데이터베이스로부터 계좌를 가져오거나 저장할 수 있어야 한다.

데이터베이스와의 통신에 스프링 데이터 JPA를 사용할 것이므로 계좌의 데이터베이스 상태를 표현하는 @Entity 애노테이션이 추가된 클래스도 필요하다.

@Entity
@Table(name = "account")
...
class AccountJpaEntity {

		@Id
		@GeneratedValue
		private Long id;

}

다음은 activity 코드다

@Entity
@Table(name = "account")
...
class ActivityJpaEntity {

		@Id
		@GeneratedValue
		private Long id;
		
		...

}

스프링 데이터를 사용

interface AccountRepository extends JpaRepository<AccountJpaEntity, Long> {
}

다음은 ActivityRepository 코드다.

interface ActivityRepository extends JpaRepository<ActivityJpaEntity, Long>
{
	@Query(~
	...
	}

이제 JPA 엔티티와 리포지토리를 만들었으니 영속성 기능을 제공하는 영속성 어댑터를 구현해보자.

@RequiredArgsConstructor
@Component
class AccountPersistenceAdapter implements
		LoadAccountPort,
		UpdateAccountStatePort {
		
		private final AccountRepository accountRepository;
		private final ActivityRepository activityRepository;
		private final AccountMapper accountMapper;
		
		@Override
		public Account loadAccount(
		...
		) { 
		
		
		...
		
		return accountMapper.mapToDomainEntity(
				...
		);
		
		
		}
		
		...

영속성 어댑터는 애플리케이션에 필요한 포트를 구현한다.

영속성 측면과의 타협 없이 풍부한 도메인 모델을 생성하고 싶다면 도메인 모델과 영속성 모델을 매핑하는 것이 좋다.

데이터베이스 트랜잭션은 어떻게 해야 할까?

트랜잭션 경계는 어디에 위치 시켜야 할까?

트랜잭션은 하나의 특정 유스케이스에 대해서 일어나는 모든 쓰기 작업에 걸쳐 있어야 한다.

그래야 그 중 하나라도 실패할 경우 다 같이 롤백될 수 있기 떄문이다.

만약 서비스가 @Transactional 애너테이션으로 오염되지 않고 깔끔하게 유지되길 원한다면 AspectJ 같은 도구를 이용해 관점 지향 프로그래밍으로 트랜잭션 경계를 코드에 위빙할 수 있다.

유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?

도메인 코드에 플러그인처럼 동작하는 영속성 어댑터를 만들면 도메인 코드가 영속성과 관련된 것들로부터 분리되어 풍부한 도메인 모델을 만들 수 있다.

좁은 포트 인터페이스를 사용하면 포트마다 다른 방식으로 구현할 수 있는 유연함이 생긴다. 심지어 포트 뒤에서 애플리케이션이 모르게 다른 영속성 기술을 사용할 수도 있다.

포트의 명세만 지켜진다면 영속성 계층 전체를 교체할 수도 있다.