DDD

DDD vs Transaction Script

inuma 2021. 4. 8. 04:51

DDD와 Transaction Script를 비교한 글을 발견?하여 정리하는 용도로 글을 작성해보았다.

 

Application Design 방식

  • DDD 방식과 Transaction Script 방식이 존재
  • 두 방식 모두 잘못된 방식은 아니며,
    두 방식중 어느 방식을 사용할지는 비즈니스 로직의 복잡성과 연관이 있다.

    (단순 query를 하고 조회를 하는 작업에서 도메인 모델을 사용하는 것은 불필요하게 복잡하다.
    이 경우, domain layer를 제거하여 infrastructure layer를 직접 호출 하는 것이 좋다.)

    (over-engineering의 위험 부담 때문에,
    복잡한 비즈니스 로직을 해결하기 위해서 transaction script 방식을 사용하는 것은 좋지 않다.
    transaction script 방식의 특징으로는 복잡한 시나리오를 처리할 때,
    코드를 엉망으로 만드는 경향이 있어서 어플리케이션이 개발됨에 따라서 복잡성이 기하 급수적으로 증가하게 된다.)

 

Domain Modeling의 장점 이해하기

다음과 같은 비즈니스 로직이 있다고 가정하고 transaction script 방식과 domain modeling 방식을 비교해보겠다.

  • 비즈니스 로직
    • 서로 다른 계좌간의 송금
  • 비즈니스 규칙
    • 각 계좌마다 초과 인출 정책이 존재한다.
      • 허용(ALLOWED)
        • 송금시, 계좌 잔고를 넘는 송금 요청에 대해서, limit 금액을 넘지 않는 선에서 인출을 허용한다.
      • 비허용(NEVER)
        • 송금시, 계좌 잔고를 넘는 송금 요청에 대해서 인출을 허용하지 않는다.
    • 돈을 송금하려는 계좌의 초과 인출 정책(overdraft policy)에 따라서 계좌의 잔고를 넘는 송금 요청을 처리한다.

 

Transaction Script 방식

@Service
@RequiredArgsConstructor
public class MoneyTransferServiceImpl implements MoneyTransferService {
  private AccountDao accountDao;
  private BankingTransactionRepository bankingTransactionRepository;
  
  @Override
  public BankingTransaction transfer(String fromAccountId, String toAccountId, double mount){
    Account fromAccount = accountDao.findById(fromAccountId);
    Account toAccount = accountDao.findById(toAccountId);
    
    double newBalance = fromAccount.getBalance() - amount;
    
    switch (fromAccount.getOverdraftPolicy()) {
      case OverdraftPolicy.NEVER:
        if(newBalance < 0){
          throw new DebitException("Insufficient funds");
        }
        break;
      default: //OverdraftPolicy.ALLOWED
        if(newBalance < -limit){
          throw new DebitException("Overdraft limit(of " + limit + ") exceeded : " + newBalance);
        }
        break;
    }
    
    fromAccount.setBalance(newBalance);
    
    toAccount.setBalance(toAccount.getBalance() + newBalance);
    
    BankingTransaction moneyTransferTransaction = new MoneyTranferTransaction(fromAccountId, toAccountId, amount);
    bankingTransactionRepository.addTransaction(moneyTransferTransaction);
    
    return moneyTransferTransaction;
  }
}
public class Account {
  private String id;
  private double balance;
  private OverdraftPolicy overdraftPolicy;
  
  public String getId() { return id; }
  public void setId(String id) { this.id = id; }
  public double getBalance() { return balance; }
  public void setBalance(double balance) { this.balance = balance; }
  public OverdraftPolicy getOverdraftPolicy() { return overdraftPolicy; }
  public void setOverdraftPolicy(OverdraftPolicy overdraftPolicy) {
    this.overdraftPolicy = overdraftPolicy;
  }
}
public enum OverdraftPolicy {
  NEVER, ALLOWED
}

 

  • Account 모델에는 단순한 getter, setter의 메소드만 가지게 된다.
  • Transaction Script 방식에서는
    비즈니스 로직을 각 모델을 찾아다니며 찾아보지 않아도 service 코드에서 볼 수 있지만,
    비즈니스 로직이 추가가 된다면 그 때마다 service 코드는 점점 더 비대하게 되고,
    점차 분석에도 오랜 시간이 걸리게 될 것이다.

 

