본문 바로가기
Book

[독서 기록] 만들면서 배우는 클린 아키텍처

by Renechoi 2023. 10. 13.
 
클린 아키텍처
우리 모두는 낮은 개발 비용으로 유연하고 적응이 쉬운 소프트웨어 아키텍처를 구축하고자 한다. 그러나 불합리한 기한과 쉬워보이는 지름길은 이러한 아키텍처를 구축하는 것을 매우 어렵게 만든다. 이 책에서는 전통적인 계층형 아키텍처(layered architecture) 스타일과 이 스타일의 단점을 논하는 것부터 시작해, 로버트 마틴(Robert C. Martin)의 클린 아키텍처(clean architecture)와 알리스테어 콕번(Alistair Cockburn)의 육각형 아키텍처(hexagonal architecture)에서 이야기하는 도메인 중심 아키텍처의 장점에 대해 이야기한다. 그러고 나서 실제 코드에서 어떻게 육각형 아키텍처를 구현하는지를 보여주기 위한 실습 단원으로 넘어가, 실습을 통해 육각형 아키텍처의 다양한 계층 간 매핑 전략들을 자세히 알아보고 아키텍처의 요소들을 어떻게 애플리케이션에 녹여낼 것인지 배운다. 이어지는 몇 개의 장에서는 아키텍처 경계를 강제하는 방법에 관해 살펴본다. 또, 어떤 지름길이 어떤 종류의 기술 부채를 만들고, 어떤 경우에 이러한 부채를 기꺼이 질 가치가 있는지 배운다. 이 책을 읽고 나면 육각형 아키텍처 스타일의 애플리케이션을 만드는 데 필요한 모든 지식을 알게 될 것이다.
저자
톰 홈버그
출판
위키북스
출판일
2021.11.26

 

 

정의에 따르면 전통적인 계층형 아키텍처의 토대는 데이터베이스다. 

-> 문제 초래 

- 2p 

 

 

우리가 만드는 대부분의 애플리케이션의 목적이 무엇인지 생각해보자. 우리는 보통 비즈니스를 관장하는 규칙이나 정책을 반영한 모델을 만들어서 사용자가 이러한 규칙과 정책을 더욱 편리하게 활용할 수 있게 한다. 

 

이때 우리는 상태가 아니라 행동을 중심으로 모델링한다. 어떤 애플리케이션이든 상태가 중요한 요소이긴 하지만 행동이 상태를 바꾸는 주체이기 때문에 행동이 비즈니스를 이끌어간다. 

 

-3p 

 

 

ORM에 의해 관리되는 엔티티들은 일반적으로 영속성 계층에 둔다. 계층은 아래 방향으로만 접근 가능하기 때문에 도메인 계층에서는 이러한 엔티티에 접근할 수 있다. ... 하지만 이렇게 되면 영속성 계층과 도메인 계층 사이에 강한 결합이 생긴다. 서비스는 영속성 모델을 비즈니스 모델처럼 사용하게 되고 이로 인해 도메인 로직뿐만 아니라 즉시로딩/지연로딩, 데이터베이스 트랜잭션, 캐시 플러시 등등 영속성 계층과관련된 작업을 해야만 한다.

 

영속성 코드가 사실상 도메인 코드에 녹아들어가서 둘 중 하나만 바꾸는 것이 어려워진다. 이는 유연하고 선택의 폭을 넓혀준다던 계층형 아키텍처의 목표와 정확히 반대되는 상황이다. 

 

- 4p 

 

영속성 계층이 비대해지는 문제 -> 헬퍼, 유틸리티 

- 5p 

 

 

계층간 넘나듦이 많아져서 도메인 로직이 웹 계층에 표출되는 문제, 영속성 계층도 모킹해야 해서 테스트 복잡도가 올라가는 문제 

- 6~ 7p 

 

특정 유즈케이스를 찾기가 어려워짐 

- 8p 

 

 

단일 책임 원칙 

 

