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

[도메인 주도 설계 철저 입문] 2. "값 객체"의 개념 & 적용 예시

동현 유 2022. 3. 7. 00:50

흔히 프로그램을 작성하다보면, 사용자의 이름이나 나이 등을 원시타입(String, int) 등에 그대로 저장하는 경우가 있는데,

 

이런 경우에는 해당 값의 특성을 제대로 나타낼 수 없다.

 

이것은 여러 개발자가 함께 작업할 때 그 단점이 명확하게 나타나는데,

 

만약 String name 이라고 했을 때, 이름 값이 성과 이름을 포함하는 이름인지, 한국인 이름만 포함하는 이름인지 알 수 없기 때문에, db 를 조회해서 기존 data 가 어떤 식으로 저장되어 있는지 조회해야하는 등의 불편함이 있다.

 

따라서 원시타입을 그대로 사용하는 것이 아니라 해당 특성 값을 잘 나타낼 수 있도록 하면서,

동시에 유효성 검사도 진행할 수 있는 "값 객체"를 활용하는 것이 좋다.

 

값 객체를 도입했을 때의 장점
  1. 표현이 분명해진다.
  2. 무경성이 유지된다.
  3. 잘못된 대입을 방지한다.
  4. 유효성 검사 로직이 이곳저곳에 흩어지는 것을 방지한다.

 


게시판 생성 예시

 

 실제로 Inhabas.com 웹페이지를 제작하면서, DDD 개념을 알기 전에 이런 작업을 했었다.

 

매번 값을 받아 저장 및 수정하기 전에 유효성 검사를 하기에는 너무 비효율적이었기 때문에

 

"해당 특성을 나타내는 객체를 만들어, 생성자 내부에서 유효성 검사를 진행하면 되겠다"는 생각을 해냈었다.

 

db의 길이 제한 값을 max length 로 지정해주어,

 

db 에서의 contraintException 이 발생하기 전에, 미리 잘못된 데이터를 걸러주도록 했다.

 

(물론 web 단에서의 bean validation 도 진행한다.)

 

// 게시판 제목 값 객체
@Embeddable
public class Title {

    @Column(name = "title")
    private String value;

    @Transient
    private static final int MAX_LENGTH = 100;

    public Title() {}

    public Title(String value) {
        if (validate(value))
            this.value = value;
        else
            throw new IllegalArgumentException();
    }

    private boolean validate(Object value) {
        if (Objects.isNull(value)) return false;
        if (!(value instanceof String))  return false;

        String o = (String) value;
        if (o.isBlank()) return false;
        return o.length() < MAX_LENGTH;
    }

    public String getValue() {
        return value;
    }
}
// 게시글 내용 값 객체

@Embeddable
public class Contents {

    @Column(name = "contents", columnDefinition = "MEDIUMTEXT")
    private String value;

    @Transient
    private static final int MAX_SIZE = 2 << 24 - 1; //16MB

    public Contents() {}

    public Contents(String value) {
        if (validate(value))
            this.value = value;
        else
            throw new IllegalArgumentException();
    }

    private boolean validate(Object value) {
        if (Objects.isNull(value)) return false;
        if (!(value instanceof String))  return false;

        String o = (String) value;
        if (o.isBlank()) return false;
        return o.length() < MAX_SIZE;
    }

    public String getValue() {
        return value;
    }

}

 

테스트 코드 예시

    @DisplayName("Title 타입에 제목을 저장한다.")
    @Test
    public void Title_is_OK() {
        //given
        String  titleString = "게시판 제목입니다.";

        //when
        Title title = new Title(titleString);

        //then
        assertThat(title.getValue()).isEqualTo("게시판 제목입니다.");
    }

    @DisplayName("Title 타입에 너무 긴 제목을 저장한다. 100자 이상")
    @Test
    public void Title_is_too_long() {
        //given
        String titleString = "지금이문장은10자임".repeat(10);

        //then
        assertThrows(IllegalArgumentException.class,
                () -> new Title(titleString));
    }

    @DisplayName("제목은 null 일 수 없습니다.")
    @Test
    public void Title_cannot_be_Null() {
        assertThrows(IllegalArgumentException.class,
                () -> new Title(null));
    }

    @DisplayName("제목은 빈 문자열일 수 없습니다.")
    @Test
    public void Title_cannot_be_Blank() {
        assertThrows(IllegalArgumentException.class,
                () -> new Title("\t"));
    }

 

 

이렇게 값 객체를 생성한 후에, 아래와 같이 게시글을 생성한다.

// 게시판 엔티티 생성자
public Board(String title, String contents) {
        this.title = new Title(title);
        this.contents = new Contents(contents);
}

 

setter 를 사용할 경우는 String 을 인자로 넘겨주어 내부적으로 값 객체를 생성하도록 했는데,

 

이로써 내부적인 값 객체나, 유효성 검사 로직등을 모르더라도

 

개발자는 게시글 엔티티를 단순히 String 값을 넘겨주면서 안전하게 개발할 수 있다!

 

(하지만 엔티티의 경우 jpa 를 이용하기 때문에 setter 를 최대한 사용하지 않고, 생성자나 팩토리 메서드를 이용해서 수정해주었다)