4 분 소요

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)은 변경의 유연함과 관련된 원칙이다
    • 기존 기능의 확장, 변경을 위해 기존의 코드를 수정해야 하는 상황은 확장과 변경을 어렵게 만든다.
  • 변화되는 부분을 추상화함으로써 사용자(변화되는 부분을 가져다 쓰는 곳) 입장에서 변화를 고정시킨다.
  • 변화되는 부분을 메서드로 고정시켜 오버라이딩을 이용하면 기존 코드의 변경없이 확장시킬 수 있다.
    • 메서드를 오버라이딩해도 조상 (부모)래클스를 변경할 필요가 없고, 가져다 쓰는 곳에서도 코드를 변경 시킬 필요가 없다.
  • 개방 폐쇄 원칙은 변화가 예상되는 것을 추상화해서 변경의 유연함을 얻도록 해준다. 변화되는 부분을 추상화하지 못하면 원칙을 지킬 수 없고 코드가 쌓여나갈 수록 기능 변경이나 확장이 어려워진다.
    • 소프트웨어는 변화하는 고객의 요구사항에 대응해야 한다. 개방 폐쇄 원칙을 지키지 못한다면 변경, 확장이 어려워지고 변화하는 요구사항에 대응할 수 없다.
  • 유연성, 재사용성, 유지보수성(변경, 확장)을 얻기 위해 개방 폐쇄 원칙을 지켜야 한다.


댓글남기기