본문 바로가기

Design/DDD

[DDD 첫걸음] 2-1. 전술적 설계 - 간단한 비즈니스 로직 구현.

 

비즈니스 로직은 소프트웨어에서 가장 중요한 부분이며 애초에 소프트웨어를 구현하는 이유이기도 하다.

  • 모든 비즈니스 하위 도메인을 동일하게 만들지 않는다.
  • 하위 도메인마다 전략적 중요성과 복잡한 정도가 다르다.
  • 다소 단순한 비즈니스 로직에 적합한 두 가지 패턴인 트랜잭션 스크립트액티브 레코드를 공부해본다.

 

트랜잭션 스크립트

 

 

프레젠테이션으로부터 단일 요청을 처리하는 여러 프로시저를 모아서 비즈니스 로직을 구현하라.
- 마틴 파울러 -

 

구현

  • 각 프로시저는 간단하고 쉬운 절차지향 스크립트(precedural script)로 구현.
  • 저장 장치와 연동하기 위해 얇은 추상화 계층을 사용할 수 있지만 DB 직접 접근도 가능.
  • 각 작업은 성공하거나 실패할 수 있지만, 유효하지 않은 상태를 만들면 안된다!
    - 트랜잭션 스크립트 실행이 실패하더라도 시스템은 오류가 발생할 때까지 변경사항을 롤백하거나 보상 조치를 실행하여 일관성을 유지해야함.
  • 패턴의 이름처럼 트랜잭션 스크립트에 트랜잭션 동작이 반영됨.

트랜잭션 스크립트 패턴의 중요성

  • 트랜잭션 스크립트 패턴은 겉보기에 단순하나 가장 틀리기 쉬운 패턴.
  • 트랜잭션 스크립트 패턴이 이후에 배우게 될 고급 비즈니스 로직 구현 패턴의 기반이된다.

트랜잭션 동작구현 실패

  • 예를 들어 한 데이터의 상태를 변경하고 로그를 적재하는 케이스의 경우.
    상태 변경 후, 네트워크 유실 혹은 데이터베이스 시간초과 등으로 장애가 생길 경우, 데이터 상태와, 로그의 일관성이 흐트러짐.
    => 해결책으로 두 트랜잭션을 하나로 묶은 후, 두 트랜잭션이 모두 완료되면 커밋이 되게끔할 수 있음.
  • 다중 레코드 트랜잭션을 지원하지 않는 데이터베이스에서 다중 업데이트를 하거나, 분산 트랜잭션에서 통합할 수 없는 여러 개의 저장 장치로 작업하는 경우에는 상황이 더 복잡해짐.

분산 트랜잭션

  • 최신 분산 시스템에서는 데이터베이스의 데이터를 변경한 다음, 메시지 버스에 메시지를 발행하여 시스템의 다른 컴포넌트에 변경사항을 알리는 것이 일반적.
  • 예를 들어 한 데이터의 상태를 변경하고 이 변경사항을 다른 컴포넌트의 메세지 버스에 메시지를 발행할 경우.
    다른 컴포넌트의 메시지 버스에 메시지를 발행하는 것에 실패할 경우, 변경사항을 다른 컴포넌트가 알지 못하게됨.
  • CQRS를 통해 여러 저장 장치를 다루는 법을 배울 수 있음. (이후 학습예정)
  • 아웃박스 패턴을 통해 서로 다른 DB에 변경사항을 커밋한 후, 안정적인 메시지 발행을 가능하게 할 수 있음. (이후 학습예정)

암시적 분산 트랜잭션

  • 클라이언트 <-> 시스템 <-> Database 의 구조는 각 요소 사이의 네트워크 혹은 시스템 장애로 데이터의 일관성이 깨질 수 있는 위험이 있는 분산 트랜잭션을 구성한다.
  • 해결책1. 멱등성으로 작업을 구성할 것.
    같은 요청을 여러 번 반복하더라도 그 결과는 매번 동일하게 만드는 것.
    > 예시에서 count update시 DB에서 count = count+1을 하는 방법 보다는, 현재 count값을 조회하고 시스템에서 count를 올린 후, DB에 업데이트를 하는 방식.
    (Update users set visit = @p1 where user_id = @p2) - p1이 시스템에서 현재의 count를 올린 값.
  • 해결책2. 낙관적 동시성 제어. (Optimistic concurrency control)
    > 작업 호출전 현재의 count를 읽고, update를 할 때, 처음 읽은 값과 동일한 경우에만 카운터를 업데이트.
    (Update users set visit = visit+1 where user_id = @p1 and visits = @p2) - p2가 처음 읽은 count값.

 

