객체 지향 프로그래밍 (3) - SOLID 원칙

Jiseong Choi·2025년 4월 6일

디자인 패턴

목록 보기
3/3

객체 지향 프로그래밍(OOP)으로 프로젝트를 개발할 때, 지켜야할 원칙이 있다!
그것이 바로 "SOLID 원칙"
SOLID 원칙은 유지보수성과 확장성이 뛰어난 코드를 설계하기 위한 핵심 철학이기 때문에 이 원칙들을 잘 지켜가며 개발하면 깔끔하고 안정적인 구조의 프로젝트를 만들 수 있을 것이다.
고로 이 포스트에선 SOLID 원칙을 지켜야하는 이유와 SOLID 원칙에 대해서 정리하고자 한다.

SOLID 원칙이란?

SOLID는 객체 지향 설계의 5가지 핵심 원칙을 의미하는 약자다!

S - 단일 책임 원칙 (Single Responsibility Principle)
O - 개방/폐쇄 원칙 (Open-Close Principle)
L - 리스코프 치환 원칙 (Liskov Substitution Principle)
I - 인터페이스 분리 원칙 (Interface Segregation Principle)
D - 의존 역전 원칙 (Dependency Inversion Principle)


1. 단일 책임 원칙 (SRP: Single Responsibility Principle)

클래스는 하나의 책임(기능)만 가져야 한다.

🧠 개념

  • 하나의 클래스는 하나의 일만 해야 한다.
  • 여러 이유로 변경되면 안 된다.

💡 비유

자동차 클래스가 운전도 하고, 정비 일정 관리도 한다면? 너무 많은 일을 하게 되는 것!
운전은 Car, 정비는 MaintenanceManager가 각각 맡아야한다!

❌ 안 좋은 예

class Vehicle {
  drive() { /* 주행 */ }
  scheduleMaintenance() { /* 정비 일정 관리 */ }  // 책임 과다!
}

✅ 개선 예

// 탈 것 클래스
class Vehicle {
  drive() { /* 주행 */ }
}

// 유지관리 클래스
class MaintenanceManager {
  scheduleMaintenance() { /* 정비 일정 관리 */ }
}

2. 개방-폐쇄 원칙 (OCP: Open-Closed Principle)

확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.

🧠 개념

  • 기존 코드를 수정하지 않고 기능을 확장할 수 있어야 한다.

💡 비유

새로운 자동차 종류(예: 전기차)를 만들기 위해 기존 Car 클래스를 수정하면, 기존 기능이 깨질 위험이 있다.
대신 확장을 통해 새로운 클래스를 만든다.

❌ 안 좋은 예

class Vehicle {
  startEngine(type: string) {
    if (type === 'electric') console.log("전기차 엔진 ON");
    else if (type === 'gas') console.log("가솔린 엔진 ON");
  }
}

✅ 개선 예

interface Engine {
  start(): void;
}

// 전기차 엔진 클래스 생성
class ElectricEngine implements Engine {
  start() { console.log("전기차 엔진 ON"); }
}

// 가솔린 엔진 클래스 생성
class GasEngine implements Engine {
  start() { console.log("가솔린 엔진 ON"); }
}

class Vehicle {
  // 생성자: 사용하는 엔진 클래스로 객체 생성
  constructor(private engine: Engine) {}
  startEngine() {
    this.engine.start();
  }
}

3. 리스코프 치환 원칙 (LSP: Liskov Substitution Principle)

부모 클래스의 객체를 사용하는 곳에 자식 클래스 객체를 넣어도 문제가 없어야 한다.

🧠 개념

  • 자식 클래스는 부모의 기능과 계약을 유지한다.

💡 비유

탈 것(Vehicle)을 사용하는 코드에 Truck이나 ElectricCar를 넣어도 정상 동작 해야한다.
근데 ElectricCar인데 기름 넣는 refule()을 필수로 구현하라고 하면 안된다!

❌ 안 좋은 예

// Vehicle을 사용하는 곳에 ElectricCar를 사용할 경우 refuel에서 에러가 발생!
class Vehicle {
  refuel() { console.log("기름 충전"); }
}

class ElectricCar extends Vehicle {
  refuel() { throw new Error("전기차는 기름을 넣을 수 없습니다."); }  // 규약 위반
}

✅ 개선 예

// refuel() 메서드를 자식 클래스에서 정의하여 Vehicle을 사용하는 곳에 
// FuelVehicle, ElectricCar을 넣어도 아무런 문제가 발생하지 않음!  
class Vehicle {}

