728x90
반응형
📌 SOLID 원칙 – 객체지향 프로그래밍의 핵심 설계 원칙
SOLID 원칙은 유지보수성과 확장성을 높이기 위해 객체지향 프로그래밍(OOP)에서 필수적으로 고려해야 하는 5가지 설계 원칙이다.
이를 준수하면 유연하고 확장 가능하며, 결합도가 낮은 코드를 작성할 수 있다.
1. SOLID 원칙 개요
원칙 설명 해결하는 문제
Single Responsibility Principle (SRP) | 하나의 클래스는 하나의 책임(기능)만 가져야 한다. | 클래스가 너무 많은 역할을 할 경우 유지보수 어려움 |
Open/Closed Principle (OCP) | 기존 코드를 수정하지 않고 기능을 확장할 수 있어야 한다. | 새로운 기능 추가 시 기존 코드 변경으로 인한 버그 발생 |
Liskov Substitution Principle (LSP) | 자식 클래스는 부모 클래스를 대체할 수 있어야 한다. | 다형성을 유지하지 않으면 코드 예측이 어려움 |
Interface Segregation Principle (ISP) | 클라이언트가 사용하지 않는 메서드에 의존하지 않아야 한다. | 불필요한 의존성으로 인해 클래스가 복잡해짐 |
Dependency Inversion Principle (DIP) | 상위 모듈이 하위 모듈에 직접 의존하지 않고, 추상화(인터페이스)에 의존해야 한다. | 변경이 어려운 코드 구조로 인해 확장성이 낮음 |
2. SRP (단일 책임 원칙)
하나의 클래스는 하나의 책임(기능)만 가져야 한다.
- 즉, 하나의 클래스가 하나의 역할만 수행해야 유지보수와 확장이 쉬워진다.
- 여러 개의 기능을 한 클래스에서 수행하면 변경이 어려워지고, 재사용성이 떨어진다.
❌ 잘못된 예제 (SRP 위반)
class UserService {
saveToDatabase() { /* DB 저장 로직 */ }
sendEmail() { /* 이메일 발송 로직 */ }
}
- UserService 클래스가 DB 저장과 이메일 전송이라는 두 개의 책임을 가짐 → 유지보수 어려움.
✅ 올바른 예제 (SRP 적용)
class UserRepository {
saveToDatabase() { /* DB 저장 */ }
}
class EmailService {
sendEmail() { /* 이메일 발송 */ }
}
- UserRepository → 데이터 저장 역할
- EmailService → 이메일 전송 역할
- 각 클래스가 하나의 책임만 가지므로 유지보수 용이!
3. OCP (개방-폐쇄 원칙)
기존 코드를 수정하지 않고 기능을 확장할 수 있어야 한다.
- 새로운 기능을 추가할 때 기존 코드 변경 없이 확장 가능하도록 설계해야 한다.
❌ 잘못된 예제 (OCP 위반)
class PaymentService {
processPayment(type: string, amount: number) {
if (type === "credit") {
console.log(`💳 ${amount}원 신용카드 결제`);
} else if (type === "paypal") {
console.log(`💻 ${amount}원 PayPal 결제`);
}
}
}
- 새로운 결제 방식 추가 시, 기존 코드 수정이 필요 → 유지보수 어려움.
✅ 올바른 예제 (OCP 적용 – 확장 가능)
interface PaymentStrategy {
pay(amount: number): void;
}
class CreditCardPayment implements PaymentStrategy {
pay(amount: number): void {
console.log(`💳 ${amount}원 신용카드 결제`);
}
}
class PaypalPayment implements PaymentStrategy {
pay(amount: number): void {
console.log(`💻 ${amount}원 PayPal 결제`);
}
}
class Order {
constructor(private payment: PaymentStrategy) {}
checkout(amount: number) {
this.payment.pay(amount);
}
}
- 새로운 결제 방식이 필요하면 새로운 클래스(BitcoinPayment 등)만 추가하면 됨.
- 기존 코드(Order 클래스)는 변경되지 않음! (OCP 적용)
4. LSP (리스코프 치환 원칙)
자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
- 즉, 부모 클래스의 객체를 자식 클래스로 교체해도 코드가 정상적으로 작동해야 한다.
- 만약 자식 클래스가 부모 클래스의 기대 동작을 깨트린다면, LSP 위반!
❌ 잘못된 예제 (LSP 위반)
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(width: number) {
this.width = width;
}
setHeight(height: number) {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number) {
this.width = width;
this.height = width; // 정사각형이므로 height도 변경됨!
}
setHeight(height: number) {
this.width = height;
this.height = height;
}
}
- Rectangle의 setWidth()와 setHeight()는 독립적으로 동작해야 하지만, Square에서는 하나를 바꾸면 다른 값도 변경됨 → LSP 위반.
✅ 올바른 예제 (LSP 적용)
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(protected width: number, protected height: number) {}
getArea(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(private side: number) {}
getArea(): number {
return this.side * this.side;
}
}
- Rectangle과 Square를 공통 인터페이스(Shape)로 추상화하여 문제 해결.
5. ISP (인터페이스 분리 원칙)
"클라이언트가 사용하지 않는 메서드에 의존하지 않아야 한다."
- 즉, 인터페이스를 최소한으로 분리하여 필요한 기능만 구현하도록 설계.
❌ 잘못된 예제 (ISP 위반)
interface Worker {
work(): void;
eat(): void;
}
class HumanWorker implements Worker {
work(): void {
console.log("사람이 일하고 있습니다.");
}
eat(): void {
console.log("사람이 점심을 먹습니다.");
}
}
class RobotWorker implements Worker {
work(): void {
console.log("로봇이 일하고 있습니다.");
}
eat(): void {
throw new Error("로봇은 음식을 먹을 수 없습니다!"); // 🚨 ISP 위반
}
}
- RobotWorker는 eat() 메서드가 필요 없음! (ISP 위반)
✅ 올바른 예제 (ISP 적용)
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
class HumanWorker implements Workable, Eatable {
work(): void {
console.log("사람이 일하고 있습니다.");
}
eat(): void {
console.log("사람이 점심을 먹습니다.");
}
}
class RobotWorker implements Workable {
work(): void {
console.log("로봇이 일하고 있습니다.");
}
}
- 인터페이스를 분리하여 각 클래스가 필요한 기능만 구현!
6. DIP (의존성 역전 원칙)
"상위 모듈이 하위 모듈에 직접 의존하지 않고, 추상화(인터페이스)에 의존해야 한다."
- 즉, 클래스가 직접 객체를 생성하지 않고, 외부에서 주입받도록 설계해야 한다.
✅ 올바른 예제 (DIP 적용)
interface Database {
connect(): void;
}
class MySQLDatabase implements Database {
connect(): void {
console.log("MySQL 연결 완료!");
}
}
class UserService {
constructor(private db: Database) {}
getUser() {
this.db.connect();
console.log("사용자 정보 가져오기");
}
}
const userService = new UserService(new MySQLDatabase());
userService.getUser();
- UserService가 MySQLDatabase에 직접 의존하지 않고, 추상화(Database 인터페이스)에 의존함.
🚀 결론
✔ SOLID 원칙을 적용하면 유지보수성과 확장성이 뛰어난 코드를 작성할 수 있다!
✔ 각 원칙을 TypeScript로 구현하여 실전에서 활용 가능!
🔥 다음 단계 → 디자인 패턴 자세히 설명! 🚀
728x90
반응형
'CS > Algorithm' 카테고리의 다른 글
📌 의존성 주입(DI - Dependency Injection) 자세히 설명 🚀 (1) | 2025.02.03 |
---|---|
📌 디자인 패턴 (Design Patterns) – TypeScript로 상세 정리 (1) | 2025.02.02 |
📌 객체지향 프로그래밍(OOP) - TypeScript로 자세히 정리 (0) | 2025.02.02 |
📌 객체지향 프로그래밍(OOP), SOLID 원칙, 디자인 패턴, DI - TypeScript 예제 포함 상세 설명 (0) | 2025.02.02 |
[CS] Typescript를 활용한 자료구조 중 Array, LinkedList (1) | 2024.12.19 |