트랜잭션 스크립트를 사용하는 경우

  • 트랜잭션 스크립트 패턴은 비즈니스 로직이 단순한 절차적 작업처럼 매우 간단한 문제 도메인에 효과적. (예. 데이터 ETL작업)
  • 트랜잭션 스크립트 패턴은 정의상 비즈니스 로직이 단순한 지원 하위 도메인에 적합.
  • 일반 하위 도메인과 같은 외부 시스템과 연동하기 위한 어댑터 혹은 충돌 방지 계층의 일부로 사용가능.
  • 비즈니스 로직이 복잡할수록 트랜잭션 간에 비즈니스 로직이 중복되기 쉽고, 결과적으로 중복된 코드가 동기화되지 않을 때 일관성 없는 동작이 발생.
  • 결과적으로 핵심 하위 도메인에는 트랜잭션 스크립트를 사용해서는 안됨.
    핵심 하위 도메인의 비즈니스 로직이 복잡한 경우 트랜잭션 스크립트 패턴이 대처할 수 없다는 문제점이 발생할 수 있음.
  • 이러한 단순함으로 인해, 트랜잭션 스크립트는 항상 좋다고 할 수 없으며, 때로는 안티패턴으로 취급하기도 함.
    (트랜잭션 스크립트로 복잡한 로직을 구현한다면 머지않아 유지보수가 불가능한 커다란 진흙덩어리가 될 것.)

액티브 레코드

 

데이터베이스 테이블 또는 뷰의 행을 감싸고 데이터베이스 접근을 캡슐화하고
해당 데이터에 도메인 로직을 추가하는 오브젝트.

- 마틴 파울러 -

  • 액티브 레코드는 비즈니스 로직이 단순한 경우 사용.
  • 하지만 좀 더 복잡한 자료구조에서도 비즈니스 로직이 작동할 수 있음.

구현

  • 이 패턴은 액티브 레코드라고 하는 전용 객체를 사용하여 복잡한 자료구조를 표현.
  • 자료구조 외에도 이러한 객체는 레코드 생성, 읽기, 업데이트, 삭제를 위한 데이터 접근 방법(소위 CRUD 작업)도 구현함.
    그 결과 액티브 레코드 객체는 ORM(Object-relational Mapping) 또는 다른 데이터 접근 프레임워크와도 관련이 있음.
  • 액티브 레코드는 트랜잭션 스크립트로 시스템의 비즈니스 로직을 만듦.
  • 두 패턴의 차이점은 액티브 레코드의 경우, 데이터베이스에 직접 접근하는 대신 트랜잭션 스크립트가 액티브 레코드 객체를 조작한다는 것.
  • 작업이 완료되면 트랜잭션의 원자성(atomic)으로 인해 작업이 성공하거나 실패함.
  • 이 패턴의 목적은 메모리 상의 객체를 DB 스키마에 매핑하는 복잡성을 숨기는데 있음.
  • 영속성을 담당하는 것 외에도 액티브 레코드 객체에는 비즈니스 로직이 포함될 수 있음.
    > 각 필드에 할당된 새 값에 대한 유효성 검사 혹은 데이터 조작하는 비즈니스 관련 절차 구현 등.
  • 즉 액티브 레코드 객체의 고유한 기능은 자료구조와 동작(비즈니스 로직)의 분리다.
    - 일반적으로 액티브 레코드의 필드에는 외부 프로시저가 상태를 수정할 수 있게 하는 public getter와 setter가 존재.

액티브 레코드를 사용하는 경우

  • 액티브 레코드는 본질적으로 데이터베이스에 대한 접근을 최적화하는  트랜잭션 스크립트이기 때문에, 이 패턴은 기껏해야 사용자 입력의 유효성을 검사하는 CRUD 작업과 같은 비교적 간단한 비즈니스 로직만 지원할 수 있음.
  • 트랜잭션 스크립트와 유사하게 지원 하위 도메인, 일반 하위 도메인과 외부 솔루션의 연동, 모델 변환 작업에 적합.
  • 액티브 레코드 패턴은 빈약한 도메인 모델 안티패턴(anemic domain model antipattern)이라고도 함.
  • 이 맥락에서 액티브 레코드는 프레임워크가 아니라 디자인 패턴을 의미.

실용적인 접근 방식

  • 비즈니스 데이터가 중요하고 설계 및 개발되는 코드의 무결성도 보호해야 하지만 실용적인 접근 방식이 더 바람직한 몇가지 경우가 있음.
  • 예로 대규모로 데이터를 다루는 시스템에서는 데이터 일관성 보장이 덜 엄격할 수 있다.
    - 100만 개 중 하나의 레코드 상태를 손상시키는 것이 비즈니스의 쇼스토퍼(showstopper)가 되는지, 그리고 비즈니스 성과와 수익성에 영향을 주는지 확인하라. (IoT에서 수십억개의 데이터중 0.001%데이터가 중복되거나 손실되는 것이 큰일은 아닐 것이다.)
  • 항상 그렇듯 보편적인 법칙은 없다. 결국 작업 중인 비즈니스 도메인에 달려있음.