[Java 기본]Section 13 - 다형성과 설계

우영·2024년 9월 22일

JAVA 기본편

목록 보기
12/13

객체 지향 프로그래밍

프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.
특징 - 추상화, 캡슐화, 상속, 다형성

다형성의 실세계 비유

역할과 구현

  • 자바
    • 역할 = 인터페이스
    • 구현 = 인터페이스를 구현한 클래스, 구현 객체
  • 클라이언트 : 요청, 서버: 응답 -> 서로 협력관계

자바 언어의 다형성

  • 오버라이딩 된 메서드가 실행
  • 유연하게 변경 가능, 클래스 상속 관계 - 다형성, 오버라이딩 적용가능

다형성의 본질

클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.

결국엔 인터페이스를 안정적으로 설계하는 것이 가장 중요!
ex) 자동차 -> 비행기 인터페이스 자체가 변하면 서버 모두에 큰 변경이 발생!!!

예제1

간단한 운전자와 자동차의 관계

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");
    }
}
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");
    }
}
package poly.car0;

public class Driver {

    private K3Car k3Car; // 자동차 객체를 담는 private 변수
    private Model3Car model3Car; // 추가

    public void setK3Car(K3Car k3Car){ //밖에서 k3car를 설정해줄 수 있다. setK3Car를 통해서만 접근 가능하게 해준다.
        this.k3Car = k3Car; // 외부에서 자동차 객체를 설정할 수 있게 하는 public 메서드
    }

    //추가
    public void setModel3Car(Model3Car model3Car) { // set을 쓰면 메소드를 알아서 만들어줆.
        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();
        }

    }

}

  • 헷갈린 부분
  1. private를 까먹음. 그래서 이해를 못했음.
  2. K3Car myCar = new K3Car(); 순간적으로 헷갈림 -> 클래스이고 public으로 되어있음. (멍청)

예제2 - 새로운 차를 추가하려고 하는데 기존 클래스의 코드를 많이 변경

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");
    }
}
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();
        }
    }
}

main 추가

Model3Car model3Car = new Model3Car();
 driver.setK3Car(null);
 driver.setModel3Car(model3Car);
 driver.drive();

추가해야 하는 차량의 종류가 계속 늘어나면 점점 더 변경해야 하는 코드가 많아질 것이다.

예제3 - 다형성을 활용해서 역할과 구현으로 분리

package poly.car1;

public interface Car {
    void StartEngine();
    void offEngine();
    void pressAccelerator();
}
package poly.car1;

public class CarMain1 {

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

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

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

        //차량변경(model3Car -> newCar)
        NewCar newCar = new NewCar();
        driver.setCar(newCar);
        driver.drive();
    }
}
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();
    }
}
  • K3car, Model3Car 아래 변수명만 바꾸면 동일해서 생략
package poly.car1;

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

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

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

NewCar를 넣어도 기존코드는 변경 없이 추가만 할 수 있음.


ocp원칙(open-closed principle)

: 기존의 코드 수정 없이 새로운 기능을 추가할 수 있다라는 의미
open for extension - 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 한다.
Closed for modification - 기존의 코드는 수정되지 않아야 한다.

  • 예제3 처럼 클라이언트 코드인 Driver 코드의 변경없이 새로운 자동차를 확장할 수 있다.

다형성을 활용하고 역할과 구현을 잘 분리한 덕분에 새로운 자동차를 추가해도 대부분의 핵심 코드들을 그대로 유지할 수 있게 되었다.


문제1

문제

  • 2가지 결제 수단을 지원하는데 5개의 결제 수단을 추가로 지원할 예정이므로 기존 코드를 리펙토링하기

요구사항

  • OCP 원칙 지키기
  • 모든 코드 변경, 추가 가능
  • patMain0 코드는 변경하지 않고, 그대로 유지
  • 리펙토링 후에도 결과가 기존과 동일
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);
 }
}


정답은 없고 payService의 변경을 최소화 할 수 있다면 성공!

해설:
1. 인터페이스를 만든다. boolean과 int amount는 상속받을 예제의 코드를 보고 적음.

package poly.ex.Pay1;

public interface Pay {

    boolean pay(int amount);

}
  1. implements를 사용하고 오버라이딩까지 했다.
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;
    }
}
  1. 앞의 payService 예제에서 if절의 중복이 보여서 Pay pay;로 선언했다.
  • result = kakaoPay.pay(amount); -> result = pay.pay(amount);로 바뀌고 if절 밖으로 빼서 한번만 적도록 설계했다. 설계하는 과정에서 pay = null;if절을 넣어서 nullpointerexception을 방지했다.

nullpointerexception - 실제 값이 아닌 null을 가지고 있는 객체/변수를 호출할 때 발생하는 예외다.

    boolean result = false;
if (option.equals("kakao")) {
        pay = new KakaoPay();
    } else if (option.equals("naver")) {
        pay  = new NaverPay();
    } else {
        pay = null;
        System.out.println("결제 수단이 없습니다.");
    }
}
if (pay != null){
    result = pay.pay(amount);
        }

3.위의 코드를 메서드로 뽑는다. (클래스를 만드는 것이 아니라 밑에다가 메서드를 뽑는 과정)

Pay pay = findPay(option);

public Pay findPay(Sring option) {
        boolean result = false;
        if (option.equals("kakao")) {
            pay = new KakaoPay();
        } else if (option.equals("naver")) {
            pay = new NaverPay();
        } else {
            pay = null;
            System.out.println("결제 수단이 없습니다.");
        }
        return pay;
    }

그런데 굳이 pay를 선언할 필요없이 return을 하면 되기 때문에 수정

public Pay findPay(Sring option) {
        boolean result = false;
        if (option.equals("kakao")) {
            return new KakaoPay();
        } else if (option.equals("naver")) {
            return new NaverPay();
        } else {
            System.out.println("결제 수단이 없습니다.");
            return null;
        }
    }
  1. payService를 건드리지 않을려면 수정된 위의 코드를 PayStore클래스로 만들어준다.
  • 만들어주는 과정에서 abstract로 설정하고 static메서드로 설정했다.
package poly.ex.Pay1;

public abstract class PayStore {

    public static Pay findPay(String option) {
        Pay pay;
        if (option.equals("kakao")) {
            return new KakaoPay();

        }  else if (option.equals("naver")) {
            return new NaverPay();

        }else if (option.equals("new")) { //추가된 코드 6번에서 
            return new NewPay();

        }
        else {
            return new DefaultPay();
        }
    }
}
  1. NewPay를 만든다.
package poly.ex.Pay1;

public class NewPay implements Pay{
    @Override
    public boolean pay(int amount) {
            System.out.println("NewPay 시스템과 연결합니다.");
            System.out.println(amount + "원 결제를 시도합니다.");
            return true;
        }
}
  1. newpay에 관련된 코드들을 추가해준다.
//new 결제 main함수 추가
        String payOption4 = "new";
        int amount4 = 7000;
        payService.processPay(payOption4, amount4);
  1. null if절 없애기(nullpointerexception)
  • DefaultPay 클래스로 따로 빼주기
package poly.ex.pay1practice;

public class DefaultPay implements Pay{

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

문제3 - 위의 코드에서 입력받기

package poly.ex.pay1practice;

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.println("결제수단을 입력하세요:");
            String payOption = scanner.nextLine();

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

            System.out.println("결제금액을 입력하세요:");
            int amount = scanner.nextInt();
            scanner.nextLine();//씹히는 오류방지

            payService.processPay(payOption, amount);
        }
        
    }

}

Scanner은 까먹었었는데 다시 상기 시킴.

0개의 댓글