예제 코드
예제 코드는 Github : blog-sample 에 공개 되어 있습니다. start 좀… 아직 많이 부족해서 미흡한 부분이 많습니다. 댓글로 지적 및 조언해주시면 감사드리겠습니다.
요구사항
해외 송금에 필요한 계산기 기능
미국 USD 에서 대한민국 KRW 로 계산 기능
ex -> $500 -> ₩539,337
미국 -> 한국 환율을 정보는 BankOfAmerica 은행사 API를 사용 해야한다
대한민국 KRW 에서 미국 USD 로 계산 기능
₩500,000 - > $463.43
한국 -> 미국 환율을 정보는 신한 은행사 API를 사용 해야한다.
도메인
보내는 곳 , 받는 곳 이 있다고 생각하고 도메인을 생각 했습니다.
Remittance : 송금
금액을 송금합니다.
금액을 송금 하기 위해서 보내는 금액 , 보내는 금액의 통화 , 보내는나라 는 필수 입니다.
Deposit : 입금
금액을 입금 받습니다.
금액을 입금 받기 하기 위해서 받는 금액 , 받는 금액의 통화 , 받는나라 는 필수 입니다.
공통 키워드
… 위해서 받는 금액 , 받는 금액의 통화 , 받는나라 는 필수 입니다.
Money
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Getter public static class Money { private double amount; private Currency currency; private Locale locale; private String amountCurrencyFormat; @Builder public Money (double amount, Currency currency, Locale locale) { this .amount = amount; this .currency = currency; this .locale = locale; this .amountCurrencyFormat = formattingCurrency(); } ... ... }
공통 키워드를 만족시키는 클래스를 작성 헸습니다.
받는 금액 : amount
, 받는 금액의 통화 : currency
, 받는나라 : locale
돈을 받는 곳, 보내는 곳 모든곳에서 사용하는 자료형(클래스)로 만들어 재사용 성을높일 수 있습니다.
아래 코드 보다 확실히 비즈니스를 이해하기 쉽고 관리하기 편합니다.
1 2 3 4 5 6 private double sendMoney;private double receiveMoney;private Currency sendCurrency;private Currency receiveCurrency;private Locale sendLocale;private Locale receiveLocale;
Transaction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static class Transaction { private Remittance remittance; private Deposit deposit; ... } public static class Remittance { private Money money; ... } public static class Deposit { private Money money; ... } public static class Money { private double amount; private Currency currency; private Locale locale; private String amountCurrencyFormat; ... }
거래를 하기 위해서 송금, 입금이 필요 하다는 것을 쉽게 알 수 있습니다.
송금, 입금을 하기 위해서는 Money라는 타입이 필요합니다.
Money 타입에는 거래를 하기 위한 필요 데이터들이 모여 있습니다.
요구사항 구현
도메인도 작업이 어느정도 완료됬으니 요구사항에 필요한 기능을 개발해 보겠습니다.
BankOfAmericaApi
1 2 3 4 5 6 7 8 9 10 11 public class BankOfAmericaApi { public ApiCommonDto.ExchangeRate consumeExchangeRate () { return ApiCommonDto.ExchangeRate.builder() .rate(1059.999963 ) .corridor("US_TO_KOR" ) .corridor("BANK_OF_AMERICA" ) .build(); } }
Bank Of America 은행사 API 호출을 담당하는 클래스입니다. 이 클래스를 통해서 KRW -> USD 환율 정보를 가져옵니다.
ShinhanApi.class
1 2 3 4 5 6 7 8 9 10 public class ShinhanApi { public ApiCommonDto.ExchangeRate consumeExchangeRate () { return ApiCommonDto.ExchangeRate.builder() .rate(0.000943 ) .corridor("KOR_TO_US" ) .company("Shinhan" ) .build(); } }
신한 은행사 API 호출을 담당하는 클래스입니다. 이 클래스를 통해서 통해서 USD -> KRW 환율 정보를 가져옵니다.
ExchangeRate
1 2 3 public interface ExchangeRate { ApiCommonDto.ExchangeRate getExchangeRate (CalculatorDto.Transaction transaction) ; }
환율 정보를 가져오는 것을 추상화 시킨 인터페이스입니다.
getExchangeRate
추상화 메소드를 통해서 하위의 세부 구현체에서 구현하게 됩니다.
ShinhanExchangeRate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ShinhanExchangeRate implements ExchangeRate { ... @Override public ApiCommonDto.ExchangeRate getExchangeRate (CalculatorDto.Transaction transaction) { return shinhanApi.consumeExchangeRate(); } ... } public class BankOfAmericaExchangeRate implements ExchangeRate { ... @Override public ApiCommonDto.ExchangeRate getExchangeRate (CalculatorDto.Transaction transaction) { return bankOfAmericaApi.consumeExchangeRate(); } ... }
ShinhanApi, BankOfAmericaApi 클래스를 이용해서 환율 정보를 가져옵니다.(실제 인행사를 호출하는 코드가 아닙니다. 그냥 하드코딩된 값을 리턴합니다.)
ExchangeRate 인터페이스를 구현하고 있습니다. 이 것을 통해서 IoC 효과를 갖을 수 있습니다.
처음에는 BankOfAmericaApi, ShinhanApi 클래스들을 인터페이스를 통해서 묶으려고 했습니다. 하지만 그것은 잘못된 설계라고 생각합니다. 객체는 자율적인 책임을 져야 하는데 인터페이스로 묶으면 객체들의 자율적인 책임을 방해하게 됩니다. 그 이유는 인터페이스의 추상화 메소드로 인해서 리턴해야할 값과 메게변수로 값이 고정됩니다. 이렇게 고정되면 은행사마다 API 호출 시 인증에 필요한 값, 넘겨야 할 데이터 등등 이 다를 수밖에 없는데 이것을 추상화시킨다는 것 자체가 바람직하지 않습니다. 예를 들어 다른 은행사의 API가 추가되면 또 그때 추상화(리턴 타입의 변경, 매개변수 변경)가 다시 요구됩니다.
Calculator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Calculator { ... public CalculatorDto.Res calculate (CalculatorDto.Transaction transaction) { final ExchangeRate exchangeRate = getInstanceByLocale(transaction); final double remittanceAmount = transaction.getRemittance().getMoney().getAmount(); final double rate = exchangeRate.getExchangeRate(transaction).getRate(); calculateDepositAmount(transaction.getDeposit(), remittanceAmount, rate); ... } ... private ExchangeRate getInstanceByLocale (CalculatorDto.Transaction transaction) { final Locale locale = transaction.getRemittance().getMoney().getLocale(); return exchangeRateFactory.getInstanceByLocale(locale); } ... }
실제로 환율 정보를 계산을 담당하는 클래스입니다.
getInstanceByLocale
메소드를 통해서 보내는 국가가 어디냐에 따라서 ExchangeRate
에 알맞는 은행 API가 의존성 주입됩니다.
은행이 추가되더라도 getInstanceByLocale
메서드에 의존성만 추가해주면 OCP 를 준수하는 코드가 됩니다.
이렇게 IoC를 이용해서 의존성 주입이 일어나느 것이 확장 및 유지보수에도 엄청난 장점이 있습니다. 이런 코드가 없다면 if, if, if 이 지속적으로 추가되며 앞에 작성된 if문을 이해하고 알맞는 위치에 또 if문을 추가해야 하는 악순환이 시작됩니다.
USD -> KOR 계산
Request
1 2 3 4 curl -X GET \ 'http://localhost:8080/calculator?remittanceAmount=1000&remittanceCurrency=USD&remittanceLocal=US&depositAmount=0&depositCurrency=KRW&depositLocal=KR' \ -H 'cache-control: no-cache' \ -H 'postman-token: a1e724a0-0ec9-195b-b744-221b3f238c3b'
Response
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "remittance" : { "money" : { "amount" : 1000 , "currency" : "USD" , "locale" : "en_US" , "amountCurrencyFormat" : "$1,000.00" } }, "deposit" : { "money" : { "amount" : 1059999.963 , "currency" : "KRW" , "locale" : "ko_KR" , "amountCurrencyFormat" : "₩1,060,000" } }, "exchangeRate" : { "value" : 1059.999963 } }
KRW -> USD 계산
Request
1 2 3 4 curl -X GET \ 'http://localhost:8080/calculator?remittanceAmount=1000000&remittanceCurrency=KRW&remittanceLocal=KR&depositAmount=0&depositCurrency=USD&depositLocal=US' \ -H 'cache-control: no-cache' \ -H 'postman-token: 33800725-5db0-1eca-926b-e0269da9d28c'
Response
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "remittance" : { "money" : { "amount" : 1000000 , "currency" : "KRW" , "locale" : "ko_KR" , "amountCurrencyFormat" : "₩1,000,000" } }, "deposit" : { "money" : { "amount" : 943 , "currency" : "USD" , "locale" : "en_US" , "amountCurrencyFormat" : "$943.00" } }, "exchangeRate" : { "value" : 0.000943 } }