"하나의 컴포넌트는 오로지 한 가지 일만 해야 하고, 그것을 올바르게 수행해야 한다." 

 

이는 좋은 조언이지만 단일 책임 원칙의 실제 의도는 아니다. ... 단일 책임 원칙의 실제 정의는 다음과 같다.

 

"컴포넌트를 변경하는 이유는 오직 하나 뿐이어야 한다." 

 

'책임'은 '오로지 한 가지 일만 하는 것'보다는 '변경할 이유'로 해석해야 한다. 

 

- 13p 

 

 

 

의존성 역전 원칙

 

영속성 계층에 대한 도메인 계층의 의존성 때문에 영속성 계층을 변경할 때마다 잠재적으로 도메인 계층도 변경해야 한다. 그러나 도메인 코드는 애플리케이션에서 가장 중요한 코드다. 영속성 코드가 바뀐다고 해서 도메인 코드까지 바꾸고 싶지는 않다 ...

 

"코드상의 어떤 의존성이든 그 방향을 바꿀 수 (역전시킬 수) 있다." 

 

도메인 계층에 인터페이스를 도입함으로써 의존성을 역전시킬 수 있고, 그 덕분에 영속성 계층이 도메인 계층에 의존하게 된다. 

 

 

 

- 17p 

 

 

클린 아키텍처에서 모든 의존성은 도메인 로직을 향해 안쪽 방향으로 향한다(출처: 클린 아키텍처, 2019) 

- 18p 

 

 

가령 영속성 계층에서 ORM 프레임워크를 사용한다고 해보자. 일반적으로 ORM 프레임워크는 데이터베이스 구조 및 객체 필드와 데이터베이스 칼럼의 매핑을 서술한 메타데이터를 담고 있는 엔티티 클래스를 필요로 한다. 도메인 계층은 영속성 계층을 모르기 때문에 도메인 계층에서 사용한 엔티티 클래스를 영속성 계층에서 함께 사용할 수 없고 두 계층에서 각각 엔티티를 만들어야 한다. 즉, 도메인 계층과 영속성 계층이 데이터를 주고받을 때, 두 엔티티를 서로 변환해야 한다는 뜻이다. 이는 도메인 계층과 다른 계층들 사이에서도 마찬가지다.

 

하지만 이것은 바람직한 일이다. 이것이 바로 도메인 코드를 프레임워크에 특화된 문제로부터 해방시키고자 했던, 결합이 제거된 상태다. 가량 Java Persistence API에서는 ORM이 관리하는 엔티티에 인자가 없는 기본 생성자를 추가하도록 강제한다. 이것이 바로 도메인 모델에는 포함해서는 안될 프레임워크에 특화된 결합의 예다. 

 

- 19p 

 

 

클린 아키텍처, 육각형 아키텍처, 혹은 포트와 어댑터 아키텍처 중 무엇으로 불리든 의존성을 역전시켜 도메인 코드가 다른 바깥쪽 코드에 의존하지 않게 함으로써 영속성과 UI에 특화된 모든 문제로부터 도메인 로직의 결합을 제거하고 코드를 변경할 이유의 수를 줄일 수 있다. 그리고 변경할 이유가 적을수록 유지보수성은 더 좋아진다. 

 

-22p 

 

 

 

새 프로젝트에서 가장 먼저 제대로 만들려고 하는 것은 패키지 구조다.

- 23p 

 

 

... 애플리케이션이 어떤 유스케이스들을 제공하는지 파악할 수 없다. AccountService와 AccountController가 어떤 유스케이스를 구현했는지 파악할 수 있겠는가? 특정 기능을 찾기 위해서는 어떤 서비스가 이를 구현했는지 추측행냐 하고, 해당 서비스 내의 어떤 메서드가 그에 대한 책임을 수행하는지 찾아야 한다. 

 

- 25p 

 

 

AccountService의 책임을 좁히기 위해 SendMoneyService로 클래스명을 바꿨다. 이제 '송금하기' 유스케이스를 구현한 코드는 클래스명만으로도 찾을 수 있게 됐다. 애플리케이션의 기능을 코드를 통해 볼 수 있게 만드는 것을 가리켜 로버트 마틴이 '소리치는 아키텍처'라고 명명한 바 있다. 

