하마
6k
2020-06-25 12:05:02 작성 2020-06-29 16:02:46 수정됨
10
2599

갑님,부장님 왈 - 왜 이렇게 오래 걸리죠? - feat) Money 클래스


소프트웨어 개발에서는 하나의 개념을 표현 할 때, 굉장한 수준의 차이가 발생합니다. 이런것들이 쌓여서 하나의 결과물로 나오게 되는데요.  모든 것을 일주일치로 생각해서 구현 할 필요는 없겠지만,  뭐가 됬건 고민을 오래 하는 버릇을 들일 수록 자산은 쌓여 가는거 같습니다. 항상 1초만 고민하는 자세면 경력 10년 20년되도 똑같지요.

간만에 마틴의 Money클래스를 떠올라서 스스로 동기부여 차원에서 글 적어봤습니다. 

이렇게 생각 할 수 (때) 도 있고 (제 수준)


public long money = 0L;


이렇게 생각 할 수 (때) 도... (마틴파울러의 구현 수준 on Patterns of Enterprise Application Architecture) 



package com.console.utils.value;

import com.console.core.exceptions.UnknownCurrencyCodeException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Currency;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.Assert;
import static java.math.RoundingMode.HALF_UP;

/**
*
* @author farouka
*/
public class Money implements Serializable {

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

private BigDecimal amount;

private Currency currency;

//private MathContext DEFAULT_CONTEXT = new MathContext( 2, HALF_UP );

private MathContext DEFAULT_CONTEXT = new MathContext( 10, RoundingMode.HALF_DOWN );

public Money(long amount, Currency currency) {
this.currency = currency;
this.amount = BigDecimal.valueOf(amount, currency.getDefaultFractionDigits());
}

/**
* Creates a currency object from the long value provided assuming the long value
* represents the base currency in the least monetary unit. For eg, new Money(500, "GBP")
* is assumed to mean 5.00 great british pounds
* @param amount in base monetary unit
* @param currCode
* @throws com.console.core.exceptions.UnknownCurrencyCodeException
*/
public Money(long amount, String currCode) throws UnknownCurrencyCodeException {
this( amount, Currency.getInstance(currCode) );
}

/**
* Construct an IMMUTABLE money object from a double. It is assumed that
* the whole part of the double is the Money with the fractional part representing
* lowest denominator of the currency. For eg, new Money (50.99, "GBP") is assumed
* to be 50 pounds and 99 pence.
* PS. 89.788 will be truncated to 89.78 based on the defaultcurrencydigit of the currency
* @param amount
* @param curr
*/
public Money(double amount, Currency curr) {
this.currency = curr;
BigDecimal bd = BigDecimal.valueOf( amount );
this.amount = bd.setScale(centFactor(), HALF_UP);
}

private Money() {
}

/**
* Construct an IMMUTABLE money object from a double. It is assumed that
* the whole part of the double is the Money with the fractional part representing
* lowest denominator of the currency. For eg, new Money (50.99, "GBP") is assumed
* to be 50 pounds and 99 pence.
* PS. 89.788 will be truncated to 89.78 based on the defaultcurrencydigit of the currency
* code supplied
* @param amount
* @param currCode iso 4217 currency code
* @throws com.console.core.exceptions.UnknownCurrencyCodeException
*/
public Money(double amount, String currCode) throws UnknownCurrencyCodeException {
this.currency = Currency.getInstance(currCode);
BigDecimal bd = BigDecimal.valueOf( amount );
this.amount = bd.setScale( currency.getDefaultFractionDigits(), HALF_UP);
}

/**
* Constructs an IMMUTABLE money from a BigDecimal. the BigDecimal provided is only scaled
* to used the default digits in currency object represented by the sting parameter
* @param bigDecimal
* @param currCode ISO 4217 cuurency code
* @throws com.console.core.exceptions.UnknownCurrencyCodeException
*/
public Money(BigDecimal bigDecimal, String currCode ) throws UnknownCurrencyCodeException {
this.currency = Currency.getInstance(currCode);
this.amount = bigDecimal.setScale( currency.getDefaultFractionDigits(), HALF_UP);
}

/**
* Constructs an IMMUTABLE money from a BigDecimal. the BigDecimal provided is only scaled
* to used the default digits in currency object represented by the sting parameter
* @param multiply
* @param currency
*/
public Money(BigDecimal bigDecimal, Currency currency) {
this.currency = currency;
this.amount = bigDecimal.setScale( currency.getDefaultFractionDigits(), HALF_UP);
}

// public boolean assertSameCurrencyAs(Money arg) {
// return this.currency.getCurrencyCode().equals(arg.currency.getCurrencyCode());
// }
//
public boolean assertSameCurrencyAs(Money money) throws IncompatibleCurrencyException{
if ( this.currency == null ) {
throw new IncompatibleCurrencyException( "currency.invalid" );
}
if ( money == null ) {
throw new IncompatibleCurrencyException( "currency.invalid" );
}
Assert.assertEquals("money math mismatch", currency, money.currency);
return true;
}

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

public BigDecimal amount() {
return amount;
}

public long amountAsLong(){
return amount.unscaledValue().longValue();
}

public Currency getCurrency() {
return currency;
}
// common currencies
public static Money dollars(double amount) {
Money result = null;
try {
result = new Money(amount, "USD");
} catch (UnknownCurrencyCodeException ex) {
Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex);
}
return result;
}

