SOLID는 객체지향 설계에서 유지보수성, 확장성, 코드에 대한 이해도를 높이기 위해 제안된 5가지 핵심 원칙이다
로버트 C. 마틴이 제안했다
S: SRP - 단일 책임 원칙 (Single Responsibility Principle)
하나의 클래스는 하나의 변경 이유만 가져야 한다
클래스나 모듈은 하나의 역할만 책임져야 하며, 이 책임이 변경되는 이유가 분리되어 있어야 한다
여러 책임이 하나의 클래스에 몰려 있으면, 그 중 하나만 변경되어도 나머지 동작이 영향을 받을 수 있다
예를 들어 UserService가 사용자 정보 저장과 UI 출력 모두를 담당한다면 SRP 위반이다
저장 책임은 UserRepository, 출력 책임은 UserView로 분리해야 한다
SRP를 지키면 코드 변경의 범위가 명확해지기 때문에 문제가 발생할 경우 원인 파악이 빨라진다
위반 시 하나의 변경이 연쇄적으로 다른 기능에 영향을 줄 수 있어 유지보수가 어려워진다
O: OCP - 개방・폐쇄 원칙 (Open・Closed Principle)
확장에는 열려 있고, 변경에는 닫혀 있어야 한다
기존 코드를 수정하지 않고 기능을 확장할 수 있는 구조를 지향해야 한다
상속, 인터페이스, 추상 클래스를 통해 확장이 가능하도록 구성하자
예를 들어 DamageCalculator 클래스가 무기 타입별 조건문으로 구성돼 있다면, 새로운 무기가 추가될 때마다 코드를 수정해야 하므로 OCP를 위반한다
각 무기 클래스를 IWeapon 인터페이스로 추상화하고, 피해량 계산은 각 클래스 내부에 구현하도록 하면 새로운 무기를 추가할 때 기존 코드는 수정 없이도 확장할 수 있어 OCP를 만족하게 된다
OCP를 지키면 새로운 기능을 안정적으로 추가할 수 있고, 기존 기능에 영향을 주지 않을 수 있다
위반 시 기능 추가 때마다 기존 코드를 수정해야 하므로, 버그가 발생할 위험이 크고 테스트 비용이 증가한다
L: LSP - 리스코프 치환 원칙 (Liskov Substitution)
자식 클래스는 언제나 부모 클래스의 역할을 대체할 수 있어야 한다
부모 타입을 사용하는 코드에서 자식 클래스로 대체했을 때도 동일하게 작동해야 한다
자식 클래스가 부모의 계약을 깨거나 예외를 유발해서는 안된다
예를 들어 Bird를 상속한 Penguin이 Fly를 override해서 예외를 던진다면 기대된 동작 흐름을 깨게 되어 LSP를 위반한다
이처럼 특정 기능이 모든 자식에게 유효하지 않다면, 해당 기능은 부모 클래스에 존재하면 안 된다
비행 능력은 Bird가 아닌 별도의 인터페이스(IFlyable)로 분리하는 것이 LSP를 지키는 방식이 될 수 있다
LSP를 지키면 코드의 일관성이 유지되어 빠르게 코드(구조)를 학습할 수 있다
위반 시 상속을 했음에도 다형성이 깨지면서 버그가 발생할 가능성이 커지고, 코드를 학습하는 데 리소스가 더 들어가게 된다
당연히 A라고 동작할 것이라고 생각했는데 a혹은 B로 동작하는 경우가 있을 수 있기 때문이다
I: ISP - 인터페이스 분리 원칙 (Interface Segregation Principle)
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다
하나의 거대한 인터페이스보다는 작고 구체적인 인터페이스 여러 개로 분리하는 것이 낫다
의존성을 불필요하게 강하게 만들지 말 것
IPrinter 인터페이스에 Print(), Scan(), Fax()가 있다면 단순 프린터는 Fax() 때문에 강제로 구현해야 하므로 ISP 위반이다
IPrintable, IFaxable, IScannable로 분리하는 게 적절하다
ISP를 지키면 클래스는 필요한 기능에만 집중하고, 의존성 관리가 쉬워진다
위반 시 필요하지 않은 기능까지 구현해야 하며 불필요한 영향 범위가 커진다
D: DIP - 의존성 역전 원칙 (Dependency Inversion Principle)
고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 추상적 개념에 의존해야 한다
구체 클래스가 아닌 추상화에 의존해야 하고, 구현 세부사항은 바뀌어도 고수준 정책은 영향받지 않아야 한다
예를 들어 Character 클래스가 Pistol 클래스에 직접 의존하고 있으면 DIP 위반이다
Character는 Gun 인터페이스에만 의존하고, 실제 구현은 생성자나 메서드를 통해 외부에서 주입받는 구조가 적절하다.
DIP를 지키면 모듈 간 결합도가 낮아지고 테스트 코드 작성과 유지보수가 용이해진다
위반 시 하나의 구현이 변경되면 전체 흐름에 영향을 미치게 되어 유연성이 크게 떨어진다
Reference
'프로그래밍 > 프로그래밍 이론' 카테고리의 다른 글
추상화(Abstraction)란? (0) | 2025.03.24 |
---|---|
다형성(Polymorphism)이란? (0) | 2025.03.24 |
상속(Inheritance)이란? (0) | 2025.03.24 |
캡슐화(Encapsulation)란? (0) | 2025.03.24 |
옵저버(Observer) 패턴 (0) | 2025.03.21 |