
🎈SOLID 원칙이란 ?
SOLID 라고 부릅니다 . SOLID 객체 지향 원칙을 적용하면 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있습니다SRP (Single Responsibility Principle) : 단일 책임 원칙
OCP (Open-Closed Principle) : 개방 폐쇄 원칙
LSP (Listov Substitution Principle) : 리스코프 치환 원칙
ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
DIP (Dependency Inversion Principle) : 의존 역전 원칙
Single Responsibility Principle ( 단일책임원칙 )
단일 책임 원칙이란 하나의 객체는 반드시 하나의 동작만의 책임을 갖는다는 원칙 입니다 . 모듈화가 강해질수록 다른 객체와의 의존/연관성이 줄어듭니다. 반대로 이야기하면 모듈화가 약해질수록 다른 객체와의 의존/연관성은 크게 늘어나며, 최악의 경우 어떠한 은닉화 정책도 존재하지 않아 모듈의 메서드에 무분별하게 접근할 수도 있게 됩니다 .
단일 책임 원칙은 특정 객체의 의존성 과중을 최대한 지양하기 위한 원칙 입니다 .
Example
SRP 적용 전
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
createUser() {
// code to create a user
}
deleteUser() {
// code to delete a user
}
sendEmail() {
// code to send an email
}
}
UserService 클래스는 단일 책임 원칙을 위반하는 User 관리 및 Email 전송을 처리합니다.
SRP 적용 후
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
createUser() {
// code to create a user
}
deleteUser() {
// code to delete a user
}
}
@Injectable()
export class EmailService {
sendEmail() {
// code to send an email
}
}
단일 책임을 처리하는 두 개의 별도 클래스가 있습니다. UserService는 User 관리를 담당하고 EmailService는 Email 전송을 담당합니다. 이를 통해 코드를 더 유지 관리하기 쉽고 이해하기 쉽게 만듭니다.
Open-Closed Principle ( 개방폐쇄원칙 )
개방 폐쇄 원칙이란 클래스나 모듈은 확장에는 열려있어야 하고, 변경에는 닫혀 있어야 한다는 원칙 입니다. 클래스 자체를 수정하지 않고도 클래스를 쉽게 확장할 수 있어야 함을 의미합니다 .
Example
OCP 적용 전
// greeter.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class GreeterService {
greeting(type: string) {
if (type === 'formal') {
return 'Good day to you.';
} else if (type === 'casual') {
return 'Hey!';
}
}
}
위의 예에서 새로운 유형의 인사말을 추가하려면 개방형 폐쇄 원칙을 위반하는 GreeterService 클래스의 인사말 메서드를 수정해야 합니다.
OCP 적용 후
// greeting.interface.ts
export interface Greeting {
greet(): string;
}
// formalGreeting.ts
import { Greeting } from './greeting.interface';
export class FormalGreeting implements Greeting {
greet() {
return 'Good day to you.';
}
}
// casualGreeting.ts
import { Greeting } from './greeting.interface';
export class CasualGreeting implements Greeting {
greet() {
return 'Hey!';
}
}
// greeter.service.ts
import { Injectable } from '@nestjs/common';
import { Greeting } from './greeting.interface';
@Injectable()
export class GreeterService {
greeting(greeting: Greeting) {
return greeting.greet();
}
}
각 클래스와 인터페이스가 자체 파일에 정의되어 코드가 더욱 체계화되고 관리가 쉬워졌습니다. 새로운 Greeting을 추가하려면 기존 클래스를 수정하지 않고 greeting Interface를 구현하기만 하면 됩니다. 이는 개방-폐쇄 원칙을 지키게 됩니다 .
Liskov Substitution Principle ( 리스코프 치환 원칙 )
리스코프 치환 원칙(Liskov Substitution Principle, LSP)은 객체 지향 프로그래밍에서 프로그램 내에서 슈퍼클래스의 객체를 서브클래스의 객체로 대체할 수 있어야 하며, 이러한 대체가 프로그램의 정확성에 영향을 주지 않아야 한다는 개념입니다. 이 원칙은 서브클래스가 부모 클래스를 대신할 수 있어야 하며, 프로그램의 기능성을 깨뜨리지 않아야 함을 보장하는 것에 관한 것입니다.
Example
LSP 적용 전
class Bird {
fly(speed: number): string {
return `Flying at ${speed} km/h`;
}
}
class Eagle extends Bird {
dive(): void {
// ...
}
fly(speed: number): string {
return `Soaring through the sky at ${speed} km/h`;
}
}
// LSP Violation:
class Penguin extends Bird {
fly(): never {
throw new Error("Sorry, I can't fly");
}
}
Bird 클래스를 상속받은 Eagle 클래스는 메소드 인자의 수를 동일하게 유지하면서 fly 메소드를 재정의함으로써 리스코프 치환 원칙을 준수합니다. 그러나 Penguin 클래스는 날 수 없기 때문에 fly 메소드가 오류를 발생시키고, 이는 Penguin 객체를 Bird 객체로 대체할 수 없게 만들어 프로그램의 정확성을 변경합니다. 이는 Penguin 클래스가 리스코프 치환 원칙을 위반한다는 것을 의미합니다.
LSP 적용 후
interface FlyingBird {
fly(speed: number): string;
}
interface NonFlyingBird {
waddle(speed: number): string;
}
class Eagle implements FlyingBird {
fly(speed: number): string {
return `Soaring through the sky at ${speed} km/h`;
}
}
class Penguin implements NonFlyingBird {
waddle(speed: number): string {
return `Waddling at ${speed} km/h`;
}
}
이제 우리는 Eagle과 Penguin에 대해 그들의 능력에 따라 다른 인터페이스를 구현하는 두 개의 별도 클래스를 가지고 있습니다. 이 방식으로, 우리는 Penguin이 날 수 있다고 가정하는 것이 아니기 때문에 리스코프 치환 원칙을 위반하지 않습니다. 대신, 각 종류의 새가 할 수 있는 것을 명확하게 정의하고 그들의 능력에 따라 다르게 처리합니다.
이 접근법은 또한 우리의 코드를 더 유연하고 유지 관리하기 쉽게 만듭니다. 앞으로 새로운 종류의 새를 추가해야 할 경우, 그 능력에 기반하여 적절한 인터페이스를 구현하는 새로운 클래스를 간단히 생성할 수 있습니다.
Interface Segregation Principle ( 인터페이스 분리 원칙)
SRP (단일 책임 원칙)가 클래스에 대해서 단일 책임을 갖도록 하는 원칙이었다면, ISP (인터페이스 분리 원칙) 인터페이스를 최대한 변경을 하지 않도록 구체적으로 구분하는 원칙 입니다.
Example
ISP 적용 전
interface FullFeatureUser {
viewAd(): void;
skipAd(): void;
startParty(): void;
}
class User {
viewAd(): void {
// ...
}
}
class FreeUser extends User implements FullFeatureUser {
skipAd(): void {
throw new Error("Sorry, I can't skip ads");
}
startParty(): void {
throw new Error("Sorry, I can't start parties");
}
}
class PremiumUser extends User implements FullFeatureUser {
skipAd(): void {
// ...
}
startParty(): void {
// ...
}
}
위의 예시에서, FreeUser 클래스는 사용하지 않는 skipAd 및 startParty 메소드를 구현하도록 강제되고 있습니다. 이는 인터페이스 분리 원칙을 위반하는 것입니다. ISP를 준수하기 위해, 더 구체적인 인터페이스를 생성할 수 있습니다
ISP 적용 후
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
createUser() {
// code to create a user
}
deleteUser() {
// code to delete a user
}
}
@Injectable()
export class EmailService {
sendEmail() {
// code to send an email
}
}
단일 책임을 처리하는 두 개의 별도 클래스가 있습니다. UserService는 User 관리를 담당하고 EmailService는 Email 전송을 담당합니다. 이를 통해 코드를 더 유지 관리하기 쉽고 이해하기 쉽게 만듭니다.
interface User {
viewAd(): void;
}
interface PremiumFeatureUser {
skipAd(): void;
startParty(): void;
}
class FreeUser implements User {
viewAd(): void {
// ...
}
}
class PremiumUser implements User, PremiumFeatureUser {
viewAd(): void {
// ...
}
skipAd(): void {
// ...
}
startParty(): void {
// ...
}
}
이러한 변경으로 인해 각 클래스는 인터페이스 분리 원칙을 준수하면서 사용하는 메서드만 구현합니다. 이 접근 방식은 버그 위험을 줄이고 코드를 더욱 유연하고 유지 관리하기 쉽게 만듭니다.
Dependency Inversion Principle ( 의존 역전 원)
이 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 된다고 말합니다. 대신, 둘 다 추상화에 의존해야 합니다. 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항은 추상화에 의존해야 합니다.
의존성 역전 원칙은 소프트웨어 모듈 간의 결합을 줄이는 데 도움을 주는 디자인 원칙입니다. 이 원칙은 프로그램의 다양한 모듈 간의 결합을 제어하는 데 중요한 역할을 합니다.
Example
DIP 적용 전
class MySQLDatabase {
save(data: string): void {
// Save data to MySQL database
}
}
class UserService {
private database: MySQLDatabase;
constructor(database: MySQLDatabase) {
this.database = database;
}
saveUser(user: string): void {
this.database.save(user);
}
}
위의 예에서 UserService 클래스는 MySQLDatabase 클래스와 긴밀하게 결합되어 있습니다. 즉, 데이터베이스 시스템을 변경하려면(예: MySQL에서 MongoDB로 전환) UserService 클래스도 변경해야 합니다. 이는 종속성 역전 원칙을 위반하는 것입니다.
DIP 적용 후
interface Database {
save(data: string): void;
}
class MySQLDatabase implements Database {
save(data: string): void {
// Save data to MySQL database
}
}
class UserService {
private database: Database;
constructor(database: Database) {
this.database = database;
}
saveUser(user: string): void {
this.database.save(user);
}
}
이제 UserService 클래스는 구체적인 MySQLDatabase 클래스가 아닌 데이터베이스 인터페이스에 의존합니다. 이는 데이터베이스 인터페이스를 구현하는 새 클래스를 생성하여 다른 데이터베이스 시스템으로 쉽게 전환할 수 있음을 의미합니다. 이 접근 방식을 사용하면 코드가 더욱 유연해지고 유지 관리가 쉬워집니다.
🫵결론

