SOLID 원칙: 소프트웨어 아키텍처의 기초
I 견고한 원칙 지향적인 코드를 작성하기 위한 다섯 가지 기본 지침이 있습니다. 품질이 좋은 물건에. Robert C. Martin(Bob 삼촌)이 소개한 이러한 원칙은 소프트웨어 유지 관리 가능, 확장 가능 e 테스트 가능. SOLID는 Single Responsibility, Open/Closed, Liskov Substitution, 인터페이스 분리 및 종속성 반전.
🎯 5가지 견고한 원칙
- S - 단일 책임 원칙(SRP)
- O - 개방형/폐쇄형 원리(OCP)
- L - 리스코프 대체 원리(LSP)
- I - 인터페이스 분리 원리(ISP)
- D - 의존성 역전 원리(DIP)
1. 단일 책임 원칙(SRP)
"클래스를 변경해야 하는 이유는 단 하나여야 합니다."
각 클래스에는 하나의 클래스가 있어야 합니다. 단일 책임 잘 정의되어 있습니다. 이것은 그것을 만든다 더 쉽게 이해하고, 테스트하고, 수정할 수 있는 코드입니다.
❌ SRP 위반:
class User {{ '{' }}
constructor(
public name: string,
public email: string
) {{ '{' }}{{ '}' }}
// Responsabilità 1: Validazione
validateEmail(): boolean {{ '{' }}
return /\S+@\S+\.\S+/.test(this.email);
{{ '}' }}
// Responsabilità 2: Persistenza
save(): void {{ '{' }}
// Salva nel database
console.log("Salvato nel DB");
{{ '}' }}
// Responsabilità 3: Notifiche
sendWelcomeEmail(): void {{ '{' }}
console.log(`Email a ${{ '{' }}this.email{{ '}' }}`);
{{ '}' }}
{{ '}' }}
✅ SRP 존중:
// Una responsabilità per classe
class User {{ '{' }}
constructor(
public name: string,
public email: string
) {{ '{' }}{{ '}' }}
{{ '}' }}
class UserValidator {{ '{' }}
validateEmail(email: string): boolean {{ '{' }}
return /\S+@\S+\.\S+/.test(email);
{{ '}' }}
{{ '}' }}
class UserRepository {{ '{' }}
save(user: User): void {{ '{' }}
console.log("Salvato nel DB");
{{ '}' }}
{{ '}' }}
class EmailService {{ '{' }}
sendWelcome(user: User): void {{ '{' }}
console.log(`Email a ${{ '{' }}user.email{{ '}' }}`);
{{ '}' }}
{{ '}' }}
2. 개방/폐쇄 원칙(OCP)
“수업은 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 합니다.”
새로운 기능을 추가할 수 있어야 합니다. 연장 기존 코드, 바꾸지 않고. 확장을 허용하려면 추상화(인터페이스, 추상 클래스)를 사용하세요.
class DiscountCalculator {{ '{' }}
calculate(type: string, amount: number): number {{ '{' }}
// Ogni nuovo tipo richiede modifica di questa classe!
if (type === 'student') {{ '{' }}
return amount * 0.9; // 10% sconto
{{ '}' }} else if (type === 'senior') {{ '{' }}
return amount * 0.85; // 15% sconto
{{ '}' }} else if (type === 'vip') {{ '{' }}
return amount * 0.8; // 20% sconto
{{ '}' }}
return amount;
{{ '}' }}
{{ '}' }}
// Interfaccia per estensione
interface DiscountStrategy {{ '{' }}
apply(amount: number): number;
{{ '}' }}
class StudentDiscount implements DiscountStrategy {{ '{' }}
apply(amount: number): number {{ '{' }}
return amount * 0.9;
{{ '}' }}
{{ '}' }}
class SeniorDiscount implements DiscountStrategy {{ '{' }}
apply(amount: number): number {{ '{' }}
return amount * 0.85;
{{ '}' }}
{{ '}' }}
class VIPDiscount implements DiscountStrategy {{ '{' }}
apply(amount: number): number {{ '{' }}
return amount * 0.8;
{{ '}' }}
{{ '}' }}
// Chiusa alla modifica, aperta all'estensione
class DiscountCalculatorOCP {{ '{' }}
calculate(strategy: DiscountStrategy, amount: number): number {{ '{' }}
return strategy.apply(amount);
{{ '}' }}
{{ '}' }}
// Aggiungere nuovo sconto = creare nuova classe, non modificare esistente
const calc = new DiscountCalculatorOCP();
console.log(calc.calculate(new StudentDiscount(), 100)); // 90
console.log(calc.calculate(new VIPDiscount(), 100)); // 80
3. 리스코프 대체 원리(LSP)
"하위 클래스의 개체는 기본 클래스의 개체를 대체할 수 있어야 합니다. 프로그램의 올바른 동작을 변경하지 않고"
서브클래스는 다음을 충족해야 합니다. 연장하다 기본 클래스의 동작, 기대를 깨는 방식으로 변경하지 마십시오.
class Rectangle {{ '{' }}
constructor(protected width: number, protected height: number) {{ '{' }}{{ '}' }}
setWidth(width: number): void {{ '{' }}
this.width = width;
{{ '}' }}
setHeight(height: number): void {{ '{' }}
this.height = height;
{{ '}' }}
getArea(): number {{ '{' }}
return this.width * this.height;
{{ '}' }}
{{ '}' }}
// Viola LSP: Square non può sostituire Rectangle
class Square extends Rectangle {{ '{' }}
setWidth(width: number): void {{ '{' }}
this.width = width;
this.height = width; // Effetto collaterale inaspettato!
{{ '}' }}
setHeight(height: number): void {{ '{' }}
this.width = height;
this.height = height; // Effetto collaterale inaspettato!
{{ '}' }}
{{ '}' }}
function increaseRectangleWidth(rect: Rectangle): void {{ '{' }}
rect.setWidth(rect.getArea() + 10);
// Aspettativa: solo la larghezza cambia
// Realtà con Square: anche l'altezza cambia!
{{ '}' }}
// Usa composizione invece di ereditarietà problematica
interface Shape {{ '{' }}
getArea(): number;
{{ '}' }}
class RectangleLSP implements Shape {{ '{' }}
constructor(private width: number, private height: number) {{ '{' }}{{ '}' }}
setWidth(width: number): void {{ '{' }}
this.width = width;
{{ '}' }}
setHeight(height: number): void {{ '{' }}
this.height = height;
{{ '}' }}
getArea(): number {{ '{' }}
return this.width * this.height;
{{ '}' }}
{{ '}' }}
class SquareLSP implements Shape {{ '{' }}
constructor(private side: number) {{ '{' }}{{ '}' }}
setSide(side: number): void {{ '{' }}
this.side = side;
{{ '}' }}
getArea(): number {{ '{' }}
return this.side * this.side;
{{ '}' }}
{{ '}' }}
// Funziona con qualsiasi Shape senza sorprese
function printArea(shape: Shape): void {{ '{' }}
console.log(`Area: ${{ '{' }}shape.getArea(){{ '}' }}`);
{{ '}' }}
4. 인터페이스 분리 원칙(ISP)
"클라이언트가 사용하지 않는 인터페이스에 의존하도록 강요해서는 안 됩니다."
가지고 있는 것이 더 낫다 작고 구체적인 인터페이스 얼마나 크고 일반적인 인터페이스입니까? 클래스가 불필요한 메소드를 구현하도록 강제하는 "팻 인터페이스"를 피하십시오.
// Interfaccia troppo grande
interface Worker {{ '{' }}
work(): void;
eat(): void;
sleep(): void;
{{ '}' }}
class HumanWorker implements Worker {{ '{' }}
work(): void {{ '{' }} console.log("Lavoro"); {{ '}' }}
eat(): void {{ '{' }} console.log("Mangio"); {{ '}' }}
sleep(): void {{ '{' }} console.log("Dormo"); {{ '}' }}
{{ '}' }}
// Robot non mangia né dorme, ma è forzato a implementarli!
class RobotWorker implements Worker {{ '{' }}
work(): void {{ '{' }} console.log("Lavoro"); {{ '}' }}
eat(): void {{ '{' }} throw new Error("Robot non mangia"); {{ '}' }}
sleep(): void {{ '{' }} throw new Error("Robot non dorme"); {{ '}' }}
{{ '}' }}
// Interfacce segregate per responsabilità
interface Workable {{ '{' }}
work(): void;
{{ '}' }}
interface Eatable {{ '{' }}
eat(): void;
{{ '}' }}
interface Sleepable {{ '{' }}
sleep(): void;
{{ '}' }}
// Umano implementa tutte le interfacce
class HumanWorkerISP implements Workable, Eatable, Sleepable {{ '{' }}
work(): void {{ '{' }} console.log("Lavoro"); {{ '}' }}
eat(): void {{ '{' }} console.log("Mangio"); {{ '}' }}
sleep(): void {{ '{' }} console.log("Dormo"); {{ '}' }}
{{ '}' }}
// Robot implementa solo ciò che serve
class RobotWorkerISP implements Workable {{ '{' }}
work(): void {{ '{' }} console.log("Lavoro 24/7"); {{ '}' }}
{{ '}' }}
// Funzioni specifiche per interfacce specifiche
function makeWork(worker: Workable): void {{ '{' }}
worker.work();
{{ '}' }}
function feedWorker(worker: Eatable): void {{ '{' }}
worker.eat();
{{ '}' }}
const human = new HumanWorkerISP();
const robot = new RobotWorkerISP();
makeWork(human); // OK
makeWork(robot); // OK
feedWorker(human); // OK
// feedWorker(robot); // ❌ Errore compile-time: robot non è Eatable
5. 의존성 역전 원리(DIP)
“구체적인 구현이 아닌 추상화에 의존합니다”
상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 됩니다. 둘 다 반드시 의존하다 추상화 (인터페이스). 추상화는 의존할 필요가 없습니다. 하지만 세부 사항은 추상화에 따라 달라져야 합니다.
// Dipendenza diretta da implementazione concreta
class MySQLDatabase {{ '{' }}
save(data: string): void {{ '{' }}
console.log(`Salvato in MySQL: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// UserService dipende dalla classe concreta MySQLDatabase
class UserService {{ '{' }}
private db = new MySQLDatabase(); // Accoppiamento forte!
createUser(name: string): void {{ '{' }}
this.db.save(name);
{{ '}' }}
{{ '}' }}
// Cambiare database richiede modifica di UserService
// Astrazione (interfaccia)
interface Database {{ '{' }}
save(data: string): void;
{{ '}' }}
// Implementazioni concrete dipendono dall'astrazione
class MySQLDatabaseDIP implements Database {{ '{' }}
save(data: string): void {{ '{' }}
console.log(`Salvato in MySQL: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class MongoDBDatabase implements Database {{ '{' }}
save(data: string): void {{ '{' }}
console.log(`Salvato in MongoDB: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class PostgreSQLDatabase implements Database {{ '{' }}
save(data: string): void {{ '{' }}
console.log(`Salvato in PostgreSQL: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// UserService dipende dall'astrazione, non dall'implementazione
class UserServiceDIP {{ '{' }}
// Dependency Injection: il database viene iniettato
constructor(private db: Database) {{ '{' }}{{ '}' }}
createUser(name: string): void {{ '{' }}
this.db.save(name);
{{ '}' }}
{{ '}' }}
// Facile cambiare implementazione senza modificare UserService
const mysqlService = new UserServiceDIP(new MySQLDatabaseDIP());
const mongoService = new UserServiceDIP(new MongoDBDatabase());
const pgService = new UserServiceDIP(new PostgreSQLDatabase());
mysqlService.createUser("Alice"); // MySQL
mongoService.createUser("Bob"); // MongoDB
pgService.createUser("Charlie"); // PostgreSQL
실제 SOLID: 전체 예
알림 시스템에서 모든 SOLID 원칙을 함께 적용하는 방법을 살펴보겠습니다.
// ISP: Interfacce segregate
interface MessageSender {{ '{' }}
send(message: string, recipient: string): void;
{{ '}' }}
// DIP: Implementazioni concrete dipendono da astrazione
class EmailSender implements MessageSender {{ '{' }}
send(message: string, recipient: string): void {{ '{' }}
console.log(`📧 Email a ${{ '{' }}recipient{{ '}' }}: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class SMSSender implements MessageSender {{ '{' }}
send(message: string, recipient: string): void {{ '{' }}
console.log(`📱 SMS a ${{ '{' }}recipient{{ '}' }}: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class PushNotificationSender implements MessageSender {{ '{' }}
send(message: string, recipient: string): void {{ '{' }}
console.log(`🔔 Push a ${{ '{' }}recipient{{ '}' }}: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// SRP: Una sola responsabilità - gestione notifiche
class NotificationService {{ '{' }}
constructor(private sender: MessageSender) {{ '{' }}{{ '}' }} // DIP
notify(message: string, recipient: string): void {{ '{' }}
this.sender.send(message, recipient);
{{ '}' }}
{{ '}' }}
// OCP: Aperto all'estensione (nuovi logger) chiuso alla modifica
interface Logger {{ '{' }}
log(message: string): void;
{{ '}' }}
class ConsoleLogger implements Logger {{ '{' }}
log(message: string): void {{ '{' }}
console.log(`[LOG] ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// SRP: Responsabilità separata per logging
class NotificationServiceWithLogging {{ '{' }}
constructor(
private sender: MessageSender,
private logger: Logger
) {{ '{' }}{{ '}' }}
notify(message: string, recipient: string): void {{ '{' }}
this.logger.log(`Invio notifica a ${{ '{' }}recipient{{ '}' }}`);
this.sender.send(message, recipient);
{{ '}' }}
{{ '}' }}
// Utilizzo
const emailService = new NotificationServiceWithLogging(
new EmailSender(),
new ConsoleLogger()
);
const smsService = new NotificationServiceWithLogging(
new SMSSender(),
new ConsoleLogger()
);
emailService.notify("Benvenuto!", "user@example.com");
smsService.notify("Codice: 1234", "+39123456789");
SOLID 원칙의 이점
✅ 혜택
- 유지 관리성: 이해하고 수정하기 쉬운 코드
- 확장성: 기존을 깨지 않고 기능 추가
- 테스트 가능성: 소규모 집중 수업이 테스트하기 더 쉽습니다.
- 재사용: 재사용 가능한 모듈형 구성요소
- 유연성: 모든 것을 다시 작성하지 않고 구현 변경
결론
SOLID 원칙은 엄격한 규칙이 아니라 고품질 코드 작성을 위한 지침입니다. 이를 적용하려면 연습과 판단이 필요합니다. 때로는 약간의 위반이 허용되는 경우도 있습니다. 단순함. 목표는 코드다 유지 관리 가능, 테스트 가능 e 확장 가능, 모든 원칙을 독단적으로 따르지 마십시오.
🎯 핵심 포인트
- SRP: 하나의 클래스 = 하나의 책임
- OCP: 확장에는 개방적이고 수정에는 폐쇄됨
- LSP: 놀라움 없이 교체 가능한 하위 클래스
- ISP: 작고 구체적인 인터페이스
- 담그다: 구체적인 것이 아닌 추상화에 의존합니다.