class FuelVehicle extends Vehicle {
  refuel() { console.log("기름 충전"); }
}

class ElectricCar extends Vehicle {
  charge() { console.log("전기 충전"); }
}

4. 인터페이스 분리 원칙(ISP: Interface Segregation Principle)

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.

🧠 개념

  • 인터페이스는 작고 명확하게 분리하자.

💡 비유

CarService 인터페이스에 chargeElectric()이 있으면, 휘발유차(Car)도 구현해야한다.
즉, 인터페이스는 각 클래스에 맞춰서 작게 나누는 것이 좋다.

❌ 안 좋은 예

// GasCar는 사용하지 않는 메서드를 구현해야하므로 ISP를 위반한다
interface VehicleService {
  refuel(): void;
  chargeElectric(): void;
}

class GasCar implements VehicleService {
  refuel() {}
  chargeElectric() {} // 휘발유차에 필요 없는 기능!
}

✅ 개선 예

// 인터페이스를 기능 단위로 분리하고, 각 클래스는 필요한 기능만 구현한다
interface Refuelable {
  refuel(): void;
}

interface Chargeable {
  chargeElectric(): void;
}

class GasCar implements Refuelable {
  refuel() {}
}

class ElectricCar implements Chargeable {
  chargeElectric() {}
}

// 두 가지 모두 가능한 차는 다중 상속을 받으면 된다.
class HybridCar implements Refuelable, Chargeable {
  refuel() {}
  chargeElectric() {}
}

5. 의존 역전 원칙 (DIP: Dependency Inversion Principle)

추상화에 의존해야지, 구현체에 의존하면 안 된다.

🧠 개념

  • 고수준 모듈은 저수준 모듈에 의존하지 않아야 한다.
  • 둘 다 인터페이스나 추상 클래스에 의존해야 한다.

💡 비유

운전자가 어떤 엔진을 쓰는지는 모르고, Engine 인터페이스만 필요하다.
구체적인 GasEngine, ElectricEngine은 바껴도 문제 없어야 한다.

❌ 안 좋은 예

// GasEngine()을 적접 생성하고 있어 고수준 모듈이 저수준 모듈에 의존해서는 안되는 원칙 위반
class GasEngine {
  start() { console.log("가솔린 엔진 ON"); }
}

class Car {
  engine = new GasEngine(); // 구체적인 구현에 직접 의존
}

✅ 개선 예

// Car 객체 생성 시 사용하는 엔진 클래스를 주입하여 사용하면 되기 때문에 저수준 모듈에 의존하지 않는다.
interface Engine {
  start(): void;
}

class GasEngine implements Engine {
  start() { console.log("가솔린 엔진 ON"); }
}

class Car {
  constructor(private engine: Engine) {}
  startCar() {
    this.engine.start();
  }
}

SOLID 원칙을 지켜야 하는 이유

1. 유지보수가 쉬워진다.

  • 시간이 지나면서 코드가 변경될 수밖에 없는데, SOLID 원칙을 적용하면 특정 부분만 고쳐도 전체 시스템이 안정적으로 유지된다.
  • 버그가 발생해도 원인을 빠르게 파악하고, 수정이 쉽다.

2. 확장에 유연하다.

  • 새로운 기능을 추가할 때 기존 코드를 건드리지 않고 확장할 수 있다.
  • 변화에 유연한 구조를 만들 수 있기 때문에, 새로운 요구사항에 대응하기 쉽다.

3. 코드의 가독성과 명확성이 높아진다.

  • 각 클래스나 모듈의 책임이 명확해지니 역할 분리가 확실해진다.
  • 다른 사람이 코드를 읽을 때도, "이 클래스는 뭐 하는 애지?"가 쉽게 보인다.

4. 재사용성과 테스트가 쉬워진다.

  • 각 구성 요소가 독립적이어서, 재사용도 쉽고 유닛 테스트 작성도 쉽다.
  • 특히 DIP(의존 역전 원칙)는 Mock 객체 활용에 최적화돼 있다.

5. 결합도를 낮추고, 응집도를 높인다.

  • 모듈 간 의존도를 줄이고, 각 모듈의 책임을 집중시킨다.
  • 즉, 한 클래스가 다른 클래스에 끌려다니지 않도록 만든다.
profile
나 혼자 공부하고, 끄적이는 공간. (Node.JS / Back-End Developer)

0개의 댓글