-26p 

 

 

육각형 아키텍처에서 구조적으로 핵심적인 요소는 엔티티, 유스케이스, 인커밍/아웃고잉 포트, 인커밍/아웃고잉 어댑터다. 이 요소들을 예제 애플리케이션의 아키텍처를 표현하는 패키지 구조로 구성해 보자. 

 

 

-27p 

 

 

 

웹 컨트롤러가 서비스에 의해 구현된 인커밍 포트를 호출한다. 서비스는 어댑터에 의해 구현된 아웃고잉 포트를 호출한다.

-31p 

 

 

 

 

Account  엔티티의 유스케이스 ->

1. 입력을 받는다

2. 비즈니스 규칙 검증

3. 모델 상태 조작

4. 출력 반환

 

서비스는 인커밍 포트인 SendMoneyUseCase를 구현하고, 계좌를 불러오기 위해 아웃고잉 포트 인터페이스인 LoadAccountPort를 호출한다.

 


@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Account {

   /**
    * The unique ID of the account.
    */
   @Getter private final AccountId id;

   /**
    * The baseline balance of the account. This was the balance of the account before the first
    * activity in the activityWindow.
    */
   @Getter private final Money baselineBalance;

   /**
    * The window of latest activities on this account.
    */
   @Getter private final ActivityWindow activityWindow;

   /**
    * Creates an {@link Account} entity without an ID. Use to create a new entity that is not yet
    * persisted.
    */
   public static Account withoutId(
               Money baselineBalance,
               ActivityWindow activityWindow) {
      return new Account(null, baselineBalance, activityWindow);
   }

   /**
    * Creates an {@link Account} entity with an ID. Use to reconstitute a persisted entity.
    */
   public static Account withId(
               AccountId accountId,
               Money baselineBalance,
               ActivityWindow activityWindow) {
      return new Account(accountId, baselineBalance, activityWindow);
   }

   public Optional<AccountId> getId(){
      return Optional.ofNullable(this.id);
   }

   /**
    * Calculates the total balance of the account by adding the activity values to the baseline balance.
    */
   public Money calculateBalance() {
      return Money.add(
            this.baselineBalance,
            this.activityWindow.calculateBalance(this.id));
   }

   /**
    * Tries to withdraw a certain amount of money from this account.
    * If successful, creates a new activity with a negative value.
    * @return true if the withdrawal was successful, false if not.
    */
   public boolean withdraw(Money money, AccountId targetAccountId) {

      if (!mayWithdraw(money)) {
         return false;
      }

      Activity withdrawal = new Activity(
            this.id,
            this.id,
            targetAccountId,
            LocalDateTime.now(),
            money);
      this.activityWindow.addActivity(withdrawal);
      return true;
   }

   private boolean mayWithdraw(Money money) {
      return Money.add(
            this.calculateBalance(),
            money.negate())
            .isPositiveOrZero();
   }

   /**
    * Tries to deposit a certain amount of money to this account.
    * If sucessful, creates a new activity with a positive value.
    * @return true if the deposit was successful, false if not.
    */
   public boolean deposit(Money money, AccountId sourceAccountId) {
      Activity deposit = new Activity(
            this.id,
            sourceAccountId,
            this.id,
            LocalDateTime.now(),
            money);
      this.activityWindow.addActivity(deposit);
      return true;
   }

   @Value
   public static class AccountId {
      private Long value;
   }

}

 

-35~38p

 

입력 유효성 검증 

 

애플리케이션 계층에서 입력 유효성을 검증해야 하는 이유는, 그렇게 하지 않을 경우 애플리케이션 코어의 바깥쪽으로부터 유효하지 않은 입력값을 받게 되고, 모델의 상태를 해칠 수 있기 때문이다. ... 입력 모델이 이 문제를 다루도록 해보자. '송금하기' 유스케이스에서 입력 모델은 예제 코드에서 본 SendMoneyCommand 클래스다. 더 정확히 말하자면 생성자 내에서 입력 유효성을 검증할 것이다.

 

 


