객체 지향 설계 원칙(SOLID)
객체 지향 언어의 등장 이후 수많은 시행착오와 베스트 프랙티스 속에서 객체 지향 설계 5가지 원칙이 등장했는데, 바로 SOLID다. SOLID는 로버트 C. 마틴(Robert C. Martin)이 2000년대 초반 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙으로 제시한 것을 마이클 페더스(Michael Feathers)가 두문자어로 소개한 것이다.
SOLID는 다음 5가지 원칙의 앞 글자를 따서 부르는 이름이다.
- SRP(Single Responsiblity Principle) : 단일 책임 원칙
- OCP(Open Closed Principle) : 개방 폐쇄 원칙
- LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle) : 의존 역전 원칙
이 원칙들은 응집도는 높이고(High Cohesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체지향의 관점에서 재정립한 것이다.
개방 폐쇄 원칙(OCP: Open Closed Principle)
“소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다.”
- 로버트 C. 마틴 -
로버트 C. 마틴이 말하는 개방 폐쇄 원칙(OCP)의 의미는 다음과 같다.
확장에 열려 있다.
- 새로운 요구 사항이나 기능 추가에 대해 쉽게 확장할 수 있어야 한다.
주변의 변화에 대해서는 닫혀 있다.
- 기존의 클래스, 모듈, 컴포넌트는 수정되지 않아야 한다.
OCP는 새로운 요구 사항이나 기능 추가가 있을 때, 기존 코드를 수정하지 않고 새로운 코드를 추가함으로써 시스템을 업데이트할 수 있어야 함을 의미한다. 이로써 소프트웨어의 유지보수성과 확장성을 향상하는 데 도움을 준다.
OCP 예시
운전자와 자동차
OCP를 쉽게 이해하기 위해 차량 운전자가 마티즈와 쏘나타를 운전하고자 한다고 생각해 보자.
OCP를 위반하는 경우
- 마티즈는 창문개방과 기어조작이 수동이고, 쏘나타는 자동이다.
- 차종을 바꾸니 운전자의 행동에도 변화가 온다.
현실 세계라면 당연히 어느 정도 변화가 있어야 하겠지만, 객체 지향 세계에서는 이를 해결하기 위한 해법이 있다.
OCP를 준수하는 경우
- 운전자와 차종 사이에 자동차 클래스 또는 인터페이스를 둠으로써 다양한 자동차가 생긴다고 해도 객체 지향 세계의 운전자는 운전 습관에 영향을 받지 않게 된다.
- 다양한 자동차가 생긴다고 하는 것은 확장에는 개방되어 있는 것이고, 운전자 입장에서는 주변의 변화에 폐쇄되어 있는 것이다.
데이터베이스 프로그래밍
혹시라도 데이터베이스 프로그래밍을 경험한 적이 있다면 OCP의 아주 좋은 예를 이미 알고 있을 것이다. 그 예란 바로 JDBC다.
- JDBC를 사용하는 클라이언트는 데이터베이스가 오라클에서 MySQL 또는 MS-SQL로 바뀌더라도 Connection을 설정하는 부분 외에는 따로 수정할 필요가 없다.
- Connection 설정 부분을 별도의 설정 파일로 분리해 두면 클라이언트 코드는 단 한 줄도 변경할 필요가 없다.
JDBC뿐만 아니라 iBatis, MyBatis, 하이버네이트 등 데이터베이스 프로그래밍을 지원하는 라이브러리와 프레임워크에서도 OCP의 예를 볼 수 있다.
OCP의 예: JDBC
- 오라클을 사용하고 있다고 가정해 보자.
- 오라클을 MySQL이나 MS-SQL로 교체할 때 자바 애플리케이션은 JDBC 인터페이스라고 하는 완충 장치로 인해 변화에 영향을 받지 않는다.
- 자바 애플리케이션은 데이터베이스라고 하는 주변의 변화에 닫혀 있는 것이다.
- 데이터베이스를 교체한다는 것은 데이터베이스가 자신의 확장에는 항상 열려 있다는 것이다.
코드로 보는 OCP
앞에서 예를 들었던 운전자와 차량에서 조금 더 극단적으로 구성하여 코드를 통해 이해해 보자.
OCP를 준수하지 않는 경우
다음은 자동차 표현할 Car 클래스로 차종을 의미하는 type 변수를 가지고 있으며, 쏘나타, 마티즈 등 다양한 차종이 될 수 있다.
class Car {
String type;
public Car(String type) {
this.type = type;
}
}
다음은 운전자를 표현할 Person 클래스로 차량 조작을 위해 창문을 여는 행위와 기어를 조작하는 행위를 할 수 있다.
- 차량이 어떤 종류인지에 따라 조작하는 방법이 다르다.
class Person {
String name;
public Person(String name) {
this.name = name;
}
void openWindow(Car car) {
if (car.type.equals("마티즈")) {
System.out.println("수동으로 창문을 개방합니다.");
} else if (car.type.equals("쏘나타")) {
System.out.println("자동으로 창문을 개방합니다.");
}
}
void operateGear(Car car) {
if (car.type.equals("마티즈")) {
System.out.println("수동으로 기어를 조작합니다.");
} else if (car.type.equals("쏘나타")) {
System.out.println("자동으로 기어를 조작합니다.");
}
}
}
- 위 경우 두 종류의 차종과 자동과 수동 두 가지 방식 밖에 없어서 크게 문제가 되지 않아 보인다.
- 하지만 이 코드는 많은 문제점이 있다.
- 만약, 자동 또는 수동인 차종이 늘어나게 된다면?
- 만약, 새로운 조작 방법이 등장하게 된다면?
- 이에 더하여 같은 차종일지라도 연식에 따라 자동 또는 수동 방식이 바뀐다면?
- 이처럼 Car의 종류나 조작 방법이 추가될 때마다 Person 클래스를 계속 수정해 주어야 한다.
- 위와 같은 방식의 설계는 유지보수 할 때 큰 문제점을 발생시킨다.
OCP를 준수하는 경우
위 문제를 해결하기 위해 다음과 같은 방법을 고려할 수 있다.
- 인터페이스 또는 추상 클래스를 사용하여 자동차 유형을 정의하고, 각 자동차 종류에 따라 구체적인 구현을 만든다.
- Person 클래스가 자동차를 조작하는 대신, 자동차 객체에 대한 메서드를 호출하도록 설계한다.
interface Car {
void openWindow();
void operateGear();
}
class Matiz implements Car {
@Override
public void openWindow() {
System.out.println("수동으로 창문을 개방합니다.");
}
@Override
public void operateGear() {
System.out.println("수동으로 기어를 조작합니다.");
}
}
class Sonata implements Car {
@Override
public void openWindow() {
System.out.println("자동으로 창문을 개방합니다.");
}
@Override
public void operateGear() {
System.out.println("자동으로 기어를 조작합니다.");
}
}
- Car 클래스를 인터페이스로 두고 창문을 여는 행위와 기어를 조작하는 행위를 정의한다.
- Car에 대한 Matiz와 Sonata 같은 구체적인 차량 클래스와 메서드를 구현한다.
- 마티즈는 수동으로 창문을 개방하고, 기어를 조작할 수 있다.
- 쏘나타는 자동으로 창문을 개방하고, 기어를 조작할 수 있다.
Person 클래스는 구체적인 차량에 대해 알지 않고도 차량을 조작할 수 있게 코드를 작성할 수 있도록 작성한다.
class Person {
String name;
public Person(String name) {
this.name = name;
}
void openWindow(Car car) {
car.openWindow();
}
void operateGear(Car car) {
car.operateGear();
}
}
- 위와 같이 코드를 수정하면 Person 클래스는 수정하지 않고도 새로운 자동차 유형을 쉽게 추가할 수 있게 된다.
- 사람은 어떤 차종이든지 차량을 열거나 기어를 조작하기만 하면 된다.
결론
- OCP를 따르지 않는다고 해서 객체 지향 프로그래밍을 구현하는 것이 불가능한 것은 아니다.
- 하지만 OCP를 무시하고 코드를 작성하면 유연성, 재사용성, 유지보수성 등을 얻을 수 없다.
- 따라서 객체 지향 프로그래밍에서 OCP는 반드시 지켜야 할 원칙이다.
- 스프링 프레임워크를 학습하다 보면, OCP를 교과서적으로 활용하고 있다는 것을 깨닫게 된다.
참고 서적
https://link.coupang.com/a/baQPj6
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
'언어(Language) > Java' 카테고리의 다른 글
[OOP] 인터페이스 분리 원칙(ISP: Interface Segregation Principle) 개념 및 예제 (1) | 2023.10.02 |
---|---|
[OOP] 리스코프 치환 원칙(LSP: Liskov Substitution Principle) 개념 및 예제 (0) | 2023.09.25 |
[OOP] 단일 책임 원칙(SRP: Single Responsiblity Principle) 개념 및 예제 (0) | 2023.09.10 |
[Java] 자바 로깅(Logging) - log4j, log4j2, slf4j, logback (0) | 2023.05.11 |
[Java] 자바 JSON 라이브러리 Gson 사용법 (0) | 2023.05.09 |