단일 책임 원칙 (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 |
고수준(비즈니스 로직)과 저수준(구현 세부)을 느슨하게 연결해 추상화로 의존하게 하자. |