관리 메뉴

나구리의 개발공부기록

다형성과 설계, 좋은 객체 지향 프로그래밍이란?, 다형성 - 역할과 구현 예제, OCP(Open-Closed principle) 원칙 본문

인프런 - 실전 자바 로드맵/실전 자바 - 기본편

다형성과 설계, 좋은 객체 지향 프로그래밍이란?, 다형성 - 역할과 구현 예제, OCP(Open-Closed principle) 원칙

소소한나구리 2025. 1. 15. 11:21

출처 : 인프런 - 김영한의 실전 자바 - 기본편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 좋은 객체 지향 프로그래밍이란?

** 스프링 핵심원리 기본편에서 배운 내용과 중복이 있음

- https://nagul2.tistory.com/123

1) 객체 지향

(1) 객체 지향 특징

  • 추상화
  • 캡슐화
  • 상속
  • 다형성

(2) 객체 지향 프로그래밍의 정의

  • 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위 즉, "객체"들의 모임으로 파악하고자 하는 것임
  • 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있음, (협력)
  • 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됨
  • 레고 블럭 조립하듯, 컴퓨터 부품을 갈아 끼우듯 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법을 유연하고 변경이 용이하다라고 이해하면 됨

(3) 다형성의 실세계 비유

  • 실세계와 객체 지향은 1:1로 정확하게 매칭하기는 쉽지않지만 역할과 구현으로 세상을 구분하여 비유로 이해하기에는 좋음
  • 운전자와 자동차의 관계를 보면 운전자 입장에서는 자동차의 기본 역할만 알고 있으면 다른 자동차를 운전하여도, 새로운 자동차가 나와도 자동차는 운전할 수 있음
  • 공연 무대를 예를 들면 로미오와 줄리엣 역할에는 배우가 누가오든 역할에 대해 숙지만 하면 누구든 로미오나 줄리엣의 역할을 할 수 있음
  • 이 외에도 A라는 정렬 알고리즘을 사용하고 있다가 B라는 정렬 알고리즘을 사용하도록 변경한다든지, A라는 할인 정책을 사용하다가 B라는 할인 정책으로 변경하는 것을 예로 들을 수 있음

(4) 역할과 구현을 분리

  • 역할과 구현으로 구분하면 세상이 단순해지고 유연해지며 변경도 편리해지며 클라이언트의 입장에서 장점이 두드러짐
  • 클라이언트는 대상의 역할(인터페이스)만 알면 되며 구현 대상의 내부 구조를 몰라도 됨
  • 또한 구현 대상의 내부 구조가 변경되어도 영향을 받지 않고 구현 대상 자체를 변경해도 영향을 받지 않음

(5) 자바 언어

  • 자바 언어의 다형성을 활용하여 표현해보자면 역할 = 인터페이스, 구현 = 인터페이스를 구현한 클래스(구현 객체)로 볼 수 있음
  • 객체를 설계할 때 역할과 구현을 명확히 분리하고 역할(인터페이스)을 먼저 부여한 후 그 역할을 수행하는 구현 개체를 만들면 됨

(6) 객체의 협력이라는 관계부터 생각해야함

  • 세상에 혼자 있는 객체는 없으며 수 많은 개체 클라이언트와 객체 서버는 서로 협력 관계를 가짐
  • 클라이언트: 요청
  • 서버: 응답

(7) 자바 언어의 다형성

  • 오버라이딩을 떠올려 보면 오버라이딩을 한 메서드가 우선권을 가지므로 다형적 참조를 통해 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있음
  • 인터페이스뿐만 아니라 상속 관계도 다형성, 오버라이딩이 적용이 가능함
  • Car라는 인터페이스(혹은 조상클래스)의 자식인 객체로 변경이 가능함

(8) 다형성의 본질

  • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있으며 다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야함
  • 클라이언트를 변경하지 않고 서버의 구현 기능을 유연하게 변경할 수 있음

(9) 정리

  • 역할(인터페이스) 자체가 변하면 클라이언트, 서버 모두에 큰 변경이 발생하므로 인터페이스를 안정적으로 잘 설계하는 것이 중요함
  • 다형성이 가장 중요하며 디자인 패턴 대부분은 다형성을 활용하는 것임
  • 스프링의 핵심인 제어의 역전(IoC), 의존관계 주입(DI)도 결국 다형성을 활용하는 것임
  • 스프링을 사용하면 마치 레고 블럭 조립하듯이 공연 무대의 배우를 선택하듯이 구현을 편리하게 변경할 수 있음

