Java 로 프로그램을 하게 될 경우, Java 의 언어적 특성인 객체지향 (OOP) 특성을 잘 반영하는 것이 중요합니다. 결국 모든 앱은 유지 보수 측면이 매우 중요한데, 이 OOP 측면을 잘 지키면, 유지 보수 및 관리에 큰 이점을 가져갈 수 있기 때문입니다. 이것이 어떻게 보면 Java 를 사용하는 이유라고 볼 수도 있을 것 같습니다.
객체 지향 설계에서 앱을 더욱 가독성 있고, 유연하고, 유지 보수 측면에서 유리하게 설계하기 위해서 제안되는 다섯가지 원칙을 SOLID 라고 합니다. Rober C. Martin 에 의해 제안된 이 다섯가지 원칙을 살펴보겠습니다.
굳이 다섯 포스트 만들면서 하나하나 까야할 내용은 아닌 것 같습니다. 차피 직접 설계해보시지 않으면 완전한 자신의 것이 되기는 힘들고, 내용에 대한 이해 자체가 어려운 것은 아니라고 생각하기 때문입니다.
Single Responsibility Principle (SRP, 단일 책임 원칙)
한 클래스는 하나의 책임을 가져야 한다는 원칙입니다. 책임이라는 것은 모호하기 때문에, 항상 유지보수 측면에서 그 책임을 판단한다고 합니다. 가령, 변경사항이 발생하 였을 때, 그 변경으로 인한 파급 효과가 커서 이것 저것 다 고쳐야 한다면 SRP 를 잘 지키지 못했다고 볼 수 있겠죠?
객체의 크기에 정해진 규제는 없습니다. SRP를 잘 치킬 수 있도록 설계하는 것, 조절하는 것이 개발자들의 중요한 역할이라고 보시면 될 것 같습니다.
Open Closed Principle (OCP, 개방 폐쇄 원칙)
확장에는 열려 있으나, 변경에는 닫혀 있어야 한다는 원칙입니다. 셀 수 없이 변경되는 것이 SW인데, 변경에 닫혀 있어야 한다는 말이 도대체 무슨 말이냐? 라고 하실 수 있습니다.
이 원칙은 객체 지향 설계에 있어서 역할과 구현의 분리의 중요성을 강조한 원칙이라고 볼 수 있습니다. 즉, 다형성을 잘 활용해서 확장에는 열려 있으나, 기존 내부 코드를 변경하는 것은 지양해야 하는 방향을 추구해야 한다는 뜻입니다.

