객체 지향 설계의 핵심, SOLID 원칙 완벽 가이드: 코드 품질을 높이는 5가지 약속

다섯 개의 기둥이 서로 연결되어 견고한 구조를 이루고 있는 모습. 이는 객체 지향 설계의 SOLID 원칙을 상징합니다.

객체 지향 설계의 핵심, SOLID 원칙 완벽 가이드

SOLID 원칙, 왜 중요할까요?

소프트웨어 개발은 정교한 건축물을 짓는 것과 같습니다. 처음에는 간단한 오두막처럼 시작할 수 있지만, 시간이 지나면서 증축하고 리모델링하며 새로운 기능을 추가해야 하는 순간이 반드시 찾아옵니다. 이때 기초가 튼튼하지 않다면 어떻게 될까요? 작은 변화에도 건물 전체가 흔들리는 아찔한 경험을 하게 될 겁니다. 코드도 마찬가지입니다. SOLID 원칙은 바로 이 '튼튼한 기초'를 다지는 객체 지향 설계의 핵심 원칙들을 모아놓은 것입니다.

이 원칙들을 지키면 코드는 훨씬 더 유연해지고, 이해하기 쉬워지며, 유지보수하기 좋은 상태가 됩니다. 예를 들어, 새로운 기능을 추가할 때 기존 코드를 거의 수정하지 않아도 되거나, 특정 부분에 문제가 생겼을 때 그 영향이 다른 곳으로 퍼지지 않도록 막을 수 있습니다. 결과적으로, 변화에 빠르고 안정적으로 대응할 수 있는 소프트웨어를 만들 수 있게 되는 것이죠. 따라서 SOLID 원칙은 단순히 '좋은 코드'를 넘어, 지속 가능한 소프트웨어를 만들기 위한 개발자들의 중요한 약속과도 같습니다.

다섯 가지 약속, 하나씩 살펴보기 (S, O, L)

SOLID는 사실 다섯 가지 원칙의 앞 글자를 따서 만든 이름입니다. 각기 다른 능력을 가지고 있지만, 함께할 때 엄청난 시너지를 발휘하죠. 그럼 지금부터 그 첫 세 가지 원칙을 구체적인 예시와 함께 파헤쳐 보겠습니다.

S: 단일 책임 원칙 (Single Responsibility Principle)

가장 기본적이면서도 강력한 원칙입니다. SRP는 '하나의 클래스는 단 하나의 책임만 가져야 한다'는 의미를 담고 있습니다. 여기서 '책임'의 기준은 '변경의 이유'가 됩니다. 즉, 클래스를 변경해야 하는 이유가 단 하나여야 한다는 뜻이죠. 예를 들어, User 클래스가 사용자의 정보를 관리하는 책임과 동시에, 그 정보를 데이터베이스에 저장하고 불러오는 책임까지 가지고 있다고 상상해 보세요. 만약 데이터베이스 스키마가 변경되면 User 클래스를 수정해야 하고, 사용자 정보 정책이 바뀌어도 User 클래스를 수정해야 합니다. 변경의 이유가 두 가지나 되니, 단일 책임 원칙을 위반한 셈입니다. 따라서 User 클래스는 순수하게 사용자 정보만 다루고, 데이터베이스 관련 로직은 UserRepository와 같은 별도의 클래스로 분리하는 것이 올바른 설계입니다.

| 구분 | SRP 위반 예시 (Bad) | SRP 준수 예시 (Good) | | :--- | :--- | :--- | | **클래스** | `Employee` | `EmployeeData`, `PayCalculator`, `EmployeeDB` | | **책임** | 직원 정보 관리, 급여 계산, DB 저장 | 정보 관리 / 급여 계산 / DB 저장 (각각 분리) | | **변경 이유** | 인사 정책 변경, 급여 정책 변경, DB 스키마 변경 | 각 클래스는 변경 이유가 단 하나뿐임 |

출처: 클린 아키텍처

O: 개방-폐쇄 원칙 (Open-Closed Principle)

OCP는 '소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만, 수정에 대해서는 닫혀 있어야 한다'는 원칙입니다. 새로운 결제 수단(예: 카카오페이)을 추가할 때 기존의 결제 로직 코드를 수정하는 것이 아니라, 새로운 결제 클래스를 추가하는 방식으로 시스템이 확장되어야 한다는 의미입니다. 이는 주로 추상화(인터페이스, 추상 클래스)와 다형성을 통해 구현됩니다.

L: 리스코프 치환 원칙 (Liskov Substitution Principle)

LSP는 '하위 타입은 언제나 상위 타입으로 대체할 수 있어야 한다'는 원칙입니다. 즉, 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 프로그램이 문제없이 동작해야 한다는 뜻입니다. 예를 들어, `Bird` 클래스를 상속받는 `Penguin` 클래스가 있다고 할 때, `Bird`의 `fly()` 메서드를 `Penguin`에서 예외를 발생시키도록 오버라이딩한다면 이는 LSP를 위반하는 것입니다. 왜냐하면 모든 `Bird`가 날 수 있다고 가정한 로직에 `Penguin` 객체가 들어가면 오류가 발생하기 때문입니다.

나머지 약속들과 국내 적용의 중요성 (I, D)

I: 인터페이스 분리 원칙 (Interface Segregation Principle)

ISP는 '클라이언트는 자신이 사용하지 않는 메서드에 의존해서는 안 된다'는 원칙입니다. 너무 많은 기능을 가진 '뚱뚱한' 인터페이스 하나를 만들기보다, 여러 개의 구체적인 '작은' 인터페이스로 분리하는 것이 좋다는 의미입니다. 예를 들어, `SmartPrinter` 인터페이스에 `print()`, `scan()`, `fax()` 기능이 모두 있다면, 스캔 기능만 필요한 클라이언트도 불필요하게 `print`와 `fax` 기능에 의존하게 됩니다. 이를 `Printable`, `Scannable` 등으로 분리하면 의존성을 최소화할 수 있습니다.

