본문 바로가기
도메인 주도 설계

핵사고날 아키텍처에서 Port(Adapter)의 의미는 무엇일까?

by simplify-len 2020. 8. 29.

이전 포스팅에서 말했던

어탭터 패턴 이렇게도 쓰일 수 있구나?

를 이해하면서 핵사고날 아키텍처에 대해서도 동시에 어느정도 이해할 수 있었습니다. 같이 스터디를 도와주시는 분들과 이야기하며 아주 사-알짝 핵사고날 아키텍처에 대해서 발을 담가볼 수 있는 이야기로 접근해보겠습니다.

0. 들어가기

안드로이드 모바일 프로그래밍을 했을 적에도 클린 아키텍처라는 말은 많이 들었지만, 코드로서 표현되는 프로젝트는 보기 힘들었습니다. 그러던 중 백엔드로 전향 이후, IDDD 프로젝트를 분석하며, 코드로서 핵사고날을 드러나는 아키텍쳐를 이해할 수 있었습니다. 

 핵사고날 아키텍처를 이해하는 것은 단순히 하나의 그림으로서 설명될 수 부분이 아니라고 생각합니다.

 다이어그램과 더불어 코드, 그리고 그 외 도메인에 대한 이해와 의존성 역전 법칙에 대한 이해가 있다라면 조금은 이해할 수 있지 않을까? 싶습니다.

 

VaughnVernon/IDDD_Samples

These are the sample Bounded Contexts from the book "Implementing Domain-Driven Design" by Vaughn Vernon: http://vaughnvernon.co/?page_id=168 - VaughnVernon/IDDD_Samples

github.com

1. 핵사고날 아키텍처란 무엇일까?

아래 그림은 Uncle bob 이 주장했던 클린 아키텍쳐입니다.

각각 layer에 대한 설명은 생략하고, 인터페이스 어댑터(초록색), 프레임워크와 드라이버(파란색) 부분에 대한 설명을 하고자합니다.

엉글밥의 클린 아키텍처

 

의존성의 방향을 가운데로 갈수록 높은 수준, 바깥으로 갈수록 낮은 수준의 컴포넌트를 구성하는 것으로- 위와같은 아키텍처라면 효율적인 설계가 가능하다는 것을 의미합니다.

위 그림에서는 인터페이스 어탭터라 불리는 곳. 핵사고날 아키텍처의 다른 그림을 살펴보겠습니다.

https://reflectoring.io/spring-hexagonal/

여기서 Input Port와, Output Port 의 관계를 간략히 감으로 생각해보면, Port로 들어와, 나간다 라는 느낌이 있습니다.

핵사고날 아키텍처에서 가장 중요한 부분이라고 할 수 있는 부분이 바로 Port로 들어와 나간다. 입니다. 핵사고날이라고 부르는 이유도 위와같은 형태를 띄기 때문에 핵사고날 또는 어니언 아키텍처라 불립니다.

UseCase > Entity < UseCase 의 의존관계는 전통적인 레이어드 아키텍처와 유사합니다. 그러므로 해당 포스팅에서는 겉을 감싸는 포트에 집중하고자 합니다.

2. 포트? 어탭터?

 위 그림에서도 어탭터라는 말이 나왔지만, 사실 이것이 의미하는 바를 일반적인 코드에서 찾기 힘들었습니다. 즉, 일반적인 프로젝트에서는 Port, Adapter 라는 단어가 쓰인 패키지를 보기 힘듭니다.


 포트란 무슨 의미일까요? 단순히 생각하면, 어떤 데이터를 들어오게 하기 위한 전용망, 전용통로, 허가된 통로 외에는 유입을 금지한다. 라는 뜻으로 받아들어집니다. 어탭터는 포트로 들어오기 위해선 외부에서 데이터를 넣기위한 포트로 끼어지는 플로그인 같은 존재라고 생각하면 좋을 듯합니다.

 물론 저 개인의 의견이고, 이는 프로젝트를 통해 느껴지는 바를 적은 것이므로, 다른 분들과 다를 수 있습니다.

어떻게 포트는 전용망이고? 어탭터는 플로그인 같은 존재인가?

