21년 6월의 책 - Get Your Hands Dirty On Clean Architecture

Gummybearr·2021년 6월 4일
1

이달의 책

목록 보기
3/30

추천을 받은 책이다. 회사 분들과의 어마어마한 실력 차이를 줄이는 데에 도움이 됐으면 좋겠다. 소중한 기회를 얻게 된 것에 감사하며 열심히 성장할 것이다🧑🏻‍💻

1장

layered architecture의 구성: web -> domain -> persistence
It promotes Database-Driven Design
중요한 것은 domain인데 디자인은 database를 기반으로 하게 됨

orm쓰면 domain의 변화가 database의 변화를 야기함
컨트롤러에서 바로 엔티티, 즉 persistence layer를 호출해서 로직을 쌓는 경우도 생기는데, 이런 식으로
persistence layer에 로직을 넣는 생기면 유지보수도 어렵고 테스트하기도 어려운 코드가 만들어진다

뚱뚱한 service layer가 만들어지는 경우도 있는데, 그러다보면 어떤 서비스에 어떤 use case가 있는지도 확인하기 어려워진다
뚱뚱한 service는 협업하기 어려운 코드가 되기도 한다

2장

SRP를 다시 정의하면 A Component should have only one reason to change이다

layer A가 B를 부르고, B가 C를 부른다면, C의 변화는 B를, B의 변화는 A에 영향을 줄 수 밖에 없을텐데, 이런 의존성을 어떻게 개선할 수 있을까?

해답은 DIP이다. 인터페이스를 정의해두고 구현체를 외부에서 연동해주는 식으로 의존성을 역전시킬 수 있다.

로버트 마틴은 이를 활용하여 클린 아키텍쳐(aka hexagonal architecture)를 주장했다.
Web, UI, DB, Device와 같은 External Interfaces가 제일 밖,
Gateway, Presenters, Controller가 그 안,
UseCase가 그 안
Entity가 가장 안에 존재하는데, 이런식으로 Core가 안에 들어있고 안으로 갈수록 의존성이 없는 코드가 된다

3장

코드를 layered 하게 작성하면 이런 형태가 된다

buckpal
-- domain
---- account
---- activity
---- accountRepository
---- accountService
--persistence
----accountRepositoryImpl
--web
----accountController

이렇게 하면 layered architecture의 단점을 개선할 수 없고 incoming and outgoing port가 보이지 않는다

좀 더 개선해서 feature별로 작성하는 방법도 있다

buckpal
-- account
---- account
---- accountController
---- accountRepository
---- accountRepositoryImpl
---- sendMoneyService

이렇게 하면 layered architecture의 의존성을 조금 개선할 수 있을 지는 몰라도 여전히 incoming and outgoing port는 드러나지 않는다

clean architecture에서는 이렇게 사용한다

buckpal
-- account
---- adapter
------ in
-------- web
---------- accountController
------ out
-------- persistence
---------- accountPersistenceAdapter
---------- springDataAccountRepository
---- domain
------ account
------ activity
---- application
------ sendMoneyService
------ port
-------- in
---------- sendMoneyUseCase
-------- out
---------- loadAccountPort
---------- updateAccountStatePort

어댑터가 incoming usecase를 부르고, usecase를 service가 구현하고 service가 outgoing port를 부르는데, outgoing port를 구현한 adapter에서 persistence layer동작을 한다
The web controller calls an incoming port, which is implemented by a service. The service calls an outgoing port, which is implemented by an adapter

4장

usecase의 역할
1. takes input
2. validates business rules
3. manipulates the model state
4. returns output

business rule을 validate할 때도 룰이 있는데, core에 dependency가 없는 코드를 검증하는 것이다.
core에 dependency가 있는 거는 안쪽에서 검증

usecase가 다르면 input, output model 또한 다른거를 사용해야 함

5장

An incoming adapter talks to the application layer through dedicated incoming ports, which are interfaces implemented by the application services
incoming port로 요청을 보내고 incoming port는 service가 구현함
outgoing port로 요청이 리턴되는데 web adapter가 이를 구현함

web adapter의 책임
1. maps http requests to java objects
2. performs authorization checks
3. validates input
4. maps input to the input model of the use case
5. calls the use case
6. maps the output of the use case back to http
7. returns an http response

web adapter도 뚱뚱하지 않게 유지해주는 것이 좋고 그렇게 하려면 usecase를 잘게 쪼개야 한다