@Value
@EqualsAndHashCode(callSuper = false)
public
class SendMoneyCommand extends SelfValidating<SendMoneyCommand> {

    @NotNull
    private final AccountId sourceAccountId;

    @NotNull
    private final AccountId targetAccountId;

    @NotNull
    private final Money money;

    public SendMoneyCommand(
            AccountId sourceAccountId,
            AccountId targetAccountId,
            Money money) {
        this.sourceAccountId = sourceAccountId;
        this.targetAccountId = targetAccountId;
        this.money = money;
        this.validateSelf();
    }
}

 

자바 표준라이브러리를 이용한 validation 

 

 


public abstract class SelfValidating<T> {

  private Validator validator;

  public SelfValidating() {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    validator = factory.getValidator();
  }

  /**
   * Evaluates all Bean Validations on the attributes of this
   * instance.
   */
  protected void validateSelf() {
    Set<ConstraintViolation<T>> violations = validator.validate((T) this);
    if (!violations.isEmpty()) {
      throw new ConstraintViolationException(violations);
    }
  }
}

 

 

SelfValidating 추상 클래스는 validateSelf() 메서드를 제공하며, 생성자 코드의 마지막 문장에서 이 메서드를 호출 -> 애노테이션을 검증하고 위반시 예외 

 

 

-38~41p 

 

 

유스 케이스마다 다른 입력 모델 

 

... '계좌 등록하기'와 '계좌 정보 업데이트하기'라는 두 가지 유스케이스를 보자. 둘 모두 거의 똑같은 계좌 상세 정보가 필요하다. ... 불면 커맨드 객체의 필드에 대해서 null을 유효한 상태로 받아들이는 것은 그 자체로 코드 냄새(code smell)다. 하지만 더 문제가 되는 부분은 이제 입력 유효성을 어떻게 검증하느냐다. 등록 유스케이스와 업데이트 유스케이스는 서로 다른 유효성 검증 로직이 필요하다. ... 

 

각 유스 케이스 전용 입력 모델은 유스케이스를 훨씬 명확하게 만들고 다른 유스케이스와의 결합도 제거해서 불필요한 부수효과가 발생하지 않게 한다. 

 

- 44~45p 

 

 

 

비즈니스 규칙 검증은 어떻게 구현할까? 

 

가장 좋은 방법은 앞에서 "출금 계좌는 초과 인출되어서는 안 된다" 규칙에서처럼 비즈니스 규칙을 도메인 엔티티 안에 넣는 것이다. 

 

public boolean withdraw(Money money, AccountId targetAccountId) {

   if (!mayWithdraw(money)) {
      return false;
   }

   Activity withdrawal = new Activity(
         this.id,
         this.id,
         targetAccountId,
         LocalDateTime.now(),
         money);
   this.activityWindow.addActivity(withdrawal);
   return true;
}

private boolean mayWithdraw(Money money) {
   return Money.add(
         this.calculateBalance(),
         money.negate())
         .isPositiveOrZero();
}

 

- 46~47p

 

 

 

'송금하기' 유스케이스 코드에서는 boolean 값 하나를 반환했다. 이는 이 맥락에서 반환할 수 있는 가장 구체적인 최소한의 값이다. 업데이트 된 Account를 통째로 반환하고 싶을 수도 있다. 아마도 호출자가 계좌의 새로운 잔액에 관심이 있을 지도 모른다.

 

그러나 '송금하기' 유스케이스에서 정말로 이 데이터를 반환해야 할까? 호출자가 정말로 이 값을 필요로 할까? 만약 그렇다면 다른 호출자도 사용할 수 있도록 해당 데이터에 접근할 전용 유스케이스를 만들어야 하지 않을까? 

 

