Java 문제, 결제 시스템 다형성으로 개발하기

자바로 결제 시스템을 구현하는 문제 풀어보기

현재 결제 수단이 두 가지 있다. 앞으로 결제 수단은 추가로 들어올 예정
제공되는 코드에서 새로운 결제 수단을 추가할 수 있도록 코드를 리팩토링 해보기

요구사항

  • OCP 원칙 지키기
  • 메소드 포함한 코드 변경 가능
  • 클래스 및 인터페이스 추가 가능
  • 프로그램 실행하는 PayMain 코드는 유지할 것
  • 리팩토링 이후에도 결과는 변하지 않아야 한다.

AlibabaPay.java

package pay;

public class AlibabaPay {

    public boolean pay(int amount) {
        System.out.println("알리바바 시스템과 연결하기");
        System.out.println(amount + "원 결제를 시도하기");

        return true;
    }
}

NHNPay.java

package pay;

public class NHNPay {

    public boolean pay(int amount) {
        System.out.println("알리바바 시스템과 연결하기");
        System.out.println(amount + "원 결제를 시도하기");

        return true;
    }
}

PayService.java

package pay;

public class PayService {
    public void processPay(String payOption, int amount) {

        boolean result = false;
        System.out.println("결제 시작하기: option=" + payOption + ", amount=" + amount);
        if (payOption.equals("alibaba")) {
            AlibabaPay alibabaPay = new AlibabaPay();
            result = alibabaPay.pay(amount);
        } else if (payOption.equals("kcp")) {
            NHNPay nhnPay = new NHNPay();
            result = nhnPay.pay(amount);
        } else {
            System.out.println("결제 수단이 없습니다.");
        }

        if (result) {
            System.out.println("결제가 성공하였습니다.");
        } else {
            System.out.println("결제가 실패하였습니다.");
        }
    }
}

Main.java

package pay;

public class PayMain {
    public static void main(String[] args) {
        PayService payService = new PayService();

        //alibaba 결제
        String pay1 = "alibaba";
        int amount1 = 2000;
        payService.processPay(pay1, amount1);

        //NHN KCP 결제
        String pay2 = "kcp";
        int amount2 = 8000;
        payService.processPay(pay2, amount2);

        //잘못된 결제 수단 선택
        String pay3 = "bad";
        int amount3 = 100000;
        payService.processPay(pay3, amount3);
    }
}

출력 결과

aaa

클래스 관계도

결제 수단을 선택하고 결제 시도하는 로직이 PayService 클래스가 담당하고 있다.
시작은 PayService 살펴보면 결제 수단 로직이 중복되어 있다. Pay 인터페이스 신규로 만들어 중복을 제거하는 것이 필요하다.
또한 Pay 인터페이스 호출에 맞게 PayService 수정이 필요하다.


리팩토링 1 - 결제 수단 추가 된 경우의 수정 사항

리팩토링의 목적은 PayMain 코드 유지하면서 결제 수단이 추가되어야 한다.
새로운 결제 수단으로 naverPay, kakaoPay 등 계속 추가될 수 있으므로 지속 가능한 코드로 만들어야 한다.

만약 결제 수단이 추가된 경우 위 코드에서 어떠한 부분이 수정될까?

KakaoPay.java

package pay;

public class KakaoPay {

    public boolean pay(int amount) {
        System.out.println("카카오 시스템과 연결하기");
        System.out.println(amount + "원 결제를 시도하기");

        return true;
    }
}

카카오 페이를 추가한다.

PayService.java

if (payOption.equals("alibaba")) {
    AlibabaPay alibabaPay = new AlibabaPay();
    result = alibabaPay.pay(amount);
} else if (payOption.equals("kcp")) {
    NHNPay nhnPay = new NHNPay();
    result = nhnPay.pay(amount);
} else if (payOption.equals("kakao")) {
    KakaoPay kakaoPay = new KakaoPay();
    result = kakaoPay.pay(amount);
} else {
    System.out.println("결제 수단이 없습니다.");
}

결제 수단을 추가하는 if 문에서 kakao 코드를 추가하였다.

PayMain.java

신규 추가한 결제 수단을 테스트로 실행한다.

//카카오 결제
String pay4 = "kakao";
int amount4 = 4000;
payService.processPay(pay4, amount4);

출력 결과

카카오 시스템과 연결하기
4000원 결제를 시도하기
결제가 성공하였습니다.

사용자가 요구되는 기능을 수행하는 PayMain 코드를 제외하고, 보다시피 PayService 에서 신규 추가한 KakaoPay 클래스를 추가한 경우 중복 코드를 볼 수 있다. result = {brand}.pay(amount);

이러한 로직 중복을 없애고, KakaoPay 클래스만 추가되어도 지속 가능한 코드로 만드는 것이 이번 리팩토링의 핵심이다.

추가한 KakaoPay와 PayService 에서 관련 코드를 다시 제거하도록 한다.


리팩토링 2 - 인터페이스 추가하기

앞서 말한 코드의 문제점은 PayService 클래스에서 결제 수단이 추가된 경우 로직의 변경이 크다.

문제 해결을 위해 다음과 같이 Pay 인터페이스를 만든다.

Pay.java

