본문 바로가기

Design Patterns

값 객체, 금액, 레코드 집합

 

값 객체(Value Object)

  • 금액이나 날짜 범위와 같이 동등성의 개념이 식별자에 기반을 두지 않는 작고 간단한 객체
  • 참조 객체와 값 객체를 구분해서 생각하면 유용하다. 예를 들어 값 객체는 금액, 날짜와 같은 작은 객체이고 참조 객체는 주문, 고객과 같이 큰 객체이다.

 

특징

일반적으로 더 작다.

언어에서 제공되는 기본형(primitive type)과 비슷하다.

작동 원리

  • 읽기 전용으로 만들어야 한다. (Aliasing bug 방지 : 공유 하는 값 객체에서 한 소유자가 변경을 하면 다른 소유자의 객체도 변경된다.)
  • 완성된 레코드로 저장해서는 안 된다. (포함 값 사용)

사용 시점

식별자(identifier)가 아닌 다른 기준을 바탕으로 동등성을 판단하는 경우 값 객체를 사용한다. 생성하기 쉬운 모든 작은 객체에 값 객체를 사용하는 것을 고려해보자.

 

// 주문장의 배송지 정보
public class ShippingAddress...

  private String address1;
  private String address2;
  private String zipcode;
    

 

금액

금액을 나타낸다.

 

금액을 기본 데이터 형식, 라이브러리로 제공하지 않아서 놀랍다!! 클래스로 만들어보자

 

작동 원리

  • 기본 개념은 각각 액수와 통화 필드를 포함하는 금액 클래스를 만드는 것이다.
  • 필요에 따라 정수 형이나 고정 소수점 수를 사용해 저장하자. 단, 부동 소수점 형식은 반올림/내림 오류를 유발할 수 있어서 절대적으로 피해야 한다.
  • 금액은 값 객체이므로 통화와 액수를 기준으로 수행하도록 동등성 및 해시 코드 연산을 재정의해야 한다.
//부동소수점 사용하면 안돼!
  double val = 0.00;
  for (int i = 0; i < 10; i++) val += 0.10;
  System.out.println(val == 1.00);
  
  -----------
  false

고려할 점

  • 다른 종류의 통화를 어떻게 계산할 것인가? 오류 처리
  • 금액을 분할할 때에는 어떻게 처리할 것인가? (소수점 문제) 무시
매트 폼멜의 난제
전체 금액을 70%와 30%로 분할해서 할당해야 한다는 비즈니스 규칙이 있다고 가정해 보자. 이때 5센트를 분할해야 한다. 이를 계산하면 3.5센트와 1.5센트가 나온다. 이 경우 어떤 방법으로 반올림을 하더라도 문제가 된다. 가장 일반적인 반올림 방법을 사용하면 1.5는 2가 되고 3.5는 4가 된다.

 

데이터베이스 역시 금액의 중요성을 인식하지 못해서인지 금액을 데이터베이스에 저장하는 것도 항상 문제가 된다. 확실한 방법은 포함 값을 사용해 모든 금액을 한 통화로 저장하는 것이다.

 

사용시점

금액이 필요한 모든 곳 ( 다중 통화 작업, 반올림 문제 해결)

 

예제: 금액 클래스(자바)

 

public class Money {
        private long amount;
        private Currency currency;

        public Money(double amount, Currency currency) {
                this.currency = currency;
                this.amount = Math.round(amount * centFactor());
        }

        public Money(long amount, Currency currency) {
                this.currency = currency;
                this.amount = amount * centFactor();
        }

        private static final int[] cents = new int[]{1, 10, 100, 1000};

        private int centFactor() {
                return cents[currency.getDefaultFractionDigits()];
        }

        public BigDecimal amount() {
                return BigDecimal.valueOf(amount, currency.getDefaultFractionDigits());
        }

        public Currency currency() {
                return currency;
        }

        public boolean equals(Object other) {
                return (other instanceof Money) && equals((Money) other);
        }

        public boolean equals(Money other) {
                return currency.equals(other.currency) && (amount == other.amount);
        }