이러한 질문에 정답은 없다. 그러나 유스케이스를 가능한 한 구체적으로 유지하기 위해서는 계속 질문해야 한다. 만약 의심스럽다면 가능한 한 적게 반환하자. 

 

- 49p

 

 

읽기 전용 유스케이스 -> 쿼리를 위한 인커밍 전용 포트를 만들고 이를 쿼리 서비스에 구현하는 것이다. 

 


@RequiredArgsConstructor
class GetAccountBalanceService implements GetAccountBalanceQuery {

   private final LoadAccountPort loadAccountPort;

   @Override
   public Money getAccountBalance(AccountId accountId) {
      return loadAccountPort.loadAccount(accountId, LocalDateTime.now())
            .calculateBalance();
   }
}

 

이처럼 읽기 전용 쿼리는 쓰기가 가능한 유스케이스(또는 커맨드)와 코드 상에서 명확하게 구분된다. 이런 방식은 CQS나 CQRS 같은 개념과 아주 잘 맞는다. 

 

- 50~51p

 

 

 

 

웹 어댑터는 일반적으로 다음과 같은 일을 한다.

1. HTTP 요청을 자바 객체로 매핑

2. 권한 검사

3. 입력 요효성 검증

4. 입력을 유스케이스의 입력 모델로 매핑

5. 유스케이스 호출

6. 유스케이스의 출력을 HTTP로 매핑

7. HTTP 응답을 반환 

 

-56p 

 

 

가급적이면 별도의 패키지 안에 별도의 컨트롤러를 만드는 방식을 선호한다. 


@WebAdapter
@RestController
@RequiredArgsConstructor
class SendMoneyController {

   private final SendMoneyUseCase sendMoneyUseCase;

   @PostMapping(path = "/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}")
   void sendMoney(
         @PathVariable("sourceAccountId") Long sourceAccountId,
         @PathVariable("targetAccountId") Long targetAccountId,
         @PathVariable("amount") Long amount) {

      SendMoneyCommand command = new SendMoneyCommand(
            new AccountId(sourceAccountId),
            new AccountId(targetAccountId),
            Money.of(amount));

      sendMoneyUseCase.sendMoney(command);
   }

}

 

-60p 

 

 

 

 

영속성 계층 대신 애플리케이션 서비스에 영속성 기능을 제공하는 영속성 어댑터 

 

 

포트는 사실상 애플리케이션 서비스와 영속성 코드 사이의 간접적인 계층이다. 영속성 문제에 신경 쓰지 않고 도메인 코드를 개발하기 위해, 즉 영속성 계층에 대한 코드 의존성을 없애기 위해 이러한 간접 계층을 추가하고 있다는 사실을 잊지 말자.

-64 p 

 

핵심은 영속성 어댑터의 입력 모델이 영속성 어댑터 내부에 있는 것이 아니라 애플리케이션 코어에 있기 때문에 영속성 어댑터 내부를 변경하는 것이 코어에 영향을 미치지 않는다는 것이다. 

- 65p 

 

 

ISP 원칙 적용

 

 

SendMoneyService, RegisterAccountService -> LoadAccountPort, UpdateAccountStatePort, CreateAccountPort <- 영속성 어댑터 

 


@RequiredArgsConstructor
@UseCase
@Transactional
public class SendMoneyService implements SendMoneyUseCase {

   private final LoadAccountPort loadAccountPort;
   private final AccountLock accountLock;
   private final UpdateAccountStatePort updateAccountStatePort;
   private final MoneyTransferProperties moneyTransferProperties;

 

public interface LoadAccountPort {

   Account loadAccount(AccountId accountId, LocalDateTime baselineDate);
}

 

 

public interface UpdateAccountStatePort {

