개발자공부일기

SOLID원칙 본문

CS지식/기타

SOLID원칙

JavaCPP 2025. 6. 2. 16:13

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

  • 클래스(또는 모듈)는 하나의 책임만 가져야 한다.
  • 하나의 책임이란 하나의 "기능 담당 또는 변경의 이유"라고도 볼 수 있어요. 즉, 클래스가 바뀌어야 하는 이유가 하나만 있어야 합니다.
  • 여러 책임을 한 클래스에 몰아넣으면 변경이 생길 때 얽혀서 버그가 생기고 테스트도 어려워집니다.

잘못된 예제

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  saveUserToDB() {
    // 사용자 정보를 DB에 저장
  }

  sendEmail() {
    // 이메일 전송
  }
}

문제점:

  • 데이터 역할(User 객체), DB 저장, 이메일 전송까지 세 가지 책임이 한 클래스에 섞여있음.
  • 유지보수 시 DB 코드 변경이나 이메일 코드 변경이 User 클래스까지 영향을 줌.

SOLID 원칙 적용 (SRP)

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class UserRepository {
  save(user) {
    // DB에 저장
  }
}

class EmailService {
  sendEmail(user) {
    // 이메일 전송
  }
}

이렇게 하면:

  • User: 사용자 데이터만 담당.
  • UserRepository: DB 작업만 담당.
  • EmailService: 이메일 작업만 담당.
  • 각 클래스는 변경 이유가 하나뿐이라 단일 책임 원칙을 충족합니다.

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

  • 소프트웨어 요소(클래스, 모듈)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
  • 즉, 새로운 기능이 필요하면 새로운 클래스를 추가(확장)하면 되고, 기존 코드는 손대지 않아도 되게 만드는 것.
  • 이렇게 하면 기존 코드가 잘 동작하는지 매번 테스트할 필요가 없어서 안정적입니다.

 잘못된 예제

class DiscountCalculator {
  calculate(price, type) {
    if (type === 'student') {
      return price * 0.9;
    } else if (type === 'veteran') {
      return price * 0.8;
    } else {
      return price;
    }
  }
}

문제점:

  • 새로운 할인 정책이 추가될 때마다 if문을 추가해야 하고, DiscountCalculator 클래스를 수정해야 함.
  • 코드가 점점 길어지고 에러 날 가능성도 증가.

 SOLID 원칙 적용 (OCP)

class DiscountCalculator {
  calculate(price, discountStrategy) {
    return discountStrategy.calculate(price);
  }
}

class StudentDiscount {
  calculate(price) {
    return price * 0.9;
  }
}

class VeteranDiscount {
  calculate(price) {
    return price * 0.8;
  }
}

이렇게 하면:

  • 새로운 할인 정책은 새로운 클래스로 만들어서 주입하면 됨.
  • 기존 DiscountCalculator는 수정할 필요가 없음.
  • 즉, 확장에는 열려 있고, 변경에는 닫혀 있음.

리스코프 치환 원칙 (LSP, Liskov Substitution Principle)

  • 자식 클래스는 부모 클래스를 대체해도 프로그램이 깨지지 않아야 한다.
  • 부모 타입으로 동작하는 코드가 자식 타입으로도 동작해야 한다는 뜻.
  • 자식 클래스가 부모의 계약(행동)을 위반하면 안 된다.

잘못된 예제

class Bird {
  fly() {
    console.log("날아갑니다!");
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error("펭귄은 날 수 없습니다!");
  }
}

문제점:

  • Penguin 객체를 Bird 타입으로 넘겼을 때 fly()가 예외를 발생시킴.
  • 프로그램이 예기치 않게 동작 → 부모 타입 대신 자식 타입을 안전하게 쓸 수 없음.

SOLID 원칙 적용 (LSP)

class Bird {}

class FlyingBird extends Bird {
  fly() {
    console.log("날아갑니다!");
  }
}

