4 분 소요


책커버

2장 시스템 특유의 값을 나타내기 위한 ‘값 객체’

2.1 값 객체란?

시스템 특유의 값을 표현하기 위해 정의하는 객체

ex)

var fullName = "한준수";
colsole.log(fullName); // "한준수"라는 값을 출력
var lastName = fullName.sbstring(0, 1);
console.log(lastName); // "한"이 출력된다.

위 코드의 문제점은 뭘까? 만약 유저가 성이 먼저가 아닌 이름을 먼저 입력한다면 lastName에 이름이 할당 될 것입니다.

이런 문제를 해결하기 위해서 객체 지향 프로그래밍에서는 일반저으로 클래스를 사용합니다.

Class FullName {
    constructor (
        firstName: string,
        lastName: string,
    ) {
        this.fullName = fullName;
        this.lastName = lastName;
    }
    public firstName: string;
    public lastName: string;
}

여기서 FullName 클래스는 이 시스템의 필요에 맞는 성명을 나타냅니다. 객체이기도 하고 동시에 값이기도 합니다. 따라서 이를 값 객체라고 합니다.

2.2 값의 성질과 값 객체 구현


값의 성질을 아는 것은 값 객체를 이해하기 위해 중요합니다. 값의 성질은 크게 세가지가 있습니다.

  • 변하지 않는다.
  • 주고 받을 수 있다.
  • 등가성을 비교할 수 있다.

2.2.1 값의 불변성


값은 변화하지 않는 성질을 갖습니다. 그렇다면 우리가 코딩을 할 때 수도 없이 수정하는 변수들은 그럼 값이 아닌가?

우리가 프로그래밍을 할때 값을 수정할 때는 새로운 값을 대입합니다.

그런데 사실 대입은 값을 수정하는 과정이 아닙니다. 대입을 통해 수정되는 것은 변수의 내용이지, 값 자체가 수정되는 것은 아닙니다.

var greet = "안녕하세요";
geet.changeTo("Hello"); //예를 들어 이런 함수가 있다면
console.log(geet); // Hello 출력


위 코드를 보면 geet이라는 변수 선언과 동시에 안녕하세요라는 값이 대입됩니다. 그 다음에 geet의 값이 Hello라는 값으로 수정됩니다. 즉, 위 코드는 말 그대로 값을 수정하는 코드입니다.


이렇게 된다면 개발자는 많은 혼란을 일으키게 됩니다. 값은 변하면 안되기 때문입니다.

즉, FullName 클래스는 값 객체로서 변하지 않아야 하며, 값을 수정하는 기능을 제공하는 메서드는

정의되어선 안됩니다.


2.2.2 교환 가능하다


값이 불변일지라도 값을 절대 수정해서는 안된다는 말은 아닙니다. (모순 처럼 들리지만 좀만 참고 읽어보자.)

우리가 평소에 값을 수정하는 방법

// 숫자값 수정
var num = 0;
num = 1;

// 문자값 수정
var c = "0";
c = "b";

// 문자열 값 수정
var geet = "안녕하세요";
geet = "hello";

위의 코드는 모두 변수에 값을 대입하는 코드입니다. 즉, 대입문 자체가 값을 수정하는 방법입니다.

위와 같이 값 객체 또한 대입문을 통해 교환의 형식으로 표현됩니다.

var fullName = new FullName("Han", "Junsu");
fullName = new FullName("Han", "Kyle");

값 객체는 불변이기 때문에 대입문을 통한 교환 외의 수단으로는 수정을 나타낼 수 없다.

2.2.3 등가성 비교 가능


프로그래밍의 세계에서 같은 종류의 값끼리는 비교가 가능합니다. 예를 들어

var num1 = 0;
var num2 = 0;
console.log(num1 === num2); // true

위의 코드에서 num1num2는 서로 별개의 인스턴스지만 값은 같은 것으로 취급됩니다.

이처럼 값객체도 값 객체를 구성하는 속성(변수)를 통해 비교됩니다.

값 객체간 비교

var nameA = new FullName("Han", "Junsu");
var nameB = new FullName("Han", "Junsu");

console.log(nameA.equals(nameB)); //equals는 두 객체 값을 비교하는 function
// 값이 같을경우 true 리턴

속성값을 직접 깨내 비교

var nameA = new FullName("Han", "Junsu");
var nameB = new FullName("Han", "Junsu");

var isEquals =
  nameA.firstName === nameB.firstName && nameA.lastName === nameB.lastName;

console.log(isEquals);

하지만 위의 방식은 잘못된 방식입니다. 생각해보면 값의 값을 꺼내어 비교한다는 행위 자체가 부자연스럽기 때문입니다.

