Files
project-auth-server/docs/standards/design/port-abstraction.md
T

5.7 KiB

port abstraction 기준

목적

포트는 “인터페이스를 많이 만들기 위한 수단”이 아니라,
애플리케이션 코어가 외부 기술과 직접 결합되지 않도록 경계를 고정하는 계약이다.

공식 의미

  • 포트는 애플리케이션과 외부 세계 사이의 purposeful conversation을 식별하는 계약이다.
  • 어댑터는 기술별 입력/출력을 포트 계약에 맞게 번역한다.
  • 헥사고날 아키텍처에서는 코어가 외부 컴포넌트에 의존하지 않고, 외부 컴포넌트가 코어가 정의한 포트에 맞춘다.
  • 포트는 inbound(입력) / outbound(출력) 성격으로 나눠 볼 수 있다.

기본 규칙

1. 포트는 “경계”에만 만든다

다음 중 하나가 아니면 포트를 만들지 않는다.

  • application use case 진입 계약
  • persistence / external API / message broker / file system 같은 외부 의존 경계
  • 교체 가능한 보안/토큰/암호화/알림 정책 경계
  • 테스트에서 fake/stub로 바꿔 끼울 가치가 큰 경계

같은 모듈 내부 helper 호출에는 포트를 만들지 않는다.

2. 코어가 소유하는 포트만 허용

포트는 코어(application/domain)가 필요로 하는 계약이어야 한다.

기본:

  • inbound port: 코어가 외부 호출자에게 제공하는 use case 계약
  • outbound port: 코어가 외부 시스템에 요구하는 계약

금지:

  • infrastructure 기술 구조에 맞춘 인터페이스를 코어에 올리기
  • adapter 편의를 위해 포트 모양을 결정하기

3. 포트는 비즈니스 의미로 말하고, 기술 세부를 숨긴다

포트 메서드는 “무엇을 원하는가”를 표현해야 한다.

좋은 방향:

  • findUserByEmail
  • saveUser
  • signToken
  • publishUserCreated

지양:

  • callHttp
  • executeQuery
  • postJson
  • sendKafkaRecord

기술 세부는 adapter 구현으로 내린다.

4. 포트 시그니처에 기술 타입을 노출하지 않는다

포트 계약에는 가능하면 아래 타입을 직접 노출하지 않는다.

  • ResponseEntity
  • HttpClient, WebClient
  • ResultSet
  • JpaRepository
  • JsonNode
  • 프레임워크 request/response 타입

포트는 domain/application에 더 가까운 command/result/value type으로 표현한다.

5. 포트는 작고 응집도 있게 유지

포트 하나는 하나의 역할/대화에 집중해야 한다.

금지:

  • unrelated use case를 한 inbound port에 몰아넣기
  • 여러 외부 시스템 책임을 한 outbound port에 섞기
  • 너무 범용적인 CommonPort, IntegrationPort

6. inbound port는 use case 단위로 생각

입력 포트는 보통 “사용자가 시스템에 요구하는 행위” 단위로 설계한다.

예:

  • 회원 가입
  • 로그인
  • 토큰 발급
  • 공개키 조회

즉 controller나 scheduler가 직접 서비스 구현을 알기보다, use case 계약을 호출하는 방향을 우선 검토한다.

7. outbound port는 코어가 정말 필요한 능력만 노출

출력 포트는 adapter가 할 수 있는 모든 기능이 아니라, 코어가 실제로 필요한 능력만 드러낸다.

예:

  • UserReader / UserAppender
  • VaultSigner
  • TokenPublisher

금지:

  • adapter의 내부 옵션/기술 선택지를 포트에 그대로 노출
  • “혹시 나중에 필요할지도” 모드를 미리 넣기

8. 포트 이름은 역할 중심으로 짓는다

좋은 방향:

  • UserReader
  • UserSaver
  • TokenSigner
  • AuthLoginUseCase
  • PublicKeyQuery

지양:

  • UserPort
  • CommonPort
  • InfraPort
  • ExternalApiPort

이름만 보고 어떤 대화를 하는지 보여야 한다.

9. 포트 반환값은 없음/실패 의미를 명확히 표현

  • 단건 조회 없음 -> Optional<T> 검토
  • 다건 조회 없음 -> empty collection
  • 실패는 예외 또는 명시적 result type으로 표현
  • null 반환 금지

10. 포트는 transaction/transport를 직접 소유하지 않는다

포트 자체는 transaction, HTTP, serialization 정책을 직접 설명하지 않는다.

기본:

  • transaction boundary는 application service/use case 쪽에서 결정
  • transport 형식은 controller/adapter에서 결정
  • serialization은 adapter에서 처리

11. adapter는 포트를 “구현”하거나 “호출”하면서 번역 책임을 진다

  • inbound adapter: HTTP, scheduler, message consumer, CLI 등에서 입력을 받아 포트 호출
  • outbound adapter: DB, external API, queue, cache 등에 맞게 포트를 구현

adapter는 기술 번역을 담당하지만 business meaning을 새로 만들지 않는다.

12. 테스트 seam이 실제 가치가 있을 때 포트를 둔다

포트는 테스트를 쉽게 만들 수 있지만, 테스트 때문에 모든 내부 호출을 포트로 만들지는 않는다.

기본:

  • 외부 경계 seam은 포트 우선
  • 내부 구현 detail seam은 concrete class 유지 가능

13. 포트와 adapter는 1:1일 필요가 없다

하나의 포트에 여러 adapter가 붙을 수 있다. 예:

  • mock repository / real repository
  • REST adapter / batch adapter / test harness

즉 포트는 기술 구현 수가 아니라 “대화 계약” 기준으로 잡는다.

14. 포트는 framework proxy/AOP 이유만으로 만들지 않는다

Spring이 인터페이스 기반 프록시를 잘 지원하더라도, 프록시 가능성만으로 포트를 만들지 않는다. 먼저 경계/계약 의미가 있는지 확인한다.

프로젝트 기준 요약

  • 포트는 경계에만 만든다
  • 포트는 코어가 소유한다
  • 포트는 비즈니스 의미로 말하고 기술 세부를 숨긴다
  • inbound는 use case 중심
  • outbound는 코어가 필요한 능력만
  • 포트는 작고 응집도 있게
  • adapter가 기술 번역을 담당
  • 모든 내부 호출을 포트로 만들지 않는다