   void updateActivities(Account account);

}

이렇게 매우 좁은 포트를 만드는 것은 코딩을 플러그 앤드 플레이(plug-and-play) 경험으로 만든다. 서비스 코드를 짤 때는 필요한 포트에 그저 '꽂기만'하면 된다. 운반할 다른 화물이 없는 것이다. 

 

- 68p 

 

 

 

 

각 바운디드 컨텍스트는 영속성 어댑터를 하나씩 가지고 있다. 바운디드 컨텍스트라는 표현은 경계를 암시한다. account 맥락의 서비스가 billing 맥락의 영속성 어댑터에 접근하지 않고, 반대로 billing 서비스도 account 영속성 어댑터에 접근하지 않는다는 의미다. 어떤 맥락이 다른 맥락에 있는 무엇인가를 필요로 한다면 전용 인커밍 포트를 통해 접근해야 한다.

-70p 

 

 

 

속성 기능을 제공하는 영속성 어댑터 

 


@RequiredArgsConstructor
@PersistenceAdapter
class AccountPersistenceAdapter implements LoadAccountPort, UpdateAccountStatePort {

   private final SpringDataAccountRepository accountRepository;
   private final ActivityRepository activityRepository;
   private final AccountMapper accountMapper;

   @Override
   public Account loadAccount(AccountId accountId, LocalDateTime baselineDate) {

      AccountJpaEntity account = accountRepository.findById(accountId.getValue()).orElseThrow(EntityNotFoundException::new);

      List<ActivityJpaEntity> activities = activityRepository.findByOwnerSince(accountId.getValue(), baselineDate);

      Long withdrawalBalance = orZero(activityRepository.getWithdrawalBalanceUntil(accountId.getValue(), baselineDate));

      Long depositBalance = orZero(activityRepository.getDepositBalanceUntil(accountId.getValue(), baselineDate));

      return accountMapper.mapToDomainEntity(account, activities, withdrawalBalance, depositBalance);

   }

   private Long orZero(Long value) {
      return value == null ? 0L : value;
   }

