지난번에는 SOLID 원칙 중 SRP와 OCP에 대해 정리했다. 궁금하다면 본문 제일 아래 링크를 첨부하였으니 참고하길 바란다.
이번에는 남아 있는 L, I, D 원칙에 대해 살펴보려고 한다.
LSP : 리스코프 치환 원칙
뜻
자식 클래스는 부모 클래스의 자리를 완전히 대신할 수 있어야 한다.
개인적으로는 이름만 봤을 때 가장 직관적으로 추측하기 어려운 원칙이라고 생각한다. 하지만 예시를 보면 쉽게 이해할 수 있다.
예시
Bird라는 클래스가 있다고 하자. 보통 새라면 모두 fly() 메서드를 가질 것이라고 생각한다.
하지만 펭귄처럼 날 수 없는 새도 존재한다.
만약 펭귄을 Bird 클래스를 상속받아 구현한다면, fly() 메서드에서 문제가 발생할 수 있다.
즉, bird.fly()를 호출하는 코드에서 if (bird,getType() != "Penguin") 같은 별도의 조건문을 넣어야 할지도 모른다.
이 문제가 발생한 이유는 리스코프 치환원칙의 뜻을 다시 읽어보면 명확하게 알 수 있다. 자식 클래스가 부모 클래스의 자리를 완전히 대신할 수 없기 때문에 이런 문제가 발생한다!
책에서는 이 원칙을 단순히 상속 관계에만 한정하지 않고, 인터페이스와 구현체, 더 나아가 아키텍처 레벨에서도 적용해야 한다고 설명한다.
치환 가능성이 보장되지 않으면, 위와 같이 별도의 예외 처리 로직을 추가해야 하는 불필요한 복잡성이 생기기 때문이다.
ISP : 인터페이스 분리 원칙
뜻
클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강요받아서는 안 된다.
다른 원칙들에 비해, 이름과 뜻만으로도 비교적 쉽게 의미를 파악할 수 있는 원칙이다.
클라이언트 코드가 사용하는 대상에 불필요한 기능이 함께 묶여 있다면, 사용하지 않는 부분에까지 의존하게 된다. 이는 원치 않는 의존 관계를 만들고, 작은 변경에도 클라이언트 코드가 영향을 받게 하는 문제를 일으킨다.
예시
클라이언트가 망치만 필요하다고 해보자.
그런데 클라이언트가 의존하는 대상이 공구상자라는 거대한 구현체라면, 그 안에 포함된 드라이버나 톱이 변경될 때에도 우리의 클라이언트는 영향을 받을 수 있다.
이 문제의 원인은 클라이언트가 실제 필요로 하는 기능(망치)에 비해, 제공되는 인터페이스(공구상자)가 지나치게 크기 때문이다.
해결책
인터페이스를 역할 단위로 잘게 쪼개어, 클라이언트가 필요한 기능에만 의존하도록 만드는 것이다.
위 사례에서는 클라이언트가 망치 인터페이스만 참조하도록 만들면, 드라이버나 톱의 변경과는 무관하게 동작할 수 있다.
DIP : 의존성 역전 원칙
뜻
고수준 정책을 구현하는 코드는 저수준의 세부사항을 구현하는 코드에 의존해서는 안 된다. 대신 세부사항이 정책에 의존해야 한다.
즉, 소스코드 의존성이 추상에 의존하며 구체에는 의존하지 않게 해야한다.
처음 뜻만 보면 다소 추상적이라 직관적으로 이해하기 어려울 수 있다.
여기서 “의존한다”는 말은 코드에서 직접 객체를 생성하고 해당 객체의 메서드를 직접 호출하는 경우를 의미한다. 왜 세부사항에 의존하면 코드 변경이 어려워지는지 예시로 살펴보자.
예시
앞서 OCP에서 인터페이스의 개념을 명확하게 하기 위해 C타입 인터페이스 예시를 들었다. 이번에도 같은 방식으로 설명하겠다.
내가 새로운 C타입 충전기를 만들기로 했다. 그런데 충전기에 고속 충전 / 저속 충전을 전환하는 스위치 버튼을 넣으면 좋겠다고 생각해서 스위치를 추가했다. (이 부분에서 문제가 발생한다.)
이 상황에서 충전기를 사용하는 쪽 코드를 보자. 충전기 사용 코드는 이런 구체적인 변경사항을 알고(구체에 의존을 하는 경우)
if(저속충전 필요) { charger.switchOff(); }
이와같이 구체적인 행위에 대해서 들어간 코드들을 작성하였다. 처음에는 문제를 쉽고 빠르게 해결하는 방법처럼 느껴진다.
이후에 내가 다른 회사나 C타입 인터페이스를 만족하는 회사의 제품으로 변경하려고해도 새로 만든 충전기의 세부사항을 너무많이 아는 코드로 인해서 어려움이 발생한다. (인터페이스에 나와있지 않는 모든 변경사항을 제거해야 한다.)
이렇듯 구체적인 구현에 의존하게 된다면 코드의 변경 및 유지 보수를 어렵게 만든다.
애초에 클라이언트 코드가 인터페이스에만 의존했다면, switchOff() 같은 메서드는 애초에 클라이언트에서 호출할 수 없었을 것이다.
여기서 인터페이스로 인한 계약을 정리해보면 다음과 같다.
- 클라이언트는 인터페이스가 정의한 기능만 사용한다.
- **충전기(구현체)**는 인터페이스 명세만 충실히 구현한다.
이렇게 하면 충전기가 교체되더라도 클라이언트는 아무런 영향을 받지 않는다.
이게 구체적인 세부사항에 의존하는 대신 인터페이스(추상)에 의존해야하는 이유이다.
(또한 구현부(충전기 구현하는 부분)도 인터페이스에 있는 내용 외에는 구현할 필요도 없다.)
정리하면
소스코드를 작성할 때에는 세부 구현사항에 의존하지말고 인터페이스에 의존하여 코드를 짜야한다.
구체적이고 변동성이 크다면 절대로 그 이름을 소스코드에서 언급하지 말라.
[Clean architecture] 왜 내 코드만 유지보수가 힘들까? SOLID 원칙에서 답을 찾다 _ 1편
'개발 > 책' 카테고리의 다른 글
| 소프트웨어 설계가 꼬이는 이유? ADP, SDP, SAP로 풀어보기[Clean architecture] (1) | 2025.09.08 |
|---|---|
| 컴포넌트 원칙은 정답이 없다: 상황별로 달라지는 REP·CCP·CRP 예시 (0) | 2025.09.07 |
| 소프트웨어 설계자의 실력이 갈리는 지점, 컴포넌트 원칙: REP·CCP·CRP [Clean architecture] (0) | 2025.09.07 |
| 왜 내 코드만 유지보수가 힘들까? SOLID 원칙에서 답을 찾다 _ 1편 [Clean architecture] (0) | 2025.09.02 |
| 프로그래밍 패러다임 : 빼앗긴 덕분에 얻은 것들 [Clean architecture] (1) | 2025.09.02 |