Domain Modeling 방식

@Service
@RequiredArgsConstructor
public class MoneyTransferServiceDomainModelImpl implements MoneyTransferService {
  private AccountRepository accountRepository;
  private BankingTransactionRepository bankingTransactionRepository;
  
  @Override
  public BankingTransaction transfer(
      String fromAccountId, String toAccountId, double amount) {
    Account fromAccount = accountRepository.findById(fromAccountId);
    Account toAccount = accountRepository.findById(toAccountId);
    
    fromAccount.debit(amount);
    toAccount.credit(amount);
    
    BankingTransaction moneyTransferTransaction = new MoneyTranferTransaction(fromAccountId, toAccountId, amount);
    bankingTransactionRepository.addTransaction(moneyTransferTransaction);
    
    return moneyTransferTransaction;
  }
}
@Entity
public class Account {
  @Id
  private String id;
  private double balance;
  private OverdraftPolicy overdraftPolicy;
  
  public double balance() { return balance; }
  public void debit(double amount) {
    this.overdraftPolicy.preDebit(this, amount);
    
    this.balance = this.balance - amount;
    
    this.overdraftPolicy.postDebit(this, amount);
  }
  public void credit(double amount) {
    this.balance = this.balance + amount;
  }
}
public interface OverdraftPolicy {
  void preDebit(Account account, double amount);
  void postDebit(Account account, double amount);
}
public class NoOverdraftAllowed implements OverdraftPolicy {
  public void preDebit(Account account, double amount) {
    double newBalance = account.balance() - amount;
    if (newBalance < 0) {
      throw new DebitException("Insufficient funds");
    }
  }
  public void postDebit(Account account, double amount) {
  }
}
public class LimitedOverdraft implements OverdraftPolicy {
  private double limit;

  public void preDebit(Account account, double amount) {
    double newBalance = account.balance() - amount;
    
    if (newBalance < -limit) {
      throw new DebitException("Overdraft limit (of " + limit + ") exceeded: " + newBalance);
    }
  }
  public void postDebit(Account account, double amount) {
  }
}

 

  • Account 모델은 행동과 도메인 로직을 포함하고 있다.
    (debit과 credit 메소드에서 해당 객체에 지정되어 있는
    overdraftPolicy에 따라서 초과 인출 로직을 제어하고 있다.)
  • Domain Modeling 방식에서는 각 모델을 찾아가며 비즈니스 로직을 확인해야 하는 번거로움이 있겠지만
    모델만 보고서 해당 도메인 로직을 확인할 수 있다는 장점이 있다.

 

비교

  • 원문에서는 Eclipse Metrics Plugin을 사용하여 각 방식으로 작성된 코드를 수치로 비교해둔 내용이 있다.
  Transaction Script Domain Model
Metric Maximum Maximum
McCabe Cyclomatic Complexity 5 2
Number of Classes 4 6
Method Lines of Code 25 9
  Total Total
Total Lines of Code 82 96

 

결론

이 글의 취지로는 Transaction Script 방식이 나쁘고 Domain Model 방식이 좋기 때문에 Domain Model 방식을 사용하자는 것은 아니다.

Transaction Script 방식에는 쉽게 구현이 가능하다는 장점이 있지만 복잡성이 증가하게 되는 경우, 유지 보수가 어려워 지게 된다는 단점이 있다.

Domain Model 방식은 좀더 많은 Object Oriented Design Skill이 필요하지만, 유지 보수의 측면에서는 수월해질 수 있다는 장점이 있다.

그렇기 때문에 원문에서는 Transaction Script 방식으로 시작하고 복잡성이 증가되는 경우, Domain Model 방식으로의 변경을 고려해볼것을 추천하고 있다.