2. 다형성 - 역할과 구현 예제

1) 다형성 활용 하지 않는 예제

(1) 설명

  • 다형성을 사용하지 않고 역할과 구현의 분리 없이 단순하게 운전자와 자동차의 관계를 개발
  • Driver는 K3Car를 운전하는 단순한 프로그램임

(2) K3Car

  • 엔진을 켜고, 끄고, 액셀러레이터를 밟는 기능을 가지고 있음
package poly.car0;

public class K3Car {
    public void startEngine() {
        System.out.println("K3Car.startEngine");
    }

    public void offEngine() {
        System.out.println("K3Car.offEngine");
    }

    public void pressAccelerator() {
        System.out.println("K3Car.pressAccelerator");
    }
}

 

(3) Driver

  • K3Car타입 변수 k3Car를 생성하고 setK3Car를 통해서 외부에서 해당 메서드를 통해 인스턴스를 전달 받도록 작성
  • drive()메서드 안에서 변수 k3Car로 인스턴스의 메서드들을 호출
package poly.car0;

public class Driver {
    private K3Car k3Car;

    public void setK3Car(K3Car k3Car) {
        this.k3Car = k3Car;
    }

    public void drive() {
        System.out.println("자동차를 운전합니다.");
        k3Car.startEngine();
        k3Car.pressAccelerator();
        k3Car.offEngine();
    }
}

 

(4) CarMain0

  • Driver와 K3Car를 생성하고 driver.setK3Car(...)를 통해서 driver에게 k3Car의 참조를 넘겨주고 driver.driver()를 호출
package poly.car0;

public class CarMain0 {
    public static void main(String[] args) {
        Driver driver = new Driver();
        K3Car k3Car = new K3Car();

        driver.setK3Car(k3Car);
        driver.drive();
    }
}
/* 실행 결과
자동차를 운전합니다.
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine
*/

 

(5) 메모리 그림

  • setK3Car()메서드를 통해 Driver의 멤버변수 k3Car에 참조값을 외부에서 주입하고 해당 참조값을 통해서 넘어온 인스턴스의 메서드들을 호출

2) 새로운 요구사항

(1) 설명

  • 새로운 Model3 차량을 추가해야 하는 요구사항을 맞추기 위해 기존의 Driver코드를 많이 변경해야 함
  • 드라이버는 K3Car도 운전할 수 있고 Model3Car도 운전할 수 있어야 하지만 둘을 동시에 운전하는 것은 아님

(2) Model3Car

  • K3Car와 동일한 기능을 하는 Model3Car 생성, 메서드 이름은 같고 내부 동작은 다름
package poly.car0;

public class Model3Car {
    public void startEngine() {
        System.out.println("Model3Car.startEngine");
    }

    public void offEngine() {
        System.out.println("Model3Car.offEngine");
    }

    public void pressAccelerator() {
        System.out.println("Model3Car.pressAccelerator");
    }
}

 

(3) Driver - 코드 변경

    • 드라이버가 K3Car, Model3Car를 모두 운전할 줄 알아야하므로 코드를 변경
    • Model3Car용 필드를 추가하고 setModel3Car()메서드도 추가해야하며 drive()메서드 내부도 차량에 따라 동작할 수 있도록 조건문을 통해 분기하는 코드로 변경해야 함
package poly.car0;

public class Driver {
    private K3Car k3Car;
    private Model3Car model3Car;    // 추가

    public void setK3Car(K3Car k3Car) {
        this.k3Car = k3Car;
    }

    public void setModel3Car(Model3Car model3Car) { // 추가
        this.model3Car = model3Car;
    }

    public void drive() {
        System.out.println("자동차를 운전합니다.");

        if (k3Car != null) {    // 변경
            k3Car.startEngine();
            k3Car.pressAccelerator();
            k3Car.offEngine();
        } else if (model3Car != null) {
            model3Car.startEngine();
            model3Car.pressAccelerator();
            model3Car.offEngine();
        }

    }
}

 