package pay;

public interface Pay {
    boolean pay(int amount);
}

AlibabaPay.java

package pay;

public class AlibabaPay implements Pay {

    @Override
    public boolean pay(int amount) {
        System.out.println("알리바바 시스템과 연결하기");
        System.out.println(amount + "원 결제를 시도하기");

        return true;
    }
}

NHNPay.java

package pay;

public class NHNPay implements Pay {

    @Override
    public boolean pay(int amount) {
        System.out.println("알리바바 시스템과 연결하기");
        System.out.println(amount + "원 결제를 시도하기");

        return true;
    }
}

PayService.java

package pay;

public class PayService {
    public void processPay(String payOption, int amount) {

        boolean result = false;
        System.out.println("결제 시작하기: option=" + payOption + ", amount=" + amount);

        Pay pay;
        if (payOption.equals("alibaba")) {
            pay = new AlibabaPay();
        } else if (payOption.equals("kcp")) {
            pay = new NHNPay();
        } else if (payOption.equals("kakao")) {
            pay = new KakaoPay();
        } else {
            pay = null;
            System.out.println("결제 수단이 없습니다.");
        }

        if (pay != null) {
            result = pay.pay(amount);
        }

        if (result) {
            System.out.println("결제가 성공하였습니다.");
        } else {
            System.out.println("결제가 실패하였습니다.");
        }
    }
}
  • result = {brand}.pay(amount); 코드 중복을 줄였다.
  • Pay 부모 인터페이스를 불려와 결제수단 처리를 각 인스턴스마다 pay(amount) 호출하도록 한다.
  • main 함수 실행 시 문제 없이 동작한다.

리팩토링 3 - 코드 분리

PayService.java 코드를 살펴보면 결제수단 선택과 결제를 완료하는 코드가 함께 있다.
결제 수단 메소드를 만들어 분리하도록 한다.

첫 번째 방법 - 결제선택, 메소드로 분리하기

PayService.java 내에서 메소드 추가하여 결제 수단을 로직을 추가하는 방법이 있다.

PayService.java

package pay;

public class PayService {
    public void processPay(String payOption, int amount) {

        boolean result = false;
        System.out.println("결제 시작하기: option=" + payOption + ", amount=" + amount);

        Pay pay = findPay(payOption);

        if (pay != null) {
            result = pay.pay(amount);
        }

        if (result) {
            System.out.println("결제가 성공하였습니다.");
        } else {
            System.out.println("결제가 실패하였습니다.");
        }
    }

    public Pay findPay(String payOption) {
        if (payOption.equals("alibaba")) {
            return new AlibabaPay();
        } else if (payOption.equals("kcp")) {
            return new NHNPay();
        } else {
            System.out.println("결제 수단이 없습니다.");
            return null;
        }
    }
}

메소드를 추가하였지만, PayService 클래스는 결제 처리에 대한 로직으로 결제 처리에 대한 역할에 충실할 필요가 있다. 결제 수단으로 만든 findPay 별도의 클래스로 만들어 외부로 이동할 필요가 있다.

두 번째 방법- 결제 수단, 클래스로 분리하기

PayStore 신규로 만들어 결제 선택 역할에 충실하도록 한다.

PayStore.java

package pay;

public abstract class PayStore {

    public static Pay findPay(String payOption) {
        if (payOption.equals("alibaba")) {
            return new AlibabaPay();
        } else if (payOption.equals("kcp")) {
            return new NHNPay();
        } else {
            System.out.println("결제 수단이 없습니다.");
            return null;
        }
    }

}

PayService.java

findPay() 메소드를 PayStore.findPay()메소드 수행하도록 한다.

Pay pay = PayStore.findPay(payOption);

정리

이제 삭제하였던 kakaoPay 결제 수단을 다시 추가하여 리팩토링이 잘 되었는지 확인한다.
변경되는 부분은 PayStore 코드에서 kakaoPay 항목만 추가하면 정상적으로 동작하는 것을 볼 수 있다.


리팩토링 4 - null 제거

PayService 에서 pay = null 부분이 있을 것이다. null 할당하는 것을 제거하는 패턴이 있다.

DefaultPay.java

package pay;

public class DefaultPay implements Pay {

    @Override
    public boolean pay(int amount) {
        System.out.println("결제 수단이 선택되지 않았습니다.");
        return false;
    }
}

DefaultPay 결제 수단을 추가하였다. 해당 값은 false 반환한다.

PayStore.java

package pay;

public abstract class PayStore {

    public static Pay findPay(String payOption) {
        if (payOption.equals("alibaba")) {
            return new AlibabaPay();
        } else if (payOption.equals("kcp")) {
            return new NHNPay();
        } else {
            System.out.println("결제 수단이 없습니다.");
            return new DefaultPay();
        }
    }

}

return false 대신 new DefaultPay() 신규로 추가한 클래스로 지정한다.

PayService.java

Pay pay = PayStore.findPay(payOption);

result = pay.pay(amount);

if (result) {
    System.out.println("결제가 성공하였습니다.");
} else {
    System.out.println("결제가 실패하였습니다.");
}

result 할당에서 if 문을 제거해도 문제 없이 동작하는 것을 볼 수 있다.