public static Money dollars(long amount) {
Money result = null;
try {
result = new Money(amount, "USD");
} catch (UnknownCurrencyCodeException ex) {
Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex);
}
return result;
}

public static Money pounds(double amount) {
Money result = null;
try {
result = new Money(amount, "GBP");
} catch (UnknownCurrencyCodeException ex) {
Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex);
}
return result;
}

public static Money pounds(long amount) {
Money result = null;
try {
result = new Money(amount, "GBP");
} catch (UnknownCurrencyCodeException ex) {
Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex);
}
return result;
}

public static Money pounds(BigDecimal amount) {
Money result = null;
try {
result = new Money(amount, "GBP");
} catch (UnknownCurrencyCodeException ex) {
Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex);
}
return result;
}


@Override
public int hashCode() {
int hash = (int) ( amount.hashCode() ^ (amount.hashCode() >>> 32) );
return hash;
}

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

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

public Money add(Money other) throws Exception{
assertSameCurrencyAs( other );
return newMoney(amount.add(other.amount, DEFAULT_CONTEXT));
}

private int compareTo(Money money) throws Exception {
assertSameCurrencyAs( money );
return amount.compareTo( money.amount );
}

public Money multiply(BigDecimal amount) {
return new Money( this.amount().multiply(amount, DEFAULT_CONTEXT), currency);
}

public Money multiply( BigDecimal amount, RoundingMode roundingMode ) {
MathContext ct = new MathContext( currency.getDefaultFractionDigits(), roundingMode );
return new Money( amount().multiply(amount, ct), currency);
}

private Money newMoney(BigDecimal amount) {
return new Money( amount, this.currency );
}

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

public Money subtract(Money other) throws Exception {
assertSameCurrencyAs(other);
return newMoney( amount.subtract(other.amount, DEFAULT_CONTEXT) );
}

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

public boolean greaterThan(Money other)throws Exception {
return (compareTo(other) > 0);
}

// public Money[] allocate(int n){
// Money lowResult = newMoney( amount.unscaledValue().longValue()/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 = 0; 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;
//
// }

public Money divideByNumber( double divisor){
BigDecimal div = BigDecimal.valueOf( divisor );
BigDecimal ans = this.amount.divide(div, DEFAULT_CONTEXT);
return new Money(ans, this.currency);
}

public int getQuotient( Money divisor ){
BigDecimal ans = this.amount.divide(divisor.amount, RoundingMode.DOWN);
return ans.intValue();
}

/**
* divides toe moneys and return the quotient and Remainder this method has been customised,
* for my money transfer needs...sorry
* @param divisor
* @return
*/
public int[] getQuotientandRemainder(Money divisor){
int[] ans = new int[2];
BigDecimal[] bdArr = this.amount.divideAndRemainder(divisor.amount, DEFAULT_CONTEXT);
BigDecimal quo = bdArr[0];
BigDecimal rem = bdArr[1];
ans[0] = quo.intValue();
if( rem.compareTo(BigDecimal.ZERO) == 0 ){
ans[1] =0;
}else{
ans[1] = 1;
}
return ans;
}

public String toFormattedString() {
NumberFormat nf = NumberFormat.getCurrencyInstance();
nf.setCurrency( currency );
nf.setGroupingUsed( true );
nf.setMaximumFractionDigits( currency.getDefaultFractionDigits() );
return nf.format( this.amount.doubleValue() );
}

/**
* Returns the ISO-4217 currency code of the currency
* attached to this money.
*
* @return The ISO-4217 currency code.
*/
public String getCurrencyCode() {
return currency.getCurrencyCode();
}

@Override
public String toString() {
return amount.toString();
}

