포스트

[Backend] SOLID 원칙 완벽 이해하기 - 개념부터 예시까지

[Backend] SOLID 원칙 완벽 이해하기 - 개념부터 예시까지

들어가며

SOLID 원칙은 객체지향 프로그래밍(OOP)에서 유지보수성과 확장성을 높이기 위한 5가지 설계 원칙입니다.
이름은 각 원칙의 앞 글자를 따온 것이며, 주로 의존성 관리와 변경에 유연한 구조를 만드는 데 목적이 있습니다. 이번 글에서는 SOLID 각 원칙을 예시와 함께 이해하고, 현실적인 적용 방법까지 함께 정리해보겠습니다.


목차

  1. 단일 책임 원칙 (SRP)
  2. 개방-폐쇄 원칙 (OCP)
  3. 리스코프 치환 원칙 (LSP)
  4. 인터페이스 분리 원칙 (ISP)
  5. 의존성 역전 원칙 (DIP)
  6. 변화에 대응하는 설계란 무엇인가?
  7. 마무리 요약

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는 도구가 아닌 가이드라인입니다. 무조건 따르기보다는, 맥락에 따라 현명하게 선택하는 유연한 개발자가 되어봅시다. 💡

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.