Java

[Java] OCP - 개방 폐쇄 원칙을 잘지키는 코드

수수한개발자 2023. 12. 13.
728x90

객체 지향의 5대 원칙(SOLID)에는 다음과 같이 있습니다.

 

  • SRP(단일 책임의 원칙: Single Responsibilty Principle) - 하나의 클래스는 하나의 책임을 가져야 한다.
  • OCP(개방 폐쇄 원칙: Open-Closed Principle) - 확장에 대해 열려있고 수정에 대해서는 닫혀 있어야 한다.
  • LSP(리스코프 치환의 원칙: Liskov Substitution Principle) - 하위 타입은 상위 타입을 대체할 수 있어야 한다.
  • ISP(인터페이스 분리 원칙: Interface segregation principle) - 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 않아야 한다.
  • DIP(의존 역전의 원칙: Dependency Inversion Principle) - 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

 

오늘은 개방 폐쇄 원칙을 잘 지키는 코드를 작성하는 법에 대해 공부하고 글을 작성해 보겠습니다.

 

예제는 사람이 저녁메뉴를 선택하는 프로그램입니다.

 

public class Pizza {
    public void selected(String menu) {
        System.out.println(menu + "를 선택했습니다.");
    }
}

public class Chicken {
    public void selected(String menu) {
        System.out.println(menu + "를 선택했습니다.");
    }
}

public class Person {

    private Chicken chicken;
    private Pizza pizza;

    public void setChicken(Chicken chicken) {
        this.chicken = chicken;
    }

    public void setPizza(Pizza pizza) {
        this.pizza = pizza;
    }

    public void choiceMenu(String menuName) {
        System.out.println("음식을 선택합니다.");
        if (chicken != null) {
            chicken.selected(menuName);
        } else if (pizza != null) {
            pizza.selected(menuName);
        }
    }
}

 

사람은 치킨과 피자 중에 널리 아닌 메뉴를 선택하게 됩니다. 

 

Person person = new Person();

Chicken chicken = new Chicken();
person.setChicken(chicken);
person.choiceMenu("후라이드 치킨");
person.setChicken(null);

Pizza pizza = new Pizza();
person.setPizza(pizza);
person.choiceMenu("포테이토 피자");

 

 

이렇게 되면 새로운 메뉴가 생길 때마다 Person 객체의 멤버 변수가 늘어나고 choiceMenu 또한 변경해야 하므로 Person의 코드를 계속 변경해주어야 한다.

 

이렇게 된 이유는 역할과 구현을 분리하지 않아서 생기는 문제점입니다.

 

역할과 구현을 분리

 

역할과 구현을 분리하면 클라이언트 즉, 위의 예제에서는 Person의 코드 변경 없이 구현 객체를 변경할 수 있게 됩니다.

 

 

 

  • Person : 사람은 Menu의 역할(인터페이스)에만 의존한다. 구현인 Chicken과 Pizza에 의존하지 않는다.
  • Person 클래스는 Menu menu 멤버 변수를 가진다. 따라서 Menu 인터페이스를 참조한다.
  • 인터페이스를 구현한 Chicken, Pizza에 의존하지 않고 Menu 인터페이스에만 의존한다.
  • 여기서 의존은 클래스 의존 관계 즉, 클래스 상에서 어떤 클래스를 알고 있는가를 뜻합니다.
  • Menu 메뉴의 역할이고 인터페이스입니다. Chicken, Pizza 클래스가 인터페이스를 구현합니다.
public interface Menu {
    void selected(String menu);
}

public class Chicken implements Menu {
    @Override
    public void selected(String menu) {
        System.out.println(menu + "를 선택했습니다.");
    }
}

public class Pizza implements Menu {
    @Override
    public void selected(String menu) {
        System.out.println(menu + "를 선택했습니다.");
    }
}

public class Person {
    private Menu menu;
    public void setMenu(Menu menu) {
        this.menu = menu;
    }
    public void choiceMenu(String menuName) {
        System.out.println("음식을 선택합니다.");
        menu.selected(menuName);
    }
}

 

 

이제 Person 즉 클라이언트에서 선택하고자 하는 메뉴만 주입해 주면 됩니다.

 

Person person = new Person();

// 치킨일때
Chicken chicken = new Chicken();
person.setMenu(chicken);
person.choiceMenu("치킨");

// 피자일때
Pizza pizza = new Pizza();
person.setMenu(pizza);
person.choiceMenu("피자");

 

 

개방 폐쇄 원칙에서 중요한 두 가지는 다음과 같습니다.

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

- Closed for modification : 기존 코드를 변경하지 않고 사용할 수 있어야 한다.

 

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

 

