본문 바로가기

DDD/도메인 주도 설계 철저 입문

값 객체의 장점 - 「도메인 주도 설계 철저 입문」 2장 (2)

본 포스트 시리즈는 「도메인 주도 설계 철저 입문」책을 요약한 내용입니다.

 

 

이전 글 보기

 


02장 시스템 특유의 값을 나타내기 위한 '값 객체'

 

(2) 값 객체의 장점

 

 

값 객체의 장점 4가지

값 객체를 도입하면 다음과 같은 4가지의 장점이 있다. 

 

  • 표현력이 증가한다.
  • 무결성을 보장할 수 있다.
  • 잘못된 대입을 방지할 수 있다.
  • 로직을 한 곳에 모아둘 수 있다.

 

1) 표현력이 증가한다.

원시타입으로 정의한 제품번호와, 값 객체로 표현한 제품번호를 비교해보자. 다음은 원시타입인 String으로 정의한 제품번호이다. 실제로 많은 정보를 담고 있는 제품번호임에도, 그 의미를 파악하기 어려워진다.

String modelNumber = "a120423-100-1";

 

 

다음은 값 객체로 나타낸 제품 번호이다. 클래스를 통해 제품번호의 의미를 더 자세히 알 수 있다. 값 객체는 정의를 통해 자신이 무엇인지에 대해 자기 문서화 한다.

public class ModelNumber {
    // 필드: productCode, branch, lot
    private final String productCode;
    private final String branch;
    private final String lot;

    // 생성자
    public ModelNumber(String productCode, String branch, String lot) {
        if (productCode == null) throw new IllegalArgumentException("productCode cannot be null");
        if (branch == null) throw new IllegalArgumentException("branch cannot be null");
        if (lot == null) throw new IllegalArgumentException("lot cannot be null");

        this.productCode = productCode;
        this.branch = branch;
        this.lot = lot;
    }

    // 필드를 문자열로 변환
    @Override
    public String toString() {
        return productCode + "-" + branch + "-" + lot;
    }

    // 메인 메서드 (테스트용)
    public static void main(String[] args) {
        ModelNumber modelNumber = new ModelNumber("1234", "A", "001");
        System.out.println(modelNumber);  // 출력: 1234-A-001
    }
}

 

 


 

 

2) 무결성을 보장할 수 있다.

'사용자명은 3글자 이상이어야 한다'는 규칙을 보장하기 위해서는 이름을 String으로 정의하는 것은 바람직하지 않다. String에는 언제든지 규칙을 위반하는 값이 들어갈 수 있기 때문이다. 따라서 시스템상 유효한 값만 허용하기 위해서는 fullName은 class로 정의되는 것이 좋다.

public class UserName {
    // 필드: value
    private final String value;

    // 생성자
    public UserName(String value) {
        if (value == null) throw new IllegalArgumentException("value cannot be null");
        if (value.length() < 3) throw new IllegalArgumentException("사용자명은 3글자 이상이어야 함");

        this.value = value;
    }

    // 값 반환 메서드
    public String getValue() {
        return value;
    }
}

 

 


 

 

3) 잘못된 대입을 방지할 수 있다.

다음은 사용자를 생성할 때 사용자 이름을 받아서 이를 사용자 ID에 대입하는 실수를 보여준다. 

public class User {
    // 필드: id
    private String id;

    // id 설정 메서드
    public void setId(String id) {
        this.id = id;
    }

    // id 반환 메서드 (선택사항)
    public String getId() {
        return id;
    }

    // CreateUser 메서드
    public static User createUser(String name) {
        User user = new User();
        // name을 받아 id에 세팅하는 논리적 오류
        user.setId(name);
        return user;
    }
}

 

 

 

이러한 실수를 방지하기 위해서 UserId와 UserName 클래스를 각각 만든 뒤 이를 사용해 User를 정의하면 다음과 같다.

public class UserId {
    // 필드: value
    private final String value;