        public int hashCode() {
                return (int) (amount ^ (amount >>> 32));
        }

        public Money add(Money other) {
                assertSameCurrencyAs(other);
                return newMoney(amount + other.amount);
        }

        private void assertSameCurrencyAs(Money arg) {
                Assert.equals("money math mismatch", currency, arg.currency);
        }

        private Money newMoney(long amount) {
                Money money = new Money();
                money.currency = this.currency;
                money.amount = amount;
                return money;
        }

        public Money subtract(Money other) {
                assertSameCurrencyAs(other);
                return newMoney(amount - other.amount);
        }

        public int compareTo(Object other) {
                return compareTo((Money) other);
        }

        public int compareTo(Money other) {
                assertSameCurrencyAs(other);
                if (amount < other.amount) return -1;
                else if (amount == other.amount) return 0;
                else return 1;
        }

        public Money multiply(double amount) {
                return multiply(new BigDecimal(amount));
        }

        public Money multiply(BigDecimal amount) {
                return multiply(amount, BigDecimal.ROUND_HALF_EVEN);
        }

        public Money multiply(BigDecimal amount, int roundingMode) {
                return new Money(amount().multiply(amount), currency, roundingMode);
        }

        public Money[] allocate(int n) {
                Money lowResult = newMoney(amount / n);
                Money highResult = newMoney(lowResult.amount + 1);
                Money[] results = new Money[n];
                int remainder = (int) amount % n;
                for (int i = 0; i < remainder; i++) results[i] = highResult;
                for (int i = remainder; i < n; i++) results[i] = lowResult;
                return results;
        }

        public Money[] allocate(long[] ratios) {
                long total = 0;
                for (int i = 0; i < ratios.length; i++) total += ratios[i];
                long remainder = amount;
                Money[] results = new Money[ratios.length];
                for (int i = 0; i < results.length; i++) {
                        results[i] = newMoney(amount * ratios[i] / total);
                        remainder -= results[i].amount;
                }
                for (int i = 0; i < remainder; i++) {
                        results[i].amount++;
                }
                return results;
        }
}

 

레코드 집합(Record Set)

테이블 형식 데이터의 인메모리 표현

 

  • 기존의 관계형 Database의 UI에서는 프로그래밍이 거의 없이 손쉽게 데이터를 보고 조작할 수 있었다. 그러나 이러한 환경에서는 데이터를 표시하고 업데이트 하는 작업은 할 수 있었지만 비즈니스 논리를 추가하는 기능은 거의 가지고 있지 않다. 
  • 레코드 집합의 기본 개념은 SQL 쿼리의 결과와 완전히 같은 형식이지만 시스템의 다른 부분에서 생성하고 관리할 수 있는 인메모리 구조를 제공해 활용할 수 있게 하는 것이다.

 

작동원리

  • 일반적으로 레코드 집합은 직접 작성하기보다 작업 환경의 소프트웨어 플랫폼에서 제공하는 경우가 많다. ADO.NET의 데이터 집합이나 JDBC 2.0의 행 집합이 이러한 예에 해당한다.
  • 레코드 집합은 데이터베이스 쿼리의 결과와 완전히 동일해 보인다
  • 레코드 집합을 도메인 논리 코드로 손쉽게 조작할 수 있다.

종류

  1. 암시적 인터페이스 : aReservation["passenger"]
  2. 명시적 인터페이스 : aReservation.passenger (선호)

 

 

사용 시점

레코드 집합의 가치는 레코드 집합을 기반으로 데이터 조작 기능을 제공하는 환경에서 발휘된다.

 

예제 : 9장 테이블 모듈의 예제를 가져왔음 (C#)

public long Insert (long contractID, Decimal amount, DateTime date) {
  DataRow newRow = table.NewRow();
  long id = GetNextID();
  newRow["ID"] = id;
  newRow["contractID"] = contractID;
  newRow["amount"] = amount;
  newRow["date"]= String.Format("{0:s}", date);
  table.Rows.Add(newRow);
  return id;
}

 

현재는 거의 대부분 레코드 집합을 사용하는 것으로 보임.