D: 의존관계 역전 원칙 (Dependency Inversion Principle)

DIP는 '상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다'는 원칙입니다. 즉, 구체적인 구현 클래스에 직접 의존하지 말고, 인터페이스나 추상 클래스와 같은 추상화에 의존하라는 의미입니다. 이를 통해 모듈 간의 결합도를 낮추고 유연성을 높일 수 있습니다.

| 원칙 | 영문명 | 핵심 개념 | 기대 효과 | | :--- | :--- | :--- | :--- | | **S** | Single Responsibility | 클래스는 단 하나의 변경 이유를 가져야 함 | 응집도 향상, 코드 이해도 증가 | | **O** | Open-Closed | 확장에 열려있고, 수정에 닫혀있어야 함 | 새로운 기능 추가 시 기존 코드 수정 최소화 | | **L** | Liskov Substitution | 하위 타입은 상위 타입으로 대체 가능해야 함 | 다형성 보장, 코드 신뢰성 확보 | | **I** | Interface Segregation | 클라이언트에 특화된 인터페이스를 사용해야 함 | 불필요한 의존성 감소, 명확한 역할 분리 | | **D** | Dependency Inversion | 추상화에 의존하고, 구체화에 의존하면 안 됨 | 유연하고 재사용 가능한 코드 구조 |

출처: Head First Design Patterns

특히 국내 소프트웨어 개발 환경에서는 이러한 원칙 준수가 더욱 중요합니다. 예를 들어, 개인정보 보호법의 잦은 개정에 대응하려면 사용자 데이터 처리 로직(책임)이 명확히 분리(SRP)되어 있어야 법규 변경에 따른 수정을 최소화할 수 있습니다. 또한, 한국정보통신기술협회(TTA)에서 권고하는 소프트웨어 품질 표준을 만족시키기 위해서도 SOLID 원칙에 기반한 설계는 필수적입니다. 잘 설계된 코드는 초기 개발 비용을 수백만 원 절감할 뿐만 아니라, 장기적으로는 수천만 원에 달하는 유지보수 비용을 아낄 수 있는 가장 확실한 투자입니다.

자주 묻는 질문 (FAQ)

Q. SOLID 원칙을 실제 프로젝트에 처음 적용할 때 가장 어려운 점은 무엇인가요?
A. 처음에는 '책임'이나 '추상화'의 경계를 정하는 것이 가장 어렵습니다. 특히 단일 책임 원칙(SRP)에서 어디까지를 '단일 책임'으로 볼 것인지, 개방-폐쇄 원칙(OCP)을 위해 어디까지 추상화할 것인지 결정하는 것이 막막할 수 있습니다. 처음에는 완벽을 추구하기보다, 코드 리뷰와 리팩토링을 통해 점진적으로 개선해 나가는 것이 좋습니다.
Q. 모든 클래스에 SOLID 원칙을 칼같이 적용해야 하나요?
A. 반드시 그렇지는 않습니다. SOLID는 가이드라인이지, 모든 상황에 적용해야 하는 절대적인 규칙은 아닙니다. 예를 들어, 변경 가능성이 거의 없는 아주 단순한 데이터 객체(DTO)나 작은 규모의 프로젝트에 과도한 추상화를 적용하는 것은 오히려 복잡성만 높일 수 있습니다. 원칙을 적용했을 때 얻는 이점(유연성, 유지보수성)과 비용(복잡성 증가)을 저울질하는 지혜가 필요합니다.
Q. SOLID 원칙을 어기면 당장 어떤 문제가 생기나요?
A. 단기적으로는 큰 문제가 없어 보일 수 있습니다. 하지만 기능 추가나 수정 요구사항이 발생했을 때 문제가 드러납니다. 예를 들어, 책임이 뒤섞인 클래스는 작은 수정이 예상치 못한 다른 기능에 버그를 유발하는 '사이드 이펙트'를 일으키기 쉽고, 수정에 닫혀있지 않은 코드는 새로운 기능을 추가할 때마다 기존 코드를 광범위하게 변경해야 해 개발 속도를 크게 저하시킵니다.
Q. SOLID 원칙은 애자일 개발 방식과 잘 맞나요?
A. 네, 아주 잘 맞습니다. 애자일은 잦은 요구사항 변경에 신속하게 대응하는 것을 목표로 합니다. SOLID 원칙을 통해 유연하고 변경에 용이한 코드 구조를 만들어두면, 스프린트마다 발생하는 새로운 요구사항이나 변경점을 훨씬 적은 비용과 시간으로 반영할 수 있습니다. 즉, SOLID는 애자일의 '변화 대응' 가치를 기술적으로 뒷받침하는 핵심 원칙입니다.
Q. 기존에 SOLID 없이 만들어진 레거시 코드에는 어떻게 적용할 수 있을까요?
A. 전체 시스템을 한 번에 바꾸려고 하기보다는, '보이스카우트 규칙(왔을 때보다 더 깨끗하게 떠난다)'을 적용하는 것이 효과적입니다. 새로운 기능을 추가하거나 버그를 수정할 때, 관련된 코드 블록만이라도 SOLID 원칙에 맞게 리팩토링하는 방식입니다. 예를 들어, 거대한 클래스에서 새로 수정하는 메서드와 관련된 책임만이라도 별도의 클래스로 분리하는 작은 시도들이 모여 점진적으로 시스템 전체의 설계를 개선할 수 있습니다.
다음 이전