[객체지향] SOLID: 개방 폐쇄 원칙(Open-Closed Principle, OCP)
Reference
스프링 입문을 위한 자바 객체 지향의 원리와 이해, 위키북스, 김종민 지음
개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴, 인투북스, 최범균 지음
📌 OCP: 개방 폐쇄 원칙
소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다.
- Robert C. Martin
- 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
- 기능을 변경하거나 확장할 수 있어야 한다.
- 그 기능을 사용하는 코드는 수정하지 않아야 한다.
- OCP를 교과서처럼 잘 지키고 있는 완벽한 예시 중 하나는 스프링 프레임워크이다.
OCP의 예시 1: JDBC
JDBC 는 OCP를 잘 지키고 있는 좋은 예시이다.
- JDBC를 사용하는 클라이언트는 데이터베이스가 바뀌더라도 연결을 설정하는 부분 외에는 따로 수정할 필요가 없다.
- JDBC 인터페이스가 변화에 영향을 받지 않도록(완충 역할) 만든다.
- 만약 연결 설정 부분을 별도의 설정 파일로 분리해두면 클라이언트 코드는 변경할 필요가 없어진다.
- 이는 Java Application이 주변의 변화에 닫혀 있는 것이다. 또 데이터베이스를 변경한다는 것은 데이터베이스가 자신의 확장(변경)에 열려 있다는 의미이다.
OCP의 예시 2: JVM
- 각 운영체제별 JVM과 목적 파일(.class)이 있어 Java는 OS를 구분하지 않고 똑같은 소스코드로 똑같은 결과를 낼 수 있다.
- 개발자가 작성한 코드는 운영체제의 변화에 닫혀 있고, 각 운영체제별 JVM은 확장에 열려 있다는 의미이다.
- 목적 파일(.class)가 변화에 영향을 받지 않도록(완충 역할) 만든다.
OCP를 지킬 수 있는 방법 1: 추상화
비즈니스 로직을 인터페이스로 구현하고, 세세한 기능들의 정의는 인터페이스를 구현한 클래스들에서 진행된다고 할 때, 세부 기능이 새로 추가되도 인터페이스를 가져다 쓰는 컨트롤러의 코드는 변화 없이 새로운 세부 기능을 사용할 수 있다.
- 변화 가능성이 있는 부분(확장되는 부분)을 추상화해서 표현해야 OCP를 지킬 수 있다.
OCP를 지킬 수 있는 방법 2: 상속(다형성)
상속은 상위 클래스의 기능을 그대로 사용하며 하위 클래스에서 일부 구현을 오버라이딩(재정의)할 수 있다.
- ResponseSender 클래스: 클라이언트의 요청이 있을 때 데이터를 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() {
// 텍스트로 데이터 전송
}
}
public class ZippedResponseSender extends ResponseSender {
public ZippedResponseSender(Data data) {
super(data);
}
@Override
protected void sendHeader() {
// 압축된 헤더 데이터 전송
}
@Override
protected void sendBody() {
// 압축된 텍스트로 데이터 전송
}
}
- ResponseSender 클래스는 확장에는 열려 있으면서 변경에는 닫혀 있게 된다.
OCP 위반 증상
- 추상화와 다형성(상속)을 이용해 OCP를 지키기 때문에 추상화와 다형성이 제대로 지켜지지 않은 코드는 OCP를 위반하게 된다.
OCP 위반 특징 1: 다운 캐스팅
예를 들어 게임의 유닛이 총 두 가지로 나뉜다고 하자. 하늘을 날 수 있는 공중 유닛과 지상 유닛이 존재할 때, 유닛들은 맵을 이동할 수 있다.
class Unit {
}
class AircraftUnit extends Unit{
}
class GroundUnit extends Unit{
}
- 두 유닛의 이동을 위한 기능이 필요하다면?
public void canMove(Unit unit) {
if (unit instanceof AircraftUnit) { // 유닛의 타입을 확인
AircraftUnit aircraftUnit = (AircraftUnit)unit; // 다운 캐스팅
aircraftUnit.moveEveryWhere();
} else {
unit.move();
}
}
만약 공중 유닛의 이동을 별도 처리를 하기 위해 다른 메서드를 구현한다면 Unit 클래스가 확장될 때 함께 수정될 수 있다. 즉, 변경에 닫혀 있지 않은 것이다.
instanceof
와 같은 타입 확인 연산자를 사용하는 코드는 OCP를 위반하고 있을 가능성이 매우 높다.- 타입 캐스팅 후 실행하는 메서드가 변화 대상인지 확인 필요. 만약 메서드가 객체들마다 다르게 동작할 가능성이 높다면 추상화시켜야 한다.
OCP 위반 특징 2: 비슷한 if-else 블록의 남발
공중 유닛은 상대방의 기지를 정찰할 때 사용할 수 있다. 이때 기지가 될 수 있는 곳을 시계방향으로 표시했을 때 플레이어 자신 포함 1시, 5시, 7시, 11시가 존재한다. 자신의 기지 위치를 기준으로 반시계방향으로 정찰을 해주는 패턴을 추가한다면 아래와 같이 작성할 수 있다.
class AircraftUnit extends Unit {
private final int position;
public AircraftUnit(int position) {
this.position = position;
}
public void move() {
if (position == 1) {
// 1시 기준 반시계 방향 회전 정찰
} else if (position == 5) {
// 5시 기준 반시계 방향 회전 정찰
} else if (position == 7) {
// 7시 기준 반시계 방향 회전 정찰
} else {
// 11시 기준 반시계 방향 회전 정찰
}
}
}
- 만약 기존 1시, 5시, 7시, 11시 방향에 기지가 있지 않고 새로운 위치에 기지가 존재하는 맵이 추가된다면 move() 메서드에는 새로운 블록이 추가 될 것이다. 즉, 새로운 정찰 패턴을 추가 하는데 AircrafUnit 클래스가 닫혀 있지 않은 것이다.
- 정찰 패턴을 추상화하고 AircraftUnit 에서 추상화 타입을 사용하는 구조로 바꾸도록 한다
interface SearchPattern {
public int newX;
public int newY;
}
class SearchPatternOne implements SearchPattern {
}
class SearchPatternFive implements SearchPattern {
}
class SearchPatternSeven implements SearchPattern {
}
class SearchPatternEleven implements SearchPattern {
}
class AircraftUnit extends Unit {
private SearchPattern pattern;
public AircraftUnit(SearchPattern pattern) {
this.pattern = pattern;
}
public void move() {
int x = pattern.newX;
int y = pattern.newY;
// move
}
}
OCP == 유연함
- 개방 폐쇄 원칙(OCP)은 변경의 유연함과 관련된 원칙이다
- 기존 기능의 확장, 변경을 위해 기존의 코드를 수정해야 하는 상황은 확장과 변경을 어렵게 만든다.
- 변화되는 부분을 추상화함으로써 사용자(변화되는 부분을 가져다 쓰는 곳) 입장에서 변화를 고정시킨다.
- 변화되는 부분을 메서드로 고정시켜 오버라이딩을 이용하면 기존 코드의 변경없이 확장시킬 수 있다.
- 메서드를 오버라이딩해도 조상 (부모)래클스를 변경할 필요가 없고, 가져다 쓰는 곳에서도 코드를 변경 시킬 필요가 없다.
- 개방 폐쇄 원칙은 변화가 예상되는 것을 추상화해서 변경의 유연함을 얻도록 해준다. 변화되는 부분을 추상화하지 못하면 원칙을 지킬 수 없고 코드가 쌓여나갈 수록 기능 변경이나 확장이 어려워진다.
- 소프트웨어는 변화하는 고객의 요구사항에 대응해야 한다. 개방 폐쇄 원칙을 지키지 못한다면 변경, 확장이 어려워지고 변화하는 요구사항에 대응할 수 없다.
- 유연성, 재사용성, 유지보수성(변경, 확장)을 얻기 위해 개방 폐쇄 원칙을 지켜야 한다.
댓글남기기