
들어가며
안녕하세요, 개발자 비니입니다.
그동안 외부 프로젝트와 회사일로 바빠서 통 글을 못 썼었네요..
생각해 보니.. 아주 기본적인 걸 잊고 있었던 것 같아
정리해 보려 합니다.
S.O.L.I.D 란?
"객체 지향 프로그래밍을 수행하는 데 있어 지켜져야 한다는 5개의 소프트웨어 개발 원칙"
이는 단골 면접 질문이라고도 할 수 있지요?
여러 디자인 패턴들 또한 SOLID 원칙에 입각해 만들어진 것이라, 이를 확실히 알고 넘어가면 나중에 이점이 많습니다.
아래 표에 작성 된 요소들의 앞 글자를 때서 부르는 것이다.
| SRP | 단일 책임 원칙 |
| OCP | 개방 폐쇄 원칙 |
| LSP | 리스코프 치환 원칙 |
| ISP | 인터페이스 분리 원칙 |
| DIP | 의존 역전 원칙 |
SOLID 원칙을 왜 지키는 걸까?
"좋은 설계"
가 실제로 이루어지면, 개발자들은 코드를 확장하거나, 불필요한 복잡도가 낮아지고, 유지보수하기에도 더 쉬워집니다.
이러한 이점들은 결국 개발 생산성의 향상으로 이어집니다.
하지만 반드시 적용해야 한다는 아니라는 점을 알고 넘어갑시다.
코드에 문제가 없고, 오히려 코드 구성이 나빠지면 원칙을 적용할 이유가 없다는 것이다.
1. 단일 책임 원칙 - Single Responsibility Principle
"클래스(객체)는 단 하나의 책임만 가져야 한다는 원칙"
- 쉽게 말해, "책임"을 "기능"으로 치환하여 읽으면 됩니다.
하나의 클래스는 하나의 책임(기능)만 담당하여 수행한다는 겁니다.
예시를 한번 들어볼까요?
플레이어를 구현한다면?
플레이어의 구성 요소가 다음과 같다고 해 봅시다.
- 플레이어의 모델
- 플레이어의 음성
- 플레이어의 움직임
- 사용자의 입/출력
원칙을 지키지 않는 경우
물론, 이를 "Player.class" 하나에 모두 작성하여 사용할 수 있을 겁니다.
하지만 이렇게 되면, "플레이어 움직임"만 수정하고 싶을 때, 모든 코드를 수정해야 합니다.
플레이어의 종류가 늘어났을 때, 각 플레이어 클래스마다 작성된 움직임 코드를 전부 수정해 주어야 하죠.
그리고, 가독성이 떨어집니다.
모든 기능이 들어있으니, 엄청난 스크립트 길이를 보여주겠죠.
원칙을 지키는 경우
간단합니다.
- 플레이어의 모델
- 플레이어의 음성
- 플레이어의 움직임
- 사용자의 입/출력
모두 각각의 클래스로 만들어 사용하는 겁니다.
별도의 컴포넌트로 합쳐 만들어진 캐릭터인 경우, "움직임"이나 "음성"을 바꾸고 싶을 때
해당 컴포넌트만 수정하면 됩니다.
해당 클래스를 상속받아 사용하면 되는 것이기에,
모듈과 같이 사용하며 재사용이 손쉬워집니다. 확장성의 증가라고도 이해할 수 있습니다.
동시에 기능이 모두 분리되어 각각의 파일로 존재하니, 가독성도 증가합니다.
2. 개방 폐쇄 원칙 Open Closed Principle
"확장에 개방적이고, 수정엔 폐쇄적이어야 한다."
- 상속을 적극적으로 사용하라고 생각하면 됩니다. 그것도 :"추상화"를 통한 구축을 권장하는 거죠.
자식들의 기존 코드를 수정하지 않고, 새로운 기능을 추가할 수 있어야 합니다.
새로운 기능을 만들거나 확장해야 할 때, 큰 힘을 들이지 않아도 할 수 있도록 해야 한다는 겁니다.
이것도 예시를 들어볼까요?
도형의 넓이를 구하는 클래스를 만들어야 한다면?
도형의 종류는 사용자의 입력으로 정한다 생각해 봅시다.
- 정사각형
- 원형
- 삼각형
원칙을 지키지 않는 경우
각 도형 별로 넓이를 구하는 메서드를 구현해야 합니다.
중복되는 코드도 많아지고, 만약 "도형" 이란 클래스에 기능을 추가해야 하는 경우엔
모든 도형(정사각형, 원형, 삼각형...) 코드를 수정해야 합니다.
어마어마하게 복잡하죠.
이것 또한 가독성과 유지보수력이 너무나 떨어지게 됩니다.
원칙을 지키는 경우
"도형"이라는 클래스를 만듭니다.
그리고, 도형의 면적을 구하는 추상 메서드를 생성합니다.
그다음, 직사각형, 원형, 삼각형 등의 클래스가 도형 클래스를 상속받도록 설정하는 겁니다.
그렇게 되면, "도형"이란 클래스에 새로운 기능을 추가해야 하는 경우
손쉽게 모든 도형에 적용되도록 기능을 만들 수 있습니다.
3. 리스코프 치환 원칙 - Liskov Substitution Principle
"서브 타입은 언제나 부모 타입으로 교체할 수 있어야 한다"
- 솔직히 무슨 소리인지 처음엔 이해가 잘 안 갑니다.
"하위 클래스는 부모 클래스에서 정의한 기능을 오버라이드할 수 있지만, 부모 클래스의 인터페이스를 준수해야 하며, 부모 클래스가 기대하는 방식대로 동작해야 한다"... 쉽지 않죠?
쉽게 말하면,
"업캐스팅" 상태에서 자식이 부모의 메서드를 사용해도 문제없이 동작해야 한다는 말입니다.
불필요한 메서드 오버라이딩도 최소화해야 하고요.
이것도 예를 들어봅시다.
탈것을 만들어야 하는 경우
상식적으로 생각했을 때, "자동차"가 "헬기"와 같이 날아간다는 건 말이 안 되지요.
2원칙인 개방 폐쇄 원칙에 따라 "탈것" 클래스를 만든 뒤,
추상 메서드로 아래 함수들을 만들었다 생각해 봅시다.
- 하늘로 상승
- 바닥으로 하강
- 전진
- 우회전
- 좌회전
- 후진
자.. 그럼 이제 우린 "헬기"와 "자동차" 클래스를 "탈것"으로부터 상속받아 만든다고 해 봅시다...
원칙을 지키지 않는 경우
자동차(자식) 클래스를 만들어서 코드를 작성합니다.
그러다가 어떤 이유로 보니 메인 코드 흐름에 "날기"를 실행하도록 구성된 부분이 있습니다.
차량은 "날기"를 수행하려 하지만, 추상 메서드에는 그냥 공백만 출력됩니다.
이처럼 자동차에게 날기를 수행하라 명령하면, 아무 일도 일어나지 않거나 오류를 발생시킵니다.
원칙을 지키는 경우
또 세분화되도록 탈것 클래스를 분리합니다.
- 도로 위를 달리는 탈것
- 하늘 위를 날아다니는 탈것
- 레일을 달리는 탈것
- ...
그리고, 각각에 맞도록 자식 클래스를 만들어 사용합니다.
결국 리스코프 치환 원칙이란,
"하위 클래스는 어떠한 경우라도 부모 클래스를 대체할 수 있어야 한다"가 됩니다.
자동차를 상속받아 여러 자동차를 만드는 건 되지만,
헬기나 비행기를 만들어서는 안 되며, 더 많이 분류하라는 말입니다.
4. 인터페이스 분리 원칙 Interface Segregation Principle
"인터페이스를 각 사용에 맞게 잘~ 분리하라는 원칙"
- 한 번에 큰 인터페이스를 사용하지 말고, 여러 개로 나눠서 사용하라는 겁니다.
인터페이스를 분리할수록 코드 간 결합도가 낮아집니다.
그러면 휴먼 에러의 발생 확률이 점점 줄어들겠죠.
사실 쉽게 이해하자면
SRP (단일 책임 원칙)의 인터페이스 버전이라 생각하면 쉽습니다.
프린터를 예시로 들어볼까요?
원칙을 지키지 않는 경우
아래와 같이 프린터는 사용하지 않는, 없는 Scan과 staple 등의 기능도 함께 작성이 됩니다.
불필요한 메서드를 구현해야 하는 것이죠.
public interface Printer {
void print(String document);
void scan(String document);
void fax(String document);
void staple(String document);
}
원칙을 지키는 경우
원칙을 지키는 경우에는 각 기능을 인터페이스로 분리하여, 필요한 것만 가져다 쓸 수 있습니다.
단순 프린터는 Printable만, 복합기는 Printable, Scannable 등을 가져갈 수 있겠죠.
public interface Printable {
void print(String document);
}
public interface Scannable {
void scan(String document);
}
public interface Faxable {
void fax(String document);
}
public interface Stapleable {
void staple(String document);
}
5. 의존성 역전 원칙 - Dependency Inversion Principle
"고수준 모듈이 저수준 모듈에서 직접 가져오면 안 된다"
- 이해하기가 어렵죠 무슨 소리인지... 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻입니다.
정말 쉽게 이해하자면,
DIP (개방 폐쇄 원칙)의 인터페이스 버전이라 생각하면 쉽습니다.
예로, "스위치"라는 기능을 만들어 사용하는 경우가 있다 합시다.
- 문을 열고 닫는다
- 불을 켜고 끈다
- 선풍기 전원을 키고 끈다
- ...
등등, 엄청 많은 역할을 수행할 수 있습니다.
이러한 요소를 각각 "문", "불", "선풍기" 클래스에 스위치 요소를 하나하나 작성하기보다,
스위치 ON/OFF 메서드를 가진 인터페이스를 만들어 사용하라는 말입니다.
정리하자면...
솔리드 원칙은 객체 지향 프로그래밍에 있어 유지보수와 확장성을 위해 중요한 요소가 됩니다.
이를 기반으로 코드를 짜는 걸 습관화하고, 몸에 익혀 두면 많은 성장을 이룰 수 있을 거라 생각해요.
- 단일 책임 원칙 (SRP)
클래스는 하나의 책임(기능)만 가져야 합니다. 이를 통해 가독성과 유지보수성을 향상할 수 있습니다. - 개방 폐쇄 원칙 (OCP)
확장에는 열려 있어야 하지만, 수정을 위해 닫혀 있어야 합니다. 상속과 추상화를 통해 이 원칙을 따를 수 있습니다. - 리스코프 치환 원칙 (LSP)
서브 타입은 언제나 부모 타입으로 교체할 수 있어야 합니다.
이를 통해 일관성을 유지하고 코드의 예측 가능성을 보장합니다. - 인터페이스 분리 원칙 (ISP)
사용하지 않는 메서드에 의존하지 않도록, 인터페이스는 구체적인 사용 사례에 맞게 분리해야 합니다.
이는 코드의 결합도를 낮추고, 유연성을 높입니다. - 의존성 역전 원칙 (DIP): 고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 대신, 둘 다 추상화에 의존해야 합니다.
이로써 모듈 간의 결합도를 낮추고, 변화에 강한 구조를 만들 수 있습니다.
마치며
개발 면접 단골 주제이기도 하고, OOP 특징과 더불어 많이 사용되는 요소이기도 하죠..
잘 쓰면, 유지보수와 가독성, 확장성까지 모두 얻을 수 있는 개발자가 될 수 있어요.
실제로 적용해 보면서 체화해 보도록 해요!
그럼 다들, 파이팅입니다! 💫
'이론' 카테고리의 다른 글
| 비동기 프로그래밍? 동시성 제어? - with Java (1) | 2025.01.13 |
|---|---|
| JWT? Json Web Token이란? (2) | 2025.01.09 |
| JAVA 8/11/17/21 버전 별 변화와 LTS에 대해 (2) | 2025.01.06 |
| REST, REST API, RESTful API 본격 알아보기 (3) | 2025.01.02 |
댓글 개