   @Override
   public void updateActivities(Account account) {
      for (Activity activity : account.getActivityWindow().getActivities()) {
         if (activity.getId() == null) {
            activityRepository.save(accountMapper.mapToJpaEntity(activity));
         }
      }
   }

}

 

 

-74~75p

 

트랜잭션은 하나의 유스케이스에 대해서 일어나는 모든 쓰기 작업에 걸쳐 있어야 한다. 그래야 그중 하나라도 실패할 경우 다같이 롤백될 수 있기 때문이다.  ... 이 책임은 영속성 어댑터 호출을 관장하는 서비스에 위임해야 한다.

- 78p 

 

 

 

 

도메인 코드에 플러그인처럼 동작하는 영속성 어댑터를 만들면 도메인 코드가 영속성과 관련된 것들로부터 분리되어 풍부한 도메인 모델을 만들 수 있다.

 

좁은 포트 인터페이스를 사용하면 포트마다 다른 방식으로 구현할 수 있는 유연함이 생긴다. 심지어 포트 뒤에서 애플리케이션 모르게 다른 영속성 기술을 사용할 수도 있다. 포트의 명세만 지켜진다면 영속성 계층 전체를 교체할 수도 있다.

 

-79p 

 

다음은 육각형 아키텍처에서 사용하는 전략이다.

- 도메인 엔티티를 구현할 때는 단위 테스트로 커버하자

- 유스케이스를 구현할 때는 단위 테스트로 커버하자

- 어댑터를 구현할 때는 통합 테스트로 커버하자

- 사용자가 취할 수 있는 중요 애플리케이션 경로는 시스템 테스트로 커버하자

 

-96p 

 

 

 

경계간 매핑하기 

 

매핑에 찬성하는 개발자: 두 계층간에 매핑을 하지 않으면 양 계층에서 같은 모델을 사용해야 하는데 이렇게 하면 두 계층이 강하게 결합됩니다. 

매핑에 반대하는 개발자: 하지만 두 계층 간에 매핑을 하게 되면 보일러플레이트 코드를 너무 많이 만들게 돼요. 많은 유스케이스들이 오직 CRUD만 수행하고 계층에 걸쳐 같은 모델을 사용하기 때문에 계층 사이의 매핑은 과합니다.

-97p 

 

 

 

매팡하지 않기 전략 -> 

 

 

Account 클래스는 웹, 애플리케이션, 영속성 계층과 관련된 이유로 인해 변경돼야 하기 때문에 단일 책임 원칙을 위반한다.

 

양방향 매핑 전략 -> 

-> 깨끗한 도메인 모델 

 

단점 1: 너무 많은 보일러플레이트 코드

단점 2: 도메인 모델이 계층 경계를 넘어서 통신하는 데 사용되고 있다는 것이다. 

 

- 101p 

 

 

완전 매핑 전략 

-> 각 연산마다 별도의 입출력 모델을 사용 -> 계층 경계를 넘어 통신할 때 도메인 모델을 사용하는 대신 SendMoneyUseCase 포트의 입력 모델로 동작하는 SendMoneyCommand 처럼 각 작업에 특화된 모델을 사용한다. 이런 모델을 가리켜 커맨드, 요청 혹은 이와 비슷한 단어로 표현한다.

-102p 

 

 

단방향 매핑 전략

-> 동일한 '상태' 인터페이스를 구현하는 도메인 모델과 어댑터 모델을 이용하면 각 계층은 다른 계층으로부터 온 객체를 단뱡향으로 매핑하기만 하면 된다. 

->팩터리 DDD 개념과 잘 어울림

-> 이 전략에서 매핑 책임은 명확하다. 만약 한 계층이 다른 계층으로부터 객체를 받으면 해당 계층에서 이용할 수 있도록 다른 무언가로 매핑하는 것이다. 그러므로 각 계층은 한 방향으로만 매핑한다. 그래서 이 전략의 이름이 단방향 매핑 전략인 것이다.

- 103~104p

 

 

언제 어떤 매핑 전략을 사용할 것인가? -> 그때 그때 다르다.

-105p 

 

 

가이드라인은 아마 다음과 같을 것이다.

 

변경 유스케이스를 작업하고 있다면 웹 계층과 애플리케이션 계층 사이에서는 유스케이스 간의 결합을 제거하기 위해 '완전 매핑' 전략을 첫 번째 선택지로 택해야 한다. 이렇게 하면 유스케이스별 유효성 검증 규칙이 명확해지고 특정 유스케이스에서 필요하지 않은 필드를 다루지 않아도 된다. 

 

변경 유스케이스를 작업하고 있다면 애플리케이션과 영속성 계층사이에서는 매핑 오버헤드를 줄이고 빠르게 코드를 짜기 위해서 '매핑하지 않기' 전략을 첫 번째 선택지로 둔다. 하지만 애플리케이션 계층에서 영속성 문제를 다뤄야 하게 되면 '양방향 ' 매핑 전략을 바꿔서 영속성 문제를 영속성 계층에 가둘 수 있게 한다. 

 

- 105~106p 

 

 

 

설정 컴포넌트 -> 중립적인 설정 컴포넌트는 인스턴스 생성을 위해 모든 클래스에 접근할 수 있다. 

-108p 

 

 

 

아키텍처 경계 

-120p

 

 

깨진 창문 이론 ->

- 품질이 떨어진 코드에서 작업할 때 더 낮은 품질의 코드를 추가하기가 쉽다.

- 코딩 규칙을 많이 어긴 코드에서 작업할 때 또 다른 규칙을 어기기도 쉽다.

- 지름길을 많이 사용한 코드에서 작업할 때 또 다른 지름길을 추가하기도 쉽다. 

 

- 134p 

 

 

 

지름길 1: 유스케이스 간 모델 공유 -> 하나의 Command로 여러 유스케이스 사이 결합 증대 

지름길 2: 도메인 엔티티를 입출력 모델로 사용

지름길 3: 인커밍 포트 건너뛰기 

지름길 4: 애플리케이션 서비스 건너뛰기 

-135~142p 

 

 

 

외부의 영향을 받지 않고 도메인 코드를 자유롭게 발전시킬 수 있다는 것은 육각형 아키텍처 스타일이 내세우는 가장 중요한 기치다.

-143p 

 

반응형