3.1 실습 프로젝트 소개
- 편의점 결제 서비스 프로그램
- 스프링 부트 앱을 아주 쉽게 만들어 준다.
-> 최근에는 maven보다는 gradle을 많이 쓰는 추세이다.
=====================인텔리제이 얼티밋 설치=======================
- 인텔리제이 얼티밋 깔고 실행시킨 화면
<hide/>
package com.ran.convpay;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ConvpayApplication {
public static void main(String[] args) {
SpringApplication.run(ConvpayApplication.class, args);
}
}
3.2 순수 자바로 이뤄진 프로젝트
Ex) 편의점 결제 서비스 만들기
- 기획자의 요구사항: 편의점 결제 가능, GS25, CU, 세븐일레븐
- 결제 수단은 일단 현금만 가능하도록 하자
1) com.ran.convpay 아래에 service라는 패키지를 만든다.
2) 그 아래에 ConvenientPayService와 MoneyAdapter라는 자바 클래스를 만든다.
3) com.ran.convpay 아래에 dto (data transfer object, 계층 간 데이터 교환을 위한 자바 빈즈)라는 패키지를 만든다.
- dto는 외부와 통신할 때 필요한 객체를 만드는 클래스를 지칭한다. 로직을 가지지 않는 데이터 객체이며 getter/ setter메서드만 가지는 클래스이다.
- dto아래에 ConvenienceType, PayRequest, PayResponse
4) PayRequest 아래에 getter, setter, 생성자 만든다.
5) PayRequest에서 편의점 종류와 결제 금액을 받아서 PayResponse(결제 결과, 결제 성공금액)을 받도록 한다.
- Ctrl + Shift + t => 해당 메서드에서 새로운 테스트를 생성 가능하다.
- dto의 하위 클래스들
<hide/>
package com.ran.convpay.dto;
public enum ConvenienceType {
G25,
CU,
SEVEN
}
<hide/>
package com.ran.convpay.dto;
public class PayRequest {
// 편의점 종류
ConvenienceType convenientType;
// 결제 금액
Integer payAmount;
public PayRequest(ConvenienceType convenientType, Integer payAmount) {
this.convenientType = convenientType;
this.payAmount = payAmount;
}
public ConvenienceType getConvenientType() {
return convenientType;
}
public void setConvenientType(ConvenienceType convenientType) {
this.convenientType = convenientType;
}
public Integer getPayAmount() {
return payAmount;
}
public void setPayAmount(Integer payAmount) {
this.payAmount = payAmount;
}
}
<hide/>
package com.ran.convpay.dto;
public class PayResponse {
// 결제 결과
PayResult payResult;
// 결제 성공 금액
Integer paidAmount;
public PayResponse(PayResult payResult, Integer paidAmount) {
this.payResult = payResult;
this.paidAmount = paidAmount;
}
public PayResult getPayResult() {
return payResult;
}
public void setPayResult(PayResult payResult) {
this.payResult = payResult;
}
public Integer getPaidAmount() {
return paidAmount;
}
public void setPaidAmount(Integer paidAmount) {
this.paidAmount = paidAmount;
}
}
<hide/>
package com.ran.convpay.dto;
public enum PayResult {
SUCCESS,
FAIL
}
- service의 하위 클래스들
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.dto.PayResult;
public class ConvenientPayService {
public PayResponse pay(PayRequest payRequest){
return new PayResponse(PayResult.SUCCESS, 100);
}
public void payCancel(){
}
}
<huide/>
package com.ran.convpay.service;
public class MoneyAdapter {
// 결제 요청
public void pay(){
}
public void payCancel(){
}
}
Note) 실행 결과
3.3 순수 자바로 이뤄진 프로젝트
- 위에 클래스 내용을 수정한다.
- "프로젝트 뷰에서 파일 선택"의 단축키로 Ctrl + Shift + G 설정한다.
Ex) money_use_success 실행
<hide/>
package com.ran.convpay.service;
import org.junit.jupiter.api.Test;
import static com.ran.convpay.service.MoneyUseResult.*;
import static org.junit.jupiter.api.Assertions.*;
class MoneyAdapterTest {
MoneyAdapter moneyAdapter = new MoneyAdapter(); // 테스트용
@Test
void money_use_fail(){
// given
Integer payAmount = 1_000_001;
// when
MoneyUseResult moneyUseResult = moneyAdapter.use(payAmount);
// then
assertEquals(USE_FAIL, moneyUseResult);
}
@Test
void money_use_success(){
// given
Integer payAmount = 1_000_000;
// when
MoneyUseResult moneyUseResult = moneyAdapter.use(payAmount);
// then
assertEquals(USE_SUCCESS, moneyUseResult);
}
}
Note) 실행 결과 - 성공
Ex)
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.ConvenienceType;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.dto.PayResult;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ConvenientPayServiceTest {
ConveniencePayService conveniencePayService = new ConveniencePayService();
@Test
void paySuccess() {
// given
PayRequest payRequest = new PayRequest(ConvenienceType.G25, 50);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.SUCCESS, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(50, payResponse.getPaidAmount()); // 금액이
}
}
- 일반적으로 fail test에 예외 케이스 여러 개 있고 성공 케이스가 있을 때, 맨 앞에 success case가 있으면 읽기가 어렵다.
-> 예외 케이스는 중간에 두고 성공 케이스는 맨 뒤에 둬야한다.
Note) 실행 결과
Ex)
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.ConvenienceType;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.dto.PayResult;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ConvenientPayServiceTest {
ConveniencePayService conveniencePayService = new ConveniencePayService();
@Test
void paySuccess() {
// given
PayRequest payRequest = new PayRequest(ConvenienceType.G25, 1_000_001);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.FAIL, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(0, payResponse.getPaidAmount()); // 금액이
}
}
Note) 실행 결과
ch-3-3 까지의 진행 상황
===========================================================================
Ex)
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.MoneyUseCancelResult;
import com.ran.convpay.type.MoneyUseResult;
import org.junit.jupiter.api.Test;
import static com.ran.convpay.type.MoneyUseResult.*;
import static org.junit.jupiter.api.Assertions.*;
class MoneyAdapterTest {
MoneyAdapter moneyAdapter = new MoneyAdapter(); // 테스트용
@Test
void money_use_fail(){
// given
Integer payAmount = 1_000_001;
// when
MoneyUseResult moneyUseResult = moneyAdapter.use(payAmount);
// then
assertEquals(USE_FAIL, moneyUseResult);
}
@Test
void money_use_success(){
// given
Integer payAmount = 1_000_000;
// when
MoneyUseResult moneyUseResult = moneyAdapter.use(payAmount);
// then
assertEquals(USE_SUCCESS, moneyUseResult);
}
@Test
void money_use_cancel_success(){
// given
Integer payCancelAmount = 101;
// when
MoneyUseCancelResult moneyUseCancelResult = moneyAdapter.useCancel(payCancelAmount); // ??
// then
assertEquals(MoneyUseCancelResult.MONEY_USE_CANCEL_SUCCESS, moneyUseCancelResult);
}
@Test
void money_use_cancel_fail(){
// given
Integer payCancelAmount = 99;
// when
MoneyUseCancelResult moneyUseCancelResult = moneyAdapter.useCancel(payCancelAmount); // ??
// then
assertEquals(MoneyUseCancelResult.MONEY_USE_CANCEL_FAIL, moneyUseCancelResult);
}
}
Note) 실행 결과
Ex)
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.*;
import com.ran.convpay.type.MoneyUseCancelResult;
import com.ran.convpay.type.MoneyUseResult;
import com.ran.convpay.type.PayCancelResult;
import com.ran.convpay.type.PayResult;
public class ConveniencePayService { // 편결이
private final MoneyAdapter moneyAdapter = new MoneyAdapter(); // final 처리한다. 바꿀 일 없다.
public PayResponse pay(PayRequest payRequest){
MoneyUseResult moneyUseResult = moneyAdapter.use(payRequest.getPayAmount());
if(moneyUseResult == MoneyUseResult.USE_FAIL){
return new PayResponse(PayResult.FAIL, 0);
}
// Success case
return new PayResponse(PayResult.SUCCESS, payRequest.getPayAmount()); // 성공한 경우에는 메서드의 값을 매개변수로 넣는다.
}
public PayCancelResponse payCancel(PayCancelRequest payCancelRequest){ // 결제 취소
MoneyUseCancelResult moneyUseCancelResult = moneyAdapter.useCancel(payCancelRequest.getPayCancelAmount());
if(moneyUseCancelResult == MoneyUseCancelResult.MONEY_USE_CANCEL_FAIL){
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_FAIL, 0);
}
// Success case
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelRequest.getPayCancelAmount());
}
}
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.PayCancelRequest;
import com.ran.convpay.dto.PayCancelResponse;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.type.PayCancelResult;
import com.ran.convpay.type.PayResult;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ConvenientPayServiceTest {
ConveniencePayService conveniencePayService = new ConveniencePayService();
@Test
void pay_success() {
// given
PayRequest payRequest = new PayRequest(ConvenienceType.G25, 50);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.SUCCESS, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(50, payResponse.getPaidAmount()); // 금액이
}
@Test
void pay_fail() {
// given
PayRequest payRequest = new PayRequest(ConvenienceType.G25, 1_000_001);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.FAIL, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(0, payResponse.getPaidAmount()); // 금액이
}
@Test
void pay_cancel_success() {
// given
PayCancelRequest payCancelRequest = new PayCancelRequest(ConvenienceType.G25, 1000);
// when
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
// then
assertEquals(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelResponse.getPayCancelResult()); // get결과가 성공이고
assertEquals(1000, payCancelResponse.getPayCanceledAmount()); // 금액이
}
@Test
void pay_cancel_fail() {
// given
PayCancelRequest payCancelRequest = new PayCancelRequest(ConvenienceType.G25, 99);
// 금액이 적을 때 나는 오류니까 줄이다.
// when
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
// then
assertEquals(PayCancelResult.PAY_CANCEL_FAIL, payCancelResponse.getPayCancelResult()); // get결과가 성공이고
assertEquals(0, payCancelResponse.getPayCanceledAmount()); // 금액이
}
}
Note) 위의 코드 중에서 pay_cancel_fail 실행한 화면
- 지금까지 공부한 내용은 클래스들로만 이루어져있고 외부 사용자가 없다.
- 스프링 어플리캐이션이 ????을 해주는 역할이지만 순수 java로 만들기 위해서 UserClient클래스를 만들어서 활용한다.
Ex)
1) convpay 아래에 UserClient 클래스를 만든다. - 클래스는 매인 메서드를 만들어서 단순히 편결이 서비스를 만들어서 결제시키는 역할을하도록 한다.
<hide/>
package com.ran.convpay;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.service.ConveniencePayService;
import com.ran.convpay.type.ConvenienceType;
public class UserClient {
public static void main(String[] args) {
// 사용자 = > 편결이 => 머니
// 사용자는 편결이가 필요한다.
ConveniencePayService conveniencePayService = new ConveniencePayService();
// GS25 결제 1000원
PayRequest payRequest = new PayRequest(ConvenienceType.G25, 1000);
PayResponse payResponse = conveniencePayService.pay(payRequest);
System.out.println(payResponse);
// GS25 취소 500원
}
}
- 아래와 같이 toString()을 오버라이드 해줘야 매인함수에서 출력했을 때, 주솟값이 나오지 않는다.
<hide/>
package com.ran.convpay.dto;
import com.ran.convpay.type.PayResult;
public class PayResponse {
// 결제 결과
PayResult payResult;
// 결제 성공 금액
Integer paidAmount;
@Override
public String toString() {
return "PayResponse{" +
"payResult=" + payResult +
", paidAmount=" + paidAmount +
'}';
}
public PayResponse(PayResult payResult, Integer paidAmount) {
this.payResult = payResult;
this.paidAmount = paidAmount;
}
public PayResult getPayResult() {
return payResult;
}
public void setPayResult(PayResult payResult) {
this.payResult = payResult;
}
public Integer getPaidAmount() {
return paidAmount;
}
public void setPaidAmount(Integer paidAmount) {
this.paidAmount = paidAmount;
}
}
Note) 실행결과
Ex)
- 아래와 같이 PayCancelResponse 클래스에 toString()을 오버라이드해준다.
<hide/>
package com.ran.convpay.dto;
import com.ran.convpay.type.PayCancelResult;
public class PayCancelResponse {
PayCancelResult payCancelResult;
Integer payCanceledAmount;
public PayCancelResponse(PayCancelResult payCancelResult, Integer payCanceledAmount) {
this.payCancelResult = payCancelResult;
this.payCanceledAmount = payCanceledAmount;
}
public PayCancelResult getPayCancelResult() {
return payCancelResult;
}
public void setPayCancelResult(PayCancelResult payCancelResult) {
this.payCancelResult = payCancelResult;
}
public Integer getPayCanceledAmount() {
return payCanceledAmount;
}
public void setPayCanceledAmount(Integer payCanceledAmount) {
this.payCanceledAmount = payCanceledAmount;
}
@Override
public String toString() {
return "PayCancelResponse{" +
"payCancelResult=" + payCancelResult +
", payCanceledAmount=" + payCanceledAmount +
'}';
}
}
<hide/>
package com.ran.convpay;
import com.ran.convpay.dto.PayCancelRequest;
import com.ran.convpay.dto.PayCancelResponse;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.service.ConveniencePayService;
import com.ran.convpay.type.ConvenienceType;
public class UserClient {
public static void main(String[] args) {
// 사용자 = > 편결이 => 머니
// 사용자는 편결이가 필요한다.
ConveniencePayService conveniencePayService = new ConveniencePayService();
// GS25 결제 1000원
PayRequest payRequest = new PayRequest(ConvenienceType.G25, 1000);
PayResponse payResponse = conveniencePayService.pay(payRequest);
System.out.println(payResponse);
// GS25 취소 500원
PayCancelRequest payCancelRequest = new PayCancelRequest(ConvenienceType.G25, 500);
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
System.out.println(payCancelResponse);
}
}
Note) 실행 결과
3.5 순수 자바로 이뤄진 프로젝트 - 4
Ex) 프로젝트 2년차 개발 (순수 자바)
- OCP(개방 폐쇄 원칙) vs DIP(의존성 역전 원칙 - 어댑터들이 우리의 규칙- 인터페이스를 지키도록 만들자)
- 결제 수단별로 결제 방법이 다를 수 있다.
-> 결제 서비스가 카드 어댑터에 지나치게 의존하면? 어댑터에 어떤 변수가 생기면 결제 서비스를 수정해야한다.
-> 아주 복잡하다.
-> PaymentInterface를 만든다.
- 카드 어댑터 만든다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.CardUseCancelResult;
import com.ran.convpay.type.CardUseResult;
public class CardAdapter {
// 1.인증
public void authorization() {
System.out.println("authorization success. ");
}
// 2. 승인
public void approval() {
System.out.println("approval success. ");
}
// 3. 매일
public CardUseResult capture(Integer payAmount) {
if (payAmount > 100) {
return CardUseResult.USE_FAIL;
}
return CardUseResult.USE_SUCCESS;
}
// 4. 매입 취소
public CardUseCancelResult cancelCapture(Integer cancelAmount){
if(cancelAmount < 1000){
return CardUseCancelResult.USE_CANCEL_FAIL;
}
return CardUseCancelResult.USE_CANCEL_SUCCESS;
}
}
- 그런데, ?? 메서드의 반환형으로 CardUserResult 로 하기 위해 아래처럼 enum을 만든다.
<hide/>
package com.ran.convpay.type;
public enum CardUseResult {
USE_SUCCESS,
USE_FAIL
}
<hide/>
package com.ran.convpay.type;
public enum CardUseCancelResult {
USE_CANCEL_SUCCESS,
USE_CANCEL_FAIL
}
- CardTest를 만든다.
-> assertEquals(x, y): 두 객체의 값이 같은지 확인한다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.CardUseResult;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CardAdapterTest {
private CardAdapter cardAdapter = new CardAdapter();
@Test
void capture_success() { // 매입 성공
// given
Integer payAmount = 99;
// when
CardUseResult cardUseResult = cardAdapter.capture(payAmount);
// then
assertEquals(CardUseResult.USE_SUCCESS, cardUseResult);
}
}
Note) 실행 결과
- 아래와 같이 fail 과 cancelCapture_success를 추가한다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.CardUseCancelResult;
import com.ran.convpay.type.CardUseResult;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CardAdapterTest {
private CardAdapter cardAdapter = new CardAdapter();
@Test
void capture_success() { // 매입 성공
// given
Integer payAmount = 99;
// when
CardUseResult cardUseResult = cardAdapter.capture(payAmount);
// then
assertEquals(CardUseResult.USE_SUCCESS, cardUseResult);
}
@Test
void capture_fail() { // 매입 실패
// given
Integer payAmount = 101;
// when
CardUseResult cardUseResult = cardAdapter.capture(payAmount);
// then
assertEquals(CardUseResult.USE_FAIL, cardUseResult);
}
@Test
void cancelCapture_success() {
// given
Integer cancelAmount = 1001;
// when
CardUseCancelResult cardUseCancelResult = cardAdapter.cancelCapture(cancelAmount);
// then
assertEquals(CardUseCancelResult.USE_CANCEL_SUCCESS, cardUseCancelResult);
}
}
Note) 각 메서드를 실행하면 테스트 성공한다.
- conceniencePayService 클래스에 다음과 같이 추가한다.
-> 이 클래스는 하위 클래스 먼저 작성한 다음에 내용을 추가한다.
-> if문의 조건식도 오류가 나고 로직이 상당히 더럽다는 것을 알 수 있다.
-> 따라서, DIP(의존 역전 원칙) 구조에 따라,
-> 결제 서비스가 머니 어댑터와 카드 어댑터에 의존하는 게 아니라 Payment 인터페이스에만 의존하도록 수정할 계획
-> 각 어댑터도 Payment 인터페이스에만 의존하도록 만든다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.*;
import com.ran.convpay.type.*;
public class ConveniencePayService { // 편결이
private final CardAdapter cardAdapter = new CardAdapter();
private final MoneyAdapter moneyAdapter = new MoneyAdapter(); // final 처리한다. 바꿀 일 없다.
public PayResponse pay(PayRequest payRequest){
CardUseResult cardUseResult;
MoneyUseResult moneyUseResult;
if(payRequest.getPayMethodType() == PayMethodType.CARD){
cardAdapter.authorization();
cardAdapter.approval();
cardUseResult = cardAdapter.capture(payRequest.getPayAmount());
}else{
moneyUseResult = moneyAdapter.use(payRequest.getPayAmount());
}
if(cardUseResult == CardUseResult.USE_FAIL ||
moneyUseResult == MoneyUseResult.USE_FAIL){ // 에러 난다.
return new PayResponse(PayResult.FAIL, 0);
}
// Success case
return new PayResponse(PayResult.SUCCESS, payRequest.getPayAmount()); // 성공한 경우에는 메서드의 값을 매개변수로 넣는다.
}
public PayCancelResponse payCancel(PayCancelRequest payCancelRequest){ // 결제 취소
MoneyUseCancelResult moneyUseCancelResult = moneyAdapter.useCancel(payCancelRequest.getPayCancelAmount()); // useCancel을 통해 사용 취소
if(moneyUseCancelResult == MoneyUseCancelResult.MONEY_USE_CANCEL_FAIL){
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_FAIL, 0);
}
// Success case
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelRequest.getPayCancelAmount()); // 성공한 경우, 내가 요청한 금액
}
}
- PayRequest 내용 추가
<hide/>
package com.ran.convpay.dto;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.type.PayMethodType;
public class PayRequest {
// 결제 수단
PayMethodType payMethodType;
// 편의점 종류
ConvenienceType convenientType;
// 결제 금액
Integer payAmount;
public PayRequest(PayMethodType payMethodType, ConvenienceType convenientType, Integer payAmount) {
this.payMethodType = payMethodType;
this.convenientType = convenientType;
this.payAmount = payAmount;
}
public PayMethodType getPayMethodType() {
return payMethodType;
}
public void setPayMethodType(PayMethodType payMethodType) {
this.payMethodType = payMethodType;
}
public ConvenienceType getConvenientType() {
return convenientType;
}
public void setConvenientType(ConvenienceType convenientType) {
this.convenientType = convenientType;
}
public Integer getPayAmount() {
return payAmount;
}
public void setPayAmount(Integer payAmount) {
this.payAmount = payAmount;
}
}
- payMethodType 열거형 추가
<hide/>
package com.ran.convpay.type;
public enum PayMethodType {
MONEY,
CARD
}
3.6 순수 자바로 이뤄진 프로젝트 - 5
Ex) PaymentIterface를 만들어서 두 개의 클래스가 구현하도록한다.
- paymentInterface를 만든다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.CancelPaymentResult;
import com.ran.convpay.type.PaymentResult;
public interface PaymentInterface {
PaymentResult payment(Integer payAmount); // 인터페이스가 public이라 여기는 접근 제어자 필요없다.
CancelPaymentResult cancelPayment(Integer cancelAmount);
}
- enum을 만든다.
<hide/>
package com.ran.convpay.type;
public enum PaymentResult {
PAYMENT_SUCCESS,
PAYMENT_FAIL
}
<hide/>
package com.ran.convpay.type;
public enum CancelPaymentResult {
CANCEL_PAYMENT_SUCCESS,
CANCEL_PAYMENT_FAIL
}
- paymentInterface를 구현하는 MoneyAdapter
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.CancelPaymentResult;
import com.ran.convpay.type.MoneyUseCancelResult;
import com.ran.convpay.type.MoneyUseResult;
import com.ran.convpay.type.PaymentResult;
public class MoneyAdapter implements PaymentInterface {
// 결제 요청
public MoneyUseResult use(Integer payAmount){
System.out.println("MoneyAdapter.use: " + payAmount);
if(payAmount > 1_000_000){ // 큰 금액을 사용하려는 경우, 실패
return MoneyUseResult.USE_FAIL;
}
return MoneyUseResult.USE_SUCCESS;
}
public MoneyUseCancelResult useCancel(Integer payCancelAmount){
System.out.println("MoneyAdapter.useCancel: " + payCancelAmount);
if(payCancelAmount < 100){ // 작은 금액을 취소하는 경우 실패
return MoneyUseCancelResult.MONEY_USE_CANCEL_FAIL; // 실패 케이스
}
return MoneyUseCancelResult.MONEY_USE_CANCEL_SUCCESS; // 성공한 케이스
}
@Override
public PaymentResult payment(Integer payAmount){
MoneyUseResult moneyUseResult = use(payAmount);
if(moneyUseResult == MoneyUseResult.USE_FAIL){
return PaymentResult.PAYMENT_FAIL;
}
return PaymentResult.PAYMENT_SUCCESS;
}
@Override
public CancelPaymentResult cancelPayment(Integer cancelAmount){
MoneyUseCancelResult moneyUseCancelResult = useCancel(cancelAmount);
if(moneyUseCancelResult == MoneyUseCancelResult.MONEY_USE_CANCEL_FAIL){
return CancelPaymentResult.CANCEL_PAYMENT_FAIL;
}
return CancelPaymentResult.CANCEL_PAYMENT_SUCCESS;
}
}
- CardAdapter도 인터페이스를 구현한다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.CancelPaymentResult;
import com.ran.convpay.type.CardUseCancelResult;
import com.ran.convpay.type.CardUseResult;
import com.ran.convpay.type.PaymentResult;
public class CardAdapter implements PaymentInterface {
// 1.인증
public void authorization() {
System.out.println("authorization success. ");
}
// 2. 승인
public void approval() {
System.out.println("approval success. ");
}
// 3. 매일
public CardUseResult capture(Integer payAmount) {
if (payAmount > 100) {
return CardUseResult.USE_FAIL;
}
return CardUseResult.USE_SUCCESS;
}
// 4. 매입 취소
public CardUseCancelResult cancelCapture(Integer cancelAmount){
if(cancelAmount < 1000){
return CardUseCancelResult.USE_CANCEL_FAIL;
}
return CardUseCancelResult.USE_CANCEL_SUCCESS;
}
@Override
public PaymentResult payment(Integer payAmount) {
authorization();
approval();
CardUseResult cardUseResult = capture(payAmount);
if(cardUseResult == CardUseResult.USE_FAIL){
return PaymentResult.PAYMENT_FAIL;
}
return PaymentResult.PAYMENT_SUCCESS;
}
@Override
public CancelPaymentResult cancelPayment(Integer cancelAmount) {
CardUseCancelResult cardUseCancelResult = cancelCapture(cancelAmount);
if(cardUseCancelResult == CardUseCancelResult.USE_CANCEL_FAIL){
return CancelPaymentResult.CANCEL_PAYMENT_FAIL;
}
return CancelPaymentResult.CANCEL_PAYMENT_SUCCESS;
}
}
-> 두 개의 다른 클래스를 동일한 방식으로 사용할 수 있도록 한다.
- PayCancelRequest로 이동해서 아래와 같이 결제 수단을 추가한다.
-> getter, setter도 추가한다.
<hide/>
package com.ran.convpay.dto;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.type.PayMethodType;
public class PayCancelRequest {
// 결제 수단
PayMethodType payMethodType;
// 편의점 종류
ConvenienceType convenienceType;
// 결제 취소 금액
Integer payCancelAmount;
public PayCancelRequest(PayMethodType payMethodType, ConvenienceType convenienceType, Integer payCancelAmount) {
this.payMethodType = payMethodType;
this.convenienceType = convenienceType;
this.payCancelAmount = payCancelAmount;
}
public PayMethodType getPayMethodType() {
return payMethodType;
}
public void setPayMethodType(PayMethodType payMethodType) {
this.payMethodType = payMethodType;
}
public ConvenienceType getConvenienceType() {
return convenienceType;
}
public void setConvenienceType(ConvenienceType convenienceType) {
this.convenienceType = convenienceType;
}
public Integer getPayCancelAmount() {
return payCancelAmount;
}
public void setPayCancelAmount(Integer payCancelAmount) {
this.payCancelAmount = payCancelAmount;
}
}
- 편결이
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.*;
import com.ran.convpay.type.*;
public class ConveniencePayService { // 편결이
private final CardAdapter cardAdapter = new CardAdapter();
private final MoneyAdapter moneyAdapter = new MoneyAdapter(); // final 처리한다. 바꿀 일 없다.
public PayResponse pay(PayRequest payRequest){
// 아래의 코드를 추가하면 그 밑에 있는 복잡한 코드를 사용할 필요가 없다.
PaymentInterface paymentInterface;
if(payRequest.getPayMethodType() == PayMethodType.CARD){
paymentInterface = cardAdapter;
}else{
paymentInterface = moneyAdapter;
}
/* 삭제 하는 부분
CardUseResult cardUseResult;
MoneyUseResult moneyUseResult;
if(payRequest.getPayMethodType() == PayMethodType.CARD){
cardAdapter.authorization();
cardAdapter.approval();
cardUseResult = cardAdapter.capture(payRequest.getPayAmount());
}else{
moneyUseResult = moneyAdapter.use(payRequest.getPayAmount());
}
*/
// 수정된 부분분
PaymentResult payment = paymentInterface.payment(payRequest.getPayAmount());
if(payment == PaymentResult.PAYMENT_FAIL){
return new PayResponse(PayResult.FAIL, 0);
}
// Success case
return new PayResponse(PayResult.SUCCESS, payRequest.getPayAmount()); // 성공한 경우에는 메서드의 값을 매개변수로 넣는다.
}
public PayCancelResponse payCancel(PayCancelRequest payCancelRequest){ // 결제 취소
PaymentInterface paymentInterface;
if(payCancelRequest.getPayMethodType() == PayMethodType.CARD){
paymentInterface = cardAdapter;
}else{
paymentInterface = moneyAdapter;
}
CancelPaymentResult cancelPaymentResult = paymentInterface.cancelPayment(payCancelRequest.getPayCancelAmount()); // useCancel을 통해 사용 취소
if(cancelPaymentResult == CancelPaymentResult.CANCEL_PAYMENT_FAIL){
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_FAIL, 0);
}
// Success case
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelRequest.getPayCancelAmount()); // 성공한 경우, 내가 요청한 금액
}
}
- UserClient
<hide/>
package com.ran.convpay;
import com.ran.convpay.dto.PayCancelRequest;
import com.ran.convpay.dto.PayCancelResponse;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.service.ConveniencePayService;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.type.PayMethodType;
public class UserClient {
public static void main(String[] args) {
// 사용자 = > 편결이 => 머니
ConveniencePayService conveniencePayService = new ConveniencePayService();
// GS25 결제 1000원
PayRequest payRequest = new PayRequest(PayMethodType.MONEY, ConvenienceType.G25, 1000);
PayResponse payResponse = conveniencePayService.pay(payRequest);
System.out.println(payResponse);
// GS25 취소 500원
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.CARD, ConvenienceType.G25, 500);
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
System.out.println(payCancelResponse);
}
}
Note) 실행 결과
- 그런데 pay_cancel은 실패했다.
- CardAdapter의 매입 취소 메서드에서 매입 취소의 기준이 1000보다 작으면 PAY_CANCEL_FAIL이기 때문이다.
- UserClient를 수정한다. CARD <=> MONEY만 서로 바꿔준다. 결제 수단 변경
<hide/>
package com.ran.convpay;
import com.ran.convpay.dto.PayCancelRequest;
import com.ran.convpay.dto.PayCancelResponse;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.service.ConveniencePayService;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.type.PayMethodType;
public class UserClient {
public static void main(String[] args) {
// 사용자 = > 편결이 => 머니
ConveniencePayService conveniencePayService = new ConveniencePayService();
// GS25 결제 1000원
PayRequest payRequest = new PayRequest(PayMethodType.CARD,
ConvenienceType.G25, 1000);
PayResponse payResponse = conveniencePayService.pay(payRequest);
System.out.println(payResponse);
// GS25 취소 500원
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.MONEY,
ConvenienceType.G25, 500);
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
System.out.println(payCancelResponse);
}
}
Note) 실행 결과
- payAmount 가 100보다 크면 실패하기 때문에 payResult 실패
- useCancel을 money로 하니까 성공한다.
- 3.5보다 훨씬 코드가 간결해진다.
- ConvenientPayServiceTest 클래스도 수정한다.
-> 결제 수단을 money로 모두 변경한다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.PayCancelRequest;
import com.ran.convpay.dto.PayCancelResponse;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.type.PayCancelResult;
import com.ran.convpay.type.PayMethodType;
import com.ran.convpay.type.PayResult;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ConvenientPayServiceTest {
ConveniencePayService conveniencePayService = new ConveniencePayService();
@Test
void pay_success() {
// given
PayRequest payRequest = new PayRequest(PayMethodType.MONEY, ConvenienceType.G25, 50);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.SUCCESS, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(50, payResponse.getPaidAmount()); // 금액이
}
@Test
void pay_fail() {
// given
PayRequest payRequest = new PayRequest(PayMethodType.MONEY, ConvenienceType.G25, 1_000_001);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.FAIL, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(0, payResponse.getPaidAmount()); // 금액이
}
@Test
void pay_cancel_success() {
// given
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.MONEY, ConvenienceType.G25, 1000);
// when
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
// then
assertEquals(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelResponse.getPayCancelResult()); // get결과가 성공이고
assertEquals(1000, payCancelResponse.getPayCanceledAmount()); // 금액이
}
@Test
void pay_cancel_fail() {
// given
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.MONEY, ConvenienceType.G25, 99);
// 금액이 적을 때 나는 오류니까 줄이다.
// when
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
// then
assertEquals(PayCancelResult.PAY_CANCEL_FAIL, payCancelResponse.getPayCancelResult()); // get결과가 성공이고
assertEquals(0, payCancelResponse.getPayCanceledAmount()); // 금액이
}
}
Note) test 폴더 ctrl + shift + F10 누른 실행 결과 (폴더 안의 내용을 모두 돌린다.)
Ex) 여기서 만약에 포인트 결제가 추가되면?
- 편결이 클래스에 다음과 같이 추가한다.
- 아래에 pointerAdapter라는 변수를 추가했는데 아직 존재하는 클래스가 아니라서 오류가 난다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.*;
import com.ran.convpay.type.*;
public class ConveniencePayService { // 편결이
private final CardAdapter cardAdapter = new CardAdapter();
private final MoneyAdapter moneyAdapter = new MoneyAdapter(); // final 처리한다. 바꿀 일 없다.
private final PointAdapter pointAdapter = new PointAdapter();
public PayResponse pay(PayRequest payRequest){
// 아래의 코드를 추가하면 그 밑에 있는 복잡한 코드를 사용할 필요가 없다.
PaymentInterface paymentInterface;
if(payRequest.getPayMethodType() == PayMethodType.CARD){
paymentInterface = cardAdapter;
}else if(payRequest.getPayMethodType() == PayMethodType.MONEY){
paymentInterface = moneyAdapter;
}else{
paymentInterface = pointAdapter;
}
/* 삭제 하는 부분
CardUseResult cardUseResult;
MoneyUseResult moneyUseResult;
if(payRequest.getPayMethodType() == PayMethodType.CARD){
cardAdapter.authorization();
cardAdapter.approval();
cardUseResult = cardAdapter.capture(payRequest.getPayAmount());
}else{
moneyUseResult = moneyAdapter.use(payRequest.getPayAmount());
}
*/
// 수정된 부분분
PaymentResult payment = paymentInterface.payment(payRequest.getPayAmount());
if(payment == PaymentResult.PAYMENT_FAIL){
return new PayResponse(PayResult.FAIL, 0);
}
// Success case
return new PayResponse(PayResult.SUCCESS, payRequest.getPayAmount()); // 성공한 경우에는 메서드의 값을 매개변수로 넣는다.
}
public PayCancelResponse payCancel(PayCancelRequest payCancelRequest){ // 결제 취소
PaymentInterface paymentInterface;
if(payCancelRequest.getPayMethodType() == PayMethodType.CARD){
paymentInterface = cardAdapter;
}else{
paymentInterface = moneyAdapter;
}
CancelPaymentResult cancelPaymentResult = paymentInterface.cancelPayment(payCancelRequest.getPayCancelAmount()); // useCancel을 통해 사용 취소
if(cancelPaymentResult == CancelPaymentResult.CANCEL_PAYMENT_FAIL){
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_FAIL, 0);
}
// Success case
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelRequest.getPayCancelAmount()); // 성공한 경우, 내가 요청한 금액
}
}
3.7 순수 자바로 이뤄진 프로젝트 - 6
Ex) 프로젝트 3년차, 할인 정책이 생기는 경우
- DiscountInterface를 구현해서 편의점 할인과 결제 수단 할인으로 구분한다.
1) DiscountInterface 인터페이스 만든다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.PayRequest;
public interface DiscountInterface {
Integer getDiscountedAmount(PayRequest payRequest);
}
2) DiscountByConvenience 클래스를 만든다. (편의점 할인의 경우)
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.PayRequest;
public class DiscountByConvenience implements DiscountInterface {
@Override
public Integer getDiscountedAmount(PayRequest payRequest) {
switch (payRequest.getConvenientType()){
case G25:
return payRequest.getPayAmount() * 8 / 10;
case CU:
return payRequest.getPayAmount() * 9 / 10;
case SEVEN:
return payRequest.getPayAmount();
}
return payRequest.getPayAmount();
}
}
- ctrl + shift + T 해서 새 테스트를 생성한다.
3) test
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.type.PayMethodType;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class DiscountByConvenienceTest {
DiscountByConvenience discountByConvenience = new DiscountByConvenience();
@Test
void discountTest(){
// given
PayRequest payRequestG25 =
new PayRequest(PayMethodType.MONEY, ConvenienceType.G25, 1000);
PayRequest payRequestCU =
new PayRequest(PayMethodType.MONEY, ConvenienceType.CU, 1000);
PayRequest payRequestSeven =
new PayRequest(PayMethodType.MONEY, ConvenienceType.SEVEN, 1000);
// when
Integer discountedAmountG25 = discountByConvenience.getDiscountedAmount(payRequestG25);
Integer discountedAmountCU = discountByConvenience.getDiscountedAmount(payRequestCU);
Integer discountedAmountSeven = discountByConvenience.getDiscountedAmount(payRequestSeven);
// then
assertEquals(800, discountedAmountG25);
assertEquals(900, discountedAmountCU);
assertEquals(1000, discountedAmountSeven);
}
}
Note) 실행 결과
Ex)
1) discountByPayMethod 클래스를 만든다.
package com.ran.convpay.service;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.type.MoneyUseCancelResult;
public class DiscountByPayMethod implements DiscountInterface{
@Override
public Integer getDiscountedAmount(PayRequest payRequest) {
switch (payRequest.getPayMethodType()){
case MONEY:
return payRequest.getPayAmount() * 7 / 10;
case CARD:
return payRequest.getPayAmount();
}
return payRequest.getPayAmount();
}
}
2) 새로운 테스트를 생성한다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.type.PayMethodType;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class DiscountByPayMethodTest {
DiscountByPayMethod discountByPayMethod = new DiscountByPayMethod();
@Test
void discountSuccess(){
// given
PayRequest payRequestMoney = new PayRequest(PayMethodType.MONEY,
ConvenienceType.G25, 1000);
PayRequest payRequestCard = new PayRequest(PayMethodType.CARD,
ConvenienceType.G25, 1000);
// when
Integer discountedAmountMoney =
discountByPayMethod.getDiscountedAmount(payRequestMoney);
Integer discountedAmountCard =
discountByPayMethod.getDiscountedAmount(payRequestCard);
// then
assertEquals(700, discountedAmountMoney);
assertEquals(1000, discountedAmountCard);
}
}
3) 편결이 클래스에 인터페이스 변수를 추가한다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.*;
import com.ran.convpay.type.*;
public class ConveniencePayService { // 편결이
private final CardAdapter cardAdapter = new CardAdapter();
private final MoneyAdapter moneyAdapter = new MoneyAdapter(); // final 처리한다. 바꿀 일 없다.
// private final PointAdapter pointAdapter = new PointAdapter();
private final DiscountInterface discountInterface = new DiscountByPayMethod();
public PayResponse pay(PayRequest payRequest){
// 아래의 코드를 추가하면 그 밑에 있는 복잡한 코드를 사용할 필요가 없다.
PaymentInterface paymentInterface = null;
if(payRequest.getPayMethodType() == PayMethodType.CARD){
paymentInterface = cardAdapter;
}else if(payRequest.getPayMethodType() == PayMethodType.MONEY){
paymentInterface = moneyAdapter;
}
Integer discountedAmount = discountInterface.getDiscountedAmount(payRequest);
PaymentResult payment = paymentInterface.payment(discountedAmount);
// 결제할 때도 discountedAmount로 바꾼다.
if(payment == PaymentResult.PAYMENT_FAIL){
return new PayResponse(PayResult.FAIL, 0);
}
// Success case
return new PayResponse(PayResult.SUCCESS, discountedAmount);
// 응답할 때도 discountedAmount로 바꾼다.
}
public PayCancelResponse payCancel(PayCancelRequest payCancelRequest){ // 결제 취소
PaymentInterface paymentInterface;
if(payCancelRequest.getPayMethodType() == PayMethodType.CARD){
paymentInterface = cardAdapter;
}else{
paymentInterface = moneyAdapter;
}
CancelPaymentResult cancelPaymentResult = paymentInterface.cancelPayment(payCancelRequest.getPayCancelAmount()); // useCancel을 통해 사용 취소
if(cancelPaymentResult == CancelPaymentResult.CANCEL_PAYMENT_FAIL){
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_FAIL, 0);
}
// Success case
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelRequest.getPayCancelAmount()); // 성공한 경우, 내가 요청한 금액
}
}
Note) 실행 결과
- 다음과 같이 pay_success, pay_fail 부분에서 실패하는 걸 볼 수 있다.
참고) 실패한 원인이 있는 편결이 클래스
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.PayCancelRequest;
import com.ran.convpay.dto.PayCancelResponse;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.type.PayCancelResult;
import com.ran.convpay.type.PayMethodType;
import com.ran.convpay.type.PayResult;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ConvenientPayServiceTest {
ConveniencePayService conveniencePayService = new ConveniencePayService();
@Test
void pay_success() {
// given
PayRequest payRequest = new PayRequest(PayMethodType.MONEY, ConvenienceType.G25, 50);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.SUCCESS, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(50, payResponse.getPaidAmount()); // 금액이
}
@Test
void pay_fail() {
// given
PayRequest payRequest = new PayRequest(PayMethodType.MONEY, ConvenienceType.G25, 1_000_001);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.FAIL, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(0, payResponse.getPaidAmount()); // 금액이
}
@Test
void pay_cancel_success() {
// given
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.MONEY, ConvenienceType.G25, 1000);
// when
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
// then
assertEquals(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelResponse.getPayCancelResult()); // get결과가 성공이고
assertEquals(1000, payCancelResponse.getPayCanceledAmount()); // 금액이
}
@Test
void pay_cancel_fail() {
// given
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.MONEY, ConvenienceType.G25, 99);
// 금액이 적을 때 나는 오류니까 줄이다.
// when
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
// then
assertEquals(PayCancelResult.PAY_CANCEL_FAIL, payCancelResponse.getPayCancelResult()); // get결과가 성공이고
assertEquals(0, payCancelResponse.getPayCanceledAmount()); // 금액이
}
}
Note) test폴더 실행 결과
(1) pay_success의 assertEquals()의 매개변수도 50에서 30% 할인한 35로 바꿔줘야 한다.
(2) moneyAdapter클래스 확인해보면 1,000,000이 넘으면 결제가 실패하도록 되어 있는데
-> 30% 할인이 들어가서 성공으로 잘못 나온다.
-> 따라서, pay_fail의 매개변수를 1,500,001로 수정해야한다.
Note) 수정 후 test폴더 실행 결과
Ex) 편의점에 따른 결제 수단이 아니라 편의점에 따라서 할인을 하고 싶다면??
- 편결이 클래스에서 아래 코드를 추가하면 된다.
- 할인 정책 변경이 간단하다.
// 제거 private final DiscountInterface discountInterface = new DiscountByPayMethod();
private final DiscountInterface discountInterface = new DiscountByConvenience();
-> 따라서, 바뀔 가능성이 있는 로직을 짤 때는 if-else문이 아닌 인터페이스를 활용하여 구현체를 다양하게 만들어야한다.
3.8 내가 만드는 심플 스프링 프레임워크
- DIP(의존 역전 원칙)와OCP를 지킨다고 지난 시간에 코드를 작성했다.
-> DIP(의존 역전 원칙 - 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.
-> OCP(개방 폐쇄 원칙 - 클래스, 메서드, 모듈은 확장에 Open, 변경에는 Close되어야 한다.)
- 그렇지만, 편결이 클래스를 보면 아직도 의존하는 면이 남아있다.
-> 심플 스프링 프레임워크를 이용하면 간편해진다.
새로운 역할의 도출
-> 새로운 Application Config를 만든다. (ApplicationConfig는 어떤 클래스가 어떤 클래스에 의존하는지 찾을 수 있어 분류가 편리하다.)
-> SRP(단일 책임 원칙)를 지키는 것과 같다.
Ex) 의존 관계를 분리한다.
1) ApplicationConfig 클래스를 만든다. (com.ran.convpay 하위에 config 폴더를 만들고 나서 거기에 클래스를 만든다.)
- 어플리케이션 전체에 설정을 담당할 클래스
- 편결이가 실제로 인터페이스에 어떤 내용을 담을지 ApplicationConfig가 담당한다.
- 따라서, 실제 구현체는 절대로 의존하지 않도록 한다.
2) 편결이 클래스 변경
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.*;
import com.ran.convpay.type.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class ConveniencePayService { // 편결이
private final Map<PayMethodType, PaymentInterface> paymentInterfaceMap = new HashMap<>();
private final DiscountInterface discountInterface;
public ConveniencePayService(Set<PaymentInterface> paymentInterfaceSet, DiscountInterface discountInterface) {
paymentInterfaceSet.forEach(
paymentInterface -> paymentInterfaceMap.put(
paymentInterface.getPayMethodType(), // key
paymentInterface // value
)
);
this.discountInterface = discountInterface;
}
public PayResponse pay(PayRequest payRequest){
// 결제 수단을 key로 가져온다.
PaymentInterface paymentInterface = paymentInterfaceMap.get(payRequest.getPayMethodType());
/* 위 코드를 추가해서 아래는 필요 없어진다.
if(payRequest.getPayMethodType() == PayMethodType.CARD){
paymentInterface = cardAdapter;
}else if(payRequest.getPayMethodType() == PayMethodType.MONEY){
paymentInterface = moneyAdapter;
}
*/
Integer discountedAmount = discountInterface.getDiscountedAmount(payRequest);
PaymentResult payment = paymentInterface.payment(discountedAmount);
// 결제할 때도 discountedAmount로 바꾼다.
if(payment == PaymentResult.PAYMENT_FAIL){
return new PayResponse(PayResult.FAIL, 0);
}
// Success case
return new PayResponse(PayResult.SUCCESS, discountedAmount);
// 응답할 때도 discountedAmount로 바꾼다.
}
public PayCancelResponse payCancel(PayCancelRequest payCancelRequest){ // 결제 취소
PaymentInterface paymentInterface = paymentInterfaceMap.get(payCancelRequest.getPayMethodType());
/* 삭제하는 코드
if(payCancelRequest.getPayMethodType() == PayMethodType.CARD){
paymentInterface = cardAdapter;
}else{
paymentInterface = moneyAdapter;
}
*/
CancelPaymentResult cancelPaymentResult = paymentInterface.cancelPayment(payCancelRequest.getPayCancelAmount()); // useCancel을 통해 사용 취소
if(cancelPaymentResult == CancelPaymentResult.CANCEL_PAYMENT_FAIL){
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_FAIL, 0);
}
// Success case
return new PayCancelResponse(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelRequest.getPayCancelAmount()); // 성공한 경우, 내가 요청한 금액
}
}
- 생성자 추가
- 그런데 결제 수단 인터페이스는 여러 개를 받아야한다. paymentInterfaceMap을 만든다.
- 3)을 먼저 수행하고나서 생성자로 Set을 매개변수로 받고 편결이 클래스에 내용을 추가한다.
-> 인터페이스 각각을 가져와서 forEach() 이용
-> 편결이는 실제 구현체에 직접적으로 의존하지 않도록 할 수 있다. 편결이는 본연의 업무만 수행한다.
3) PaymentInterfacce를 수정한다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.CancelPaymentResult;
import com.ran.convpay.type.PayMethodType;
import com.ran.convpay.type.PaymentResult;
public interface PaymentInterface {
PayMethodType getPayMethodType();
PaymentResult payment(Integer payAmount); // 인터페이스가 public이라 여기는 접근 제어자 필요없다.
CancelPaymentResult cancelPayment(Integer cancelAmount);
}
4) 그러면 CardAdapter에서 에러가 난다.
- CardAdapter, MoneyAdapter에 getPayMethod()를 오버라이드한다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.*;
public class CardAdapter implements PaymentInterface {
// 1.인증
public void authorization() {
System.out.println("authorization success. ");
}
// 2. 승인
public void approval() {
System.out.println("approval success. ");
}
// 3. 매일
public CardUseResult capture(Integer payAmount) {
if (payAmount > 100) {
return CardUseResult.USE_FAIL;
}
return CardUseResult.USE_SUCCESS;
}
// 4. 매입 취소
public CardUseCancelResult cancelCapture(Integer cancelAmount){
if(cancelAmount < 1000){
return CardUseCancelResult.USE_CANCEL_FAIL;
}
return CardUseCancelResult.USE_CANCEL_SUCCESS;
}
@Override
public PayMethodType getPayMethodType() {
return PayMethodType.CARD;
}
@Override
public PaymentResult payment(Integer payAmount) {
authorization();
approval();
CardUseResult cardUseResult = capture(payAmount);
if(cardUseResult == CardUseResult.USE_FAIL){
return PaymentResult.PAYMENT_FAIL;
}
return PaymentResult.PAYMENT_SUCCESS;
}
@Override
public CancelPaymentResult cancelPayment(Integer cancelAmount) {
CardUseCancelResult cardUseCancelResult = cancelCapture(cancelAmount);
if(cardUseCancelResult == CardUseCancelResult.USE_CANCEL_FAIL){
return CancelPaymentResult.CANCEL_PAYMENT_FAIL;
}
return CancelPaymentResult.CANCEL_PAYMENT_SUCCESS;
}
}
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.type.*;
public class MoneyAdapter implements PaymentInterface {
// 결제 요청
public MoneyUseResult use(Integer payAmount){
System.out.println("MoneyAdapter.use: " + payAmount);
if(payAmount > 1_000_000){ // 큰 금액을 사용하려는 경우, 실패
return MoneyUseResult.USE_FAIL;
}
return MoneyUseResult.USE_SUCCESS;
}
public MoneyUseCancelResult useCancel(Integer payCancelAmount){
System.out.println("MoneyAdapter.useCancel: " + payCancelAmount);
if(payCancelAmount < 100){ // 작은 금액을 취소하는 경우 실패
return MoneyUseCancelResult.MONEY_USE_CANCEL_FAIL; // 실패 케이스
}
return MoneyUseCancelResult.MONEY_USE_CANCEL_SUCCESS; // 성공한 케이스
}
@Override
public PayMethodType getPayMethodType() {
return PayMethodType.MONEY;
}
@Override
public PaymentResult payment(Integer payAmount){
MoneyUseResult moneyUseResult = use(payAmount);
if(moneyUseResult == MoneyUseResult.USE_FAIL){
return PaymentResult.PAYMENT_FAIL;
}
return PaymentResult.PAYMENT_SUCCESS;
}
@Override
public CancelPaymentResult cancelPayment(Integer cancelAmount){
MoneyUseCancelResult moneyUseCancelResult = useCancel(cancelAmount);
if(moneyUseCancelResult == MoneyUseCancelResult.MONEY_USE_CANCEL_FAIL){
return CancelPaymentResult.CANCEL_PAYMENT_FAIL;
}
return CancelPaymentResult.CANCEL_PAYMENT_SUCCESS;
}
}
5) 1)에서 만든 클래스에 아래와 같이 내용을 추가한다.
- Application 클래스는 어떤 클래스가 어떤 하위 클래스에 의존하는지 설정해주는 역할을 한다. (직접 관리할 필요 없다.)
<hide/>
package com.ran.convpay.config;
import com.ran.convpay.service.CardAdapter;
import com.ran.convpay.service.ConveniencePayService;
import com.ran.convpay.service.DiscountByPayMethod;
import com.ran.convpay.service.MoneyAdapter;
import java.util.Arrays;
import java.util.HashSet;
public class ApplicationConfig {
public ConveniencePayService conveniencePayService(){
return new ConveniencePayService(
new HashSet<>(
Arrays.asList(new MoneyAdapter(), new CardAdapter())
),
new DiscountByPayMethod()
);
}
}
- 예를 들어, 아래처럼 만들면 편결이를 두 가지 버전으로 만들어놓고 편리하게 쓸 수 있다.
package com.ran.convpay.config;
import com.ran.convpay.service.*;
import java.util.Arrays;
import java.util.HashSet;
public class ApplicationConfig {
public ConveniencePayService conveniencePayServiceDiscountConvenience(){
return new ConveniencePayService(
new HashSet<>(
Arrays.asList(new MoneyAdapter(), new CardAdapter())
),
new DiscountByConvenience()
);
}
public ConveniencePayService conveniencePayServiceDiscountPayMethod(){
return new ConveniencePayService(
new HashSet<>(
Arrays.asList(new MoneyAdapter(), new CardAdapter())
),
new DiscountByPayMethod()
);
}
}
- UserClient클래스에 다음과 같이 적용 가능하다.
<hide/>
package com.ran.convpay;
import com.ran.convpay.config.ApplicationConfig;
import com.ran.convpay.dto.PayCancelRequest;
import com.ran.convpay.dto.PayCancelResponse;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.service.ConveniencePayService;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.type.PayMethodType;
public class UserClient {
public static void main(String[] args) {
// 변경된 부분
ApplicationConfig applicationConfig = new ApplicationConfig();
ConveniencePayService conveniencePayService =
applicationConfig.conveniencePayServiceDiscountPayMethod();
// GS25 결제 1000원
PayRequest payRequest = new PayRequest(PayMethodType.CARD,
ConvenienceType.G25, 1000);
PayResponse payResponse = conveniencePayService.pay(payRequest);
System.out.println(payResponse);
// GS25 취소 500원
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.MONEY,
ConvenienceType.G25, 500);
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
System.out.println(payCancelResponse);
}
}
Note) 실행 결과
- 편결이 클래스 오류를 해결한다.
-> 여러 가지 클래스가 연결된 상태라서 테스트가 자주 깨질 수 있다.
<hide/>
package com.ran.convpay.service;
import com.ran.convpay.dto.PayCancelRequest;
import com.ran.convpay.dto.PayCancelResponse;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.type.PayCancelResult;
import com.ran.convpay.type.PayMethodType;
import com.ran.convpay.type.PayResult;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.HashSet;
import static org.junit.jupiter.api.Assertions.*;
class ConvenientPayServiceTest {
// 변경된 부분
ConveniencePayService conveniencePayService = new ConveniencePayService(
new HashSet<>(
Arrays.asList(new MoneyAdapter(), new CardAdapter())
),
new DiscountByConvenience()
);
@Test
void pay_success() {
// given
PayRequest payRequest =
new PayRequest(PayMethodType.MONEY, ConvenienceType.G25, 50);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.SUCCESS, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(35, payResponse.getPaidAmount());
}
@Test
void pay_fail() {
// given
PayRequest payRequest =
new PayRequest(PayMethodType.MONEY, ConvenienceType.G25, 1_500_001);
// when
PayResponse payResponse = conveniencePayService.pay(payRequest);
// then
assertEquals(PayResult.FAIL, payResponse.getPayResult()); // get결과가 성공이고
assertEquals(0, payResponse.getPaidAmount()); // 금액이
}
@Test
void pay_cancel_success() {
// given
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.MONEY, ConvenienceType.G25, 1000);
// when
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
// then
assertEquals(PayCancelResult.PAY_CANCEL_SUCCESS, payCancelResponse.getPayCancelResult()); // get결과가 성공이고
assertEquals(1000, payCancelResponse.getPayCanceledAmount()); // 금액이
}
@Test
void pay_cancel_fail() {
// given
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.MONEY, ConvenienceType.G25, 99);
// 금액이 적을 때 나는 오류니까 줄이다.
// when
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
// then
assertEquals(PayCancelResult.PAY_CANCEL_FAIL, payCancelResponse.getPayCancelResult()); // get결과가 성공이고
assertEquals(0, payCancelResponse.getPayCanceledAmount()); // 금액이
}
}
3.9 스프링 프레임워크로 전환하기
Ex)
1) ApplicationConfig 클래스를 수정한다.
<hide/>
package com.ran.convpay.config;
import com.ran.convpay.service.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.HashSet;
@Configuration
public class ApplicationConfig {
@Bean // 빈은 스프링 컨테이너 엥
public ConveniencePayService conveniencePayService(){
return new ConveniencePayService(
new HashSet<>(
Arrays.asList(moneyAdapter(), cardAdapter())),
discountByConvenience()
);
}
@Bean
public CardAdapter cardAdapter() {
return new CardAdapter();
}
@Bean
public MoneyAdapter moneyAdapter() {
return new MoneyAdapter();
}
@Bean
public DiscountByConvenience discountByConvenience() {
return new DiscountByConvenience();
}
}
- bean은 스프링 컨테이너에 의해 관리되는 bean을 생성해주는 메서드
- moneyAdapter, cardAdapter, discountByConvenience 각각에 대해 메서드를 추출한다.
- 이렇게 만들고 나면 4개의 bean을 만들어서 스프링 어플리케이션에 넣을 준비가 된 것이다.
- ApplicationConfig는 심플 스프링이라고 볼 수 있다.
2) userClient
<hide/>
package com.ran.convpay;
import com.ran.convpay.config.ApplicationConfig;
import com.ran.convpay.dto.PayCancelRequest;
import com.ran.convpay.dto.PayCancelResponse;
import com.ran.convpay.dto.PayRequest;
import com.ran.convpay.dto.PayResponse;
import com.ran.convpay.service.ConveniencePayService;
import com.ran.convpay.type.ConvenienceType;
import com.ran.convpay.type.PayMethodType;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class UserClient {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationConfig.class);
// 전체 설정을 관리하는 context를 만든다.
// 우리의 설정 파일ApplicationConfig을 참고해서 만든다.
ConveniencePayService conveniencePayService =
applicationContext.getBean("conveniencePayService", ConveniencePayService.class);
// GS25 결제
PayRequest payRequest = new PayRequest(PayMethodType.CARD,
ConvenienceType.G25, 50);
PayResponse payResponse = conveniencePayService.pay(payRequest);
System.out.println(payResponse);
// GS25 취소
PayCancelRequest payCancelRequest = new PayCancelRequest(PayMethodType.MONEY,
ConvenienceType.G25, 500);
PayCancelResponse payCancelResponse = conveniencePayService.payCancel(payCancelRequest);
System.out.println(payCancelResponse);
}
}
- 스프링 컨테이너에 있는 컨텍스트를 관리하도록 한다. (ApplicationConfig를 보면서 관리하도록 한다.)
- 우리가 만든 클래스를 bean으로 등록해서 스프링 컨테이너에 등록할 수 있는 설정을 만들었다.
Note) 실행 결과
- @Bean: 해당 설정을 활용해 생성된 객체를 DI Container(Application Context)에 담는다.
'Spring Projcect > 계좌 관리 시스템 프로젝트' 카테고리의 다른 글
Chapter 09. Account(계좌) 시스템 업그레이드 (0) | 2022.08.05 |
---|---|
Chapter 08. Account(계좌) 시스템 개발 (0) | 2022.08.02 |
Chapter 07. 사전 준비 (0) | 2022.08.02 |
Chapter 06. 스프링 MVC(Model-View-Controller) (0) | 2022.07.27 |
Chapter 02. OOP(Object Oriented Pragramming)와 스프링 프레임워크 (0) | 2022.07.19 |