일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 자바의 정석 기초편 ch1
- @Aspect
- 게시글 목록 api
- 스프링 고급 - 스프링 aop
- 자바 기본편 - 다형성
- 자바의 정석 기초편 ch13
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch14
- 2024 정보처리기사 수제비 실기
- 2024 정보처리기사 시나공 필기
- 스프링 mvc2 - 타임리프
- 스프링 mvc2 - 로그인 처리
- 스프링 mvc1 - 서블릿
- 코드로 시작하는 자바 첫걸음
- 스프링 입문(무료)
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch7
- 스프링 db1 - 스프링과 문제 해결
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch12
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch5
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch3
- jpa - 객체지향 쿼리 언어
- Today
- Total
나구리의 개발공부기록
다형성과 설계, 좋은 객체 지향 프로그래밍이란?, 다형성 - 역할과 구현 예제, 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
프로그램을 종료합니다.