새로운 메뉴를 추가해도 Person의 코드는 전혀 변경하지 않습니다.

 

확장에 열려있다는 의미

  • Menu 인터페이스를 사용해서 새로운 메뉴를 자유롭게 추가할 수 있다.
  • Menu 인터페이스를 구현해서 새로운 메뉴를 만들면 됩니다.
  • Menu 인터페이스를 사용하는 클라이언트 코드는 Person객체는 Menu 인터페이스를 통해 새롭게 추가된 메뉴를 자유롭게 호출할 수 있습니다.

 

변경에 닫혀 있다는 의미

새로운 메뉴를 추가하면 기능이 추가하기 때문에 코드의 수정은 불가피하다. 당연히 어딘가의 코드는 수정되어야 한다.

여기서는 새로운 메뉴, main 메서드 등등..

 

그러므로 변하지 않는 부분과 변하는 부분을 구분해야 합니다.

 

변하지 않는 부분

이 예제에서 클라이언트는 Person으로 새로운 메뉴를 추가할 때마다 가장 영향을 많이 받는 중요한 클라이언트입니다.

그러므로 핵심은 Menu 인터페이스를 사용하는 클라이언트인 Person의 코드를 수정하지 않아도 된다는 뜻입니다.

 

변하는 부분

main(), 새로운 메뉴를 생성하는 코드 등 Person에게 필요한 메뉴를 전달하는 역할은 당연히 코드 수정이 불가피합니다.

main() 메서드는 프로그램을 설정하고 조율하는 역할을 하기 때문에 이런 부분은 OCP를 지켜도 변경이 불가피하게 됩니다.

 

 

 

위의 코드를 java 8에서 추가된 @FunctionalInterface를 사용하여 리팩토링을 해보겠습니다.

 

함수형 인터페이스에 대해서는 다음 블로그에 잘 정리되어 있습니다. -> https://bcp0109.tistory.com/313

 

@FunctionalInterface
public interface MenuFunction<String, Menu> {
    Menu apply(String menuType);
}

 

 

MenuFunction 인터페이스는 String 타입을 받아 Menu 인터페이스를 리턴합니다.

 

// 1번 방법
public enum MenuList {
    CHICKEN("치킨"),
    PIZZA("피자")
    ;
    
    private final String menuName;
    
    private final MenuFunction<String, Menu> menuFunction = menuType -> switch (menuType) {
        case "치킨" -> new Chicken();
        case "피자" -> new Pizza();
        default -> throw new IllegalArgumentException("존재하지 않는 메뉴입니다.");
    };
    
    MenuType(String menuName) {
        this.menuName = menuName;
    }
    
    public String getMenuName() {
        return menuName;
    }
    
    public Menu getMenu() {
        return menuFunction.apply(this.menuName);
    }
}

// 2번 방법
public enum MenuList {
    CHICKEN("치킨", menuName -> new Chicken()),
    PIZZA("피자", menuName -> new Pizza())
    ;
    
    private final String menuName;
    private final MenuFunction<String, Menu> menuFunction;

    MenuList(String menuName, MenuFunction<String, Menu> menuFunction) {
        this.menuName = menuName;
        this.menuFunction = menuFunction;
    }
    
    public String getMenuName() {
        return menuName;
    }
    public Menu getMenu() {
        return menuFunction.apply(this.menuName);
    }
}

 

enum을 활용하여 Functional Interface 호출할 수 있도록 해줍니다. 기본적으로 람다식을 지원하기 때문에 1번과 2번 방법 중 선택해서 사용하면 될 것 같습니다.

 

public class Person {
    public void choiceMenu(MenuList menuList) {
        System.out.println("음식을 선택합니다.");
        Menu menu = menuList.getMenu();
        menu.selected(menuList.getMenuName());
    }
}

 

이제 Person 객체에서는 파라미터로 전달받은 MenuList, 메뉴 목록에서 메뉴를 가져와 선택하기만 하면 됩니다.

 

Person person = new Person();

MenuList pizza = MenuList.PIZZA;
person.choiceMenu(pizza);

MenuList chicken = MenuList.CHICKEN;
person.choiceMenu(chicken);

 

이제 새로운 메뉴가 추가되어도 MenuList에만 추가만 해주면 되는 OCP 원칙을 지키는 코드로 변경되었습니다.

 

OCP를 포함에 SOLID가 얘기하는 핵심은 다형성인 것 같다.

구체 클래스에 의존하지 않고 역할(인터페이스)에 의존함으로써 유연하고 확장 가능한 애플리케이션을 만들 수 있게 되었다.

 

 

ref

https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%9E%90%EB%B0%94-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

728x90

댓글