[SOLID] 개방 폐쇄 원칙(OCP)이란?
안녕하세요? 제이온입니다.
저번 시간에는 단일 책임 원칙에 대해서 알아보았습니다. 오늘은 개방 폐쇄 원칙을 설명하겠습니다.
개방 폐쇄 원칙 (Open-Closed Principle)의 정의
개방 폐쇄 원칙은 "확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다."를 의미합니다. 조금 더 쉽게 풀어 쓰자면, "기능을 변경하거나 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않는다."를 뜻합니다.
한 가지 예시를 봅시다.
위 사진은 자바 어플리케이션에서 JDBC 매니저를 이용하기 위해 설계된 구조입니다. JDBC 매니저를 상속받는 PostgreSQL, Oracle, Sybase는 모두 변경에는 확장적이지만, 자바 어플리케이션은 수정에 폐쇄적인 것을 알 수 있습니다. 더 쉽게 이야기하자면, Oracle DB의 변화가 발생하여도 자바 어플리케이션에서 수정할 코드는 없다는 것입니다. 즉, 개방 폐쇄 원칙은 하나의 변화가 다른 곳에도 연쇄적으로 변화를 일으키는 것을 방지하기 위해 만들어졌습니다.
그렇다면, 개방 폐쇄 원칙을 어떻게 지킬 수 있을까요?
개방 폐쇄 원칙을 지키는 방법
개방 폐쇄 원칙의 핵심은 변화하는 부분을 추상화하는 것입니다. 위의 3가지 DB는 기능의 이름은 같더라도 구체적으로 어떻게 동작하는지는 다를 수 있습니다. 이 부분에 대해서 추상화함으로써 기능을 고정시킬 수 있습니다. 주로, 인터페이스를 통해서 구현을 합니다.
두 번째는 상속을 이용하는 것입니다. 예를 들어, 클라이언트의 요청이 왔을 때 데이터를 HTTP 응답 프로토콜에 맞춰 데이터를 전송해 주는 ResponseSender가 있다고 합시다.
public class ResponseSender {
private Data data;
public ResponseSender(Data data) {
this.data = data;
}
public Data getData() {
return data;
}
public void send() {
sendHeader();
sendBody();
}
protected void sendHeader() {
// 헤더 데이터 전송
}
protected void sendBody() {
// 텍스트로 데이터 전송
}
}
ResponseSender 클래스의 send() 메소드는 헤더와 몸체 내용을 전송하기 위해 sendHeader() 메소드와 sendBody() 메소드를 차례대로 호출하며, 이 두 메소드는 알맞게 HTTP 응답 데이터를 생성합니다. 이때, 이 두 메소드는 protected 공개 범위를 갖고 있기 때문에 하위 클래스에서 오버라이딩이 가능합니다.
public class ZippedResponseSender extends ResponseSender {
public ZippedResponseSender(Data data) {
super(data);
}
@Override
protected void sendBody() {
// 데이터 압축 처리
}
}
ZippedResponseSender 클래스는 기존 기능에 압축 기능을 추가해 주는데, 이 기능을 추가하기 위해 ResponseSender 클래스의 코드는 바뀌지 않았습니다. 즉, ResponseSender 클래스는 확장에는 열려 있으면서 변경에는 닫혀있는 것이죠.
개방 폐쇄 원칙을 지키지 않았을 때의 문제점
추상화와 다형성을 이용해서 개방 폐쇄 원칙을 구현하기 때문에, 추상화와 다형성이 제대로 지켜지지 않은 코드는 개방 폐쇄 원칙을 어기게 됩니다. 이제, OCP 원칙을 어기는 특징을 살펴봅시다.
(1) 다운 캐스팅을 한다.
예를 들어, 슈팅 게임을 개발하는 경우 플레이어, 적, 미사일 등을 그리기 위해 Character 클래스의 draw() 메소드를 정의한 후, Player, Enemy, Missile 클래스에 상속을 할 수 있습니다. 그런데, 화면에 이들 캐릭터를 표시해 주는 코드가 아래와 같다면 어떨까요?
public void drawCharacter(Character character) {
if(character instanceof Missile) { // 타입 확인
Missile missile = (Missile) character; // 타입 다운 캐스팅
missile.drawSpecific();
} else {
character.draw();
}
}
다른 캐릭터는 그리는 방식이 동일하지만, 미사일만 다른 방식으로 표현해 주고 싶을 수도 있습니다. 가장 쉽게 할 수 있는 방법은 다운 캐스팅을 취하는 것이지만, Character 클래스에서 변경이 일어날 때마다 해당 메소드를 수정해 주어야 할 수도 있습니다. 즉, 변경에 닫혀 있지 않은 것이죠.
따라서, instanceof와 같은 타입 확인 연산자가 사용된다면 해당 코드는 개방 폐쇄 원칙을 위반할 가능성이 높으므로 타입 캐스팅 후 실행되는 메소드가 변화 대상인지 확인해야 합니다. 가령, drawSpecific() 메소드가 다른 객체들에서도 적용할 만한 메소드라면, Character 타입에 추가하라는 것입니다.
(2) 비슷한 if~else 블록이 존재한다.
이번에는 Character 클래스를 상속받은 Enemy 클래스가 있다고 합시다. 정해진 패턴에 따라 경로를 이동하는 코드가 필요하다면, 아래와 같이 작성할 수 있습니다.
public class Enemy extends Character {
private int pathPattern;
public Enemy(int pathPattern) {
this.pathPattern = pathPattern;
}
public void draw() {
if(pathPattern == 1) {
x += 4;
} else if(pathPattern == 2) {
y += 10;
} else if(pathPattern == 4) {
x += 4;
y += 10;
}
...; // 그려 주는 코드
}
}
Enemy 클래스에 새로운 경로 패턴을 추가할 때마다 draw() 메소드에는 새로운 if문이 생깁니다. 즉, 경로를 추가하는데 Enemy 클래스는 변경에 닫혀있지 않은 것입니다. 따라서, (x, y)와 같은 경로 패턴을 추상화 해야합니다.
public class Enemy extends Character {
private PathPattern pathPattern;
public Enemy(PathPattern pathPattern) {
this.pathPattern = pathPattern;
}
public void draw() {
int x = pathPattern.nextX();
int y = pathPattern.nextY();
...; // 그려 주는 코드
}
}
이렇게 추상화를 하고나면, 새로운 이동 패턴이 생기더라도 draw() 메소드는 변경되지 않습니다.
정리
개방 폐쇄 원칙은 유연함에 관련된 원칙입니다. 변화하는 부분을 추상화함으로써 기존 코드를 수정하지 않고도, 확장을 할 수 있게 만들어 줍니다. 따라서, 우리는 개발을 하다가 코드에 대한 변화 요구가 발생하면, 변화와 관련된 구현을 추상화해서 개방 폐쇄 원칙에 맞게 수정할 수 있는지 확인해 보는 습관을 길러야 합니다.
출처
개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 - 최범균
'개발 이야기 > OOP' 카테고리의 다른 글
[OOP] 다형성(Polymorphism)이란? (1) | 2021.05.28 |
---|---|
[SOLID] 의존 역전 원칙(DIP)이란? (3) | 2021.03.12 |
[SOLID] 인터페이스 분리 원칙(ISP)이란? (1) | 2021.03.11 |
[SOLID] 리스코프 치환 원칙(LSP)이란? (2) | 2021.03.11 |
[SOLID] 단일 책임 원칙(SRP)이란? (2) | 2021.02.25 |
댓글