우리는 위 내용을 이해하기 위해서는 객체지향원칙 중 의존성 역전 법칙을 이해할 수 있어야 합니다. 의존성 역전 법칙은 간단히 이야기하면 의존성이 역전됨을 의미합니다.

의존성 역전에 대해서 코드로 확인하고 싶다면, 해당 Github 레포의 내용을 참고해주세요.

의존성 역전이 어떻게 포트와 어탭터를 설명할 수 있을까요? 이 부분에 대해서는 반버논의 도메인 주도 설계 4장에 아키텍처라는 부분에서 설명되어 있습니다.  책의 일부분 내용을 빌려 포트와 어탭터에 대한 설명을 이어가겠습니다.

그림 4.4

"외부 타입마다 어탭터가 존재하고, 외부 영역은 애플리케이션의 API를 통해 내부 영역과 이어진다."

 위 그림에서 클라이언트 타입은 자신만의 고유한 어탭터를 갖는데, 이는 입력 프로토콜을 애플리케이션의 API와 호환되는 '내부' 입력으로 변환한다

위 글이 포트와 어탭터를 이해시켜주는 대목이라고 생각한다.

그렇다면 코드로서 어떻게 들어낼까?

3. Show me the code

 도메인주도설계 책에서 말하는 코드를 인용하기보다는 제가 보고 느꼈던 핵사고날 코드를 말하고자 합니다.

여기서 등장인물은 아래와 같습니다.

Model 패키지 안에

com/saasovation/collaboration/domain/model/ collaborator


그리고 Port 패키지에 안에 있는

com/saasovation/collaboration/port/adapter

 위 코드의 기법으로서의 자세한 내용은 어탭터 패턴 이렇게도 쓰일 수 있구나?에서도 확인할 수 있습니다.

그럼 어떻게 포트와 어탭터가 어떻게 활용됐는지 살펴봅시다.

가장 먼저 인상깊게 봐야될 부분은 CollaboratorService 위치입니다. 해당 파일의 위치는 Model 패키지에 존재합니다. 즉, 해당 서비스는 도메인서비스라는 것을 상기시킵니다. 다시말해, 도메인 모델링에서 동작되는 코드 중 일부라는 말입니다. 이 얘기는 즉, 외부 요소와는 어떠한 결합 관계가 없음을 의미합니다. 이런 상태에서 TranslatingCollaboratorService 를 살펴보겠습니다.

public class TranslatingCollaboratorService implements CollaboratorService {

	...

    public TranslatingCollaboratorService(UserInRoleAdapter aUserInRoleAdapter) {
        super();

        ...
    }

    @Override
    public Author authorFrom(Tenant aTenant, String anIdentity) {
	    ...
    }

    @Override
    public Creator creatorFrom(Tenant aTenant, String anIdentity) {
    	...
    }

    @Override
    public Moderator moderatorFrom(Tenant aTenant, String anIdentity) {
		...
	}

    @Override
    public Owner ownerFrom(Tenant aTenant, String anIdentity) {
        ...
    }

    @Override
    public Participant participantFrom(Tenant aTenant, String anIdentity) {
        ...
    }
}

 위 클래스의 패키지를 살펴보면, Port 라고 하는 이름 패키지에 존재합니다. 패키지가 중요합니다. 즉, Port 라는 패키지로 명시하는 바와 같이 이는 외부에서 유입될 것이라는 것을 암시합니다. 즉, 포트와 같은 전용망으로서 외부에서 유입될 것이다. 라는 것이 암시합니다.

 그렇다면, 외부에서 어떻게 TranslatingCollaboratorService 라는 포트를 통해 도메인 패키지로 유입될 수 있을까?

여기서부터 앞서 설명했던 Adapter 의 개념이 출연합니다. 전용망을 열었다면, 이제 플로그인을 꽂아봅시다.

위 코드에서 드러내지 않았는데, 해당 코드의 구현체는 사실 아래와 같습니다.

public class TranslatingCollaboratorService implements CollaboratorService {

    private final UserInRoleAdapter userInRoleAdapter;

    public TranslatingCollaboratorService(UserInRoleAdapter aUserInRoleAdapter) {
        super();

        this.userInRoleAdapter = aUserInRoleAdapter;
    }