사실... 많은 자료들을 보면서 공부했지만, 명확하게 내 프로젝트에 적용할 수 있을지 모르겠다... 몇몇 원칙들은 적용되어 있는 부분들도 있고, 아닌 것들도 있는데, 내가 의도했다기 보다는 NestJS 좋은 설계의 좋은 예시들을 참고하다보니 자연스럽게 이 원칙들이 적용이 되었던 것 같다. 이젠 의식적으로 이 원칙들을 지키려고 노력하면서 코딩을 해야겠다 !!
![[til] 프로그래머스 신규아이디 추천](https://cdn.hashnode.com/res/hashnode/image/upload/v1745249371004/97aa7a0b-1b1b-4f81-a5ef-790b9b682f08.png)
![[til] 알고리즘 백준 리그 오브 레전설](https://cdn.hashnode.com/res/hashnode/image/upload/v1745007840153/c6cf7c45-0d8f-4bee-ae9a-55cc454f3c92.png)
![[til] 알고리즘 백준 진우의 달 여행 (Small)](https://cdn.hashnode.com/res/hashnode/image/upload/v1744914681507/e80e8747-d4ff-4fd4-b595-33024a238ee1.png)
![[til] 알고리즘 JadenCase 문자열 만들기](https://cdn.hashnode.com/res/hashnode/image/upload/v1744823424388/57f5c5c1-7e85-4071-88e8-1ec09ed64828.png)
![[til] 알고리즘 백준 포도주 시식](https://cdn.hashnode.com/res/hashnode/image/upload/v1744724798661/286b75a2-50e0-481e-8e3f-2a6cb500d678.png)