[Backend] SOLID 원칙 완벽 이해하기 - 개념부터 예시까지
들어가며
SOLID 원칙은 객체지향 프로그래밍(OOP)에서 유지보수성과 확장성을 높이기 위한 5가지 설계 원칙입니다.
이름은 각 원칙의 앞 글자를 따온 것이며, 주로 의존성 관리와 변경에 유연한 구조를 만드는 데 목적이 있습니다. 이번 글에서는 SOLID 각 원칙을 예시와 함께 이해하고, 현실적인 적용 방법까지 함께 정리해보겠습니다.
목차
- 단일 책임 원칙 (SRP)
- 개방-폐쇄 원칙 (OCP)
- 리스코프 치환 원칙 (LSP)
- 인터페이스 분리 원칙 (ISP)
- 의존성 역전 원칙 (DIP)
- 변화에 대응하는 설계란 무엇인가?
- 마무리 요약
1. 단일 책임 원칙 (Single Responsibility Principle)
“클래스는 하나의 변경 이유만 가져야 한다.”
하나의 클래스가 여러 역할을 수행하면, 한 기능의 변경이 다른 기능에 영향을 줄 수 있습니다.
책임을 나누면 테스트, 유지보수, 확장이 쉬워집니다.
❌ 위반 예시
1
2
3
4
5
6
7
8
class UserService {
saveUser(user: User) {
// DB 저장
}
sendWelcomeEmail(user: User) {
// 이메일 전송
}
}
✅ 개선 예시
1
2
3
4
5
6
7
class UserRepository {
save(user: User) { /* DB 저장 */ }
}
class EmailService {
sendWelcomeEmail(user: User) { /* 이메일 전송 */ }
}
2. 개방-폐쇄 원칙 (Open-Closed Principle)
“확장에는 열려 있고, 변경에는 닫혀 있어야 한다.”
기능을 추가할 때 기존 코드를 변경하지 않고, 새로운 코드를 추가함으로써 동작을 확장해야 한다는 원칙입니다.
❌ 위반 예시
1
2
3
4
5
6
class Payment {
pay(method: string) {
if (method === 'card') { /* 카드 결제 */ }
else if (method === 'cash') { /* 현금 결제 */ }
}
}
✅ 개선 예시 (OCP 적용)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface PaymentStrategy {
pay(): void;
}
class CardPayment implements PaymentStrategy {
pay() { /* 카드 결제 */ }
}
class CashPayment implements PaymentStrategy {
pay() { /* 현금 결제 */ }
}
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
process() {
this.strategy.pay();
}
}
3. 리스코프 치환 원칙 (Liskov Substitution Principle)
“서브 타입은 언제나 상위 타입으로 교체할 수 있어야 한다.”
하위 클래스가 상위 클래스의 규약(계약)을 위반하지 않고, 대체 가능해야 한다는 원칙입니다.
❌ 위반 예시 (고전적인 Rectangle 예제)
1
2
3
4
5
6
7
8
9
10
11
class Rectangle {
setWidth(w: number) { /* ... */ }
setHeight(h: number) { /* ... */ }
}
class Square extends Rectangle {
setWidth(w: number) {
super.setWidth(w);
super.setHeight(w); // 정사각형은 너비=높이
}
}
이 경우 Rectangle을 사용하는 클라이언트가 예상하지 못한 동작을 할 수 있습니다.
4. 인터페이스 분리 원칙 (Interface Segregation Principle)
“클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.”
너무 많은 기능이 하나의 인터페이스에 모이면, 불필요한 의존성으로 인해 코드 변경 시 ripple effect(파급 효과)가 발생할 수 있습니다.
❌ 위반 예시
1
2
3
4
5
6
7
8
9
interface Animal {
fly(): void;
swim(): void;
}
class Dog implements Animal {
fly() { /* 의미 없음 */ }
swim() { /* 구현 */ }
}
✅ 개선 예시
1
2
3
4
5
6
7
8
9
10
11
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
class Dog implements Swimmable {
swim() { /* 구현 */ }
}
5. 의존성 역전 원칙 (Dependency Inversion Principle)
“상위 모듈은 하위 모듈에 의존하면 안 되고, 둘 다 추상화에 의존해야 한다.”
구체적인 클래스가 아닌 인터페이스나 추상 클래스에 의존함으로써, 코드를 더 유연하고 테스트 가능하게 만듭니다.
❌ 위반 예시
1
2
3
4
5
6
7
class MySQLDatabase {
connect() { /* ... */ }
}
class UserService {
db = new MySQLDatabase(); // 직접 의존
}
✅ 개선 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Database {
connect(): void;
}
class MySQLDatabase implements Database {
connect() { /* 구현 */ }
}
class UserService {
constructor(private db: Database) {}
init() {
this.db.connect();
}
}
6. 변화에 대응하는 설계란?
많은 사람들이 OCP를 지키기 위해 미리 추상화를 하려고 노력하지만, 실제로는 변화를 예측하는 것보다 변화에 대응하는 것이 더 실용적일 수 있습니다.
“앞으로 어떤 변경이 올지 모르기 때문에, 모든 가능성에 대비하기보다 지금 필요한 기능을 빠르게 만들고, 실제 변경이 발생했을 때 그 변화를 기반으로 추상화를 도입하는 것이 낫습니다.”
현실적인 설계 전략
- 처음부터 완벽한 추상화를 기대하지 말 것
- 요구사항이 바뀌었을 때 그 패턴이 반복될 가능성이 있다면 그 시점에서 OCP 적용
- 작은 리팩토링을 통해 SOLID 원칙을 점진적으로 적용하기
7. 마무리 요약
원칙 | 설명 |
---|---|
SRP | 하나의 클래스는 하나의 책임만 가져야 한다 |
OCP | 기능은 확장 가능하지만, 기존 코드는 변경하지 않도록 |
LSP | 하위 타입은 상위 타입을 완벽히 대체할 수 있어야 한다 |
ISP | 클라이언트가 사용하지 않는 메서드에 의존하지 않아야 한다 |
DIP | 고수준 모듈과 저수준 모듈 모두 추상화에 의존해야 한다 |
참고 자료
- Uncle Bob - SOLID Principles
- 마틴 파울러 - OOP 설계와 변경의 유연성
- [책] 객체지향의 사실과 오해 - 조영호
SOLID는 도구가 아닌 가이드라인입니다. 무조건 따르기보다는, 맥락에 따라 현명하게 선택하는 유연한 개발자가 되어봅시다. 💡