/**
* Returns the precision for this money. The precision is the total number
* of digits that the value can represent. This includes the integer part.
* So, 18 would be able to represent:
*

* 1234567890.12345678
*

* 1234567890123456.78
*

* 123456789012345678
*

* 0.123456789012345678
*
* @return The precision.
*/
public int precision() {
return amount.precision();
}

/**
* Returns the 'scale' for this money. The scale is the number of
* digits that are moved to the fractional part, assuming that all
* digits are represented by a single integer value. For example:
*

* If: 123456789012345678 has scaling 2, it would be :
*

* 1234567890123456.78
*
* @return The scale value.
*/
public int scale() {
return amount.scale();
}

/**
* Returns the sign for the money (negative or positive).
* -1 if negative, 0 if 0.00 (zero), 1 if positive.
*
* @return The sign of the money.
*/
public int signum() {
return amount.signum();
}
}

8
3
  • 댓글 10

  • fender
    17k
    2020-06-25 12:12:35 작성 2020-06-25 12:16:27 수정됨

    개발 분야의 어려운 점을 잘 짚어주신 내용인 듯 합니다.

    개발 관련해서는 아랫쪽에 있을 때는 윗쪽에서 어떤 내용으로 무슨 고민을 하는지 알기 어렵기도 하지만, 또 어떻게 그걸 깨고 올라가더라도 어느 시점에선 자신감이 붙어 "여기가 정상인가 보다"라고 착각하기도 쉬운 것 같습니다.

    아마 그런식으로 한 때 실력 있었던 개발자들도 기술 동향이 바뀌면 적응 못하고 도태되는 게 아닐까 싶습니다.

    사실 저 부터도 기술의 변화를 민감하게 안챙기게 된지 한참된 것 같네요.

    최근들어 파이선을 쓸 일이 생겨서 오랜만에 새로운 걸 배우는데 열심히 해봐야겠습니다.

    (그리고 보니 관련 내용을 찾다가 하마님 블로그도 읽은 기억이 있습니다. 역시 좋은 글 많이 쓰시더군요 :) 오키에도 가끔 수준 높은 내용 많이 소개해주시면 좋겠습니다.)

    2
  • fender
    17k
    2020-06-25 12:25:08 작성 2020-06-25 14:46:08 수정됨

    여담이지만 마틴파울러의 구현인데도 "throws Exception" 같은 시그네쳐가 보이는 게 좀 의아하네요. 다른 사람이 작성한 내용이라면 잘 몰라서 그랬다고 하겠지만, 혹시 어떤 의도 같은 게 있을까요?

    0
  • ty82lee
    3k
    2020-06-25 12:53:44

    지금도 "돈" 하면 맨윗줄 한줄만 생각나는데... 

    많은걸 생각하게 하는 글이네요. 좋은 글 감사합니다. 

    0
  • daywalker
    513
    2020-06-25 14:50:09

    유지보수와 확장성이 좋을수록 개발자는 행복해지는게 아닐까 생각합니다.

    그리고 요구사항에 적절한 설계와 구현이 이루어져야겠죠.


    요구사항을 분석하고 설계해서 어떻게 하면 좀더 확장성도 좋고 유지보수도 쉽고 보기에도 편한

    그런 아름다운 개발을 할 수 있을까를 항상 고민하게 되죠.


    0
  • 무명소졸
    6k
    2020-06-25 16:49:38

    오래만이십니다.

    0
  • 하마
    6k
    2020-06-25 18:09:57

    무명소졸// 안녕하세요.  😄 

    0
  • 하마
    6k
    2020-06-26 09:18:19

     fender // 잘 모르겠습니다 -.-a  글쩍~

    0
  • 개발자만다린
    -201
    2020-06-28 01:27:21

    하마님의 깊이와 능력은 저도 인정합니다! ㅎ

    (제 상관이기도 하셨고~ㅋ)


    -3
  • choju
    1k
    2020-07-01 10:33:51

    뭐가 맞는건지 가치판단이 쉽지 않은것 같습니다.

    결국 타겟을 명확히해서 구현한다고하면 심플하게 1번이 오히려 나을때도 있을 것 같아요..

    기획의도가 명확한가? 가더 중요한 문제이지 않나 싶습니다..

    1
  • 익스플로
    85
    2020-07-01 16:27:15

    도메인에서 요구하는 범위에 따라 달라질 수 있지 않을까요?

    사용하지도 않을것들을 미리 만들어버리면 오버엔지니어링이 되어버리고...

    제 얕은 경험으로는 정답은 없는것 같고 도메인에 따라 달라질 수 있는것 같습니다...

    1
  • 로그인을 하시면 댓글을 등록할 수 있습니다.