6장

The services from the core use ports to access the persistence adapter
outgoing port가 있고 persistence adapter가 이를 구현

persistence adapter의 책임
1. takes input
2. maps input into a database format
3. sends input to the database
4. maps database output into an application format
5. returns output

이전에서 말한 것과 같은 논리로 port를 잘게 쪼개고 adapter 또한 잘게 쪼개는 것이 좋다

7장

유닛 테스트 - usecase를 검증, mockito같은거로 목 객체 만들어서 쓰기도 함
통합 테스트 - webadapter를 검증, 여전히 mock을 쓰기는 하지만 controller 응답이나 testcontainer를 써서 db와 같은 어댑터 영역을 테스트
시스템 테스트 - 사용자가 도달할 수 있는 main path를 검증, 다 띄워서 한다고 보면 됨

테스트할 때에는 수치보다는 중요한 path를 테스트하는게 더 의미있음

8장

layer 간에 mapper를 사용해야 하는가?

  1. no mapping strategy - 매퍼 사용 안함. 도메인이 input output model로 공유됨, 다른 layer의 변화가 domain의 변화를 야기하기도 하고 domain을 해석하기 위해 다른 layer에 어노테이션을 붙이는 작업이 필요할 수 있음 -> SRP 위반. 처음에는 괜찮아보일 수 있지만 장기적으로는 좋지 않은 전략

  2. two-way mapping strategy - web adapter랑 db adapter에서 모델을 반들어서 변환해서 사용함. 구조상 분리가 된다는 점에서 조금 더 깔끔함. 하지만 boilerplate code가 늘어나고 도메인 로직이 여러 layer에서 사용되는 건 마찬가지임

  3. full mapping strategy - adapter영역말고 port나 usecase단에서도 매핑을 시켜줌. 구조상 더 분리가 잘 되는 구조이지만 boilerplate코드가 증가. 좀 과한 감이 있기도 함

  4. one-way mapping strategy - interface로 쓸 수 있는 모델을 서비스 레이어쪽에 하나 만들어두고 이거를 web, db adapter가 사용하도록 하는 방식. 의존성이 클린 아키텍쳐가 의도한대로 코어쪽으로 가기 때문에 좋은 구조이고 ddd창시자인 Eric Evans의 견해랑도 맞는 구조이지만 잘 사용하기에는 난이도가 있는편

정답은 없고 상황에 따라 잘 판단해서 쓰는 게 모범답안

9장

만들었으면 조립해야 되고, 조립에도 방법이 여러 가지 존재
클린 아키텍쳐의 핵심 중 하나는 의존성을 역전시키는거고 의존성을 역전시켜주기 위해서는 어디선가 configuration으로 의존성을 지정해줘야 하는데, 그렇게 하는 방법이 여러가지 존재

configuration component의 역할
1. create web adapter instances
2. ensure that HTTP requests are actually routed to the web adapters
3. create use case instances
4. provide web adapters with use case instances
5. create persistence adapter instances
6. provide use cases with persistence adapter instances
7. ensure that the persistence adapters can actually access the database

역할이 좀 많긴 하지만, 어디선가는 필요한 작업. 따라서 이 파트에서만 SRP를 깨고 다른 파드들에서는 SRP를 지키는 방향을 선택

  1. plain code로 조립 - 가장 기본적임, 하지만 코드의 양이 좀 많아서 엔터프라이즈급 서비스에는 어울리지 않을 수 있다. 그리고 모든 클래스가 public해야 되기 때문에 깨진창문의 시작점이 될 수 있음
  2. classpath scanning으로 조립 - 편하긴 하지만 프레임워크 의존성이 생김. 그러다보니 프레임워크에 익숙하지 않으면 의도하지 않은 오류와 마주할 수도 있음
  3. java config로 조립 - 생성의 책임을 configuration으로 돌려줌. 하지만 visibility를 제한하기 위해 sub-package을 쓰는 경우에는 이 방법이 통하지 않을 수 있음
profile
버그가 아니고 기능입니다만

1개의 댓글

comment-user-thumbnail
2021년 12월 16일

좋은 책이고 재미있게 읽었습니다. 번역서가 잘 번역되었으나 일부 오역이나 누락된 부분이 있어 원서를 같이 찾아봤었네요. 추천합니다.

답글 달기