SOLID 원칙 - (2) 개방 폐쇄 원칙 (OCP)



(2) 개방 폐쇄 원칙 (OCP)

👴 A Module should be open for extension but closed for modification

말은 제일 쉽다. 확장에는 열려있어야 하고, 변경에는 닫혀있어야 한다. 어떠한 기능을 변경하거나 확장하면서, 그 기능을 사용하는 기존 코드의 수정을 최소화한다는 것이다. 개방 폐쇄 원칙의 핵심은 변경이 예상되는 곳을 추상화하고 다형성을 이용하는 것이다. 개방 폐쇄 원칙을 적용하는 방법은 대표적으로 두 가지가 있다.


OCP 적용 방법

EX1) OCP 적용 방법 : 추상화

img

(상황 ) 롤의 스킬은 Q,W,E,R로 구성되어 있다1. 슬기는 게임을 할 때 각 캐릭터별로 구현된 Q,W,E,R 스킬을 누르면서 게임을 한다.


(추상화 ) 여기서 스킬 Q,W,E,R을 Character라는 인터페이스로 추상화시켰다2. 야스오와 티모는 Character 인터페이스를 구현한다. 슬기는 야스오와 티모를 플레이할 때 Q,W,E,R을 클릭한다.

만약 새로운 캐릭터인 그웬을 추가하는 경우(확장) 이미 추상화한 Character를 구현하면 된다. 하지만 슬기는 기존과 동일하게 그웬을 플레이할 때 Q,W,E,R을 누르는 것은 변함없다!(기존 코드 변함 없음)

:bulb: 1 사실 더 있다.
:bulb: 2 명답은 아닌 것 같다. 인터페이스를 이용할 거면 Skill이라는 인터페이스로 만들거나 Character내에 QWER을 추상메소드로 만들어서 상속받는 방법이 더 나을 것 같지만.. 귀찮으니 그냥 패스한다.


EX2) OCP 적용 방법 : 상속

img

(상황 ) 롤을 할 때 가장 중요한 것이 있다. 바로 한타라는 중요한 순간에 스킬 콤보를 넣는 것이다. 하지만 캐릭터마다 스킬이 다르므로 스킬 콤보도 다 다르다. 슬기는 롤린이라 캐릭터 스킬은 다 모르지만 항상 스킬 콤보를 넣고 싶다! fight()라는 메소드만을 호출함으로써 캐릭터별로 알맞은 콤보를 넣으면 어떨까?


(상속 ) 개방 폐쇄 원칙을 상속을 통해 적용하기 위해 템플릿 메소드 패턴을 사용할 것이다. 오버라이딩할 상위 클래스의 메소드에 접근 제어자를 protected로 설정한다.

슬기는 한타 때 fight() 메소드를 호출하면되고, fight()내에선 스킬 콤보를 사용해 줄 combo() 함수를 사용한다.

public class Character {
    public void fight() {
        combo();
    }
    protected void combo() {
				Q(); W(); E(); R();         //대충 기본 콤보라 가정
    }
}
public class 야스오 extends Character{
    @Override
    protected void combo() {
        E(); Q(); E(); Q(); R();    //야스오 안 써서 사실 모름
    }
}

Character를 상속받은 야스오combo()를 오버라이딩해서 야스오에 맞는 스킬 콤보를 재정의한다.

public void 한타(Character character) {
		character.fight();
}

슬기는 야스오의 콤보가 정확히 어떤 것인지 알 필요없이 fight()만을 호출하면 알아서 해당 캐릭터에 맞게 콤보를 적용할 수 있다! MVP SSAP가능


OCP가 깨졌을 때의 주요 증상

그렇다면 개방 폐쇄 원칙을 잘 지켰는지 확인하는 방법을 무엇일까? 개방 폐쇄 원칙이 깨졌을 때의 주요 증상들을 확인하면 된다.

EX1) 다운 캐스팅

개방 폐쇄 원칙이 깨졌을 때의 주요 증상 중 하나로는 다운캐스팅이다. 추상화를 제대로 시키지 않은 경우 다음과 같이 다운캐스팅이 일어난다. 야스오일땐 이 콤보, 티모일땐 이 콤보, ….

만약 여기서 새로운 캐릭터들이 계속 추가된다면 각 클래스의 타입마다 다운캐스팅을 해주어야 한다.

public void 한타(Character character) {
    if(character instanceof 야스오){
        E(); Q(); E(); Q(); R();
    }
    else if(character instanceof 티모){
        R(); R(); R();
    }
    else {
        ...
    }
}


EX2) 비슷한 if-else 구문

두번째 증상은 비슷한 if-else 구문이 많아진다는 것이다.

(상황 ) 롤 캐릭터는 직업군별로 가지고 있는 기본 스탯이 다르다. 기본적으로 체력은 가지지만 직업마다 마나, 기력, 분노 등을 다르게 가지고 있다. 나는 캐릭터의 기본 스탯을 초기화하는 코드를 작성하고 싶다. 근데 캐릭터 직업군별로 다르게 스탯을 설정하고 싶다! 처음 작성한 코드는 다음과 같다.

private int characterType;

public void initialize() {
    if(characterType==1) {
        health = 100;
        mana = 100;
        energy = 0;
        fury = 0;
    }
    else if(characterType==2){
        health = 100;
        mana = 0;
        energy = 100;
        fury = 0;
    }
    else {
        ...
    }
		...
		// 초기화 로직
}

비슷한 if-else 구문이 반복된다. if-else 블록 내에 들어가는 부분을 추상화시킨다면 코드가 좀 더 깔끔해질 것 같다.


img

private CharacterType characterType;

public void initialize() {
    int health = characterType.getHealth();
    int mana = characterType.getMana();
    int energy = characterType.getEnergy();
    int fury = characterType.getFury();
    ...
		//초기화 로직
}

(추상화 ) 중복되는 부분을 CharacterType으로 추상화시켰다. 각 직업군별로 스탯을 다르게 설정하고 싶으면 CharacterType을 구현하면 된다. 코드를 사용하는 경우에는 중복된 코드를 작성할 필요가 없어져 코드가 훨씬 간결해진다.


결론

개방 폐쇄 원칙은 코드 변경의 유연함과 관련되어 있다. 가장 핵심은 기능을 확장해도 기존 코드의 수정을 최소화하는 것이다. 또한 SOLID에서의 리스코프 치환 원칙과 의존 역전 원칙은 개방 폐쇄 원칙을 지원한다. 아마 다음 포스팅에서 나올 것이다.



:bookmark: REFERENCE
최범균, 「개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴」, 인투북스
Solid Relevance