책 내용 단순 나열에 가까운 기록용도의 글입니다. 특별한 내용은 거의 없습니다
https://github.com/wikibook/clean-architecture
계층형 아케텍쳐는 선택의 폭을 넓히고 변화하는 요구사항과 외부요인에 빠르게 적응할 수 있게 해준다.
문제점은 코드에 나쁜 습관들이 스며들기 쉽게 만들고 시간이 지날수록 소프트웨어를 점점 더 변경하기 어렵게 만드는 허점들을 노출한다.
레포지토리의 인터페이스를 도메인 계층에 넣어두고 구현은 영속 계층에서 하도록
내가 구현하면서 느껴본 바로는 의존방향과 제어 방향이 일치하므로 의존 역전이 필요 없을 수 있음
이건 그냥 클래스 여러개 만들어서 관리하면 되는거 아닌가? 하는 생각도 든다
애플리케이션의 기능을 코드를 통해 볼 수 있게 만드는 것을 로버트 마틴은 소리치는 아키텍쳐라고 명명한다.
buckpal
- account
- adapter
- in
- web
- AccountController
- out
- persistence
- AccountPersistenceAdapter
- SpringDataAccountRepository
- domain
- Account
- Activity
- application
- SendMoneyService
- port
- in
- sendMoneyUseCase
- out
- LoadAccountPort
- UpdateAccountStatePort
Adapter들은 packege private으로 둬도 된다. 어차피 소통은 port의 in/out 인터페이스로만 하니까
TODO: https://www.georgefairbanks.com/software-architecture/model-code-gap/
AccountController -> <I>SendMoneyUseCase -> SendMoneyService -> <I>LoadAccountPort -> AccountPersistenceAdapter
AccountController -> <I>SendMoneyUseCase -> SendMoneyService <- <I>LoadAccountPort <- AccountPersistenceAdapter
책의 구현은 SendMoneyUseCase를 SendMoneyService가 구현하도록 하고, SendMoneyService에서 LoadAccountPort를 사용하도록 한다.
- Account 엔티티는 실제 계좌의 현재 스냅샷을 제공한다.
- 입금과 출금은 Activity 엔티티에 포착된다.
- 한 계좌의 모든 Activity를 미리 메모리에 올려두는 것은 비효율적이므로 ActivityWindow VO 에서 포착한 지난 며칠 혹은 몇 주간의 범위에 해당하는 활동만 보유한다.
- 현재 잔고 계산을 위해 baselineBalance가 있다. 현재 총 잔고는 baselineBalance에 활동창의 모든 활동들의 잔고를 합한 값이 된다.
Money.add( this.baselineBalance, this.activityWindow.calculateBalance(this.id) );
- 입금과 출금은 각각 새로운 활동을 활동창에 추가하는 것을 의미한다. (withdraw() 와 deposit() 메서드)
- 출금하기 전에는 잔고를 초과하는 금액은 출금할 수 없도록 검사하는 비즈니스 규칙이 있다.
유스케이스가 하는 일?
저자는 유스케이스 코드가 도메인 로직에만 신경써야 한다고 생각한다. 유스케이스에 입력 유효성 검증이 들어가면 오염된다고 생각한다.
비즈니스 규칙을 충족하면 유스케이스는 입력을 기반으로 어떤 방법으로든 모델의 상태를 변경한다.
일반적으로 도메인 객체의 상태를 바꾸고 영속성 어댑터를 통해 구현된 포트로 이 상태를 전달해서 저장될 수 있게 한다. 그리고 또 다른 아웃고잉 어댑터를 호출 할 수도 있다.
넓은 서비스 문제를 피하려면 각 유스케이스별로 분리된 각각의 서비스를 만든다.
유스케이스 책임은 아니지만 애플리케이션 게층의 책임에는 해당한다.
어댑터가 유스케이스에 입력을 저날하기 전에 입력 유효성을 검증한다면? 이걸 모두 검증했다고 믿을 수 있나? 그리고 유스케이스는 하나 이상의 어댑터에서 호출되는데 유효성 검증이 각 어댑터마다 구현되어야 한다.
애플리케이션 게층에서 입력 유효성 검증 하는 이유
그래서 입력 모델(input model)을 만든다.
SendMoneyCommand 는 생성자에 많은 책임을 부여했다. 클래스가 불변이고 유효성 검증까지 하고 있으니 유효하지 않은 상태를 만드는게 불가능
매개변수가 많으면 빌더로 만들어도 됨. 어차피 유효성 검사 하니까 빌더를 써도 잘못된 객체가 만들어지기 어려움.
그럼에도 빌더는 런타임에 감지가 되고 컴파일 타임에 감지를 하기 힘듦.
감지하려면 테스트 코드가 있어야 할 것이다
그래서 책에서는 생성자 사용을 권장
서로 다른 유스케이스에 같은 입력 모델을 사용하고 싶을 수 있다.
그런데 이건 코드 스멜일 가능성이 생긴다.
예를 들어, 같은 불변 커맨드 객체를 사용하지만 특정 유스케이스에서는 허용해야 하는게 다른 유스케이스에서는 불허해야 하는 경우가 있으니(e.g. nullable, not null)
따라서 유스케이스 전용 입력 모델을 만들어야 불필요한 부수효과가 생기지 않는다고 설명한다.
대신 입력 모델에 매핑하는 비용이 생긴다.
입력 유효성과 비즈니스 규칙 검증을 실용적으로 구분할 수 있는 방법은 도메인 모델의 현재 상태에 접근해야 하는지 여부
입력 유효성 검증은 @NotNull
같은거 붙이면 땡일 가능성이 높다.
책에서는 입력 유효성은 구문상의(syntactical) 유효성 검증이라 하고 비즈니스 규칙 검증은 의미적인(semantical) 유효성 검증이라 표현한다.
비즈니스 규칙 검증의 예시로 출금 계좌는 초과 출금 되어서는 안 된다는 조건을 얘기한다. 초과 출금 되지 않으려면 출금 계좌의 현재 상태에 접근해야 하므로 비즈니스 규칙이다.
반대로 송금되는 금액은 0보다 크다라는 조건은 모델에 접근하지 않아도 해결된다. 이러면 유효성 검증이다.
관점 따라 논쟁이 될 수 있다고 하는데 책에서는 코드의 일관성 측면에서 유지보수 하기 쉬운 방법이라 설명한다.
컨트롤러에서 받는 Request 모델에서 검증해야 하면 틀린 말일 수 있는데, 책에서 처럼 커맨드 객체를 만들어 사용하는거면 이게 맞는 것 같다.
그러면 비즈니스 규칙검증은 어떻게 하나? 도메인 객체에 넣어준다. 이러면 비즈니스 로직 옆에 존재하니 위치 고민도 없고 추론도 쉽다.
이게 애매하면 유스케이스 코드(서비스 구현체)에서 도메인 사용 전에 해준다.
풍부한 도메인 모델에서는 엔티티에서 가능한 많은 도메인 로직을 구현한다.
엔티티 자체가 굉장히 얇다.
상태를 표현하는 필드와 getter, setter 메서드만 포함되고 도메인 로직을 가지지 않는다.
다른 말로 하면 도메인 로직이 유스케이스 클래스에 구현되어 있는 것
상황과 필요에 따라서~
굳이 객체 상태를 다룰 필요성이 없는 아주 단순한 도메인이라면 리치 도메인이 없어도 된다. 개인적으론 그렇더라도 디미터 법칙은 지켰줬으면 하는 생각...
출력도 각 유스케이스에 맞게 구체적인이 좋다고 한다. Account를 직접 반환하는게 좋은 경우가 생길 수도 있긴 한데, 이건 계속 고민해야 하는 문제. 의심스러우면 가능한 적게 반환한다.
유스케이스들 간에 같은 출력 모델을 공유하면 유스케이스들도 강하게 결합된다.(단일 책임원칙 위반. 공유모델은 장기적으로 보면 무조건 커지게 되어있다.)
같은 이유로 도메인 엔티티를 출력 모델로 사용하는 것도 지양해야 한다. 도메인 엔티티를 변경해야 할 이유가 늘어나게 된다.
책에서 하는 방법은 인커밍 전용 포트를 만들고 쿼리 서비스에서 구현하는 것.
CQRS 처럼 구현할 수도 있다.
조회 전용은 여러 계층에서 같은 모델을 사용한다면 지름길을 써보는 것을 고려해볼 수도 있다.
입출력 모델을 독립적으로 모델링하면 원치 않는 부수효과를 피할 수 있다.
대신 작업은 늘어난다. 매핑해줘야 하니까.
대신에 장기적으로 보면 유스케이스 이해도를 높일 수 있고 유지보수성도 높아진다. 서로 작업중인 유스케이스를 건드리지 않을 수 있으니.
지속 가능한 코드.
웹 어댑터는 인커밍(주도하는) 어댑터.
웹 어댑터는 애플리케이션 계층에 있는 포트를 호출. 여기도 의존성을 역전시켜놨다.
제어의 흐름 : 컨트롤러 -> 포트 -> 서비스
의존성 방향 : 컨트롤러 -> 포트 <- 서비스
컨트롤러에서 서비스 직접 호출을 막은 이유는 포트를 외부와 통신하는 명세로 사용하기 때문
웹소켓은 인 아웃 포트에 모두 의존하도록 그림을 그려놨다.
웹소켓 컨트롤러 -> 인 포트 <- 서비스
-> 아웃포트 <- 서비스
카프카 스트림즈 앱 구현하면서 난관?에 봉착했던 부분인데, in/out을 겸해야 하는 상황이다. 저렇게하면 순환 의존성은 확실히 피할 수 있을 것 같긴 한데, 스트림즈에 종속적인 코드가 어쩔 수 없이 나와야 하는 부분들은 어떻게 해야할지 아직도 고민이다.
유스케이스 입력모델과는 별개의 유효성 검증을 해야 한다고 한다. 웹 어댑터의 입력 모델을 유스케이스 입력 모델로 변환할 수 있는지를 확인하고, 이걸 방해하는 모든 것이 유효성 검증 에러라고 한다.
HTTP와 관련된 것이 애플리케이션 계층으로 침투하면 안된다. 만약 HTTP가 아닌 다른 방식을 사용한다면 동일한 도메인 로직을 수행할 수 있는 선택지를 잃게 된다.
도메인과 애플리케이션 계층부터 개발하면, 어댑터 보다 유스케이스를 먼저 구현하면 경계가 명확해진다고 한다.
컨트롤러도 너무 적은 것 보다는 너무 많은게 낫다고 한다(넓은 서비스 문제와 동일하게).
같은 리소스라고 하나의 컨트롤러에 묶어놓으면 아무리 메소드를 잘 나눠놓는다 해도 파악이 힘들어진다. 테스트도 비대해진다.
같은 dto 를 공유하게 되는 경우도 생기는데, 이게 진짜 해당 연산에서 필요한 것인지 생각해봐야 한다.
책에서 예시든건, Account와 User 객체가 1:N 관계일때 계좌를 생성하거나 업데이트 할때 User 객체도 필요한지에 관한 것. list 연산에 사용자 정보도 포함 시켜야 하는가?
전용 모델들을 패키지 private으로 선언하면 다른 곳에서 재사용 될 위험에서도 벗어나게 된다.
이렇게 나누면 동시 작업도 수월해진다.
웹 어댑터는 어떠한 도메인 로직도 수행하지 않는다. 애플리케이션 계층은 HTTP와 관련된 작업을 해서는 안 된다.
세분화된 컨트롤러는 처음에 공수가 더 들지만 유지보수하기 훨씬 좋다.
영속성 어댑터는 아웃고잉(주도되는) 어댑터다.
어댑터에서 애플리케이션을 호출하지 않는다. 애플리케이션에게 호출 당한다.
코어의 서비스가 포트를 사용한다. 여기서 포트는 애플리케이션 레이어와 영속성 레이어 사이의 간접적 계층이다. 서비스에서 영속성 계층에 대한 코드 의존성을 없앨 수 있다.
포트 인터페이스에 의해 입력 메세지를 받으면 인터페이스가 지저한 특정 도메인 엔티티나 데이터베이스 연산 전용 객체가 입력 모델이 될 것이다.
영속성 어댑터는 이걸 데이터베이스에 쿼리 날리거나 변경하는데 사용할 수 있는 포멧으로 입력 모델을 매핑한다.
JPA를 사용하면 입력 모델을 JPA 엔티티로 변환시킬 것이다. 그런데 이건 가성비가 떨어질 수 있어서 매핑하지 않는 전략을 선택할 수도 있다.
영속성 어댑터의 입력 모델이 애플리케이션 코어에 존재한다. 따라서 영속성 어댑터 내부를 변경해도 코어에 영향을 주지 않는다.
출력 모델도 애플리케이션 코어에 위치해야 한다.
잘 생각해보면 당연한 얘기인데, 코어에서는 영속성 레이어를 몰라야 하기 때문에 애플리케이션 레이어에 있는걸 가져온 뒤 변환하거나, 좀 느슨하게 만들어서 그게
@Entity
객체라면 바로 사용하거나 하는 식으로 진행 될것이다.
특정 엔티티가 필요로 하는 모든 데이터베이스 연산을 하나의 리포지토리 인터페이스에 넣어두는게 일반적인 방법이다. 그런데 이러면 넓은 포트 인터페이스가 된다. 애플리케이션 레이어 입장에서 필요한 메소드 이외의 메소드도 알게 된다. 이러면 불필요한 의존성이 생긴다.
이러면 코드를 이해하고 테스트하기 힘들어진다. 어떤 메소드가 필요했던건지, 모킹을 어디까지 해야하는지 처음 보는 사람이 보면 모호해진다.
로버트 마틴 : 필요없는 화물을 운반하는 무언가에 의존하고 있으면 예상하지 못했던 문제가 발생할 수 있다.
이걸 해결하는 방법이 인터페이스 분리 원칙(Interface Segregation Principle; ISP)
포트(인터페이스)를 여러개 만들고 영속 어댑터에서 이걸 다 구현하도록 한다. 책에서는 플러그 앤 플레이로 비유한다.
굳이 이렇게까지? 싶긴 하다. 뒤를 보면 알겠지만 어차피 어댑터에서 레포지토리를 다시 호출해야 한다. 그렇지 않은 경우는 효과적일듯
영속성 어댑터는 도메인 클래스(혹은 DDD의 어그리거트) 하나당 하나의 영속성 어댑터를 구현하는 방식이 되어도 된다.
class AccountPersistenceAdapter implements LoadAccountPort, UpdateAccountStatePort {
// ...
}
이렇게 하면 영속성 어댑터들은 도메인 경계를 따라 자동으로 나눠진다. 나중에 여러개의 바운디드 컨텍스트의 영속성 요구사항을 분리하기도 좋아진다.
포트가 여러개면 경우에 따라 영속 레이어 구현 기술을 필요에 따라 선택해 사용하면 된다.
도메인 코드는 영속성 포트의 구현이 어떻게 됐는지에 대해 관심이 없기 때문이다. 포트 구현만 할 수 있으면 영속성 계층에서 하고싶은대로 하면 된다.
updateActivities(Account account)
를 보면 도메인 엔티티인 Account를 그대로 가져온다. 그 뒤 JPA 엔티티로 바꿔준다.
for (Activity activity : account.getActivityWindow().getActivities()) {
if (activity.getId() == null) {
activityRepository.save(accountMapper.mapToJpaEntity(activity));
}
}
이게 불필요하다고 판단해 매핑하지 않기 전략을 취할 수도 있다. 그런데 현재 Account 도메인의 요구사항을 지키려면 기본 생성자가 있으면 안 된다. 특정 기술을 위해 도메인 모델을 타협해야 하는 상황이고 책에서는 풍부한 도메인 모델을 원하기 때문에 매핑을 시켜줬다.
영속성 어댑터는 본인이 어떤 유스케이스에 포함되는지 알지 못한다. 따라서 언제 트랜잭션을 열고 닫을지 결정할 수 없다. 따라서 트랜잭션에 관한 책임은 서비스에 위임해야 한다.
가장 쉬운 방법은 유스케이스를 구현한 서비스 클래스에 @Transactional
을 붙이는 것인데, 이게 싫으면 aop를 이용해 위빙하면 된다.
영속성 어댑터를 플러그인처럼 동작하게 만들면 도메인 코드가 영속성 계층과 분리되어 풍부한 도메인 모델을 만들 수 있게 된다.
좁은 포트 인터페이스를 사용하면 포트 구현이 유연해진다.
시스템 테스트 -> 통합 테스트 -> 단위 테스트 순으로 이루어진다
비용이 적고 유지보수하기 쉽고 빨리 실행되고 안정적인 작은 크기의 테스트들에 대해 높은 커버리지를 유지해야 한다는 것
경계를 넘거나 결합하는 테스트는 만드는 비용이 비싸지고 실행이 느리고 깨지기 쉬워진다.
테스트 비용이 높으면 커버리지 목표를 낮게 잡아야 한다.
단위 테스트
통합 테스트
시스템 테스트
단위테스트는 만들고 이해하는 것도 쉽고 아주 빠르게 실행된다. 비즈니스 규칙을 검증하기에 가장 적절한 방법이다. 도메인 엔티티는 다른 클래스에 거의 의존하지 않기 때문
클래스 모킹 같은 세팅해야 될게 생기면서 가독성이 떨어지니 예제에서는 BDD 패턴으로 작성
테스트 대상이 외부 서버라 모킹을 했을 경우 상호작용 했는지 여부를 검증한다. 그런데 이러면 코드의 행동 변경 뿐만 아니라 구조 변경에도 취약해진다. 리팩토링 하면 테스트도 변경 될 확률이 높아진다.
따라서 모든 동작을 검증하기보단 꼭 필요한 상호작용만 테스트하는게 좋다. 그리고 이런 테스트는 사실상 통합테스트에 가깝다.
예제 코드에서는 MockMvc를 이용해 모킹했기 때문에 실제로 HTTP 프로토콜로 테스트한 것은 아니다. 대신 JSON에서 커맨드 모델 객체로 매핑하는 과정을 검증한다.
또한 기대한 응답을 반환하는지도 확인한다.
단위테스트처럼 보일 수 있지만, 내부적으로 스프링 프레임워크와 관련된 작업이 이루어진다(@WebMvcTest
를 사용했기 때문에). 프레임워크와 강하게 묶여 때문에 프레임워크와 통합된 상태로 테스트하는 것이 합리적이다.
매핑, 유효성 검증, HTTP 입력 검증 등
비슷한 이유로 영속성 어댑터도 통합 테스트로 진행하는게 합리적이다. 데이터베이스 매핑도 검증해야 하기 때문에 @DataJpaTest
를 사용한다.
이 테스트에서는 데이터베이스를 모킹하지 않았는데, 커버리지와 별개로 실제 데이터베이스에서 구문 오류나 매핑 에러 등이 생길 수 있기 때문이다. 인메모리 테스트도 실용적이긴 하지만 같은 문제점이 있다. 테스트컨테이너 같은걸 사용하면 실제 환경과 같은 환경으로 진행할 수 있어 유용하다.
싹다 띄워야 하니까 SpringBootTest
어노테이션 사용한다. TestRestTemplate을 이용해 실제 HTTP 통신을 한다.
실제 영속성 어댑터도 사용한다. 만약 서드파티 시스템을 실행해야 할 경우 해당 출력 포트 인터페이스들을 모킹해준다.
테스트 가독성을 위해 헬퍼 메서드를 만들었는데 이 헬퍼 메서드는 DSL(도메인인 특화 언어)를 형성한다.
적절한 어휘를 사용하면 도메인 전문가가 테스트에 대해 생각하고 피드백을 줄 수도 있다(큐큠버?).
위에서 작성한 통합테스트와 겹치는 부분도 있을 수 있는데, 계층간 매핑 문제 같은 통합테스트만으로 알 수 없는 문제도 알 수 있게 해주기 때문에 의미가 있다.
사용자가 애플리케이션을 사용하면서 거쳐갈 특정 경로로 시나리오를 만들어 커버할 수 있다면 최신 변경 사항들을 배포할 준비가 되었다고 확신할 수 있다.
답하기 어려운 문제
라인 커버리지가 중요한 부분을 커버했다는 것을 알려주지는 않기 때문에 100퍼센트가 아니면 의미가 없을 수 있다.
저자는 마음 편하게 소프트웨어를 배포할 수 있는 것을 기준으로 삼는다고 한다. 그리고 배포 주기가 짧으면 테스트도 자주 돌아가니까 테스트를 더 신뢰할 수 있다고 한다.
따라서 처음에는 신뢰의 도약이 필요하다. 문제가 생기면 수정하고 이걸로 배우는 것을 우선순위로 삼으면 맞는 방향성이다.
테스트가 버그를 잡지 못한 이유를 생각하고 커버할 수 있는 테스트를 추가한다.
아예 테스트를 정의하는 것도 좋다.
테스트가 개발 중에 이뤄진다면 귀찮은 작업이 아닌 개발 도구로 생각해볼 수 있다.
새로운 필드를 추가할때마다 테스트를 고치는데 한 시간을 써야 한다면 뭔가 잘못된 것
테스트가 코드의 구조적 변경에 너무 취약한 것이므로 개선해야 한다. 이러면 테스트로서의 가치를 잃는다.
내 생각에 테스트의 큰 장점 중 하나가 믿음을 가지고 변경이나 리팩토링할 수 있게 해준다는 점인데, 이 장점이 사라지기 때문인 것 같음
육각형 아키텍처는 도메인 로직과 바깥으로 향한 어댑터를 분리해놔서 내부는 단위테스트, 외부는 통합테스트로 명확하게 전략을 정할 수 있다.
입출력 포트는 뚜렷한 모킹 지점이 된다. 모킹할지 사용할지만 정하면 된다. 포트의 크기가 작으면 모킹하기 쉽고 어떤 메소드를 모킹해야할지 더 명확해진다.
모킹하는 것이 버거워지거나 코드의 특정 부분을 커버하기 위해 어떤 종류의 테스트를 사용해야 할지 모호하면 경고 신호다. 이런 측면에서 테스트코드가 카나리의 역할도 한다고 할 수 있다.
매핑 찬성 : 두 계층 간 매핑을 하지 않으면 같은 모델을 사용해야 하기 때문에 두 계층이 강하게 결합됨
매핑 반대 : 보일러 플레이트 코드가 너무 많아지고, 대부분의 유스케이스들은 CRUD만 수행하고 계층에 걸쳐 같은 모델을 사용하기 때문에 과하다
둘 다 맞다. 장단을 따지고 상황에 따라 적합한 방법을 사용해야 한다.
입출력 모델로 도메인 모델을 사용하는 방법
도메인에 영속 계층과 JSON 직렬화를 위한 로직도 포함되므로 단일 책임 원칙을 위반한다.
하지만 간단한 CRUD에는 굳이 매핑을 할 필요가 없다.
모든 계층이 정확히 같은 구조의 같은 정보를 필요로 한다면 매핑하지 않기 전략은 완벽한 선택지다.
대신 애플리케이션 계층이나 도메인 계층에서 웹과 영속성 문제를 다루게 되면 다른 전략을 취해야 한다.
도메인 모델 외에 웹 모델과 영속성 모델을 별도로 둔다. 따라서 도메인 모델이 단일 책임 원칙을 만족한다.
개념적으로 매핑하지 않기 전략 다음으로 간단하다.
단점은 보일러 플레이트 코드가 많이 생긴다는 점. 매핑 프레임워크를 사용해도 시간이 들고 디버깅도 어렵다.
도메인 모델이 계층 경계를 넘어 사용된다(각 포트에서 도메인 모델을 입출력 값으로 사용). 따라서 도메인 모델이 바깥 계층의 변경에 취약해진다.
커맨드 모델 같이 각 계층을 넘나들때도 별도의 모델을 사용하도록 한다.
더 많은 코드가 필요하다. 대신 여러 유스케이스의 요구사항을 함께 다뤄야 하는 매핑에 비해 구현 및 유지보수하기가 쉽다.
여러 유스케이스의 요구사항을 함께 다뤄야 한다 -> 매핑하지 않기나, 양방향은 하나의 도메인 모델이 여러 유스케이스의 입출력에 사용될 것
모든 계층의 모델들이 같은 인터페이스를 구현하도록 한다. 도메인 계층을 바깥으로 전달하고 싶을때도 매핑 없이 할 수 있다. 인터페이스로만 주고 받으면 되니까.
DDD의 팩터리(어떤 특정한 상태로부터 도메인 객체를 재구성할 책임을 가진다) 개념과 잘 어울린다.
계층 간의 모델이 비슷할때 효과적이다. 예를 들어 읽기 전용 연산의 경우
단점은 개념적으로 어렵다.
각 매핑 전략은 저마다 장단을 가지고 있다.
요구사항에 비해 과하게 복자한 전략은 개발을 불필요하게 더디게 만든다. 어떤 매핑 전략도 철칙처럼 여겨져서는 안된다.
필요하다면 섞어쓴다. 특정 전략이 전역 규칙일 필요는 없다.
어떤 매핑 전략을 선택했더라도 나중에 언제든 바꿀 수 있다. 지금은 최선의 전략처럼 보여도 시간이 지나면 아닐 수 있다.
따라서 어떤 상황에서 어떤 매핑전략을 최우선으로 고려해야 하는지에 대한 가이드라인을 정해둬야 한다.
좁은 포트를 사용하면 유스케이스마다 다른 매핑 전략을 사용할 수 있고, 다른 유스케이스에 영향을 미치지 않으면서 코드를 개선할 수 있다.
상황별로 매핑전략을 선택하면 더 어렵고 더 많은 커뮤니케이션을 필요로 할 것이다. 대신 코드가 정확히 해야하는 일만 수행하면서 더 유지보수하기 쉽게 될 것이다.
최상위의 설정 관련된 것. 의존성 관리 프레임워크의 원리에 대해 간단하게 설명해줌. 뒷 내용 빌드업용인 것 같아 메모 X
경계를 강제한다는 것
클린 아키텍처애서는 계층 경계를 넘는 의존성을 항상 안쪽으로 향하게 해야 한다. 이 규칙을 강제하는 것
경계를 강제하기 위해 자바에서 제공하는 가장 기본적인 도구
package-private(default)을 이용한다.
예를 들어, 각 인터페이스만 public으로 두고 그 구현체들은 package-private으로 만든다.
의존성 주입은 보통 리플렉션을 이용하니 package-private이어도 의존성 주입이 가능하다는 점을 이용. 대신 클래스 패스 스캐닝을 이용해야 함(직접 설정할 경우 인스턴스화 하기 위해 public 선언이 필요)
작은 모듈에서 효과적이다. 하지만 클래스가 특정 개수를 넘어가기 시작해 하위 패키지를 사용하게 된다면 불가능한 방법
public 제한자를 사용하면 컴파일 단계에서 의존 방향이 달라졌는지 체크하기 힘들다.
컴파일 후 체크(post-compile check)를 사용할 수 있다. 테스트할때 런타임 체크를 해준다.
ArchUnit 이라는 도구가 있다. JUnit과 같은 단위 테스트 프레임워크 기반에서 잘 동작한다. 의존성 규칙을 위반하면 테스트가 실패한다.
단점은 리팩토링에 취약하다. 항상 코드와 함께 유지보수돼야 한다.
maven이나 gradle 같은 자동화된 빌드 프로세스 이용
빌드 도구의 주요한 기능 중 하나는 의존성 해결. 모든 아티팩트가 사용 가능한지 확인해준다.
각 계층을 모듈로 나누고 각 모듈에서 의존할 수 있는 모듈을 강제하면 컴파일 에러를 발생시킬 수 있다.
어댑터 계층은 서로 의존하는걸 엄격하게 막지는 않지만 대부분의 경우 서로 격리시켜 유지하는 것이 좋다.
영속 계층이 웹 영역에 영향을 미치거나, 반대거나 서드파티에 영향을 받거나 하는 일이 발생할 수 있으므로.
모듈을 세분화 할 수록 의존성을 더 잘 제어할 수 있다. 대신 매핑이 그만큼 많이 필요하다.
빌드 도구로 나눴을때 장점
소프트웨어 아키텍처는 아키텍처 요소간의 의존성을 관리해주는 것.
의존성이 꼬이면 아키텍처 역시 꼬인것
의존성이 올바른 방향으로 가고있는지 확인
세가지 접근 방식을 조합해서 사용할 수도 있음
어떤 지름길이 있는지 알면 우발적으로 사용되는 지름길을 막고, 정당한 지름길이라면 그 효과를 택할 수도 있다.
깨진 창문 실험? 차의 유리창에 깨지니 좋은 동네에서도 행인들이 차를 망가뜨리기 시작했다.
레거시라고 불리는 많은 코드의 품질은 시간이 가면서 심하게 낮아진다.
코드 작성할때도 심리적 영향을 받기 때문에 가능한 지름길을 쓰지 않고 기술 부채를 만들지 않은 채로 깨끗하게 시작하는 것이 중요하다.
만약 프로젝트를 마무리하지 못하고 다른 이들이 인계 받는다면, 인계받는 입장에서는 레거시이기 때문에 깨진 창문을 만들어 내기가 더 쉽다.
비교적 중요하지 않은 부분이거나 프로토 타이핑 작업 중이거나 경제적인 이유가 결부된다면 지름길을 취하는 것이 더 실용 적일 수도 있다.
대신 의도적인 지름길에 대해서는 잘 기록해둬야 한다.
유스케이스간 모델을 공유하는 것은 유스케이스들이 특정 요구사항을 공유할때 괜찮다 -> 특정 세부사항을 변경했을때 두 유스케이스 모두에 영향을 주고 싶은 경우
여러 유스케이스가 같은 모델을 공유한다면 해당 모델은 변경할 이유가 여러개가 된다(SRP 위반).
따라서 두 유스케이스가 서로 독립적으로 진화해야 한다면 똑같은 모양을 사용하더라도 분리해서 시작해야 한다.
도메인 모델에 존재하지 않는 값을 추가하고 싶을때 다른 도메인이나 다른 바운디드 컨텍스트에 포함 시켜야 하는 경우에도 도메인에 값을 추가하게 될 수 있다.
간단한 생성이나 업데이트 유스케이스에서는 괜찮을 수 있다.
도메인 로직을 복잡하게 구현해야 한다면 유스케이스 전용 입출력 모델을 만들어야 한다. 유스케이스의 변경이 도메인 엔티티까지 전파될 수 있기 때문에
인커밍 포트는 의존성 역전에 필수 요소는 아니다.
인커밍 포트를 제거하면 추상화 계층을 줄일 수 있다.
대신 어떤 서비스 메서드를 호출해야 하는지와 같은 세부사항이나 내부 동작에 대해 잘 알아야 한다.
인커밍 포트를 유지하면 아키텍처를 강제할 수 있다. 인커밍 포트만 호출할 수 있도록 강제하면 인커밍 어댑터에서 호출하면 안 되는 메서드를 호출할 일이 없어진다.
도메인 로직이 어댑터에 추가 될 위험이 있다(한방쿼리?).이러면 도메인 로직이 흩어져서 도메인 로직을 찾거나 유지보수 하기 어려워진다.
단순한 전달만 하는 보일러플레이트를 줄일 수 있다. 이를 위해 간단한 CRUD 케이스에서는 애플리케이션 서비스를 건너 뛸 수도 있다.
대신 유스케이스가 단순 CRUD보다 더 많은 일을 해야 한다면 애플리케이션 서비스를 만들어야 한다.
경제적인 관점에서 지름길이 합리적일 때도 있다.
유스케이스가 단순한 CRUD에서 벗어나는 시점이 언제인지에 대해 팀이 합의하는 것이 매우 중요하다.
단순 CRUD 상태에서 벗어나지 않는 유스케이스는 이대로 두는게 더 경제적이다.
어떤 경우든 지름길을 선택한 이유를 기록해두고 나중에 다시 평가할 수 있도록 해야 한다.
외부의 영향을 받지 않고 도메인 코드를 자유롭게 발전시킬 수 있다는 점이 육각형 아키텍처 스타일의 가장 중요한 가치다.
도메인을 중심에 두고 의존성을 역전시키지 않은 구조에서는 DDD를 제대로 할 가능성이 없다. 설계가 항상 다른 요소들에 의해 주도되기 때문에
다르게 말하면 도메인 코드가 애플리케이션에서 가장 중요한 것이 아니라면 이 아키텍처 스타일은 필요하지 않을 것이다.
계층형을 자주 사용했으니 편하다. 육각형 아키텍처도 편해지면 뭐가 더 좋은지 결정내리기 쉬울 것이다.
육각형 아키텍처를 작은 모듈에 적용해보고 편하게 느껴지는 스타일을 찾아보면 다음번 결정에 도움이 될 것이다.
어떤 소프트웨어를 만드느냐에 따라, 도메인 코드의 역할에 따라서도 다르다. 팀의 경험이나 내린 결정이 마음에 드느냐에 따라서도 다르다.