따라서 값 객체는 값 객체 내부에 값 객체를 비교하는 메서드를 제공하여 비교를 진행하는게 자연스럽습니다.


이렇게 비교 함수를 값 객체 내부에 두면 여러가지 이점이 있습니다.

속성을 추가해도 수정이 필요 없다

만약 FullName 객체에 Middle Name이 추가되는 상황을 가정해봅시다. 이 경우 두 객체를 비교 할 때 속성값을 직접 꺼내 비교하는 방식은 매우 비효율적이며 수정 난이도가 크게 상승합니다.

2.3 값 객체가 되기 위한 기준

여기까지 보았을때 어떤 클래스를 값객체로 만들지 고민이 되거나, 혹은 어디까지 값 객체로 만들어야 하지? 라는 의문이 생길 수도 있습니다.

예를들어 FullName을 구성하는 firstName, lastName은 값 객체가 되어야 할까요? 이에 대한 판단을 돕기 위해서 이 책의 저자는

규칙이 존재하는가?낱개로 다루어야 하는가? 두 관점에서 판단하기를 추천합니다.

앞서 계속 나왔던 FullName으로 예를 들어 보겠습니다.

FullName은 성과 이름으로 구성되어야 한다라는 규칙이 존재합니다. 동시에 이는 낱개로 다뤄지는 정보입니다.

그렇다면 firstName, lastName은 어떨까요? 서비스마다 다르겠지만 일반적으로 성, 이름에 대한 제한은 존재하지 않습니다. 또한 성만 사용하거나 이름만 사용하는 경우도 극히 드뭅니다.

따라서 이 책의 기준으로 FullName은 값 객체입니다.

다음으로 고려해야 할 사항은 성과 이름을 따로 다룰 필요가 있는가? 입니다. 성이나 이름에 대한 다른 제약사항이 없는 경우에는

Name이라는 별도 class를 통해 하나의 타입으로 관리하는 것도 가능합니다.

class Name {
  constructor(name: string) {
    this.name = name;
  }
  public name: string
}

class FullName {
  constructor (firstName: Name, lastName: Name) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  public fistName: Name;
  public lastName: Name;
}


2.4 행동이 정의된 객체


값 객체는 단지 데이터만 담는 것이 목적이 아니라, 그와 관련된 행동 까지도 스스로 가지고 있어야 한다.

class Money {
  ...
  public ammount: number;
  public currency: string;

  add(otherMoney: Money) {
    if(otherMoney.currency !== this.currency) {
      throw Error("화폐 단위가 다릅니다");
    }
    this.ammount += othermoney.ammount
    return this
  }
}

2.5 값 갹채 도입의 장점


값 객체의 장점은 크게 4가지가 있습니다.

  • 표현력이 증가한다.
  • 무결성이 유지된다.
  • 잘못된 대입을 방지한다.
  • 로직이 코드 이곳저곳에 흩어지는 것을 방지한다.

2.5.1 표현력의 증가


예를 들어 다음과 같은 식별 번호가 있다고 가정하겠습니다.

var serialNumber = '00001-20211118-seoul-mod1';

이를 값 객체로 변환하게 되면 더욱 자세한 표현으로 관계자가 이해하기 쉬워집니다.

class SerialNumber {
  public modelNumber: string;
  public createdDate: string; 
  public branch: string;  
  public modelType: string;

  ...
}

2.5.2 무결성 유지


만약 이름은 2글자 이상이여야 한다는 조건이 있다고 가정해보겠습니다. 이 경우

var name = 'i'

위의 코드는 컴파일러 관점에서 전혀 문제가 없는 코드이므로 무결성을 검사하기 쉽지 않습니다.

따라서

class Name {
  constructor(
    name: string;
  ) {
    if(name.length < 2) {
      throw Error('이름은 2글자 이상이여야 합니다.')
    }
    this.name = name;
  }
  ...
}

위와 같이 값 객체 활용하서 사전에 유효성을 검사하게 하는 방안을 통해 무경성을 검사하면 됩니다.

2.5.3 잘못된 대입 방지


class User {
  public id: UserId;
  public name: UserName;
}

var user = new User();
var name: string = "준수";
user.Name = name; // 컴파일 에러
user.Id = name; // 컴파일 에러

위와 같이 객체의 타입을 미리 정의함으로서 잘못된 대입을 방지하는 역활을 하기도 합니다.

2.6 정리


값 객체는 시스템 고유의 값을 의미힙니다. 이를 통해 해당 시스템에 필요한 규칙을 객체 안에 문서화 하여 표현력을 풍부하게 할 수 있습니다.

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

댓글남기기