가령, 자동차라는 인터페이스가 있다고 가정하겠습니다. 자동차라는 역할을 Avante, K3, Tesla 가 수행할 수 있습니다. 자동차라는 역할이 수행해야 하는 핵심 기능들은 운전, 주차, 시동 등이 있을 것입니다. 확장에 열려 있다 함은, Tesla 가 자동 주행이라는 추가 기능으로 기능이 확장되기도 하며, Sonata 라는 새로운 역할 구현체가 언제든 추가되어 확장될 수 있는 의미와 같습니다.
기존 내부 변경을 지양해야 함은, 자동차의 핵심 기능들인 운전, 주차, 시동 등을 바꾸는 것입니다. 만약 핵심 기능들을 바꾼다면, 모든 구현 객체들에 영향이 가기 때문에 큰 유지 보수 책임이 따르게 됩니다. 따라서, 처음부터 5대 원칙을 준수하며 OCP 원칙을 생각하며 설계해 나가는 것이 중요합합니다.
Liskov Substitution Principle (LSP, 리스코프 치환 원칙)
상위 타입의 객체를 하위 타입으로 치환하여도 프로그램은 정상적으로 동작해야 한다는 원칙입니다. 말 그대로 하위 타입은 상위 타입의 핵심 로직을 건드리지 않고 로직의 의도대로 수행하여, 바뀌어도 전체적인 앱 동작에 영향을 주지 않아야 한다는 뜻입니다.
LSP를 지키기 위해서는 다음 두 가지 생각을 해야 하는 것 같습니다.
- 상위 타입의 핵심 로직들을 모두 상속받을 수 있는가?
- 상속 받은 핵심 로직들을 로직의 의도대로 수행할 수 있는가?
가장 유명한 문제는 "정사각형-직사각형 문제"라고 합니다. 우리는 정사각형은 직사각형이라는 사실을 알고 있습니다. 따라서 Square 가 Rectangle 을 상속받게 하면 어떻게 될까요?
public class Rectancle{
private int height;
private int width;
public int getHeight(){
return this.height;
}
public int getWidth(){
return this.width;
}
public void setHeight(int height){
this.height = height;
}
public void setWidth(int width){
this.width = width;
}
}
만약 Square class 가 Rectangle class 를 상속받아야 한다면, Square class 의 정의상 set method를 다음과 같이 바꾸어 주어야 합니다.
public class Square extends Rectangle {
private int height;
private int width;
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
이 방식을 통해서 오류가 나는 다른 상황들을 설명하는 경우가 많은데, 개인적으로는 이렇게 된 상황부터 바로 LSP가 위반되는 상황이라고 생각합니다. "원래 의도에 맞춰진 Setter Method 가 Square 에서는 의도대로 행동하지 않기 때문" 입니다. 원래 의도에 연장되어 Square Class 만의 로직을 수행하기 때문입니다. 핵심 로직을 의도대로 수행하지 못하므로, 해당 로직은 하위 클래스의 기능 로직(자체 Method)이 되거나, 아니면 상속받으면 안됩니다.
하지만 정사각형의 정의상 setter 를 해주려면 당연히 height, width 모두 setting 이 필요합니다. 위와 같은 경우에선 setter 를 상속받지 않고 그냥 자체 함수 (기능로직) setLength 따위로 바꿔주고 다른 핵심 로직을 상속받거나, 다른 핵심로직이 없을 경우에는 아예 상속받을 필요가 없는 (LSP 에 따르면 상속받으면 안되는) 관계라고 보시면 되겠습니다.
Interface Segregation Principle (ISP, 인터페이스 분리 원칙)
인터페이스는 인터페이스를 사용하는 클라이언트 기준으로 분리해야 한다는 원칙입니다. 개인적으로 이해한 바를 얹자면, 인터페이스 (역할체)를 만들 때는 해당 인터페이스를 사용할 친구들을 생각하며 설계해야 한다는 것입니다.
Keep interfaces small so that users don't end up depending on things they don't need
많은 클라이언트들을 위한 적은 인터페이스 보단, 사용할 클라이언트들의 기능들을 중심으로 구분하여 여러 인터페이스를 구축하는 것이 낫다는 뜻이기도 합니다. 이 원칙은 SRP를 잘 치킬 수 있도록 도와주며, OCP 역시 잘 지킬 수 있도록 도와줍니다. OCP 를 보면서 개인적으로 든 생각은, "어떻게 처음부터 완벽히 인터페이스를 설계해.. "라는 생각이였는데, ISP가 이를 잘 보완해주는 것 같습니다 .
위 OCP에서 들던 예시를 연장해보겠습니다. 자동차라는 인터페이스를 만들고, Avante, K3, Tesla 등의 클래스들이 해당 인터페이스를 상속받는 구현 객체들입니다. 자동차에는 핵심 기능들인 운전, 시동, 주차 등이 있기 때문에, 이를 자동차란 큰 인터페이스에서, 조금 더 구체적인 인터페이스로 운전, 시동, 주차의 인터페이스를 걸어두는 설계를 하자는 것입니다. 그렇게 된다면, 어느날 자동 주차로만 사용되는 날이 와서 주차란 기능이 삭제되어야 할 때, 모든 클래스에서 주차의 기능을 바꿔줄 필요가 없기 때문입니다. (그냥 주차란 인터페이스를 다음부턴 상속 안받으면 됩니다)
Dependency Inversion Principle (DIP, 의존관계 역전 원칙)
개발을 할 때 구체화에 대해 의존하지 말고, 추상화시키는 것에 의존해야 한다는 원칙입니다. 설계의 중요성을 강조했다라고 보일 수는 있지만, Interface를 사용하려는 Client 객체는 해당 인터페이스만을 바라봐야 한다는 뜻입니다. 즉, 해당 인터페이스를 사용하는 클라이언트는 어떻게 그 인터페이스가 구체화되는지는 알 필요가 없고, 그냥 그 인터페이스만을 바라봐야 한다는 뜻입니다.
의존이란 해당 클래스의 존재를 알면, 해당 클래스가 사용된다면 의존한다고 표현합니다. 위 예시에서 만약 DriveHome 이라는 클래스에 있는 User 객체가 자동차 인터페이스를 사용해야 한다고 합시다.
public class DriveHome{
...
private User user;
private Car car; // 자동차 인터페이스
public static void main(String [] args){
...
car.drive(user, ..);
...
}
위와 같은 관계에서, User 와 Car 을 사용하는 DriveHome 이라는 클라이언트는, 위 설계대로 Car의 인터페이스에만 의존해야 합니다. 즉, Car 가 어떤 자동차를 타고 가는지는 상관할 필요 없고 (DIP에 따르면 알면 안됩니다), 오직 drive 해서 간다는 로직에만 집중을 해야 합니다.
그 Car 가 어떤 구현 객체를 사용할지는, K3, Tesla, Avante 중 어떤 객체를 사용하여 drive() 를 수행하는지는 다른 방법을 통해서 지정이 되어야 합니다. 이 부분은 Spring이라면 DI, Java 프로그램이라면 패키지 Configuration 부분에서 조금 더 알아보시면 될 것 같습니다.
1) 비둘기, 독수리가 있다.
2) 둘이 같이 날 수 있다. 새라는 인터페이스를 만들었다. 날다라는 함수를 만들었다.
3) 날치가 등장했다. 날치도 날 수 있다.
4) 전투기가 등장했다. 전투기도 날 수 있다.
전투기와 날치가 새인가?
> 이건 잘못된 인터페이스이다.
Flyable - 날다 >> 이거 자체가 인터페이스가 되고, 각 녀석들마다 어떻게 날지를 정의해야 한다.
비둘기, 독수리를 새로 묶는 것은 abstact class 로 종류를 가지게 하는 것이 맞다.
포스트 요약
- 객체 지향 프로그램은 다형성이 핵심이다.
- 다형성, 오버라이딩, 오버로딩에 대해 정확히 이해하고, 5대 원칙에 맞춰 Java 프로그램을 디자인 할 수 있도록 하자
출처
여러 블로그들을 보면서 정리하였습니다. 개인적으로 잘 정리되었다고 생각되어 참고한 블로그들을 첨부합니다.
https://huisam.tistory.com/entry/ISP
SOLID - ISP(Interface Segregation Principle)란? : 인터페이스 분리 원칙
ISP Interface Segregation Principle(인터페이스 분리 원칙) : 클라이언트가 자신이 이용하지 않는 메서드에 의존하면 안된다는 원칙 역시나 이번에도 도통 무슨 말인지 모르시겠죠? ㅎㅎ 그래서 조금
huisam.tistory.com
https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html
Clean Coder Blog
Solid Relevance 18 October 2020 Recently I received a letter from someone with a concern. It went like this: For years the knowledge of the SOLID principle has been a standard part of our recruiting procedure. Candidates were expected to have a good workin
blog.cleancoder.com
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
'Java' 카테고리의 다른 글
| [실전 Java 고급 1편] - 5. Executor Framework 에 대하여 (0) | 2025.04.27 |
|---|---|
| [실전 Java 고급 1편] - 4. CAS와 동시성 컬렉션 (0) | 2025.01.31 |
| [실전 Java 고급 1편] - 3. 생산자 소비자 문제 (BlockingQueue 만들기까지) (0) | 2025.01.31 |
| [실전 Java 고급 1편] - 2. 메모리 가시성과 동기화 (0) | 2025.01.31 |
| [실전 Java 고급 1편] - 1. Thread 의 제어 (0) | 2025.01.31 |