SOLID 원칙

지리·2025년 10월 11일

SOLID 탄생

SOLID는 <클린코드> <클린 아키텍처>의 저자인 로버트 C. 마틴이 인터넷 게시판에서 오랜기간 토론을 통해 좋은 설계원칙을 모아 2000년대 초반에 발표한 원칙을 최초로 탄생하게 되었다.

SOLID 원칙 따르면 좋은점?

SOLID 원칙이란 객체지향 설계에서 지켜줘야 할 5개의 소프트웨어 개발 원칙( SRP, OCP, LSP, ISP, DIP )을 말한다.

SOLID 객체 지향 원칙을 적용하면 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있다.

SRP (Single Responsibility Principle) - 단일 책임 원칙

단일 모듈(module)은 변경의 이유가 하나, 오직 하나뿐 이어야 한다.

이말은 하나의 모듈은 하나의 사용자또는 하나의 이해관계자에 대해서만 책임을 진다는 의미이다.

class UserService {
  createUser(user) {
    console.log("User created:", user);
  }

  sendWelcomeEmail(user) {
    console.log("Welcome email sent to:", user.email);
  }
}

위에서 UserService에서는 user에 관해서만 책임을 져야한다.

1) 이메일 서비스가 변경시
2) 유저구조 변경시

위의 예제에서는 이렇게 2가지 경우일때 변경이 일어나게 되기에 SRP원칙에 위배된다.

class UserService {
  createUser(user) {
    console.log("User created:", user);
  }
}

class EmailService {
  sendWelcomeEmail(user) {
    console.log("Welcome email sent to:", user.email);
  }
}

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

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

class PaymentProcessor {
  pay(type: string) {
    if (type === "card") console.log("Pay with card");
    else if (type === "paypal") console.log("Pay with PayPal");
  }
}

위의 클래스에서 새로운 결제 방식을 추가할경우 pay메서드를 수정해야한다.

interface Payment {
  pay(): void;
}

class CardPayment implements Payment {
  pay() { console.log("Pay with card"); }
}

class PayPalPayment implements Payment {
  pay() { console.log("Pay with PayPal"); }
}

class PaymentProcessor {
  process(payment: Payment) {
    payment.pay();
  }
}

위와 같이 변경시엔 의존성을 주입하는 방식으로, PaymentProcessor는 수정없이 새로운 결제 클래스만 추가하면 된다.

LSP (Liscov Substitution Principle) - 리스코프 치환 원칙

부모 클래스 객체는 자식 클래스로 대체 가능해야 한다.
즉, 자식 클래스가 부모 클래스의 행위를 깨뜨리면 안 된다.

class Rectangle {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }
  
  set width(value) {
    this._width = value;
  }

  set height(value) {
    this._height = value;
  }

  get width() {
    return this._width;
  }

  get height() {
    return this._height;
  }

  area() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor(width) {
    super(width, width);
  }

  // width와 height를 바꾸면 항상 정사각형 유지하도록 오버라이드
  set width(value) {
    this._width = this._height = value;
  }

  set height(value) {
    this._width = this._height = value;
  }
}

function printArea(rect) {
  rect.width = 5;
  rect.height = 10;
  console.log(rect.area());
}

const r = new Rectangle(2, 3);
const s = new Square(5);

printArea(r); // 50
printArea(s); // 100

위의 예제에서 Rectangle과 Square에 동일한 width, height 값을 넣었을때 예상과는 다른 동작이 다르게 나오는것을 알 수 있다. 즉, 정사각형과 직사각형이 올바른 상속관계가 아니며, 자식객체가 부모 객체의 역할을 대체하기 못한다는 의미이다.

class Shape {
  area() { throw new Error('must implement'); }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  area() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(size) {
    super();
    this.size = size;
  }
  area() {
    return this.size * this.size;
  }
}

const r2 = new Rectangle(5, 10);
const s2 = new Square(7);

r2.area() // 50
s2.area(); // 49

위의 예제에서는 더이상 Rectangle2과 Square2는 상속관계가 아니게 되며, 공통규약(area를 계산할 수 있다)만 공유하고 서로의 동작 계약을 깨지 않으므로 LSP를 만족하게 된다.

ISP (Interface Segregation Principle) - 인터페이스 분리 원칙

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
기능별로 인터페이스를 나눈다고 하더라도 만약 사용하지 않는 함수가 존재한다면, 인터페이스를 분리해야 한다는 뜻

interface Worker {
  work(): void;
  eat(): void;
}

class Robot implements Worker {
  work() { console.log("Working"); }
}

class Human implements Workable, Eatable {
  work() { console.log("Working"); }
  eat() { console.log("Eating"); }
}

위의 코드에서 Robot은 eat 메서드를 사용하지 않는다. 모든 Worker가 wrok와 eat을 하지 않는다면 아래의 코드처럼 work, eat 메서드를 분리해서 인터페이스를 만들어야 한다.

interface Workable { work(): void; }
interface Eatable { eat(): void; }

class Human implements Workable, Eatable {
  work() { console.log("Working"); }
  eat() { console.log("Eating"); }
}

class Robot implements Workable {
  work() { console.log("Working"); }
}

DIP (Dependency Inversion Principle) - 의존 관계 역전 원칙

고수준 모듈(비지니스 로직을 담은 상위 개념)은 저수준 모듈(실제 구현 세부사항)에 의존하지 말고, 추상화에 의존해야 한다.
구현체는 변동성이 크기 때문에, 안정된 인터페이스를 참조해야한다.

class MySQLDatabase {
  save(data: string) {
    console.log("Saving to MySQL:", data);
  }
}

class UserRepository {
  private db = new MySQLDatabase(); // ❌ 직접 의존

  saveUser(user: string) {
    this.db.save(user);
  }
}

위의 예제에서 db를 MySQLDatabase에서 가져온다. 하지만 db는 localStorage에서도 가져올수있고,MongoDB에서도 가져와질수 있다. 이를 DIP 원칙을 적용해 수정하면 아래와 같다.

interface Database {
  save(data: string): void;
}

class MySQLDatabase implements Database {
  save(data: string) { console.log("Saving to MySQL:", data); }
}

class MongoDatabase implements Database {
  save(data: string) { console.log("Saving to MongoDB:", data); }
}

class UserRepository {
  constructor(private db: Database) {}

  saveUser(user: string) {
    this.db.save(user);
  }
}

Database라는 추상체에서 db를 가져오며 어떤 database에서 가져올지는 외부에서 의존성을 주입해주는 방식으로 수정한다.

profile
공부한것들, 경험한 것들을 기록하려 노력합니다✨

0개의 댓글