이 문서의 과거 버전 (r1)을 보고 있습니다. 수정일: 2026.02.13 07:11
객체 지향 프로그래밍의 설계 원칙 중 가장 핵심이 되는 다섯 가지 원칙을 통칭하는 용어이다. 로버트 C. 마틴이 정리하고 명명하였으며, 각 원칙의 첫 글자를 따서 SOLID라는 약어로 널리 알려졌다. 이 원칙들은 소프트웨어 공학에서 유지보수성과 확장성이 뛰어난 소프트웨어를 설계하기 위한 지침을 제공한다.
SOLID 원칙은 단순히 원칙 자체를 나열하는 것을 넘어, 서로 긴밀하게 연결되어 더 견고한 설계를 가능하게 한다. 예를 들어, 단일 책임 원칙을 준수하는 작은 클래스들은 인터페이스 분리 원칙을 적용하기 더 쉽게 만들며, 의존관계 역전 원칙은 개방-폐쇄 원칙을 실현하는 주요 메커니즘이 된다. 이 원칙들은 초기 설계 단계부터 리팩토링에 이르기까지 객체 지향 설계의 전 과정에 걸쳐 적용된다.
원칙 | 약어 | 핵심 개념 |
|---|---|---|
단일 책임 원칙 | SRP | 한 클래스는 하나의 책임만 가져야 한다. |
개방-폐쇄 원칙 | OCP | 확장에는 열려 있고, 변경에는 닫혀 있어야 한다. |
리스코프 치환 원칙 | LSP | 자식 클래스는 부모 클래스를 대체할 수 있어야 한다. |
인터페이스 분리 원칙 | ISP | 클라이언트가 사용하지 않는 메서드에 의존하지 않아야 한다. |
의존관계 역전 원칙 | DIP | 추상화에 의존해야 하며, 구체화에 의존하지 않아야 한다. |
이 원칙들의 궁극적인 목표는 결합도를 낮추고 응집도를 높여, 요구사항이 변경되거나 시스템이 성장할 때 발생하는 변경의 영향을 최소화하는 것이다. 이를 통해 코드의 가독성, 재사용성, 테스트 용이성을 크게 향상시킬 수 있다.
SOLID는 객체 지향 프로그래밍 설계의 다섯 가지 기본 원칙을 기억하기 쉬운 약어로 정리한 것이다. 이 원칙들은 로버트 C. 마틴이 2000년대 초반에 제안한 설계 원칙들을 재정리하여 명명한 것으로, 소프트웨어의 유지보수성과 확장성을 높이는 데 중점을 둔다.
SOLID는 각 원칙의 첫 글자를 따서 만들어진 두문자어이다. 각각 단일 책임 원칙(SRP), 개방-폐쇄 원칙(OCP), 리스코프 치환 원칙(LSP), 인터페이스 분리 원칙(ISP), 의존관계 역전 원칙(DIP)을 가리킨다. 이 원칙들의 공통된 목적은 시간이 지나도 변경에 유연하고 이해하기 쉬운 소프트웨어 구조를 만드는 것이다. 즉, 요구사항이 변경되거나 새로운 기능이 추가될 때 기존 코드를 최소한으로 수정하면서 시스템을 진화시킬 수 있도록 돕는다.
이 원칙들은 서로 독립적이지만 깊이 연관되어 있다. 하나의 원칙을 적용하다 보면 자연스럽게 다른 원칙을 준수하게 되는 경우가 많다. 예를 들어, 인터페이스 분리 원칙을 잘 지키면 단일 책임 원칙도 함께 강화되는 경우가 있다. 전체적으로 이 원칙들은 응집도를 높이고 결합도를 낮추는 방향으로 설계를 유도하여, 모듈 간의 의존성을 관리하고 리팩토링을 용이하게 만든다.
원칙 | 약어 | 주요 목적 |
|---|---|---|
단일 책임 원칙 | SRP | 하나의 클래스는 하나의 변경 이유만 가져야 한다. |
개방-폐쇄 원칙 | OCP | 확장에는 열려 있고, 수정에는 닫혀 있어야 한다. |
리스코프 치환 원칙 | LSP | 자식 클래스는 부모 클래스를 대체할 수 있어야 한다. |
인터페이스 분리 원칙 | ISP | 클라이언트가 사용하지 않는 메서드에 의존하지 않아야 한다. |
의존관계 역전 원칙 | DIP | 추상화에 의존해야 하며, 구체화에 의존하지 말아야 한다. |
단일 책임 원칙(Single Responsibility Principle, SRP)은 하나의 클래스 또는 모듈이 변경되어야 하는 이유는 단 하나여야 한다는 원칙이다. 로버트 C. 마틴에 의해 명확히 정의된 이 원칙은, 하나의 클래스가 하나의 책임 또는 역할만을 가져야 함을 의미한다. 여기서 '책임'이란 변경의 이유를 가리킨다.
이 원칙을 위반하는 대표적인 사례는 데이터 접근, 비즈니스 로직, 사용자 인터페이스 렌더링 등 여러 관심사를 하나의 클래스가 모두 처리하는 경우이다. 예를 들어, Order 클래스가 주문 정보를 저장하는 동시에 데이터베이스에 저장하고, 영수증을 PDF로 출력하는 기능까지 포함한다면, 이 클래스는 데이터 모델 변경, 데이터베이스 스키마 변경, 출력 형식 변경 등 여러 이유로 수정될 수 있다. 이는 단일 책임 원칙에 명백히 위배된다.
적용 예시로, 위의 Order 클래스를 리팩토링하면 Order(주문 데이터 구조), OrderRepository(데이터 저장/조회), OrderPrinter(영수증 출력) 등으로 책임을 분리할 수 있다. 이렇게 하면 데이터 구조 변경은 Order 클래스만, 데이터베이스 교체는 OrderRepository만, 출력 포맷 변경은 OrderPrinter만 영향을 받게 되어 시스템의 유지보수성과 재사용성이 크게 향상된다.
원칙 준수 상태 | 클래스 구성 | 변경 이유 (예시) |
|---|---|---|
위반 |
| 1. 주문 속성 추가 2. 데이터베이스 종류 변경 3. 출력 형식 변경 |
준수 |
|
|
단일 책임 원칙은 다른 SOLID 원칙들의 기초가 된다. 책임이 명확히 분리된 클래스는 자연스럽게 더 좁고 구체적인 인터페이스를 가지게 되어 인터페이스 분리 원칙(ISP)을 준수하기 쉬워지며, 변경으로부터 격리되어 개방-폐쇄 원칙(OCP)을 적용하는 데도 유리해진다.
단일 책임 원칙은 하나의 클래스나 모듈이 변경되어야 하는 이유는 단 하나여야 한다는 원칙이다. 이는 "책임"을 "변경의 이유"로 정의하며, 하나의 클래스가 여러 가지 이유로 수정될 가능성을 가진다면 그 클래스는 여러 책임을 지고 있다고 판단한다. 이 원칙의 목적은 결합도를 낮추고 응집도를 높여 코드의 유지보수성과 재사용성을 향상시키는 데 있다.
개방-폐쇄 원칙은 소프트웨어 구성 요소는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 한다는 원칙이다. 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있도록 설계해야 함을 의미한다. 이는 주로 추상화와 다형성을 통해 달성되며, 변경이 필요한 부분을 인터페이스나 추상 클래스로 격리함으로써 구현된다.
리스코프 치환 원칙은 상속 관계에 있는 자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있어야 한다는 원칙이다. 즉, 프로그램의 정확성을 깨뜨리지 않으면서 부모 클래스의 객체를 자식 클래스의 객체로 치환할 수 있어야 한다. 이 원칙은 상속이 "is-a" 관계를 올바르게 모델링하도록 유도하며, 자식 클래스가 부모 클래스의 계약을 반드시 지켜야 함을 강조한다.
인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 메서드에 의존하도록 강요받아서는 안 된다는 원칙이다. 하나의 범용적인 인터페이스보다는, 특정 클라이언트를 위한 여러 개의 구체적인 인터페이스가 더 낫다. 이 원칙은 인터페이스를 가능한 한 작고 집중적인 단위로 분리함으로써 시스템의 결합도를 낮추고 불필요한 의존성을 제거하는 데 목적이 있다.
의존관계 역전 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙이다. 또한, 추상화는 세부 사항에 의존해서는 안 되고, 세부 사항이 추상화에 의존해야 한다. 이 원칙은 전통적인 의존성 방향을 역전시켜, 고수준 모듈의 정책이 저수준 모듈의 구현 세부사항으로부터 독립적이도록 보장한다.
단일 책임 원칙을 위반하는 전형적인 사례는 하나의 클래스가 여러 가지 이유로 변경될 수 있는 경우다. 예를 들어, 사용자 정보를 관리하는 User 클래스가 데이터베이스 연결, 사용자 데이터 검증, 그리고 사용자 정보를 파일로 저장하는 기능을 모두 담당한다면, 이 클래스는 데이터 모델링, 영속성 로직, 비즈니스 규칙이라는 서로 다른 세 가지 책임을 지고 있는 것이다. 데이터베이스 스키마 변경, 검증 규칙 수정, 파일 저장 형식 변경 중 어떤 이유로든 이 클래스는 수정될 수 있으며, 이는 원칙 위반을 의미한다.
이를 준수하는 설계는 각 책임을 별도의 클래스로 분리하는 것이다. User 클래스는 순수한 데이터 구조나 도메인 모델의 책임만 지고, UserRepository 클래스는 데이터베이스 저장 및 조회를, UserValidator 클래스는 검증 로직을, UserFileExporter 클래스는 파일 출력을 담당하도록 구성할 수 있다. 이렇게 하면 한 책임의 변경이 다른 책임을 가진 클래스에 영향을 미치지 않으며, 코드의 응집도는 높아지고 결합도는 낮아진다.
책임 | 위반 사례 (하나의 클래스 내) | 준수 사례 (분리된 클래스) |
|---|---|---|
데이터 표현 |
|
|
영속성 (데이터베이스) |
|
|
비즈니스 규칙 (검증) |
|
|
다른 형식의 출력 (파일) |
|
|
위반 사례에서 User 클래스는 너무 많은 의존성을 가지게 되어 단위 테스트가 어려워지고, 기능 하나를 수정할 때 다른 무관한 기능까지 재컴파일 및 재배포해야 할 수 있다. 반면, 책임이 분리된 설계는 각 모듈이 명확한 역할을 가지므로 이해하기 쉽고, 유지보수성과 재사용성이 향상된다. 이 원칙의 핵심은 '변경의 축'을 기준으로 코드를 구성하는 것이다[1].
개방-폐쇄 원칙은 소프트웨어 구성 요소(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 원칙이다. 이는 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있도록 설계해야 함을 의미한다. 수정에 '닫혀 있다'는 말은 기존의 동작을 담당하는 코드가 안정적이고 변경될 필요가 없음을 뜻하며, 이는 시스템의 안정성과 유지보수성을 크게 향상시킨다.
이 원칙을 달성하는 핵심 메커니즘은 추상화이다. 공통적인 기능을 인터페이스나 추상 클래스로 정의하고, 구체적인 구현은 이를 상속하거나 구현하는 별도의 클래스에서 담당하게 한다. 새로운 기능이 필요할 때는 기존 인터페이스를 준수하는 새로운 클래스를 추가함으로써, 기존 코드를 전혀 수정하지 않고 시스템을 확장할 수 있다. 예를 들어, 다양한 결제 방식을 지원하는 시스템에서 PaymentProcessor라는 인터페이스를 정의하고, CreditCardProcessor, PayPalProcessor 등 구체적인 클래스로 구현하는 방식이 여기에 해당한다[2].
개방-폐쇄 원칙을 위반하는 전형적인 사례는 새로운 기능을 추가할 때마다 기존 함수나 클래스에 if-else나 switch 문을 추가하여 분기 처리를 하는 것이다. 이는 기존 코드를 수정해야 하므로 버그 발생 가능성을 높이고, 코드를 복잡하게 만든다. 반면, 올바르게 적용된 시스템은 새로운 요구사항이 발생했을 때 새로운 코드만 작성하여 추가하면 되므로, 기존에 동작하던 부분에 대한 회귀 테스트 부담이 줄어든다. 이 원칙은 의존관계 역전 원칙과 깊은 연관이 있으며, 추상화를 통해 저수준 모듈의 변경으로부터 고수준 모듈을 보호하는 데 기여한다.
단일 책임 원칙은 하나의 클래스나 모듈은 변경할 이유가 단 하나만 가져야 한다는 원칙이다. 이 원칙에서 '책임'은 '변경의 이유'로 해석된다. 즉, 하나의 클래스는 하나의 기능 또는 하나의 액터(사용자)에 대한 일만 담당해야 한다. 여러 책임이 하나의 클래스에 결합되면, 한 책임의 변경이 다른 책임과 무관한 코드에 영향을 미쳐 시스템이 취약해진다. 이 원칙의 궁극적 목표는 결합도를 낮추고 응집도를 높여 유지보수성을 향상시키는 것이다.
개방-폐쇄 원칙은 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하며, 수정에는 닫혀 있어야 한다는 원칙이다. 기존 코드를 변경하지 않고도(닫혀 있음) 새로운 기능을 추가(열려 있음)할 수 있어야 한다. 이 원칙은 주로 추상화와 다형성을 통해 구현된다. 공통적인 부분을 추상 클래스나 인터페이스로 정의하고, 구체적인 동작의 변화는 이를 상속하거나 구현하는 새로운 클래스를 만들어 처리한다. 이를 통해 기존 모듈의 안정성을 유지하면서 시스템을 확장할 수 있다.
리스코프 치환 원칙은 상속 관계에 있는 클래스들 사이의 계약을 정의한다. 이 원칙에 따르면, 프로그램에서 부모 클래스의 객체를 사용하는 모든 곳에서 그 객체를 자식 클래스의 객체로 치환해도 프로그램의 정확성이 깨지지 않아야 한다. 즉, 자식 클래스는 부모 클래스가 제공하는 행위(메서드)의 사전 조건을 더 강화하거나, 사후 조건을 더 약화시켜서는 안 된다. 상속은 'is-a' 관계가 아니라 '행위적 대체 가능성'에 기반해야 한다. 이 원칙을 위반하면 예상치 못한 오류가 발생할 수 있다.
인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 메서드에 의존하도록 강요받아서는 안 된다는 원칙이다. 하나의 범용적인 인터페이스보다는, 클라이언트의 필요에 맞게 구체적이고 작은 인터페이스 여러 개로 분리하는 것이 바람직하다. '굵은' 인터페이스는 불필요한 의존성을 생성하여, 인터페이스의 일부만 사용하는 클라이언트도 다른 부분의 변경 영향을 받게 만든다. 이 원칙은 인터페이스를 기능별로 세분화함으로써 시스템의 결합도를 낮추고 유연성을 높인다.
의존관계 역전 원칙은 다음과 같은 두 가지 선언으로 구성된다. 첫째, 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다. 둘째, 추상화는 구체적인 사항에 의존해서는 안 되며, 구체적인 사항이 추상화에 의존해야 한다. 여기서 '고수준 모듈'은 정책이나 비즈니스 로직과 같은 추상적인 개념을, '저수준 모듈'은 데이터베이스 접근이나 외부 API 호출과 같은 구체적인 구현을 의미한다. 이 원칙은 의존성 주입과 같은 기법을 통해 구체화되어, 모듈 간의 직접적인 결합을 끊고 테스트 용이성과 확장성을 제공한다.
개방-폐쇄 원칙의 핵심은 추상화를 통해 달성된다. 구체적인 구현에 의존하지 않고, 인터페이스나 추상 클래스 같은 추상적인 계층에 의존함으로써 모듈의 확장을 가능하게 한다. 이는 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있는 구조를 만드는 것을 의미한다.
확장성을 위한 일반적인 방법은 다형성을 활용하는 것이다. 예를 들어, 다양한 결제 방식을 지원하는 시스템에서 PaymentProcessor라는 인터페이스를 정의하고, CreditCardProcessor, PayPalProcessor 등 구체적인 클래스가 이를 구현하도록 설계한다. 새로운 결제 방식(예: 암호화폐 결제)이 필요할 때는 기존의 PaymentProcessor 인터펇스를 구현하는 CryptoProcessor 클래스만 새로 추가하면 된다. 기존의 결제 처리 로직은 변경되지 않으며, 새로운 클래스를 시스템에 주입하기만 하면 기능이 확장된다.
이 원칙을 적용하면 시스템의 유지보수성과 재사용성이 크게 향상된다. 모듈 간의 결합도가 낮아져, 한 부분의 변경이 다른 부분으로 전파되는 것을 최소화할 수 있다. 그러나 과도한 추상화는 불필요한 복잡성을 초래할 수 있으므로, 실제 변경 가능성이 예상되는 영역에 집중하여 적용하는 것이 중요하다[3].
리스코프 치환 원칙은 바바라 리스코프가 1987년 논문에서 제안한 개념으로, 로버트 C. 마틴이 SOLID 원칙 중 하나로 정리했다. 이 원칙의 핵심 정의는 "프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다"는 것이다. 간단히 말해, 상속 관계에 있는 하위 클래스는 상위 클래스가 사용되는 모든 곳에서 아무런 문제 없이 대체되어야 함을 의미한다.
이 원칙은 상속을 "is-a" 관계로만 생각하는 것을 넘어, 행위적 호환성에 초점을 맞춘다. 하위 클래스는 상위 클래스의 계약을 준수해야 하며, 상위 클래스의 사전 조건을 더 강화하거나 사후 조건을 더 약화시켜서는 안 된다[4]. 또한 상위 클래스가 던지는 예외의 종류를 바꾸거나 새로운 예외를 추가하는 것도 원칙 위반에 해당할 수 있다.
위반 사례 | 설명 | 문제점 |
|---|---|---|
정사각형-직사각형 문제 |
|
|
예외 추가 | 상위 클래스의 메서드가 | 상위 클래스의 클라이언트 코드는 |
이 원칙을 준수하면, 다형성을 안전하게 활용할 수 있어 코드의 유연성과 재사용성이 향상된다. 또한, 클라이언트 코드가 구체적인 하위 클래스 타입이 아닌 추상적인 상위 클래스 타입에 의존하게 함으로써 의존관계 역전 원칙과도 깊이 연결된다. 반대로 원칙을 위반하면, 상속을 사용했음에도 불구하고 타입 체크나 조건문이 코드 전반에 산재하게 되어 확장을 어렵게 만드는 결과를 초래한다.
단일 책임 원칙은 하나의 클래스나 모듈은 변경할 이유가 단 하나만 가져야 한다는 원칙이다. 이 원칙의 핵심은 책임을 명확히 분리하여 결합도를 낮추고 응집도를 높이는 데 있다. 여기서 '책임'이란 변경의 이유를 의미하며, 하나의 모듈이 여러 이유로 변경될 가능성을 가진다면 그 모듈은 여러 책임을 지고 있다고 판단한다.
개방-폐쇄 원칙은 소프트웨어 구성 요소는 확장에는 열려 있어야 하며, 수정에는 닫혀 있어야 한다는 원칙이다. 이는 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있도록 설계해야 함을 의미한다. 주로 추상화와 다형성을 활용하여 구현하며, 변경이 필요한 부분을 인터페이스나 추상 클래스로 격리함으로써 달성한다.
리스코프 치환 원칙은 상속 관계에 있는 객체들 사이의 대체 가능성을 규정한다. 이 원칙에 따르면, 자식 클래스는 언제나 그 부모 클래스를 대체할 수 있어야 한다. 즉, 부모 클래스가 사용되는 모든 곳에서 자식 클래스를 사용해도 프로그램의 정확성이 깨지지 않아야 한다. 이는 상속이 'is-a' 관계가 아닌 단순한 코드 재사용을 위해 사용될 때 위반되기 쉽다.
인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다. 하나의 범용적인 인터페이스보다는, 특정 클라이언트의 요구에 맞춘 여러 개의 세분화된 인터페이스를 설계하는 것이 바람직하다. 이는 불필요한 의존성을 제거하여 시스템의 복잡성을 줄이고, 인터페이스 변경의 영향을 최소화하는 데 목적이 있다.
의존관계 역전 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙이다. 또한, 추상화는 세부 사항에 의존해서는 안 되고, 세부 사항이 추상화에 의존해야 한다. 이 원칙은 의존성의 방향을 전통적인 상위-하위 관계에서 역전시켜, 시스템의 유연성과 재사용성을 크게 향상시킨다.
리스코프 치환 원칙에서 말하는 '계약'은 상속 관계에 있는 부모 클래스와 자식 클래스 사이에 암묵적으로 존재하는 규칙의 집합이다. 이 계약은 주로 클래스의 공개 인터페이스를 통해 정의되며, 사전 조건, 사후 조건, 불변식으로 구성된다[5].
자식 클래스는 부모 클래스와의 이 계약을 반드시 준수해야 한다. 구체적으로, 자식 클래스는 부모 클래스의 메서드가 기대하는 사전 조건(입력 조건)을 더 강화해서는 안 되며, 보장하는 사후 조건(출력 결과나 상태)과 불변식(객체가 항상 유지해야 하는 조건)을 약화시켜서도 안 된다. 예를 들어, 부모 클래스의 calculateArea() 메서드가 음수가 아닌 정수를 반환한다는 사후 조건을 가진다면, 자식 클래스에서 이를 오버라이드하여 음수를 반환하는 것은 계약 위반이다.
이 계약 위반은 직관에 반하는 동작을 초래한다. Rectangle(직사각형) 클래스를 상속받은 Square(정사각형) 클래스가 setWidth와 setHeight 메서드를 독립적으로 변경할 수 없다는 제약을 추가하면, 이는 사전 조건을 강화한 것이 되어 클라이언트 코드의 기대를 저버린다. 클라이언트는 부모 타입인 Rectangle을 사용할 때 너비와 높이를 독립적으로 설정할 수 있다고 믿고 코드를 작성했기 때문이다. 이러한 위반은 시스템의 신뢰성을 해치고, 다형성의 이점을 무효화한다.
인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다. 이 원칙은 하나의 범용적인 인터페이스보다는, 특정 클라이언트를 위한 여러 개의 구체적인 인터페이스가 더 낫다는 것을 강조한다. 클라이언트는 오직 필요한 기능만을 제공하는 인터페이스를 통해 협력해야 한다.
굵은 인터페이스, 즉 많은 메서드를 포함한 인터페이스의 주요 문제점은 관련 없는 메서드에 대한 의존성을 강제한다는 점이다. 예를 들어, 프린터, 스캐너, 팩스 기능이 모두 결합된 IMultiFunctionDevice 인터페이스를 구현한 클래스가 있다고 가정하자. 간단한 프린터 클라이언트는 스캔이나 팩스 메서드를 전혀 사용하지 않음에도 불구하고 그 메서드들에 대한 의존성을 가지게 된다. 이는 인터페이스 분리 원칙의 위반이며, 클라이언트 코드를 불필요하게 복잡하게 만들고, 인터페이스가 변경될 때 영향을 받지 말아야 할 클라이언트까지 변경의 영향을 받게 할 수 있다[6].
이 원칙을 준수하기 위해서는 범용 인터페이스를 더 작고 응집된 단위로 분리해야 한다. 위의 예시에서는 IPrinter, IScanner, IFax와 같은 세분화된 인터페이스로 나누는 것이 적절하다. 그 후, 실제 다기능 장치는 필요한 모든 인터페이스를 구현하고, 각 클라이언트는 자신이 필요로 하는 인터페이스만 참조하면 된다. 이 접근 방식은 시스템을 더 유연하고 유지보수하기 쉽게 만든다. 클라이언트는 자신이 실제로 사용하는 기능에만 결합되며, 사용하지 않는 기능의 변경으로부터 안전해진다.
단일 책임 원칙은 하나의 클래스나 모듈은 변경할 이유가 단 하나만 가져야 한다는 원칙이다. 여기서 '책임'은 변경의 이유를 의미한다. 이 원칙의 핵심은 기능적 응집성을 높이고, 서로 다른 이유로 발생하는 변경이 하나의 구성 요소에 영향을 미치지 않도록 분리하는 데 있다.
개방-폐쇄 원칙은 소프트웨어 구성 요소(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 원칙이다. 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있도록 설계해야 함을 의미한다. 이는 주로 추상화와 다형성을 통해 달성된다.
리스코프 치환 원칙은 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다는 원칙이다. 간단히 말해, 자식 클래스는 부모 클래스가 사용되는 모든 곳에서 대체 가능해야 한다. 이 원칙은 올바른 상속 관계를 정의하고, 하위 클래스가 상위 클래스의 '계약'을 위반하지 않도록 보장한다.
원칙 | 약자 | 핵심 개념 요약 |
|---|---|---|
단일 책임 원칙 | SRP | 한 클래스는 하나의 책임(변경 이유)만 가져야 함 |
개방-폐쇄 원칙 | OCP | 확장에는 열려 있고, 수정에는 닫혀 있어야 함 |
리스코프 치환 원칙 | LSP | 하위 타입은 상위 타입을 완전히 대체할 수 있어야 함 |
인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다. 하나의 범용적인 인터페이스보다는, 특정 클라이언트를 위한 여러 개의 구체적인 인터페이스가 더 낫다. 이는 불필요한 의존성을 제거하여 시스템을 더욱 견고하고 유연하게 만든다.
의존관계 역전 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙이다. 또한, 추상화는 세부 사항에 의존해서는 안 되고, 세부 사항이 추상화에 의존해야 한다. 이 원칙은 의존성 주입과 함께 모듈 간의 결합도를 낮추는 핵심 메커니즘이다.
인터페이스 분리 원칙을 위반하는 굵은 인터페이스는 클라이언트가 필요하지 않은 메서드에 대한 의존성을 강제하게 만든다. 이는 시스템에 불필요한 결합을 초래하고, 변경의 파급 효과를 증가시키며, 유지보수성을 저하시킨다.
주요 문제점은 다음과 같다. 첫째, 사용하지 않는 메서드의 구현을 강제함으로써 인터페이스 오염이 발생한다. 클라이언트는 자신의 관심사와 무관한 메서드에 대해 더미 구현(예: 예외를 던지거나 null을 반환)을 제공해야 할 수 있다. 둘째, 인터페이스가 변경될 때, 그 변경이 해당 메서드를 실제로 사용하지 않는 모든 클라이언트에게도 영향을 미친다. 이는 변경이 국소적으로 머물러야 하는 원칙을 깨뜨린다. 셋째, 인터페이스가 지나치게 복잡해져 이해하기 어렵고, 잘못된 사용을 유발할 가능성이 높아진다.
문제점 | 설명 | 결과 |
|---|---|---|
불필요한 결합 | 클라이언트가 사용하지 않는 메서드에 의존하게 됨 | 변경의 영향 범위 확대, 재사용성 감소 |
인터페이스 오염 | 더미 구현 필요, 책임이 불분명해짐 | 코드 품질 저하, 오류 가능성 증가 |
불안정한 의존성 | 인터페이스 일부 변경이 모든 클라이언트에 영향 | 시스템 전반의 취약성 증가 |
이러한 문제를 해결하기 위해 인터페이스 분리 원칙은 하나의 범용적인 인터페이스보다는, 클라이언트의 특정 요구에 초점을 맞춘 여러 개의 세분화된 인터페이스를 사용할 것을 권장한다. 각 인터페이스는 응집된 하나의 역할을 정의하며, 클라이언트는 정확히 자신이 필요한 기능에만 의존하게 된다. 이는 결합도를 낮추고, 모듈성을 높이며, 시스템을 더 유연하고 안정적으로 만든다.
의존관계 역전 원칙(DIP, Dependency Inversion Principle)은 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙이다. 또한, 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항이 추상화에 의존해야 한다. 이 원칙은 전통적인 계층형 아키텍처에서의 의존성 방향을 '역전'시켜 모듈 간의 결합도를 낮추고 시스템의 유연성을 높이는 데 목적이 있다.
핵심 개념은 고수준 모듈과 저수준 모듈의 분리에 있다. 고수준 모듈은 애플리케이션의 핵심 비즈니스 로직이나 정책을 포함하는 부분이다. 반면 저수준 모듈은 데이터베이스 접근, 외부 API 호출, 파일 입출력 등 구체적인 구현 세부사항을 다룬다. DIP에 따르면, 고수준의 정책을 정의하는 코드는 '어떻게' 작업을 수행하는지(저수준 세부사항)에 직접적으로 의존해서는 안 된다. 대신, '무엇을' 수행할지에 대한 인터페이스나 추상 클래스 같은 추상화에 의존해야 한다. 이때 저수준 모듈은 이 추상화를 구체적으로 구현하는 형태로 작성된다.
이 원칙의 적용은 시스템 설계에 큰 변화를 가져온다. 예를 들어, 고수준의 결제 처리 모듈이 특정 결제 게이트웨이의 구체적인 클래스에 직접 의존하면, 게이트웨이를 변경할 때마다 고수준 모듈을 수정해야 한다. DIP를 적용하면, 고수준 모듈은 PaymentProcessor라는 인터페이스에만 의존하고, 실제 CreditCardProcessor나 PayPalProcessor 같은 저수준 구현체는 그 인터페이스를 따르게 된다. 이로 인해 고수준 모듈은 변경 없이 저수준 모듈을 쉽게 교체하거나 확장할 수 있게 된다[7].
결과적으로, 의존관계 역전 원칙은 다른 SOLID 원칙들의 기반이 되며, 특히 개방-폐쇄 원칙(OCP)을 지키는 핵심 메커니즘을 제공한다. 모듈 간의 결합이 추상화를 통해 느슨해지면, 시스템은 더욱 유지보수하기 쉽고 테스트하기 용이한 구조를 갖게 된다.
단일 책임 원칙은 하나의 클래스나 모듈은 변경할 이유가 단 하나만 가져야 한다는 원칙이다. 여기서 '책임'이란 변경의 이유를 의미한다. 이 원칙의 핵심은 기능이 아닌, 변경의 축에 초점을 맞추어 설계하는 것이다. 하나의 클래스가 여러 변경 이유를 가지면, 한 가지 이유로 인한 수정이 다른 책임과 관련된 코드에 영향을 줄 수 있어 시스템의 취약성이 증가한다.
개방-폐쇄 원칙은 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하며, 수정에는 닫혀 있어야 한다는 원칙이다. 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있도록 설계해야 함을 의미한다. 이는 주로 추상화와 다형성을 통해 달성된다. 예를 들어, 새로운 기능이 필요할 때 기존 코드를 수정하는 대신, 새로운 클래스를 만들어 인터페이스나 추상 클래스를 통해 시스템에 통합한다.
리스코프 치환 원칙은 상속 관계에 있는 하위 타입의 객체는 상위 타입의 객체를 대체할 수 있어야 한다는 원칙이다. 즉, 프로그램의 정확성을 깨뜨리지 않으면서 부모 클래스의 객체를 그 자식 클래스의 객체로 치환할 수 있어야 한다. 이 원칙은 하위 클래스가 상위 클래스의 행동 규약(계약)을 반드시 지켜야 함을 강조한다. 하위 클래스가 상위 클래스의 메서드 시그니처는 유지하지만, 의도된 동작을 변경하면 이 원칙을 위반하게 된다.
인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 메서드에 의존하도록 강요받아서는 안 된다는 원칙이다. 하나의 범용적인 인터페이스보다는, 클라이언트의 특정 요구에 맞춰 여러 개의 구체적이고 작은 인터페이스로 분리하는 것이 바람직하다. 이는 인터페이스를 최소한의 기능 단위로 설계하도록 유도하여, 시스템의 결합도를 낮추고 불필요한 의존성을 제거한다.
의존관계 역전 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙이다. 또한, 추상화는 구체적인 사항에 의존해서는 안 되며, 구체적인 사항이 추상화에 의존해야 한다. 이 원칙은 전통적인 의존성 방향(고수준 → 저수준)을 역전시켜, 추상화된 인터페이스나 추상 클래스를 사이에 둠으로써 모듈 간의 결합도를 낮추고 유연성을 높인다.
의존관계 역전 원칙(DIP)에서 고수준 모듈은 핵심 비즈니스 로직이나 정책과 같은 추상적인 개념을 다루는 모듈이다. 예를 들어, 결제 처리 시스템에서 '결제를 실행한다'는 추상적인 작업이 고수준 모듈의 책임이다. 반면, 저수준 모듈은 이러한 고수준의 정책을 실행하기 위한 구체적인 세부 사항을 구현한다. 신용카드 결제, 페이팔 결제, 특정 데이터베이스에 정보를 저장하는 것 등이 저수준 모듈의 예시이다.
전통적인 설계에서는 고수준 모듈이 저수준 모듈에 직접 의존하는 형태를 띤다. 이는 시스템의 변경을 어렵게 만든다. 결제 방식을 신용카드에서 암호화폐로 변경하려면, 고수준의 결제 로직 자체를 수정해야 할 수 있다. 의존관계 역전 원칙은 이 관계를 역전시켜, 두 모듈 모두 추상화(인터페이스나 추상 클래스)에 의존하도록 만든다.
이 원칙을 적용한 구조는 다음과 같은 특징을 가진다.
모듈 유형 | 책임 | 의존 대상 |
|---|---|---|
고수준 모듈 | 핵심 정책/비즈니스 로직 | 추상화(인터페이스) |
저수준 모듈 | 구체적인 구현 세부사항 | 추상화(인터페이스) |
추상화 | 고수준과 저수준 모듈 사이의 계약 정의 | - |
이렇게 구성하면 고수준 모듈은 '결제 처리기'라는 추상화에만 의존하고, 실제로 어떤 결제 수단(신용카드, 모바일 결제 등)이 사용되는지는 알 필요가 없다. 새로운 결제 수단이 추가되더라도 고수준 모듈은 변경되지 않으며, 단지 새로운 저수준 모듈을 만들어 추상화 계약을 구현하기만 하면 된다. 이는 시스템을 확장에는 열려 있고(Open), 기존 코드의 수정에는 닫혀 있게(Closed) 만드는 개방-폐쇄 원칙(OCP)을 실현하는 핵심 메커니즘이 된다.
각각의 SOLID 원칙은 독립적으로 존재하지만, 실제 설계에서 서로 긴밀하게 연결되어 시너지를 발휘한다. 예를 들어, 인터페이스 분리 원칙을 준수하면 의존관계 역전 원칙을 적용하기 위한 세분화된 인터페이스를 자연스럽게 얻게 된다. 또한 개방-폐쇄 원칙을 통해 확장에 유연한 구조를 만들기 위해서는 리스코프 치환 원칙을 만족하는 올바른 상속 관계가 전제되어야 한다.
이 원칙들은 공통적으로 결합도를 낮추고 응집도를 높이는 방향으로 설계를 유도한다. 단일 책임 원칙은 모듈의 응집도를 높이는 출발점이 되며, 이렇게 분리된 책임들은 인터페이스 분리 원칙과 의존관계 역전 원칙을 통해 느슨하게 연결된다. 결과적으로 시스템은 개방-폐쇄 원칙이 요구하는 변경에 대한 폐쇄성과 확장에 대한 개방성을 동시에 갖출 수 있게 된다.
다음 표는 주요 원칙 간의 상호 보완적 관계를 보여준다.
원칙 A | 원칙 B | 연관성 설명 |
|---|---|---|
하나의 클래스가 하나의 책임만 가질 때, 그 책임에 특화된 인터페이스로 분리하기 쉬워진다. | ||
하위 타입이 상위 타입을 완전히 대체할 수 있어야만, 기존 코드 수정 없이 새로운 하위 타입을 확장하여 시스템에 추가할 수 있다. | ||
클라이언트에 맞게 세분화된 인터페이스는 고수준 모듈이 구체적인 저수준 모듈이 아닌 추상화에 의존하도록 만드는 기반이 된다. |
따라서 SOLID 원칙을 개별 규칙의 집합이 아닌, 유기적으로 연결된 하나의 설계 철학으로 이해하는 것이 중요하다. 한 원칙을 적용하려는 노력은 자연스럽게 다른 원칙의 준수로 이어지며, 이는 전반적으로 유지보수성과 재사용성이 높은 소프트웨어 구조를 형성한다.
실무에서 SOLID 원칙을 적용하면 코드의 유지보수성과 확장성이 향상되며, 테스트 용이성이 높아진다. 각 원칙은 특정 설계 문제를 해결하도록 돕는다. 예를 들어, 단일 책임 원칙은 클래스의 변경 이유를 하나로 제한하여 수정의 영향을 최소화하고, 개방-폐쇄 원칙은 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있는 구조를 만든다. 이는 특히 대규모 애플리케이션과 장기적인 프로젝트에서 변경 관리 비용을 절감하는 데 효과적이다.
그러나 원칙의 적용에는 비용과 복잡성이 수반된다. 지나치게 엄격하게 적용하면 불필요하게 많은 수의 작은 클래스와 인터페이스가 생성되어 시스템의 전반적인 복잡도를 오히려 높일 수 있다. 이는 초기 개발 속도를 저하시키고, 새로운 팀원의 학습 곡선을 가파르게 만드는 요인이 된다. 또한, 간단한 스크립트나 소규모 프로젝트, 혹은 프로토타입 개발에서는 SOLID 원칙의 이점이 투입 비용을 상쇄하기 어려울 수 있다.
적용 시 주요 고려사항은 다음과 같다.
고려사항 | 설명 |
|---|---|
프로젝트 규모와 생명주기 | 장기적이고 규모가 큰 프로젝트일수록 원칙 적용의 이점이 명확해진다. |
팀의 숙련도 | 원칙에 대한 팀의 이해도가 낮으면 잘못된 해석과 적용으로 이어질 수 있다. |
변경 빈도 | |
성능 최적화 | 매우 제한된 환경에서는 설계의 유연성과 성능 간의 트레이드오프를 고려해야 한다. |
결론적으로, SOLID 원칙은 목적이 아닌 수단으로 이해해야 한다. 이 원칙들은 변화에 유연하게 대응하고 결합도를 낮추려는 지침을 제공하지만, 맹목적인 준수는 과잉 설계로 이어질 수 있다. 실무에서는 프로젝트의 맥락, 팀의 상황, 비즈니스의 요구를 종합적으로 판단하여 원칙을 적절히 절충하고 적용하는 지혜가 필요하다.
SOLID 원칙을 실무에 적용할 때는 맥락과 비용을 고려해야 한다. 모든 원칙을 완벽하게 준수하려는 시도는 오히려 불필요한 복잡성과 과도한 설계를 초래할 수 있다. 프로젝트의 규모, 변경 빈도, 팀의 숙련도, 개발 일정 등을 종합적으로 판단하여 적절한 수준의 적용이 필요하다. 작고 변경이 거의 없는 유틸리티성 코드보다는 핵심 비즈니스 로직이 집중된 도메인 모델에 우선적으로 적용하는 것이 효과적이다.
초기에는 단순한 설계로 시작하여, 변경 요구사항이 실제로 발생할 때 리팩토링을 통해 점진적으로 원칙을 도입하는 접근법이 권장된다. 예를 들어, 단일 책임 원칙을 지나치게 세분화하여 수많은 작은 클래스를 만들기보다는, 명확한 변경 이유가 확인된 시점에서 클래스를 분리하는 것이 실용적이다. 테스트 코드의 작성 용이성은 원칙 적용의 적절성을 판단하는 좋은 지표가 될 수 있다.
다음은 프로젝트 특성에 따른 원칙 적용 우선순위를 고려할 수 있는 요소를 정리한 표이다.
고려 요소 | 원칙 적용을 강화할 경우 | 원칙 적용을 완화할 경우 |
|---|---|---|
프로젝트 규모/수명 | 대규모, 장기 유지보수 프로젝트 | 소규모, 단기 프로토타입 또는 스크립트 |
변경 빈도/예측 가능성 | 요구사항 변경이 빈번하거나 예측하기 어려운 영역 | 안정적이고 변경이 거의 없는 영역 |
팀의 숙련도 | 원칙에 대한 이해도와 리팩토링 경험이 풍부한 팀 | 학습 중이거나 경험이 적은 팀 |
테스트 전략 | 자동화된 단위 테스트가 중요한 프로젝트 | 테스트 부담이 상대적으로 낮은 프로젝트 |
결국 SOLID 원칙은 목적이 아닌 수단이다. 이 원칙들이 궁극적으로 지향하는 것은 결합도를 낮추고 응집도를 높여 변경에 유연한 소프트웨어를 만드는 것이다. 원칙 자체에 매몰되기보다는 이러한 근본적인 목표를 상기하며, 현재 상황에서 가장 합리적인 설계 결정을 내리는 것이 중요하다.
SOLID 원칙을 지나치게 엄격하게 적용하거나 맥락을 고려하지 않고 적용하면 과잉 설계로 이어질 수 있다. 이는 불필요하게 복잡한 소프트웨어 아키텍처를 만들어 유지보수 비용을 증가시키고 개발 속도를 저하시킨다. 특히 작고 단순한 애플리케이션이나 빠른 프로토타이핑이 필요한 상황에서는 원칙의 적용이 오히려 방해가 될 수 있다.
잠재적 과잉 설계의 대표적인 예는 변경될 가능성이 거의 없는 부분에 대해 미리 추상화 계층을 과도하게 도입하는 것이다. 예를 들어, 단 하나의 데이터베이스만 사용할 것이 확실한 프로젝트에서 의존관계 역전 원칙을 적용해 데이터 접근 계층에 대한 인터페이스를 만드는 것은 불필요한 간접 참조와 파일 수의 증가를 초래한다. 마찬가지로, 현재 단일 책임을 잘 수행하는 클래스를 "미래의 변경"을 이유로 지나치게 세분화하는 것은 시스템을 파편화시킬 수 있다.
원칙 | 과잉 설계로 이어질 수 있는 상황 | 합리적 절충의 예시 |
|---|---|---|
안정적이고 변경이 없는 작은 클래스를 강제로 분리함 | 관련성이 높은 책임들을 하나의 클래스에 응집시킴 | |
사용자가 하나뿐인 인터페이스를 여러 개로 분리함 | 클라이언트가 단일하고, 변경 가능성이 낮은 경우 통합 인터페이스 유지 | |
변하지 않을 구체 모듈에 대한 추상화 계층을 강제로 도입함 | 안정적인 라이브러리나 모듈에 대해서는 직접 의존 허용 |
따라서 SOLID 원칙은 목적이 아닌 수단으로 여겨야 한다. 원칙의 적용은 프로젝트의 규모, 예상 수명, 변경 빈도, 팀의 역량과 같은 실용적 요소와 균형을 이루어야 한다. 가장 중요한 목표는 작동하는 소프트웨어를 효율적으로 전달하고 유지보수하는 것이며, 원칙은 이 목표를 달성하는 데 도움이 될 때만 적용되어야 한다.