(4) CarMain - 수정

  • K3를 운전하던 운전자가 Model3로 차량을 변경하여 운전하는 코드
  • driver.setK3Car(null)로 기존의 K3Car의 참조를 제거하고 driver.setModel3Car(model3Car)로 새로운 model3Car의 참조를 추가
  • driver.driver()메서드를 호출하면 요구사항대로 출력문이 출력되는 것을 확인할 수 있음
package poly.car0;

public class CarMain0 {
    public static void main(String[] args) {
        Driver driver = new Driver();
        K3Car k3Car = new K3Car();

        driver.setK3Car(k3Car);
        driver.drive();

        // 추가
        Model3Car model3Car = new Model3Car();
        driver.setK3Car(null);
        driver.setModel3Car(model3Car);
        driver.drive();
    }
}
/* 실행 결과
자동차를 운전합니다.
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine
자동차를 운전합니다.
Model3Car.startEngine
Model3Car.pressAccelerator
Model3Car.offEngine
*/

 

(5) 메모리 그림

  • Driver는 K3Car도 알고있고 Model3Car도 알고있으며 각각 생성한 set메서드로 외부에서 참조값을 받아서 인스턴스에 접근하여 메서드를 호출
  • 여기에서의 문제는 새로운 차량을 추가하는데 운전자인 Driver의 코드를 많이 변경이 필요함
  • 만약 운전할 수 있는 차량의 종류가 계속 늘어난다면 점점 더 변경해야 하는 코드가 많아질 것임

3) 다형성 활용

(1) 설명

  • 다형성을 활용하면 역할과 구현을 분리하여 클라이언트인 Driver의 코드의 변경 없이 구현 객체를 변경할 수 있음
  • Driver: 운전자는 자동차의 역할에만 의존하고 구현인 K3, Model3 자동차에 의존하지 않음
  • Driver 클래스는 Car car 멤버변수를 가지므로 Car 인터페이스를 참조하며 인터페이스를 구현한 K3Car, Model3Car에 의존하지 않음
  • 여기서 설명하는 의존은 클래스 의존 관계를 뜻하며 클래스 상에서 어떤 클래스를 알고 있는가를 뜻함
  • Car: 자동차의 역할을 하는 인터페이스로 K3Car, Model3Car 클래스가 인터페이스를 구현함

(2) Car - 인터페이스

package poly.car1;

public interface Car {
    void startEngine();
    void offEngine();
    void pressAccelerator();
}

 

(3) K3Car, Model3Car

  • Car 인터페이스를 구현하고 Car 인터페이스가 가진 추상 메서드들을 각각의 구현체가 다르게 구현
package poly.car1;

public class K3Car implements Car {
    @Override
    public void startEngine() {
        System.out.println("K3Car.startEngine");
    }

    @Override
    public void offEngine() {
        System.out.println("K3Car.offEngine");
    }

    @Override
    public void pressAccelerator() {
        System.out.println("K3Car.pressAccelerator");
    }
}

package poly.car1;

public class Model3Car implements Car {
    @Override
    public void startEngine() {
        System.out.println("Model3Car.startEngine");
    }

    @Override
    public void offEngine() {
        System.out.println("Model3Car.offEngine");
    }

    @Override
    public void pressAccelerator() {
        System.out.println("Model3Car.pressAccelerator");
    }
}

 

(4) Driver

  • 첫번째 예제에서 생성한 Driver와 흡사하지만 Driver는 Car car로 인터페이스 타입 변수를 멤버 변수로 가짐
  • setCar(Car car) 메서드로 매개변수의 타입을 Car인터페이스로 주입 받도록 설정하고 외부에서 해당 메서드를 호출하여 Car 인터페이스를 구현한 구현체라면 해당 매서드의 인수로 참조값을 주입시킬 수 있음
package poly.car1;

public class Driver {
    
    private Car car;

    public void setCar(Car car) {
        System.out.println("자동차를 설정합니다: " + car);
        this.car = car;
    }

    public void drive() {
        System.out.println("자동차를 운전합니다.");
        car.startEngine();
        car.pressAccelerator();
        car.offEngine();
    }
}

 

