만들면서 배우는 클린아키텍처 코드 톺아보며
일단 나불대기
코드 구조를 살펴보니 클린스프링에서 학습한 것과 비교해보면서 톺아보면 더 재미있을 것 같다.
진행하면서 클린스프링도 다시 찾아봐야지
계층 우선? 도메인 우선?
클린 아키텍처
의 개념을 학습하기 전에 코드의 구조를 살펴보고 느낀건
- 클린스프링과 마찬가지로
헥사고날 아키텍처
+도메인 모델 패턴
+완화된 계층
을 적용한거구나 - 클린스프링과 조금 다른점은
-
클린 스프링에서는 계층 우선 방식(Layered-first) 으로 계층 하위에 애그리거트가 위치함
tobyspring/splearn/ ├── 🏛️ domain/ # 도메인 계층 (비즈니스 핵심) │ ├── member/ # 회원 도메인 모델 │ └── shared/ # 공통 도메인 객체 │ ├── 🔧 application/ # 애플리케이션 계층 (유스케이스) │ └── member/ # 회원 관련 유스케이스 │ ├── provided/ # 1차 포트 (외부 노출) │ └── required/ # 2차 포트 (외부 의존) │ └── 🔌 adapter/ # 어댑터 계층 (외부 연동) ├── webapi/ # REST API 어댑터 │ └── dto/ # 웹 API용 DTO ├── persistence/ # 영속성 어댑터 (비어있음) ├── security/ # 보안 어댑터 └── integration/ # 외부 통합 어댑터
-
반면에 도서에서는 **도메인 우선 방식(Domain-first)**으로 애그리거트 하위에 계층들이 위치함
src/ ├── main/ # 📦 메인 소스 코드 │ ├── java/ │ │ └── io.reflectoring.buckpal/ │ │ ├── common # 공통 유틸리티 │ │ └── account # 📋 계좌 바운디드 컨텍스트 │ │ ├── domain # 도메인 계층 │ │ ├── application # 애플리케이션 계층 │ │ │ ├── port │ │ │ │ ├── in # 인바운드 포트 │ │ │ │ └── out # 아웃바운드 포트 │ │ │ └── service # 서비스 구현체 │ │ └── adapter # 어댑터 계층 │ │ ├── in │ │ │ └── web # 웹 어댑터 │ │ └── out │ │ └── persistence/ # 영속성 어댑터
-
학습 차원에서는 경계 사이의 규칙(의존성의 방향)을 강제하거나 식별하기 쉬운 계층 우선방식을 권하지만 MSA, 수많이 복잡한 도메인 등을 다룰때는 도메인 우선이 더 응집도있는 패키지 모습으로 권장된다고 함.
나는 개인적으로는 도메인 우선이 더 가시성도 좋고 마음에 든다. 현업에서 개발할 때도 도메인 우선 패키지 구조를 주로 사용하는 편임.
다만, ‘이게 도대체 어떤 도메인에 속해야 하는건가’ 하고 애매한 경우가 많을 때는 구조적 일관성이나 경계의 강제성을 깨뜨리는 경우도 발생했던 것 같음.
그럼에도 계층 우선 방식은 도메인에 속하는 코드들의 응집도가 너무 흩어져있는 것 처럼 보여지기도 해서 마음에 들지 않는다.
(생각해보니 kacss는 계층형으로 구현하긴 했다.)
JpaEntity를 adapter/persistence에? 그리고 매퍼 객체까지?
클린스프링의 의견과 다르게 JpaEntity를 어댑터/영속 계층에 두었음.
JpaEntity가 기술관심사에 해당한다고 판단한 것 같음.
그리고 JpaEntity ↔ Domain Object 사이의 매핑을 위한 별도의 객체(AccountMapper
)를 두었는데, 그냥 JpaEntity에서 수행하면 되는거 아닐까? 왜 굳이 중간 매퍼 객체를 두는걸까?
SelfValidating?!
꽤 흥미로운 객체를 만났다.
추상클래스 타입으로, 자체적으로 ValidationFactory
를 이용하여 validator에 대한 의존성을 갖고서 특정 메서드 호출 시 validator를 이용하여 검증을 수행하는 방식이다.
이렇게 하면 클래스 레벨에 @Validated
를 명시하지 않아도 검증 트리거링이 되는 것 같은데…
그런데 굳이 이렇게 할 필요가 있나? 그냥 @Validated
를 정의하는게 더 낫지 않나?
혹시 @Validated
가 스프링(기술) 의존적이라고 판단해서 그런걸까? 최대한 jakarta
표준에만 의존하기 위해서?
음.. 코드의 패턴 자체는 흥미로워서 익혀볼만 하지만 의도 자체는 별로 공감되지 않는다. 아마 클린스프링 학습하면서 스프링과같은 디펙토 기술에 대한 선언적 의존(?)은 크게 개의치 않아서 그런 듯?
필요하게 되면 그 때 @Validated
를 걷어내고 다른 패턴을 사용하는게 크게 어렵지 않을 듯 함. 어차피 프록시 방식이고 적용하는데 조금만 고민하면 될 것 같은데 뭐.
포트 구현체에 대한 애노테이션?
포트를 구현하는 개념들을 @WebAdapter
, @UseCase
, @PersistenceAdapter
와 같이 애노테이션으로 구분했다.
가시성 때문에 그런 것 같은데… 이렇게 함으로써 얻는 큰 이득이 있나?
고민되네… 생각보다 차용해볼만 한 것 같기도 하고.
port in/out, adapter in/out, 누가 주인인가.
in
: 애플리케이션 입장에서 ‘들어오는’ 포트로,provided port interface
를 말하는 것 같음.- in 포트와 연동하는 책임을 담당하는건 adapter/in 패키지에
- 보통 presentation, web 등의 유저 인터페이스로써, ‘포트에 전달’ 하는 역할 수행
- ‘포트 구현체’는 보통 ‘애플리케이션 서비스’로써 여기서는
usecase
에 해당하는 듯
out
: 애플리케이션 입장에서 ‘나가는’ 포트로,required port interface
를 말하는 것 같음- out 포트와 연동하는 책임을 담당하는건 adapter/out 패키지에
- 보통 persistence로써 ‘포트를 구현’ 함으로써 연동
곰곰히 생각해봤는데, in
, out
도 헥사고날 아키텍처의 그림으로 보면 충분히 직관적이라서 좋은 것 같기도 하다.
음.. 그런데, 그래도 나름대로 provided port interface
, required port interface
를 멘탈모델로 표현하는걸 더 좋아하는 이유를 좀 생각해봤는데.
제공하는
,요청하는
이라는 표현 자체가 다분히 코어 애플리케이션이주인
이자중심
이라는 표현을 더 잘 하는 것 같다.- 반면
들어온다
,나간다
는 주체가 서로 다르다. 외부(들어온다)가 주체인 경우도 있고 코어(나간다)가 주체인 경우도 있다. - 전체적으로 ‘애플리케이션을 중심에 둔다’ 라는 멘탈 모델을 가지는데 더 적합한건
provided
,required
가 아닐까 싶다. - 그리고 어플리케이션 특성 상 상황에 따라서 ‘나간다’, ‘들어온다’ 라는 표현이 어색해지는 협력 구조도 생기지 않을까? (당장 떠오르지는 않지만 말야.)
- 그래서 나는
in
,out
이라는 표현은 다소 경직되게 느껴지기도 하고, 헥사고날 아키텍처에서 ‘헥사곤, 코어’라는 표현으로 내내 중요하다고 표현한 애플리케이션 코어를 중심에 둔다는 표현에 적합한건required
,provided
인 것 같다.- 그래서 나는 이걸 쓸래
Command, Query, UseCase도 in port구나.
- port가 무조건 ‘인터페이스’ 일 필요는 없구나.
- 생각해보면 그렇다. 헥사고날 아키텍처에서 ‘포트는 인터페이스여야만 한다’ 라는 건 없다.
- 단지 액터와 애플리케이션 사이의 연결을 위해 어댑터와 닿아있는 접점일 뿐이다.
Command
,Query
도in
포트에 두었네.- 그런데
Command
는 일종의 Request DTO 처럼 활용되는 것 처럼 보이긴 한다. 음… - 반면
Query
는 인터페이스로써 애플리케이션 서비스가 구현하는 모습으로 비춰진다. - 헷갈리는건 왜
Command
를 처리하는 포트는UseCase
라는 이름이 붙고Query
는 별도의 이름을 두었을까?Command
와Query
의 계위가 다른 느낌이다. 지금까지는 동일한 수준으로 이해했었는데 흠.
- 그런데
적당히 복잡한 도메인 오브젝트
Account
, Activity
, Money
,.. 등의 도메인 오브젝트들이 보이는데
ActivitiWindow
같은 개념들도 보이네.
아마 도메인 모델링 과정에서 필요에 의해 추출되고 표현되고 개념화된 도메인 오브젝트가 아닐까 하는 기대가 있다. 어떤 흐름으로 이런 도메인 오브젝트를 추출했을까. 그리고 어떤 책임을 가지고 있을까?
→ 코드를 좀 더 살펴봤는데 내가 배울게 많은 것 같다.
우선 도메인 모델링
을 역시 ‘상태’가 아니라 ‘행위’가 기반이 되어야 한다는 도서의 내용이 코드에 잘 드러난 것 같다.
예를들어 Account
, Activity
, AccountWindow
가 서로 협력하는 과정, 즉 거래가 발생했을 때 각 객체가 어떤 책임/역할을 수행하는지 적절히 알고리즘도 반영되어있고 ‘행위’와 ‘책임’을 바탕으로 적절한 도메인 오브젝트가 추출된, ‘도메인 모델링’이 잘 된 사례가 아닌가 싶다.
만약 내가 했다면 아무래도 ‘거래’ 암묵지를 갖고 ‘거래 데이터에 대해 매번 DB에 접근’ 한다는 다소 간단하지만 ‘상태’ 를 우선해서 접근하지 않았을까?
적절한 도메인 개념을 추출하기 위한 고민은 매우 중요하면서도 어려운 것 같다.
AccountLock도 포트로 정의? Lock 개념도 비즈니스 규칙이니까?!
이런 부분들은 좀 헷갈리긴 함. Transaction
도 비슷한 개념일 것 같다.
곰곰이 생각해보면 개념 자체는 비즈니스 규칙
에 해당하기 때문에 애플리케이션에 속하는게 맞지만 어떻게 구현
하는지는 기술요소에 해당하기 때문에 구현체는 어댑터에 두는게 맞는 것 같다.
관련해서 예전에 PasswordEncoder
가 도메인 오브젝트에 해당하는가? 에 대해 GPT에 던져본적이 있다.
- ‘비밀번호는 해시로 저장/검증해야 한다.’ → ‘도메인/비즈니스 규칙’ → ‘도메인 소유’
- ‘도메인 규칙을 충족하기 위해 외부 기능을 필요로 함’ → ‘port”
마찬가지로 AccountLock
과 Transaction
의 경우에도
- ‘어떤 계좌가 거래중일 때는 다른 거래와 계산이 동시에 발생해서는 안된다’ → ‘도메인/비즈니스 규칙’
- 도메인 규칙을 지키기 위해 상세한 구현은 외부의 기능을 필요로 함 → port
흐름을 적용할 수 있겠다.