• 빈약한 도메인 모델, 풍부한 도메인 모델, DDD 그리고 캡슐화
    DEV 2023. 12. 31. 20:56

    캡슐화(Encapsulation)

    대부분의 비즈니스 시스템은 MVC 아키텍처를 사용하고, 빈약한 도메인 모델(anemic domain model)을 기반으로 작성되고 있다.

    이 개발 방식(빈약한 도메인 모델을 기반으로 한 MVC)은 객체지향 프로그래밍 스타일에 위반될 뿐 아니라, 철저하게 절차적 프로그래밍 스타일에 해당하기 때문에 안티 패턴으로 간주되기도 한다.

    The anemic domain model is described as a programming anti-pattern where the domain objects contain little or no business logic like validations, calculations, rules, and so forth. The business logic is thus baked into the architecture of the program itself, making refactoring and maintenance more difficult and time-consuming.
    - wikipedia
    • 빈약한, 풍부한 도메인 모델은 무엇인가?
    • 빈약한 도메인 모델이 객체지향 프로그래밍을 위반하는 이유는?
    • 객체지향 프로그래밍을 위반함에도 여전히 많이 사용되는 이유는?
    • 풍부한 도메인 모델을 사용하면 뭐가 좋은가?

    MVC 아키텍처

    MVC architecture

    • 기존의 MVC(model-view-controller)구조는 Model, View, Controller 계층으로 나뉨
    • 그러나 front-end, back-end가 분리되기 시작하면서 백엔드에서는 컨트롤러, 서비스, 저장소 계층으로 다시 나누는 경향이 생김
      - 컨트롤러 계층 → 인터페이스를 프런트엔드에 노출하는 역할
      - 서비스 계층 → 핵심 비지니스 논리
      - 저장소 계층 → 데이터로의 접근

    빈약한 도메인 모델 (Anemic domain model)

    • 대부분 알지 못하는 사이에 빈약한 도메인 모델로 개발했을 가능성이 높고, 거의 모든 비즈니스 벡엔드 시스템이 빈약한 도메인 모델을 기반으로 개발되었다고 해도 과언이 아니다. 
    // Controller + VO(View Object)
    public class UserController {
        private UserService userservice;
    
        public UserVo getUserById(Long userId) {
            UserBo userBo = userservice.getUserById(userId);
            UserVo userVo = convertUserBoToVo(userBo);
            return userVo;
        }
    }
    
    // Service + BO(Business Object)
    public class UserService {
        private UserRepository UserRepository;
    
        public UserBo getUserById(Long userId) {
            UserEntity userEntity = UserRepository.getUserById(userId);
            UserBo userBo = convertUserEntityToBo(userEntity);
            return userBo;
        }
    
        private void updatePhone(Long userId, String phone) {
            UserEntity userEntity = getUserByID(userId);
            if(!isVaildPhone(phone)) {
                throw new NoVaildPhoneException(...);
            }
            UserRepository.updatePhone(userId, phone);
        }
    }
    
    // Repository + Entity
    public class UserRepository {
        public UserEntity getUserById(Long userId) {...}
        public void updatePhoneById(Long userId, String phone) {...}
    }
    
    // Domain
    public class UserVo {
        private Long id;
        private String name;
        private String phone;
    }
    public class UserBo {
        private Long id;
        private String name;
        private String phone;
    }
    public class UserEntity {
        private Long id;
        private String name;
        private String phone;
    }
    • 일반적인 웹 백엔드 코드

    빈약한 도메인 모델 layer

    • Repository에서 생성한 Entity를 controller 레이어까지 그대로 사용하는 경우도 있지만,
      - 레이어, 서비스간 이동시 DTO(Data transfer object)에 따로 담아서 이동시키는 것이 원칙상 맞을 수 있다.
    • UserEntity, UserRepository class → 데이터 접근 계층
    • UserBo, UserService class → 비지니스 논리 계층
    • UserVo, UserController class → 인터페이스 계층 
    • UserBo 클래스는 데이터만 포함, 비즈니스 논리는 UserService 클래스에 집중
    • 데이터만 포함, 비지니스 논리는 포함하지 않는 UserBo와 같은 클래스 = 빈약한 도메인 모델

    풍부한 도메인 모델 (Rich domain model)

    • 데이터와 비지니스 논리가 하나의 클래스에 포함
    • 풍성한 도메인 모델은 객체지향 프로그래밍의 캡슐화를 만족 → 전형적인 객체지향 프로그래밍 스타일에 속함
    • DDD의 인기 이후 빈약한 도메인 모델을 기반으로 한 전통적인 개발 방식이 비판받기 시작, 풍성한 도메인 모델에 기반한 DDD 개발 방식이 지지되기 시작함 

    캡슐화

    • 객체지향 프로그래밍의 4가지 특성 중 하나
      - 캡슐화, 추상화, 상속, 다형성
    • 정보 은닉, 데이터 액세스 보호
    • 접근 가능한 인터페이스를 제한하여 클래스가 제공하는 메서드를 통해서만 내부 정보나 데이터에 대한 외부 접근을 허가
    • 결국 UserBO와 같이 데이터만 포함하고 비즈니스 로직은 포함되지 않는다면 service, controller layer에서 UserBo의 데이터에 직접 접근하여 비즈니스 로직을 수행하게 되고 객체지향 프로그래밍의 캡슐화를 불만족 → 절차지향적 프로그래밍

    도메인 주도 설계 (Domain driven design, DDD)

    DDD architecture

    • DDD는 소프트웨어가 도메인과 일치하도록 소프트웨어를 모델링 하는데 초점을 둔 소프트웨어 디자인 접근방식
    • 주로 비지니스 시스템을 분리하고, 비즈니스 모듈을 분할하고, 비즈니스 도메인 모델과 상호 작용을 정의하는 방법을 설계할 때 사용
    • 위 그림에서 repository, infrastructure, application은 연결되어 있지만, 도메인 모듈을 하나의 원으로 떨어져 있듯이 시스템에서 비즈니스 모듈을 분리, 분할
    • DDD를 잘 하기 위한 핵심은 DDD의 개념에 대한 이해가 아니라 비즈니스에 친숙해지는 것, DDD의 개념을 잘 알고 있어도 비즈니스에 익숙하지 않으면 합리적인 도메인 설계를 얻을 수 없기 때문

    풍부한 도메인 모델에 기반한 DDD 개발 방식

    • 일반적으로 MVC 아키텍처에 따라 계층화
      - 컨트롤러 계층 → 인터페이스 노출
      - 저장소 계층 → 데이터 액세스 담당
      - 서비스 계층 → 비지니스 논리 담당
    • 빈약한 모델과 차이 → 서비스 계층 내부
      - 빈약한 도메인 모델에 기반한 전통적인 개발 방식 → service 클래스와 가벼운 Bo (데이터만 가지고 있는 UserBo)
      - 풍부한 도메인 모델에 기반한 DDD 개발 방식 → 가벼운 service 클래스와 Domain 클래스 (데이터와 비즈니스 로직을 포함한 class)
    // UserBo -> User
    public class User {
        private Long id;
        private String name;
        private String phone;
    
        public User(Long userId, String userName) {
            this.id = userid;
            this.name = name;
        }
    
        public void updatePhone(String phone) {
            if(!isVaildPhone(phone)) {
                throw new NoVaildPhoneException(...);
            }
            this.phone = phone;
        }
    }
    
    public class UserService {
        private UserRepository UserRepository;
    
        public User getUserById(Long userId) {
            UserEntity userEntity = UserRepository.getUserById(userId);
            User user = convertUserEntityToUser(userEntity);
            return User;
        }
    
        private void updatePhone(Long userId, String phone) {
            User user = getUserByID(userId);
            user.updatePhone(phone);
            UserRepository.updatePhone(userId, phone);
        }
    }

     

    • 앞서 설명한 2방식의 주요 차이점은 서비스 계층에 있으며, 컨트롤러, 저장소 계층은 기본적으로 다르지 않다.
    • 풍부한 도메인 모델 기반의 DDD 개발 방식
      - User 클래스를 풍부한 도메인 모델로 설계, service 클래스에 있던 비즈니스 코드를 User로 옮김
    • 매우 간단한 비지니스비즈니스 코드를 예로 들었기 때문에 빈약한 도메인 모델의 원래 설계와 비교해 이점이 없는 것처럼 보이고, 그렇기 때문에 대부분의 비즈니스 시스템이 빈약한 도메인 모델을 기반으로 개발되는 이유
    • 하지만 점점 복잡한 비즈니스 논리를 지원해야 하는 경우 풍부한 도메인 모델의 이점이 드러나기 시작
    • 자주 변경되기 쉬운 비지니스 코드의 수정 시 도메인 모델 안으로 수정범위를 좁힐 수 있다.

    그렇다면 DDD에서 service layer의 책임은 무엇인가?

    1. 저장소 계층과의 통신 담당

    • service 클래스는 저장소 계층(repository)과 상호 작용하여 데이터베이스에서 데이트를 얻고, Entity를 도메인 모델로 변환, 도메인 모델의 작업을 처리, 데이터 저장
    • 도메인 모델(User)이 저장소 계층과 상호작용 하지 않고 UserService가 상호작용 하는 이유는 도메인 모델을 저장소 계층과 분리하고, 데이터 매핑 등 프레임웍 코드가 비즈니스 코드와 분리

    2. 여러 도메인 모델의 비지니스 논리를 결합

    • 여러 User 간 관계, 연결이 필요한 비즈니스 논리 처리

    3. 기능과 무관한 타 시스템과의 상호작용 담당

    • 트랜잭션, 로깅, 메시지 보내기 등

    컨트롤러, 저장소 계층은 아직 빈약한 도메인 모델로 작성되어 있는데, 풍성한 계층으로 변경이 필요한가?

    • 필요하지 않다.
    • 컨트롤러 계층은 인터페이스를 노출시키는 역할, 저장소 계층은 데이터베이스를 처리하는 역할을 하는데 이 두 계층은 그다지 많은 비즈니스 로직이 필요하지 않다.
    • 비즈니스 로직이 비교적 단순하다면 굳이 풍부한 도메인 모델로 설계할 필요가 없다.
    • 저장소 레이어의 Entity는 빈약한 도메인 모델로 설계되면 객체지향 프로그래밍의 캡슐화 특성을 위반하고 임의로 수정될 위험이 있지만, Entity의 생명주기는 서비스 계층으로 전달되면서 BO, Domain으로 변환되기 때문에 임의로 수정될 여지가 없다.
    • 컨트롤러 레이어의 VO는 DTO(Data transfer object)인데 주로 다른 시스템으로 데이터를 보내기 위한 인터페이스의 데이터 전송 캐리어로 사용되기 때문에 비즈니스 로직을 포함하지 않고 데이터만 갖고 있는 빈약한 도메인 모델로 설계하는 것이 합리적

    그럼에도 빈약한 도메인 모델에 기반한 전통적 개발 방식이 주로 사용되는 이유

    • 대부분의 비즈니스 로직은 비교적 단순하고, SQL 기반의 CRUD 작업이 많기 때문에 간단한 비즈니스 개발에는 빈약한 도메인 모델이 더 적합
    • 풍부한 도메인 모델의 설계가 훨씬 까다롭다. 처음부터 어떤 데이터가 어떻게 사용되며, 해당 데이터를 다루는 비즈니스 로직이 정리되어 있어야 하기 때문. 반면 빈약한 도메인 모델은 초기에 데이터를 정의하기만 하면 되고, 기능 개발 요구사항이 있는 경우 그때그때 서비스 레이어에서 해당 작업을 추가하면 된다.
    • 전통적 개발 방식이 수년간 사용되어 왔기 때문, 전환을 위해선 비용이 증가

    Fin.

    • 빈약한 도메인 모델 기반 전통적 개발 방식
      → 데이터, 비지니스 로직을 분리
      → 객체지향 프로그래밍 캡슐화 위반하는 절차적 프로그래밍 스타일
    • 빈약한 도메인 모델 기반 전통적 개발방식
      → 간단한 비지니스를 가진 시스템 개발에 적합
    • 풍부한 도메인 모델 기반 DDD 개발 방식
      → 복잡한 비지니스 모델을 포함하는 시스템 개발에 적합
    • 두 방식을 비교했을 때 단순히 하나는 Service layer에 비즈니스 로직을 넣고, 하나는 Domain model에 비즈니스 로직을 넣는 것이라 생각할 수도 있지만, 눈에 보이는 코드 수준의 차이와 별개로 중요한 차이점이 존재

    개발 방식의 차이로 인해 개발 프로세스가 바뀌게 된다는 점

    • 일반적인 개발 작업은 대부분 SQL 기반이라고 해도 과언이 아니다.
    • 벡엔드 인터페이스에 대한 개발 요청을 받으면 인터페이스에 필요한 데이터를 살펴본 후, 데이터를 가져오기 위한 SQL 문을 작성
    • 그다음 Entity, BO, VO를 정의하고, 해당하는 Repository, Service, Controller class 코드 추가
    • 비즈니스 로직이 보통 하나의 큰 SQL 문으로 묶이고, 큰 SQL 문이 대부분의 작업을 수행
    • 여기서 서비스 레이어가 할 일은 별로 없다.
    • 작업이 추가될 때마다 별도의 SQL문이 필요하기 때문에 코드 재사용성도 극히 낮다.
    • 유사한 비즈니스 기능이 추가될 때마다 별도의 SQL 작성이 필요, 거의 차이 없는 SQL 문들이 코드에 추가됨
    • 단순한 시스템의 경우 문제가 없지만, 복잡한 비즈니스 시스템의 경우 유지보수가 힘들어짐

    하지만 풍부한 도메인 모델을 기반으로 DDD 개발 방식을 적용한다면

    • 개발 프로세스가 뒤바뀐다.
    • 모든 비즈니스를 미리 정의하고 도메인 모델에 포함된 속성과 메서드를 우선 정의
    • 도메인 모델은 재사용 가능한 비지니스 중간 계층과 동일, 새로운 요구사항의 개발은 이 재사용 가능한 비지니스 중간계층을 기반으로 작업하게 된다.

    절차적 프로그래밍 스타일 코드의 작성을 중단해야 하는가?

    • 간단한 프로그램, 데이터 처리 관련 프로그램은 스크립트 스타일인 절차적 프로그래밍 스타일이 더 적합
    • 객체지향 프로그래밍, 절차적 프로그래밍은 양자택일의 대상이 아니다.
      - 일부 표준 개발 라이브러리에도 절차적 프로그램 스타일 코드가 많이 포함되어있다.
    • 결과적으로 어떤 스타일을 사용하든 궁극적인 목표는 유지하기 쉽고, 읽기 쉽고, 재사용이 쉬우며, 확장하기 쉬운 고품질 코드를 작성하는 것이라는 너무나 당연한 말로 마무리. ㅎ

    Ref.

    728x90
go.