(5) CarMain1

  • K3Car와 Model3Car를 생성한 참조변수를 setCar()의 인수로 입력해주기만 하면 구현체의 메서드가 호출 되는것을 확인할 수 있음
package poly.car1;

public class Car1Main {
    public static void main(String[] args) {
        Driver driver = new Driver();

        // 차량 선택 - k3
        K3Car k3Car = new K3Car();
        driver.setCar(k3Car);
        driver.drive();

        // 차량 변경 - k3 -> model3
        Model3Car model3Car = new Model3Car();
        driver.setCar(model3Car);
        driver.drive();

    }
}
/* 실행 결과
자동차를 설정합니다: poly.car1.K3Car@2f92e0f4
자동차를 운전합니다.
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine
자동차를 설정합니다: poly.car1.Model3Car@5305068a
자동차를 운전합니다.
Model3Car.startEngine
Model3Car.pressAccelerator
Model3Car.offEngine
*/

 

(6) 그림으로 설명

  • 먼저 Driver와 K3Car를 생성 후 driver.setCar(k3Car)를 호출하며 Driver의 Car car 필드가 K3Car의 인스턴스를 참조하도록 함
  • driver.driver()를 호출하면 drive()메서드바디에 기능들을 추가로 호출하기 위해 K3Car의 인스턴스에서 실행할 메서드들을 찾음
  • 호출하는 참조 변수 타입이 Car 타입이므로 Car 인터페이스에서 먼저 찾는데 해당 메서드가 K3Car 클래스에서 오버라이딩 되었으므로 K3Car의 메서드들이 출력이됨
  • 위와 동일한 매커니즘으로 Model3Car의 인스턴스를 생성하고 driver.setCar(model3Car)로 참조값을 주입만 해주면 다른 코드는 전혀 변경 없이 Car1Main의 코드에서 구현체만 변경함으로써 출력결과가 달라지는 것을 확인할 수 있음

3. OCP(Open-Closed principle) 원칙

1) OCP

(1) 좋은 객체 지향 설계 원칙 중 하나

  • Open for extension: 새로운 기능의 추가나 변경 사항이 생겼을 때 기존 코드는 확장할 수 있어야 함
  • Closed for modification: 기존의 코드는 수정되지 않아야 함
  • 확장에는 열려있고 변경에는 닫혀 있다는 뜻으로 기존의 코드 수정 없이 새로운 기능을 추가 할 수 있도록 설계 해야 한다는 의미이며 바로 위에서 만들었던 코드가 바로 OCP 원칙을 잘 지키고 있는 코드임

(2) 새로운 차량의 추가 - NewCar

package poly.car1;

public class NewCar implements Car {
    // Car 인터페이스의 메서드 오버라이딩
}

package poly.car1;

public class Car1Main {
    public static void main(String[] args) {
        Driver driver = new Driver();

        // 기존 코드 동일 생략

        // 차량 변경 - model3 -> newCar
        NewCar newCar = new NewCar();
        driver.setCar(newCar);
        driver.drive();
    }
}
  • NewCar 클래스를 생성 후 Car 인터페이스를 구현한 뒤 Car1Main메서드에서 새로운 차가 동작하도록 하여도 실제 자동차 객체를 사용하는 Driver의 코드는 전혀 변경하지 않아도 됨
  • 기능 확장을 해도 main()의 일부를 제외하면 프로그램의 핵심 부분 코드는 전혀 수정하지 않아도 됨

(3) 확장에 열려 있다는 의미

  • Car 인터페이스를 사용하여 새로운 차량을 자유롭게 추가할 수 있듯이 인터페이스를 구현하여 기능을 추가할 수 있다는 의미임
  • Car 인터페이스를 사용하는 클라이언트 코드인 Driver도 인터페이스를 통해 새롭게 추가된 차량을 자유롭게 호출할 수 있으며 이것이 확장에 열려 있다는 의미임

(4) 코드 수정은 닫혀 있다는 의미

  • 새로운 차를 추가하게 되면 기능이 추가 되기 때문에 기존 코드의 수정은 불가피하며 당연히 어딘가의 코드는 수정해야 함
  • 변하지 않는 부분: 새로운 자동차를 추가할 때 가장 영향을 받는 중요한 클라이언트는 Car의 기능을 사용하는 Driver인데 인터페이스를 사용하는 클라이언트인 Driver의 코드를 수정하지 않아도 된다는 것이 핵심임
  • 변하는 부분: main()과 같이 새로운 차를 생성하고 Driver에게 필요한 차를 전달해주는 역할은 당연히 코드 수정이 발생하며 전체 프로그램을 설정하고 조율하는 역할을 하는 부분은 OCP를 지켜도 변경이 필요함

