3 분 소요


책커버

4장 부자연스러움을 해결하는 도메인 서비스


4.1 서비스란?


소프트웨어 개발에서 말하는 서비스란 클라이언트를 위해 무언가를 해주는 객체를 의미합니다.

DDD에서 말하는 서비스는 크게 두 가지로 나뉩니다.

  1. 도메인을 위한 서비스
  2. 애플리케이션을 위한 서비스

4.2 도메인 서비스란?


앞선 장에서 살펴 봤듯이 도메인 객체에는 객체의 행동을 정의할 수 있습니다. 하지만 몇몇 비지니스 로직에는 엔티티나 값 객체로 구현하기 어려운 행동들도 존재합니다.

물론 구현할 수는 있지만 표현이 어색해 질 수 있습니다. 이러한 어색함을 해결하기 위해 나온 것이 도메인 서비스입니다.

4.2.1 값 객체나 엔티티에 정의하기 어색한 행동


예를 들어 도메인 규칙 중 하나가 중복된 사용자명을 허용하지 않는 것이라고 가정합시다. 이를 구체화하기 위해 사용자 클래스에 이름 중복 여부를 확인하는 로직을 추가해봅시다.

class User {
    constructor(
        id: UserId,
        name: UserName
    ) {
        if(id === null ) throw Error();
        if(name === null || name.length === 0) throw Error();
        this.id = id;
        this.name = name;
    }
    private id: UserId;
    private name: UserName;

    public isExists(user: User): bool {
        /// 사용자 중복 확인 코드
    }
}

위 코드는 언뜻 봐서는 어색함을 느끼지 못합니다. 하지만 실제 메서드가 사용되는 흐름을 따라가 보면 이상함을 느끼게 됩니다.

const userId = new UserId("id");
const userName = new UserName("kyle");
const user = new User(userId, userName);

// 중복 여부 확인
const duplicateCheckResult = user.isExists(user);

위와 같이 중복 여부를 확인하기 위해서는 객체 자기 자신에게 중복 여부를 묻는 구조가 됩니다.

이런 부자연스러움을 해결하기 위해 도메인 서비스를 구축하는 것이 좋습니다.

4.2.2 부자연스러움을 해결해주는 객체

위의 상황을 도메인 서비스를 도입하면 어떻게 바뀌는지 확인해 봅시다.

class UserService {
    public isExists(user: User): bool {
        // 사용자 중복 확인 코드
    }
}

const userService = new UserService();

const userId = new UserId("id");
const userName = new UserName("kyle");
const user = new User(userId, userName);

const duplicateCheckResult = userService.isExists(user);

위와 같이 도메인 서비스를 사용함으로써 자기 자신에게 중복 확인을 하거나 중복 확인만을 위해 생성되고 버려지는 객체를 만들 필요가 없어졌습니다.

4.3 도메인 서비스를 남용한 결과


도메인 서비스를 사용하면서 주의해야 할 점은 이를 무분별하게 사용하면 안된다는 점입니다.

도메인 서비스는 앞서 말했듯 부자연스러운 처리에 한정해야 합니다.

아래 예를 들어 설명하겠습니다.

class UserService {
    public changeName(user: User, userName: UserName): void {
        if(user === null) throw Error();
        if(userName === null) throw Error();

        user.name = userName;
    }
}

class User {
    constructor(
        id: UserId,
        name: UserName
    ) {
        this.id = id;
        this.name = name;
    }

    private readonly id: UserId;
    public name: UserName;
}


위와 같이 객체 내부에서 처리해도 되는 로직조차 도메인 서비스에서 처리하게 된다면 엔티티 객체에는 게터와 세터만 남게 됩니다.

이렇게 자신이 처리해야 할 내용을 도메인 서비스에게 모두 빼앗긴 객체를 빈혈 도메인 모델이라고 합니다.

이런 행위는 객체는 데이터와 행위를 함께 모아 놓는다는 객체 지향 설계의 기본 원칙을 위반하는 것임으로 지양해야 합니다.

4.3.1 도메인 서비스는 가능한 피할 것

어떤 행위를 어디에 구현할지 망설여 진다면 우선 엔티티나 값 객체에 정의하는 것이 좋습니다. 즉 가능한 도메인 서비스를 피하는 것이 좋습니다.

만약 도메인 서비스가 계속 남용된다면 데이터와 행위가 단절되 로직이 흩어질 가능성이 높기 때문입니다.

4.4 엔티티/값 객체와 함께 유스케이스 수립하기


도메인 서비스의 사용법을 살펴보기 위해 실제 유스케이스를 세워 익혀봅시다.

4.4.1 사용자 엔티티 확인

우선 사용자를 나타내는 User 클래스를 정의합니다.

class User {
    constructor(
        name: UserName
    ) {
        if(name === null) throw new Error();
        const id = new UserId(generateUid().toString());

        this.id = id;
        this.name = name;
    }

    public id: UserId;
    public name: UserName;
}

class UserId {
    constructor(value: string) {
        if(value === null) throw new Error();

        this.value = value;
    }
    public value:string;
}

class UserName {
    constructor(value: string) {
        if(value === null) throw new Error();
        if(value.length < 3) throw new Error('사용자 명은 3글자 이상이여야 합니다.');

        this.value = value;
    }
    public value: string
}

4.4.2 사용자 생성 처리 구헌

다음으로 사용자 생성 처리 코드를 살펴봅시다.

class Program {
    public createUser(userName: string): void {
        // 유저 객체 생성
        const user = new User(new UserName(userName));

        // 중복 확인
        const userService = new UserService();
        if(userService.isExists(user)) {
            throw new Error('이미 존재하는 유저');
        }

        // DB 저장
        var connection = await getConnection()
                            .createEntityManager()
                            .query(`INSERT INTO users (id, name) VALUES (${user.id.value}, ${user.name.value}`)
                            .excute();
    }
}

class UserService {
    public isExists(user: User): boolean {
        var exist = await getConnection()
        .createEntityManager()
        .query(
          `SELECT * FROM users where name = ${user.name.value}`,
        );

        if (!exist || exist === null || exist === undefined) {
            return false;
        }
        return true;
    }
}

위의 코드는 언뜻 보기엔 잘 동작하고 문제가 없어 보입니다. 하지만 코드가 지나치게 데이터스토어를 다루는 것에 집중되어 있다고 느끼실겁니다.

이렇게 작성하면 코드의 유연성이 부족해 집니다. 예를 들어 만약 데이터베이스가 RDBM에서 NoSQL로 변경된다면 코드의 전면 수정이 불가피해 질 것입니다.

사용자 생성 처리의 본질은 ‘사용자를 생성하는 것’‘사용자명 중복 여부를 확인 하는 것’, ‘생성된 사용자 정보를 저장하는 것’입니다.

도메인 서비스는 이런 본질적인 로직을 다루는 것에 집중해야지 데이터스토에를 다루는데 집중해서는 안됩니다.

이는 다음 장에서 후술할 리포지토리 패턴으로 해결이 가능합니다.

:bulb: 본 글은
도메인 주도 설계 시리즈로 작성 됩니다.
도메인 주도 설계 1
도메인 주도 설계 2
도메인 주도 설계 3
도메인 주도 설계 4

댓글남기기