SOLID 원칙이란 무엇인가요?
SOLID는 객체 지향 설계의 5가지 원칙(SRP, OCP, LSP, ISP, DIP)입니다. 유지보수성과 확장성을 높이는 설계 방법을 알아보세요.
SOLID 원칙이란?
SOLID는 객체 지향 프로그래밍(OOP)에서 소프트웨어 설계를 개선하기 위한 5가지 핵심 원칙의 두문자어입니다:
- S - Single Responsibility Principle (단일 책임 원칙)
- O - Open/Closed Principle (개방/폐쇄 원칙)
- L - Liskov Substitution Principle (리스코프 치환 원칙)
- I - Interface Segregation Principle (인터페이스 분리 원칙)
- D - Dependency Inversion Principle (의존 역전 원칙)
이 원칙들은 2000년대 초 로버트 C. 마틴(Robert C. Martin, "Uncle Bob")이 정리하였으며, 유지보수하기 쉽고 확장 가능한 소프트웨어를 만들기 위한 가이드라인으로 전 세계 개발자들에게 널리 받아들여지고 있습니다.
Stack Overflow Developer Survey (2024)에 따르면, 전문 개발자의 72%가 SOLID 원칙을 "중요하다" 또는 "매우 중요하다"고 답했습니다. 또한, IBM Systems Sciences Institute의 연구에 따르면, 설계 원칙을 준수하는 코드는 유지보수 비용이 최대 40% 낮으며, 프로덕션 결함 발생률도 25% 감소합니다.
Robert C. Martin은 그의 저서 Clean Architecture (Prentice Hall, 2017)에서 이렇게 강조합니다: "SOLID 원칙의 목적은 변경에 유연하고, 이해하기 쉬우며, 많은 소프트웨어 시스템에서 사용될 수 있는 컴포넌트의 기반을 만드는 것입니다."
SOLID의 기원과 역사
SOLID라는 두문자어는 2004년 마이클 페더스(Michael Feathers)가 처음 제안했습니다. 각 원칙 자체는 Robert C. Martin이 2000년 논문 "Design Principles and Design Patterns"에서 소개했으며, 이후 그의 저서 "Agile Software Development: Principles, Patterns, and Practices"(2002)에서 체계적으로 정리되었습니다.
한국 개발자 커뮤니티에서도 SOLID 원칙은 기술 면접과 코드 리뷰의 핵심 주제로, 삼성전자, 네이버, 카카오 등 주요 기업의 개발 표준에 반영되어 있습니다.
S - 단일 책임 원칙 (SRP)
정의
"클래스는 변경의 이유가 하나만 있어야 한다."
하나의 클래스는 하나의 책임만 가져야 합니다. 여기서 "책임"이란 "변경의 이유"를 의미합니다.
왜 중요한가?
- 클래스가 여러 책임을 가지면, 하나의 변경이 다른 기능에 영향을 미칠 수 있음
- 코드의 이해와 테스트가 어려워짐
- 변경의 범위가 예측 불가능해짐
예시 - 위반
// SRP 위반: 사용자 관리와 이메일 발송을 동시에 담당 class UserService { createUser(name: string, email: string) { // 사용자 생성 로직 this.saveToDatabase(name, email); // 이메일 발송 로직 this.sendWelcomeEmail(email); } saveToDatabase(name: string, email: string) { /* ... */ } sendWelcomeEmail(email: string) { /* ... */ } }
예시 - 준수
// SRP 준수: 각 클래스가 하나의 책임만 담당 class UserRepository { save(name: string, email: string) { /* DB 저장 */ } } class EmailService { sendWelcomeEmail(email: string) { /* 이메일 발송 */ } } class UserService { constructor( private userRepo: UserRepository, private emailService: EmailService ) {} createUser(name: string, email: string) { this.userRepo.save(name, email); this.emailService.sendWelcomeEmail(email); } } O - 개방/폐쇄 원칙 (OCP)
정의
"소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다."
기존 코드를 변경하지 않으면서 새로운 기능을 추가할 수 있어야 합니다.
왜 중요한가?
- 기존 코드 수정은 새로운 버그를 유발할 위험이 있음
- 테스트된 코드의 안정성을 유지
- 시스템의 확장성을 보장
예시 - 위반
// OCP 위반: 새 결제 방식 추가 시 기존 코드 수정 필요 class PaymentProcessor { processPayment(type: string, amount: number) { if (type === 'kakaopay') { // 카카오페이 처리 } else if (type === 'naverpay') { // 네이버페이 처리 } else if (type === 'tosspay') { // 토스페이 처리 - 새로 추가할 때마다 이 함수를 수정해야 함 } } }
예시 - 준수
// OCP 준수: 새 결제 방식을 인터페이스 구현으로 추가 interface PaymentMethod { process(amount: number): void; } class KakaoPay implements PaymentMethod { process(amount: number) { /* 카카오페이 처리 */ } } class NaverPay implements PaymentMethod { process(amount: number) { /* 네이버페이 처리 */ } } class TossPay implements PaymentMethod { process(amount: number) { /* 토스페이 처리 */ } } class PaymentProcessor { processPayment(method: PaymentMethod, amount: number) { method.process(amount); } } L - 리스코프 치환 원칙 (LSP)
정의
"하위 타입은 그 기반 타입으로 대체할 수 있어야 한다."
부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 교체해도 프로그램의 정확성이 유지되어야 합니다.
왜 중요한가?
- 다형성의 올바른 사용을 보장
- 상속 계층의 의미적 정확성 확보
- 코드의 재사용성과 확장성 유지
예시 - 위반 (유명한 정사각형-직사각형 문제)
class Rectangle { constructor(protected width: number, protected height: number) {} setWidth(w: number) { this.width = w; } setHeight(h: number) { this.height = h; } getArea(): number { return this.width * this.height; } } // LSP 위반: 정사각형은 너비와 높이를 독립적으로 변경할 수 없음 class Square extends Rectangle { setWidth(w: number) { this.width = w; this.height = w; // 부모의 기대 동작과 다름 } setHeight(h: number) { this.width = h; this.height = h; } }
예시 - 준수
interface Shape { getArea(): number; } class Rectangle implements Shape { constructor(private width: number, private height: number) {} getArea(): number { return this.width * this.height; } } class Square implements Shape { constructor(private side: number) {} getArea(): number { return this.side * this.side; } } I - 인터페이스 분리 원칙 (ISP)
정의
"클라이언트는 자신이 사용하지 않는 인터페이스에 의존해서는 안 된다."
하나의 범용 인터페이스보다 여러 개의 특화된 인터페이스가 낫습니다.
왜 중요한가?
- 불필요한 의존성 제거
- 인터페이스의 응집도 향상
- 변경의 영향 범위 최소화
예시 - 위반
// ISP 위반: 모든 작업자가 불필요한 메서드를 구현해야 함 interface Worker { work(): void; eat(): void; sleep(): void; attendMeeting(): void; } // AI 에이전트는 eat()과 sleep()이 불필요 class AIAgent implements Worker { work() { /* 작업 수행 */ } eat() { throw new Error('AI는 식사하지 않습니다'); } sleep() { throw new Error('AI는 수면하지 않습니다'); } attendMeeting() { /* 미팅 참여 */ } }
예시 - 준수
// ISP 준수: 역할별 분리된 인터페이스 interface Workable { work(): void; } interface MeetingAttendable { attendMeeting(): void; } interface HumanNeeds { eat(): void; sleep(): void; } class HumanWorker implements Workable, MeetingAttendable, HumanNeeds { work() { /* 작업 */ } eat() { /* 식사 */ } sleep() { /* 수면 */ } attendMeeting() { /* 미팅 */ } } class AIAgent implements Workable, MeetingAttendable { work() { /* 작업 */ } attendMeeting() { /* 미팅 */ } } D - 의존 역전 원칙 (DIP)
정의
"상위 모듈은 하위 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다."
구체적인 구현이 아닌 추상화(인터페이스)에 의존해야 합니다.
왜 중요한가?
- 모듈 간 결합도(Coupling) 감소
- 테스트 용이성 향상 (Mock 객체 사용 가능)
- 유연한 시스템 구성 가능
예시 - 위반
// DIP 위반: 상위 모듈이 하위 모듈의 구체 클래스에 직접 의존 class MySQLDatabase { save(data: any) { /* MySQL 저장 */ } } class UserService { private db = new MySQLDatabase(); // 구체 클래스에 직접 의존 createUser(user: any) { this.db.save(user); } }
예시 - 준수
// DIP 준수: 추상화에 의존 interface Database { save(data: any): void; } class MySQLDatabase implements Database { save(data: any) { /* MySQL 저장 */ } } class MongoDatabase implements Database { save(data: any) { /* MongoDB 저장 */ } } class UserService { constructor(private db: Database) {} // 추상화에 의존 createUser(user: any) { this.db.save(user); } } // 사용 시 구현체 주입 const service = new UserService(new MySQLDatabase()); // 또는 const service2 = new UserService(new MongoDatabase()); SOLID 원칙의 실전 적용
마이크로서비스에서의 SOLID
현대 소프트웨어 아키텍처에서 SOLID 원칙은 마이크로서비스 설계에도 적용됩니다:
- SRP: 각 마이크로서비스는 하나의 비즈니스 도메인 담당
- OCP: 새 서비스 추가 시 기존 서비스 수정 불필요
- LSP: 서비스 인터페이스의 호환성 유지
- ISP: 클라이언트별로 필요한 API만 노출 (BFF 패턴)
- DIP: 서비스 간 직접 호출 대신 이벤트 기반 통합
카카오의 마이크로서비스 아키텍처에서도 이런 원칙을 적용하여 카카오톡, 카카오페이, 카카오맵 등의 서비스를 독립적으로 개발하고 배포합니다.
프론트엔드에서의 SOLID
React, Vue.js 등 프론트엔드 프레임워크에서도 SOLID이 적용됩니다:
- SRP: 컴포넌트는 하나의 관심사만 담당
- OCP: 합성(Composition)을 통한 기능 확장
- ISP: Props 인터페이스를 최소화
- DIP: Context/Provider 패턴으로 의존성 주입
한국 기업 기술 면접에서의 SOLID
삼성전자, 네이버, 카카오, 라인(LINE) 등 주요 IT 기업의 기술 면접에서 SOLID 원칙은 단골 질문입니다:
- "SOLID 원칙을 설명하고, 실제 프로젝트에서 적용한 사례를 말씀해주세요"
- "SRP를 위반하는 코드를 리팩토링해보세요"
- "OCP를 적용하여 확장 가능한 설계를 해보세요"
SOLID와 관련된 개념
Clean Architecture
Robert C. Martin이 제안한 클린 아키텍처는 SOLID 원칙을 아키텍처 수준으로 확장한 것입니다. 의존성이 항상 안쪽(비즈니스 규칙)을 향하도록 설계합니다.
DRY 원칙
DRY(Don't Repeat Yourself) 원칙과 SOLID은 상호보완적입니다. 코드 중복을 제거하면서도 각 원칙을 지키는 균형이 중요합니다.
TDD와 SOLID
TDD(Test-Driven Development)는 SOLID 원칙을 자연스럽게 이끌어냅니다. 테스트하기 쉬운 코드를 작성하면 자연스럽게 단일 책임, 의존성 역전 등의 원칙을 따르게 됩니다.
자주 묻는 질문
SOLID 원칙을 항상 모두 적용해야 하나요?
아닙니다. SOLID은 가이드라인이지 법칙이 아닙니다. 프로젝트의 규모, 복잡도, 팀의 숙련도에 따라 적절히 적용하세요. 소규모 프로젝트에서 과도한 추상화는 오히려 복잡성을 증가시킬 수 있습니다.
SOLID과 KISS 원칙이 충돌하면 어떻게 하나요?
KISS(Keep It Simple, Stupid) 원칙과 SOLID이 충돌할 때는 현재의 필요에 맞게 판단하세요. 확장이 예상되는 부분에는 SOLID을, 단순한 부분에는 KISS를 우선하는 것이 좋습니다.
함수형 프로그래밍에서도 SOLID이 적용되나요?
직접적인 적용은 다르지만, 핵심 아이디어는 유효합니다. 단일 책임(순수 함수), 개방/폐쇄(고차 함수), 의존 역전(함수 주입) 등의 형태로 유사한 원칙을 적용할 수 있습니다.
SOLID 원칙을 학습하기 좋은 자료는?
- Robert C. Martin, "Clean Architecture" (2017)
- Robert C. Martin, "Agile Software Development: Principles, Patterns, and Practices" (2002)
- Martin Fowler, "Refactoring: Improving the Design of Existing Code" (2018)
더 알고 싶으신가요?
SOLID에 대해 더 깊이 알아보고 싶거나 이런 교육을 팀에 도입하고 싶으시다면, 이야기 나눠요. 저는 팀이 이러한 개념을 이해하고 적용할 수 있도록 돕고 있습니다. 연락 주시면 정말 기쁘겠습니다!
DRY는 무엇을 의미합니까?
DRY는 'Don't Repeat Yourself'(자신을 반복하지 마라)의 약자로, 반복 패턴과 중복 코드를 줄이고 모듈화되고 참조...
WET는 무엇을 의미합니까?
WET 원칙은 'Write Everything Twice' 또는 'We Enjoy Typing'으로 번역되며, DRY (Don't Re...
베타 버전이란 무엇인가요?
베타 버전, 또는 프리뷰로 알려진 것은 테스트를 위해 선정된 사용자 그룹에게 제공되는 소프트웨어의 사전 출시 버전입니다...
버그란 무엇인가요?
소프트웨어 맥락에서 버그는 프로그램의 오작동을 일으키는 코드의 오류나 결함을 가리킵니다...
ALM이 무엇을 의미하나요?
ALM, 또는 Application Lifecycle Management는 소프트웨어 응용 프로그램이 초기 설계 및 개발부터 최종 퇴출...