(5) 정리

  • Car를 사용하는 클라이언트 코드인 Driver 코드의 변경없이 새로운 자동차를 확장할 수 있음
  • 다형성을 활용하고 역할과 구현을 잘 분리한 덕분에 새로운 자동차를 추가해도 대부분의 핵심 코드들을 그대로 유지할 수 있게 되었음

** 전략 패턴(Strategy Pattern)

  • 디자인 패턴 중 가장 중요한 패턴을 하나 뽑자면 전략 패턴을 뽑을 수 있는데, 알고리즘을 클라이언트 코드의 변경 없이 쉽게 교체할 수 있는 디자인 패턴임
  • 지금 예제에서 구현하고 설명했던 코드가 전략 패턴을 사용한 코드이며 Car 인터페이스가 바로 전략을 정의하는 인터페이스가 되고, 각각의 차량이 전략의 구체적인 구현이 되며 전략을 사용하는 클라이언트 코드인 Driver의 변경없이 손쉽게 교체할 수 있음

4. 문제와 풀이

1) 다중 메시지 발송

(1) 문제 및 요구사항

  • 한번에 여러 곳에 메시지를 발송하는 프로그램을 개발
  • 아래 코드를 참고해서 클래스를 완성
  • 다형성을 활용하고 Sender 인터페이스를 사용
  • EmailSender, SmsSender, FaceBookSender를 구현
더보기
package poly.ex.sender;

public class SendMain {
    public static void main(String[] args) {
        Sender[] senders = {new EmailSender(), new SmsSender(), new FaceBookSender()};
        
        for (Sender sender : senders) {
            sender.sendMessage("환영합니다!");
        }
    }
}

 

실행 결과

메일을 발송합니다: 환영합니다!

SMS 발송합니다: 환영합니다!

페이스북에 발송합니다: 환영합니다!

(2) 정답

더보기

인터페이스 추가

package poly.ex.sender;

public interface Sender {
    void sendMessage(String message);
}

 

인터페이스를 구현하여 메서드를 완성

package poly.ex.sender;

public class EmailSender implements Sender {
    @Override
    public void sendMessage(String message) {
        System.out.println("메일을 발송합니다: " + message);
    }
}
package poly.ex.sender;

public class SmsSender implements Sender {
    @Override
    public void sendMessage(String message) {
        System.out.println("SMS를 발송합니다: " + message);
    }
}
package poly.ex.sender;

public class FaceBookSender implements Sender {
    @Override
    public void sendMessage(String message) {
        System.out.println("페이스북에 발송합니다: " + message);
    }
}

 

실행 결과는 동일

2) 결제 시스템 개발

(1) 문제 및 요구사항

  • 결제 시스템 개발팀에서 2가지 결제 수단을 지원하는데 앞으로 5개의 결제 수단을 추가로 지원할 예정임
  • 새로운 결제수단을 쉽게 추가할 수 있도록 기존 코드를 리펙토링
  • OCP 원칙을 지켜야함
  • 메서드를 포함한 모든 코드를 변경해도 되며 클래스나 인터페이스를 추가해도 됨
  • 단 프로그램을 실행하는 PayMain0 코드는 변경하지 않고 그대로 유지해야 하며 리펙토링 후에도 실행 결과는 기존과 같아야 함
더보기
package poly.ex.pay0;

public class KakaoPay {
    public boolean pay(int amount) {
        System.out.println("카카오페이 시스템과 연결합니다.");
        System.out.println(amount + "원 결제를 시도합니다.");
        return true;
    }
}

package poly.ex.pay0;

public class NaverPay {
    public boolean pay(int amount) {
        System.out.println("네이버페이 시스템과 연결합니다.");
        System.out.println(amount + "원 결제를 시도합니다.");
        return true;
    }
}

package poly.ex.pay0;

public class PayService {

