JAVA의 가장 기본이 되는 내용

OCP (Open-Closed Principle) 원칙

cwchoiit 2024. 3. 30. 14:46
728x90
반응형
SMALL

참고 자료:

 

김영한의 실전 자바 - 중급 1편 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., [사진]국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바

www.inflearn.com

좋은 객체 지향 설계 원칙 중 하나로 OCP 원칙이라는 것이 있다.

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

확장에는 열려있고, 변경에는 닫혀 있다는 뜻인데, 쉽게 이야기해서 기존의 코드 수정 없이 새로운 기능을 추가할 수 있다는 의미다. 인터페이스(또는 순수 추상 클래스)와 다형성을 이용해서 새로운 구현 클래스가 계속 늘어나더라도 그 인터페이스를 사용하는 클라이언트는 구현 클래스가 늘어난 사실조차 몰라도 아무런 코드의 변경없이 원래 의도대로 동작하는 것을 말한다. 

 

예를 들어, 다음 그림을 보자.

Driver라는 클래스는 인터페이스 Car를 의존한다. 여기서 의존이라는 말은 알고 있다는 사실로 봐도 무방하다. 그래서 Car라는 인터페이스가 가지고 있는 메서드 startEngine(), offEngine(), pressAccelerator() 이 세가지 메서드를 사용한다. 그리고 실제 Car라는 인터페이스를 구현하는 여러 구현 클래스가 있을 것이다. 그 중 K3, Model 3, Model Y, Genesis 등 여러 구현 클래스가 계속해서 늘어나더라도 Driver라는 클라이언트 입장에서는 늘어났다는 사실조차 몰라도 아무런 코드의 변경이 없이 기존 코드 그대로 동작가능하게 설계하는 것이 Open-Closed Principle이다.

 

코드로 하나하나 이해해보자.

 

Car

다음은 인터페이스 Car 코드이다. 이 Car라는 인터페이스는 3개의 메서드를 가지고 있다. 이제 Car를 구현하는 클래스는 반드시 이 세개의 메서드를 오버라이딩해야 한다.

public interface Car {

    void startEngine();
    void offEngine();
    void pressAccelerator();
}

 

K3

다음은 K3 클래스이다. Car를 구현하게 설계했으므로 반드시 오버라이딩 해야하는 3개의 메서드를 전부 오버라이딩한다.

public class K3 implements Car {

    @Override
    public void startEngine() {
        System.out.println("K3 시동 켜기");
    }

    @Override
    public void offEngine() {
        System.out.println("K3 시동 끄기");
    }

    @Override
    public void pressAccelerator() {
        System.out.println("K3 엑셀 밟기");
    }
}

 

Model 3

다음은 Model 3 클래스이다. K3와 마찬가지이다.

public class Model3 implements Car {

    @Override
    public void startEngine() {
        System.out.println("Model 3 시동 켜기");
    }

    @Override
    public void offEngine() {
        System.out.println("Model 3 시동 끄기");
    }

    @Override
    public void pressAccelerator() {
        System.out.println("Model 3 엑셀 밟기");
    }
}

 

Driver

Driver 클래스는 Car라는 인터페이스를 사용하는 클라이언트 입장이다. Car라는 인터페이스를 구현하는 클래스가 계속해서 늘어나더라도 이 코드의 변경사항은 없다. 즉, 여기서 바로 OCP원칙이 두각이 드러난다. 아무리 기능이 확장되어도(차종이 늘어나는것) 코드의 변경이 없다. 

public class Driver {

    private Car car;

    public void drive() {
        car.startEngine();
        car.pressAccelerator();
        car.offEngine();
    }

    public void setCar(Car car) {
        this.car = car;
    }

    public Car getCar() {
        return car;
    }
}

 

Main

실행 코드에서 확인해보자. 일단 K3를 Driver 클래스가 사용한다. K3는 Car라는 인터페이스를 구현하기 때문에 Driver의 메서드 setCar()에 파라미터로 K3를 넘겨줄 수 있다. 부모는 자식을 허용하기 때문에. 정확히는 객체와 메모리 구조를 생각해보면 K3라는 인스턴스는 참조 공간에 부모와 같이 쌓아 올려진다. 그렇기 때문에 부모 타입 변수에 담을 수 있는것이다. (업캐스팅)

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

        driver.setCar(k3);

        driver.drive();

    }
}

이렇게 해서 실행해보면 다음과 같이 잘 실행된다.

실행결과:

K3 시동 켜기
K3 엑셀 밟기
K3 시동 끄기

 

근데 여기서 Model 3 로 자동차를 바꿔보자. 아예 없던 클래스라고 생각하고 새로 만들었다고 생각해보자. 즉, 기능의 확장이 일어난것이다. 근데 Driver 코드는 변경되지 않는다. 바뀌는 부분은 사용하는 코드만 변경될 뿐이다. 다음 코드처럼.

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

        Model3 model3 = new Model3();
        driver.setCar(model3);
        driver.drive();
    }
}

 

실행결과:

Model 3 시동 켜기
Model 3 엑셀 밟기
Model 3 시동 끄기

 

이것을 OCP 원칙이라고 한다. 그러니까 기존의 코드에 대한 변경이 아예 없을 순 없다. 새로운 기능을 추가하는 것 자체가 기존 코드에 변경이 일어나는데 어떻게 아예 변경을 안하겠는가? 그러나 절대적인 원칙은 지켜져야한다. 클라이언트의 코드는 변경되지 않거나 변경하더라도 최소화해야 한다. 이 코드에서 클라이언트는 누구인가? Driver다. 서버는 누구인가? Car라는 인터페이스다. 클라이언트는 서버만 알면 된다. 그 서버를 실제로 구현한 클래스가 100개든 1000개든 알 필요가 없다. 그리고 클라이언트의 코드는 변경이 필요가 없다. 이게 중요한 것이다. 사용할 때는 당연히 코드 변경이 필요하다. 위에 예시처럼, K3에서 Model3로 바꾸는 그런 과정들. 그리고 이것이 디자인 패턴 중에정말 중요한 하나인 전략 패턴이랑 매우매우 유사한데 전략 패턴을 하나 배웠다고 해도 과언이 아니다. 

728x90
반응형
LIST