    // 생성자
    public UserId(String value) {
        if (value == null) {
            throw new IllegalArgumentException("value cannot be null");
        }
        this.value = value;
    }
}

public class UserName {
    // 필드: value
    private final String value;

    // 생성자
    public UserName(String value) {
        if (value == null) {
            throw new IllegalArgumentException("value cannot be null");
        }
        this.value = value;
    }
}
public class User {
    // 필드
    private UserId id;
    private UserName name;

    // Getter 및 Setter 메서드
    public UserId getId() {
        return id;
    }

    public void setId(UserId id) {
        this.id = id;
    }

    public UserName getName() {
        return name;
    }

    public void setName(UserName name) {
        this.name = name;
    }
}

 

 

위와 같이 User를 UserId와 UserName 클래스를 활용해 정의하고 나면 실수를 했을 때 컴파일 에러가 발생한다. 따라서 자연스럽게 논리적 오류를 방지해준다.

public class Program {
    
    // User 생성 메서드
    private User createUser(UserName name) {
        User user = new User();
        UserId userId = name; // 컴파일 에러 발생
        return user;
    }
}

 

 


 

 

4) 로직을 한 곳에 모아둘 수 있다.

다음은 값 객체를 사용하지 않고 로직이 여기저기 흩어져 있는 예시다. User 클래스는 원시적 타입을 사용해서 이름을 정의하였고, '사용자 명은 3글자 이상이어야 한다'라는 규칙을 사용자 생성 및 수정시에 강제하고 있다.

public class User {
    // 필드: name
    private String name;

    // 생성자
    public User(String name) {
        this.name = name;
    }
}
// 사용자 생성 메서드
public void createUser(String name) {
    if (name == null) {
        throw new IllegalArgumentException("name cannot be null");
    }
    if (name.length() < 3) {
        throw new IllegalArgumentException("사용자명은 3글자 이상이어야 함");
    }

    User user = new User(name);
}

// 사용자 업데이트 메서드
public void updateUser(String id, String name) {
    if (name == null) {
        throw new IllegalArgumentException("name cannot be null");
    }
    if (name.length() < 3) {
        throw new IllegalArgumentException("사용자명은 3글자 이상이어야 함");
    }
}

 

 

반면 값 객체를 사용해  User를 정의하면 '사용자명은 3글자 이상이어야 한다'라는 규칙을 값 객체 내부에 모아둘 수 있다. 먼저 userName을 String이 아닌 UserName이라는 클래스를 이용해 정의한 모습이다.

class UserName
    {
        private readonly string value;

        public UserName(string value)
        {
            if (value == null) throw new ArgumentNullException(nameof(value));
            if (value.Length < 3) throw new ArgumentException("사용자명은 3글자 이상이어야 함", nameof(value));

            this.value = value;
        }
    }

 

 

createUser와 updateUser는 다음과 같다.

public class Program {

    // 사용자 생성 메서드
    public void createUser(String name) {
        // UserName 객체 생성
        UserName userName = new UserName(name);

        // User 객체 생성
        User user = new User(userName);
    }

    // 사용자 업데이트 메서드
    public void updateUser(String id, String name) {
        // UserName 객체 생성
        UserName userName = new UserName(name);
    }
}

 

 

요약

값 객체의 장점

1. 표현력이 증가한다 : 클래스 정의를 통해 자기자신이 무엇인지에 대해 자기문서화 한다.

2. 무결성을 보장할 수 있다 : 원시타입으로 정의할 때와 달리 생성자 등을 통해 특정 규칙을 보장할 수 있다.

3. 잘못된 대입을 방지할 수 있다 : 의미적으로나 논리적으로 값을 알맞게 대입할 수 있게 보장할 수 있다.

4. 로직을 한 곳에 모아둘 수 있다 : 클래스 내부에 로직을 모아두면 수정사항이 생겨도 클래스만 고치면 된다.