    public void processPay(String option, int amount) {

        boolean result;
        System.out.println("결제를 시작합니다: option=" + option + ", amount=" + amount);
        
        if (option.equals("kakao")) {
            KakaoPay kakaoPay = new KakaoPay();
            result = kakaoPay.pay(amount);
        } else if (option.equals("naver")) {
            NaverPay naverPay = new NaverPay();
            result = naverPay.pay(amount);
        } else {
            System.out.println("결제 수단이 없습니다.");
            result = false;
        }
        
        if (result) {
        } else {
            System.out.println("결제가 성공했습니다.");
            System.out.println("결제가 실패했습니다.");
        }
    }
}

package poly.ex.pay0;

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

        //kakao 결제
        String payOption1 = "kakao";
        int amount1 = 5000;
        payService.processPay(payOption1, amount1);

        //naver 결제
        String payOption2 = "naver";
        int amount2 = 10000;
        payService.processPay(payOption2, amount2);

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

 

실행 결과

결제를 시작합니다: option=kakao, amount=5000
카카오페이 시스템과 연결합니다.
5000원 결제를 시도합니다.
결제가 성공했습니다.
결제를 시작합니다: option=naver, amount=10000
네이버페이 시스템과 연결합니다.
10000원 결제를 시도합니다.
결제가 성공했습니다.
결제를 시작합니다: option=bad, amount=15000
결제 수단이 없습니다.
결제가 실패했습니다.

 

(2) 정답

  • 이 문제는 정답은 없고 새로운 결제 수단을 추가했을 때 Pay를 사용하는 클라이언트 코드인 PayService의 변경을 최소화 할 수 있으면 성공임
  • GooglePay 사용하는 코드까지 추가를해도 PayService는 변경이 없음
더보기

인터페이스 추가

package poly.ex.pay1;

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

 

기존 결제 수단은 Pay 인터페이스를 구현하고 결제 실패도 구현체로 변경

  • 결제 수단을 찾지 못했을 때 null 대신에 항상 결제 실패가 되도록 결제 실패 로직을 별도의 객체로 생성
  • 이런 패턴을 널 오브젝트 패턴(Null Object Pattern)이라고 함
  • 즉, 정상적인 결제 수단이 들어오면 true가 반환되고, null이거나 그외의 조건이 오면 무조건 false가 반환됨
package poly.ex.pay1;

public class KakaoPay implements Pay {

    @Override
    public boolean pay(int amount) {
        System.out.println("카카오페이 시스템과 연결합니다.");
        System.out.println(amount + "원 결제를 시도합니다.");
        return true;
    }
}

package poly.ex.pay1;

public class NaverPay implements Pay {

    @Override
    public boolean pay(int amount) {
        System.out.println("네이버페이 시스템과 연결합니다.");
        System.out.println(amount + "원 결제를 시도합니다.");
        return true;
    }
}

package poly.ex.pay1;

public class DefaultPay implements Pay {

    @Override
    public boolean pay(int amount) {
        System.out.println("결제 수단이 없습니다.");
        System.out.println("결제가 실패했습니다.");
        return false;
    }
}

 

새로운 결제 수단

package poly.ex.pay1;

public class GooglePay implements Pay {

    @Override
    public boolean pay(int amount) {
        System.out.println("구글페이 시스템과 연결합니다.");
        System.out.println(amount + "원 결제를 시도합니다.");
        return true;
    }
}

 

결제 수단을 변경해주는 설정 클래스

  • if - else 문으로 구현해도 상관없으나 특정 문자와 일치하는 조건이기 때문에 switch 표현식으로 구현(break를 안써도 되는 장점이 있음)
  • switch 표현식은 자바12에서 preview로 도입되었고 자바 14에서 정식 도입되었음
package poly.ex.pay1;

public abstract class PayConfig {

    public static Pay processPay(String option) {
        return switch (option) {
            case "kakao" -> new KakaoPay();
            case "naver" -> new NaverPay();
            case "google" -> new GooglePay();
            default -> new DefaultPay();
        };
    }
}

 

클라이언트 코드를 결제수단이 변경해도 수정이 없도록 리팩토링

  • 널 오브젝트 패턴을 사용한 덕분에 Service 코드에서 null 검증을 하지 않아도되기 때문에 코드가 매우 간결해짐
