본문 바로가기

Studying/JAVA

[JAVA]_다형성과 설계

▶ 객체지향 특징

 

- 추상화

- 캡슐화

- 상속

- 다형성

 

▶ 객체지향 프로그래밍

 

컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것.

각각의 객체는 메시지를 주고받고, 데이처를 처리할 수 있음.

객체지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용.

유연하고 변경이 용이하다는 것은 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법을 말함.

 

역할과 구현을 분리하면 유연해지며, 단순해지고 변경도 편리해짐.

 

- 클라이언트는 대상의 역할(인터페이스)만 알면 됨.

- 클라이언트는 구현 대상의 내부 구조를 몰라도 됨.

- 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않음.

- 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않음.


▶ 예제01

 

package poly.car0;

public class K3Car {

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

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

    public void processAccelerator() {
        System.out.println("K3Car.processAccelerator");
    }
}
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.processAccelerator();
        k3Car.offEngine();
    }
}
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

Driver와 K3Car를 먼저 생성.

그리고 driver.setK3Car(...)를 통해 driver에게 K3Car의 참조를 넘겨줌.

driver.drive()를 호출.


▶ 예제02

 

새로운 Model3 차량을 추가해야 하는 요구사항이 발생했을 때 이 요구사항을 맞추려면 기존 Driver 코드를 많이 변경해야 함.

드라이버는 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 processAccelerator() {
        System.out.println("Model3Car.processAccelerator");
    }
}
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.processAccelerator();
            k3Car.offEngine();
        } else if (model3Car != null) {
            model3Car.startEngine();
            model3Car.processAccelerator();
            model3Car.offEngine();
        }
    }
}
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();

        System.out.println();

        // 추가
        Model3Car model3Car = new Model3Car();
        driver.setK3Car(null);
        driver.setModel3Car(model3Car);
        driver.drive();
    }
}
실행결과

자동차를 운전합니다.
K3Car.startEngine
K3Car.processAccelerator
K3Car.offEngine

자동차를 운전합니다.
Model3Car.startEngine
Model3Car.processAccelerator
Model3Car.offEngine

[Model3Car]

 

필드 추가.

setModel3Car(...) 메서드 추가.

drive() 메서드에 가지고 있는 차량에 따른 분기.

 

[CarMain0]

 

K3를 운전하던 운전자가 Model3로 차량을 변경해서 운전하는 코드.

driver.setK3Car(null)을 통해 기존 K3Car의 참조 제거.

driver.setModel3Car(model3Car)를 통해 새로운 model3Car의 참조 추가.

driver.drive() 호출.

 

여기에서의 문제점은 새로운 차량을 추가할 시 또다시 Driver 코드를 많이 변경해야 함.


▶ 예제03

 

① Driver

다형성 활용하면 역할과 구현을 분리해서, 클라이언트 코드의 변경 없이 구현 객체 변경 가능.

Driver 클래스는 Car car 멤버 변수를 가지며, Car 인터페이스를 참조.

인터페이스를 구현한 K3Car, Model3Car에 의존하지 않고 Car 인터페이스를 참조.

 

② Car

자동차의 역할이고 인터페이스임.

K3Car, Model3Car 클래스가 인터페이스를 구현.

 

package poly.car01;

public interface Car {

    void startEngine();
    void offEngine();
    void pressAccelerator();
}
package poly.car01;

public class K3Car implements Car {

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

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

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

public class Model3Car implements Car {

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

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

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

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();
    }
}
package poly.car01;

public class CarMain1 {
    public static void main(String[] args) {

        Driver driver = new Driver();

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

        System.out.println();

        // 차량 변경 (model3 → newCar)
        NewCar newCar = new NewCar();
        driver.setCar(newCar);
        driver.drive();
    }
}
실행결과

자동차를 설정합니다 : poly.car01.K3Car@214c265e
자동자를 운전합니다.
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine

자동차를 설정합니다 : poly.car01.NewCar@7cca494b
자동자를 운전합니다.
NewCar.startEngine
NewCar.pressAccelerator
NewCar.offEngine

