Spring Projcect/계좌 관리 시스템 프로젝트

Chapter 03. 자바에서 스프링으로

계란💕 2022. 7. 19. 15:00

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) 편의점 결제 서비스 만들기

클래스 UML

    - 기획자의 요구사항: 편의점 결제 가능, 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를 만든다.

기획자의 요구 사항 기획 내용, 분석 결과
클래스 UML

    - 카드 어댑터 만든다.

<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

 

클래스 UML

 

 

  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) 50원 넣었을 때 할인되면 35원이 맞다. 그런데 50이 나온다.
(2) 결제 실패되야하는 상황에 성공이 되서 틀림

    (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 내가 만드는 심플 스프링 프레임워크

 

클래스 UML

 

  - DIP(의존 역전 원칙)와OCP를 지킨다고 지난 시간에 코드를 작성했다.

    -> DIP(의존 역전 원칙 - 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.

    -> OCP(개방 폐쇄 원칙 - 클래스, 메서드, 모듈은 확장에 Open, 변경에는 Close되어야 한다.)

  - 그렇지만, 편결이 클래스를 보면 아직도 의존하는 면이 남아있다.

     -> 심플 스프링 프레임워크를 이용하면 간편해진다.

 

 

새로운 역할의 도출

어플리케이션 설정을 분리한 클래스 UML

    -> 새로운 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)에 담는다.