package poly.ex.pay1;

public class PayService {

    public void processPay(String option, int amount) {
        System.out.println("결제를 시작합니다: option=" + option + ", amount=" + amount);

        Pay pay = PayConfig.processPay(option);
        boolean result = pay.pay(amount);


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

 

기존의 main코드는 변경이 없고 새로운 결제 수단까지 사용하는 main()

  • 기존의 문제와 동일한 main()메서드 부분까지는 출력결과가 동일함
package poly.ex.pay1;

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

        //kakao 결제
        String payOption1 = "kakao";
        int amount1 = 5000;
        payService.processPay(payOption1, amount1);

        //naver 결제
        String payOption2 = "naver";
        int amount2 = 10000;
        payService.processPay(payOption2, amount2);

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

        //구글 추가
        String payOption4 = "google";
        int amount4 = 1000000;
        payService.processPay(payOption4, amount4);
    }
}

 

실행 결과

결제를 시작합니다: option=kakao, amount=5000
카카오페이 시스템과 연결합니다.
5000원 결제를 시도합니다.
결제가 성공했습니다.
결제를 시작합니다: option=naver, amount=10000
네이버페이 시스템과 연결합니다.
10000원 결제를 시도합니다.
결제가 성공했습니다.
결제를 시작합니다: option=bad, amount=15000
결제 수단이 없습니다.
결제가 실패했습니다.
결제를 시작합니다: option=google, amount=1000000
구글페이 시스템과 연결합니다.
1000000원 결제를 시도합니다.
결제가 성공했습니다.

3) 결제 시스템 개발 - 사용자 입력

(1) 문제 및 요구사항

  • 기존 결제 시스템이 사용자 입력을 받도록 수정
더보기

실행 결과

결제 수단을 입력하세요:kakao

결제 금액을 입력하세요:5000

결제를 시작합니다: option=kakao, amount=5000

카카오페이 시스템과 연결합니다.

5000 결제를 시도합니다.

결제가 성공했습니다.

결제 수단을 입력하세요:exit

프로그램을 종료합니다.

 

(2) 정답

  • 결제 수단 입력 시 결제 수단을 보여주도록 간단히 추가
더보기
package poly.ex.pay1;

import java.util.Scanner;

public class PayMain2 {
    public static void main(String[] args) {
        PayService payService = new PayService();
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println("결제 수단: kakao, naver, google, 종료: exit");
            System.out.print("결제 수단을 입력하세요:");
            String payOption = scanner.next();

            if (payOption.equals("exit")) {
                System.out.println("프로그램을 종료합니다.");
                return;

            } else if (!(payOption.equals("kakao") || payOption.equals("naver") || payOption.equals("google"))) {
                payOption = "bad";
            }

            System.out.print("결제 금액을 입력하세요:");
            int amount = scanner.nextInt();
            payService.processPay(payOption, amount);
            System.out.println();

        }
    }
}

 

실행 결과

결제 수단: kakao, naver, google, 종료: exit
결제 수단을 입력하세요:kakao
결제 금액을 입력하세요:10000
결제를 시작합니다: option=kakao, amount=10000
카카오페이 시스템과 연결합니다.
10000원 결제를 시도합니다.
결제가 성공했습니다.

결제 수단: kakao, naver, google, 종료: exit
결제 수단을 입력하세요:naver
결제 금액을 입력하세요:20000
결제를 시작합니다: option=naver, amount=20000
네이버페이 시스템과 연결합니다.
20000원 결제를 시도합니다.
결제가 성공했습니다.

결제 수단: kakao, naver, google, 종료: exit
결제 수단을 입력하세요:google
결제 금액을 입력하세요:5000
결제를 시작합니다: option=google, amount=5000
구글페이 시스템과 연결합니다.
5000원 결제를 시도합니다.
결제가 성공했습니다.

결제 수단: kakao, naver, google, 종료: exit
결제 수단을 입력하세요:nono
결제 금액을 입력하세요:1000
결제를 시작합니다: option=bad, amount=1000
결제 수단이 없습니다.
결제가 실패했습니다.

결제 수단: kakao, naver, google, 종료: exit
결제 수단을 입력하세요:exit
프로그램을 종료합니다.