[Driver]

 

멤버변수로 Car car를 가짐.

setCat(Car car)

     - 멤버 변수에 자동차를 설정.

     - 외부에서 누군가 이 메서드를 호출해 주어야 Driver는 새로운 자동차를 참조하거나 변경 가능.

drive() : Car 인터페이스가 제공하는 기능들을 통해 자동차를 운전함.


▶ OCP(Open-Closed Principle) 원칙

 

- Open for extension : 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 함.

- Closed for modification : 기존 코드는 수정되지 않아야 함.

 

확장에는 열려있고, 변경에는 닫혀 있다는 뜻인데, 쉽게 이야기해서 기존의 코드 수정 없이 새로운 기능을 추가할 수 있다는 의미.

 

위에 코드에서처럼 새로운 차량을 추가해도 Driver의 코드는 전혀 변경하지 않는 것을 볼 수 있음.

기능을 확장해도 main() 일부를 제외한 프로그램의 핵심 부분의 코드는 전혀 수정하지 않아도 됨.

 

● 전략 패턴(Strategy Pattern) ●

전략패턴은 알고리즘을 클라이언트 코드의 변경 없이 쉽게 교체 가능.

Car 인터페이스가 바로 전략을 정의하는 인터페이스가 되고, 각각의 차량이 전략의 구체적인 구현이 됨,

그리고 전략을 클라이언트 코드(Driver)의 변경 없이 손쉽게 교체 가능.


▶ 문제01 ◀

 

한 번에 여러 곳에 메시지를 발송하는 프로그램 개발.

다음 코드를 참고해서 클래스 완성.

 

요구사항

- 다형성 활용.

- 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를 발송합니다 : 환영합니다!
페이스북에 발송합니다 : 환영합니다!
package poly.ex.sender;

public interface Sender {

    void sendMessage(String message);
}
package poly.ex.sender;

public class EmailSender implements Sender{

    private Sender sender;

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

public class SmsSender implements Sender {

    private Sender sender;

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

public class FaceBookSender implements Sender {

    private Sender sender;

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

▶ 문제02 ◀

 

현재 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) {
            System.out.println("결제가 성공했습니다.");
        } else {
            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
결제 수단이 없습니다.
걸제가 실패했습니다.
package poly.ex.pay1;

public interface Pay {
    boolean pay(int amount);
}
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("결제 수단이 없습니다.");
        return false;
    }
}
package poly.ex.pay1;

public class PayStore {

    public static Pay findPay(String option) {
        if (option.equals("kakao")) {
           return new KakaoPay();
        } else if (option.equals("naver")) {
            return new NaverPay();
        } else {
            return new DefaultPay();
        }
    }
}
package poly.ex.pay1;

public class PayService {

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

        Pay pay = PayStore.findPay(option);
        boolean result = pay.pay(amount);

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

public class PayMain1 {
    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
결제 수단이 없습니다.
걸제가 실패했습니다.

▶ 문제03 ◀

 

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

 

실행결과

결제 수단을 입력하세요:kakao
결제 금액을 입력하세요:5000
결제를 시작합니다: option=kakao, amount=5000
카카오페이 시스템과 연결합니다.
5000원 결제를 시도합니다.
결제가 성공했습니다.
결제 수단을 입력하세요:exit
프로그램을 종료합니다.
package poly.ex.pay1;

import java.util.Scanner;

public class PayMain2 {
    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);
        PayService payService = new PayService();

        while (true) {
            System.out.print("결제 수단을 입력하세요 : ");
            String option = scanner.nextLine();

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

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

            scanner.nextLine();

            payService.processPay(option, amount);
        }


    }
}

'Studying > JAVA' 카테고리의 다른 글

[JAVA]_Polymorphism2(다형성2)  (0) 2025.03.19
[JAVA]_Polymorphism1(다형성1)  (0) 2025.03.12
[JAVA]_Inheritance(상속)  (0) 2025.02.24
[JAVA]_final  (0) 2025.02.18
[JAVA]_자바 메모리 구조와 static  (1) 2025.02.11