    @Override
    public Author authorFrom(Tenant aTenant, String anIdentity) {
        Author author =
                this.userInRoleAdapter()
                    .toCollaborator(
                            aTenant,
                            anIdentity,
                            "Author",
                            Author.class);

        return author;
    }

    @Override
    public Creator creatorFrom(Tenant aTenant, String anIdentity) {
        Creator creator =
                this.userInRoleAdapter()
                    .toCollaborator(
                            aTenant,
                            anIdentity,
                            "Creator",
                            Creator.class);

        return creator;
    }

    @Override
    public Moderator moderatorFrom(Tenant aTenant, String anIdentity) {
        Moderator moderator =
                this.userInRoleAdapter()
                    .toCollaborator(
                            aTenant,
                            anIdentity,
                            "Moderator",
                            Moderator.class);

        return moderator;
    }

    @Override
    public Owner ownerFrom(Tenant aTenant, String anIdentity) {
        Owner owner =
                this.userInRoleAdapter()
                    .toCollaborator(
                            aTenant,
                            anIdentity,
                            "Owner",
                            Owner.class);

        return owner;
    }

    @Override
    public Participant participantFrom(Tenant aTenant, String anIdentity) {
        Participant participant =
                this.userInRoleAdapter()
                    .toCollaborator(
                            aTenant,
                            anIdentity,
                            "Participant",
                            Participant.class);

        return participant;
    }

    private UserInRoleAdapter userInRoleAdapter() {
        return this.userInRoleAdapter;
    }
}

 

플로그인 역할을 하는 것이 바로 위 코드에서는 userInRoleAdapter 입니다. userInRoleAdapter 통해 전용망을 구축하고 외부로부터 Input을 노출시킵니다. userInRoleAdapter의 코드는 간결합니다.

public interface UserInRoleAdapter {

    <T extends Collaborator> T toCollaborator(
            Tenant aTenant,
            String anIdentity,
            String aRoleName,
            Class<T> aCollaboratorClass);
}

처음에는 userInRoleAdapter 라는 클래스는 말 그대로 교체할 수 형태를 띄우기 위해 명시적으로 adpater 라는 말을 썼구나 싶었습니다. 그러나 핵사고날 아키텍처를 이해하면서, adapter 라는 클래스는 제가 생각한 방식도 맞지만, 더 나아가 설계의 한 부분으로서 동작함을 이해했습니다.

그리고, 눈치채신 분들이 많겠지만, 핵사고날 아키텍처가 설계되기 위해서는 간결한 결합이 의존성 역전 원칙에 따라서 아래 그림과 같이 계속해서 이루어져야 합니다.

의존성 역전 법칙에 대한 또다른 참고 링크로서 vandbt.tistory.com/42의 내용이 좋아 공유드립니다.

 

 

의존 관계 역전의 원칙- Dependency Inversion Principle for Primer

이 포스트의 주제는 이전의 S.O.L.I.D 의  LSP에 이어 의존 역전의 원칙 Dependency Inversion Principle (DIP) 입니다. 포스팅의 동기 또한 LSP의 동기와 같습니다. 개념이해를 돕기  위해 뽑아낸 간략화��

vandbt.tistory.com

4. 풀리지 않는 의혹...

사실 위 코드에서 

    private UserInRoleAdapter userInRoleAdapter() {
        return this.userInRoleAdapter;
    }

이런 식으로 인터페이스를 가져와 사용했는지에 대한 이해가 잘 안된다. 왜일까? 외부의 노출을 막기 위해? 다른 개념을 처리하기 위해서일까요?

---

PortAdapter 에 대한 설명은 여기까지입니다. 핵사고날을 이해하기 위한 키로서 동작되는데, 이해하는 것이 굉장히 중요하다고 생각합니다. 그러나, Port 와 Adapter 만 잘 활용한다하여 핵사고날이 됨을 의미하는 바는 아닙니다. 도메인, 유저케이스, 어플리케이션 등 클린아키텍처의 형태를 잘 따라가야만 핵사고날의 Port와 Adapter 도 유용하게 쓰일 수 있다고 생각됩니다.

 

댓글