class Penguin extends Bird {
  swim() {
    console.log("헤엄칩니다!");
  }
}

이렇게 하면:

  • fly()는 오직 날 수 있는 새에서만 제공.
  • Bird 타입으로 받으면 fly()를 바로 호출하지 않으므로 안전.
  • 부모 타입으로 자식 타입을 대체할 수 있음 → LSP 만족.

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

  • 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않게 인터페이스를 분리하라.
  • 한 클래스가 너무 많은 인터페이스(또는 메서드)를 강제하면 일부 구현체가 쓰지도 않는 기능까지 구현해야 해서 불필요한 부담이 생김.

잘못된 예제

class MultiFunctionPrinter {
  print() {
    console.log("출력합니다!");
  }

  scan() {
    console.log("스캔합니다!");
  }

  fax() {
    console.log("팩스를 보냅니다!");
  }
}

class SimplePrinter extends MultiFunctionPrinter {
  fax() {
    throw new Error("팩스 기능을 지원하지 않습니다!");
  }
}

문제점:

  • SimplePrinter는 팩스 기능을 원하지 않지만 인터페이스상 반드시 구현해야 해서 에러 처리 코드가 필요.
  • 쓰지 않는 메서드에 의존하게 됨.

SOLID 원칙 적용 (ISP)

class Printer {
  print() {
    console.log("출력합니다!");
  }
}

class Scanner {
  scan() {
    console.log("스캔합니다!");
  }
}

class Fax {
  fax() {
    console.log("팩스를 보냅니다!");
  }
}

이렇게 하면:

  • 필요한 기능만 골라서 조합.
  • SimplePrinter는 Printer만 구현.
  • 팩스가 필요 없는 프린터에 팩스 기능을 강제하지 않음 → ISP 만족.

의존 역전 원칙 (DIP, Dependency Inversion Principle)

  • 고수준 모듈(비즈니스 로직)이 저수준 모듈(구현 세부)에 의존하면 안 된다.
  • 둘 다 추상화에 의존해야 한다.
  • 이렇게 하면 구현 변경이 고수준 모듈까지 영향을 미치지 않음.

잘못된 예제

class MySQLDatabase {
  save(data) {
    console.log(`MySQL에 저장: ${data}`);
  }
}

class UserService {
  constructor() {
    this.database = new MySQLDatabase();
  }

  saveUser(user) {
    this.database.save(user);
  }
}

문제점:

  • UserService가 MySQLDatabase에 직접 의존.
  • 데이터베이스가 MongoDB로 바뀌면 UserService까지 손봐야 함.
  • UserService가 하위 모듈(구현)에 의존 → DIP 위반.

SOLID 원칙 적용 (DIP)

class Database {
  save(data) {
    throw new Error("구현 필요!");
  }
}

class MySQLDatabase extends Database {
  save(data) {
    console.log(`MySQL에 저장: ${data}`);
  }
}

class UserService {
  constructor(database) {
    this.database = database;
  }

  saveUser(user) {
    this.database.save(user);
  }
}

이렇게 하면:

  • UserService는 Database라는 추상화에 의존.
  • 실제 구현체(MySQLDatabase)는 나중에 주입해줌.
  • DB가 바뀌어도 UserService는 고칠 필요가 없음 → DIP 만족.

 

SRP 클래스는 하나의 책임만 가져야 함. 유지보수와 변경 이유가 한 가지여야 함.
OCP 기능 추가(확장)는 가능하지만 기존 코드는 그대로 두자.
LSP 자식 클래스가 부모 클래스를 대신해도 프로그램이 깨지지 않아야 함.
ISP 한 클래스에 불필요한 기능(인터페이스)를 강제하지 말자. 필요한 인터페이스만 분리해 제공.
DIP 고수준(비즈니스 로직)과 저수준(구현 세부)을 느슨하게 연결해 추상화로 의존하게 하자.

 

'CS지식 > 기타' 카테고리의 다른 글

HTML Method  (0) 2025.11.06