본문 바로가기
개발/script

[script] 디자인패턴 기본기 정리 - SOLID 법칙

by 밤즈라라2 2025. 1. 10.
728x90
반응형

 

 

https://youtu.be/4O6k9GN8FPo?si=ji-jjOpZPv5kbuuc

 

 

>> 이 강의를 토대로 정리했습니다. 예제는 클로드를 통해 정리했습니다. 강의 정말 좋습니다 추천~

 

 

 

SOLID 원칙

 

1. Single Responsibility principle (SRP, 단일 책임 원칙)

2. Open/ closed principle (OCP, 개발 / 폐쇄 원칙)

3. Liskov substitution principle (LSP, 리스코프 치환 원칙)

4. Interface segregation principle (ISP, 인터페이스 분리법칙)

5. Dependency inversion principle (DIP, 의존성 역전 법칙)

 

 

 

1. Single Responsibility principle (SRP, 단일 책임 원칙)

* 각 클래스는 하나의 책임만 가져간다.
* 직업을 가졌다고 생각하면 편하다.
* 클래스안에 여러 메소드는 생성 가능하다. 단, 이를 통해 수행하는 대표적인 업무는 하나어야만 한다.
*각 클래스는 하나의 임무와 목적을 가진다.
*각각 책임의 변경사항이 있는 때만 해당하는 곳을 변경하면 된다.
* 책임별로 분리가 되어 있어, 재 사용하기도 좋다.

 

 

 


 

 

 

2. Open/ closed principle (OCP, 개발 / 폐쇄 원칙)


*각 클래스는 확장에는 열려있어야하고, 변경에는 닫혀있어야한다.


 

 

나는 저 닫혀있어야 한다는 말이 캡슐화를 시켜서 닫아놔야한다는 뜻인가? 했는데 다르다고 합니다. 

 

캡슐화와는 조금 다른데:

  • 캡슐화는 "비밀 레시피를 감추는 것"
  • 개방/폐쇄는 "기본 레시피는 유지하면서 새로운 메뉴를 추가할 수 있게 하는 것" 이라고 보시면 됩니다 😊
1. 변경에 닫혀있다는 뜻은 
 
 
/
        // 기본 피자 만드는 과정
const createPizza = (type) => {
    const makeDough = () => console.log("도우를 만듭니다");
    const addSauce = () => console.log("소스를 넣습니다");
    const bake = () => console.log("굽습니다");

    // 이 기본 과정은 절대 변경되지 않음
    return {
        prepare: () => {
            makeDough();
            addSauce();
            bake();
        }
    };
};
  • 피자를 만드는 기본 과정(도우 만들기 → 소스 넣기 → 굽기)은 절대 변경되지 않아요
  • 마치 맥도날드의 햄버거 만드는 기본 과정이 항상 동일한 것처럼요!

 

2. 확장해야 한다는 뜻은

        // 페퍼로니 피자
const createPepperoniPizza = () => {
    const pizza = createPizza();
    return {
        ...pizza,
        addToppings: () => console.log("페퍼로니를 올립니다")
    };
};

// 치즈 피자
const createCheesePizza = () => {
    const pizza = createPizza();
    return {
        ...pizza,
        addToppings: () => console.log("치즈를 듬뿍 올립니다")
    };
};

// 새로운 피자도 쉽게 추가할 수 있음!
const createHawaiianPizza = () => {
    const pizza = createPizza();
    return {
        ...pizza,
        addToppings: () => console.log("파인애플과 햄을 올립니다")
    };
};
  • 기본 과정은 그대로 두고, 새로운 종류의 피자를 자유롭게 추가할 수 있어요
  • 마치 맥도날드가 기본 햄버거 만드는 과정은 그대로 두고, 새로운 햄버거를 메뉴에 추가하는 것처럼요!

즉, 개방/폐쇄 원칙은:

  • 레시피의 기본은 바꾸지 않고 (변경에 닫힘)
  • 새로운 메뉴는 얼마든지 추가할 수 있다 (확장에 열림) 는 의미입니다!

캡슐화와는 조금 다른데:

  • 캡슐화는 "비밀 레시피를 감추는 것"
  • 개방/폐쇄는 "기본 레시피는 유지하면서 새로운 메뉴를 추가할 수 있게 하는 것" 이라고 보시면 됩니다 😊

 

 

<OCP 예제>


// 나쁜 예제 - OCP 위반
const badProcessPayment = (paymentType, amount) => {
    if (paymentType === 'credit') {
        console.log(`신용카드로 ${amount}원 결제 처리중...`);
        return true;
    }
    else if (paymentType === 'kakao') {
        console.log(`카카오페이로 ${amount}원 결제 처리중...`);
        return true;
    }
    return false;
};

// 좋은 예제 - OCP 준수
// 결제 처리기 생성 함수
const createPaymentProcessor = (type) => {
    return {
        process: (amount) => {
            console.log(`${type}${amount}원 결제 처리중...`);
            return true;
        }
    };
};

// 각 결제 수단별 프로세서 생성
const creditCardProcessor = createPaymentProcessor('신용카드');
const kakaoPayProcessor = createPaymentProcessor('카카오페이');
const naverPayProcessor = createPaymentProcessor('네이버페이');

// 결제 서비스 생성 함수
const createPaymentService = (processor) => ({
    processPayment: (amount) => processor.process(amount)
});

// 더 진보된 방식 - 커링과 합성 사용
const processPayment = (processor) => (amount) => processor.process(amount);

// 사용 예시
console.log("===== 잘못된 방식 =====");
badProcessPayment('credit', 50000);
badProcessPayment('kakao', 30000);

console.log("\n===== 좋은 방식 =====");
// 기본적인 방식
const creditCardService = createPaymentService(creditCardProcessor);
creditCardService.processPayment(50000);

const kakaoPayService = createPaymentService(kakaoPayProcessor);
kakaoPayService.processPayment(30000);

const naverPayService = createPaymentService(naverPayProcessor);
naverPayService.processPayment(20000);

// 커링을 사용한 더 함수형다운 방식
console.log("\n===== 함수형 방식 =====");
const processCreditCard = processPayment(creditCardProcessor);
const processKakaoPay = processPayment(kakaoPayProcessor);
const processNaverPay = processPayment(naverPayProcessor);

processCreditCard(50000);
processKakaoPay(30000);
processNaverPay(20000);

// 새로운 결제 수단 추가가 매우 간단
const tossPayProcessor = createPaymentProcessor('토스페이');
const processTossPay = processPayment(tossPayProcessor);
processTossPay(40000);

 

 

 

 


 

 

3. Liskov substitution principle (LSP, 리스코프 치환 원칙)


*부모 클래스가 사용되는 곳에 자식 클래스도 문제 없이 사용될 수 있어야 한다.

 

<LSP 예제>

 


        // 인터페이스 정의 (날 수 있는 능력)
const Flyable = {
    fly: () => {
        throw new Error('fly 메서드를 구현해야 합니다');
    }
};

// 기본 조류 행동 정의
const createBird = () => ({
    eat: () => {
        console.log('새가 먹이를 먹습니다');
    }
});

// 날 수 있는 참새 만들기
const createSparrow = () => {
    const bird = createBird();  // 기본 조류 행동 상속
   
    // Flyable 인터페이스 구현
    const flyable = {
        fly: () => {
            console.log('참새가 날고 있습니다');
        }
    };
   
    // 참새는 Bird이면서 Flyable
    return {
        ...bird,
        ...flyable
    };
};

// 날지 못하는 펭귄 만들기
const createPenguin = () => {
    const bird = createBird();  // 기본 조류 행동만 상속
   
    // 펭귄만의 특별한 행동 추가
    const swim = () => {
        console.log('펭귄이 수영합니다');
    };
   
    return {
        ...bird,
        swim
    };
};

// 사용 예시
const sparrow = createSparrow();
const penguin = createPenguin();

// 모든 새는 먹이를 먹을 수 있음 (LSP 만족)
function feedBird(bird) {
    bird.eat();  // 어떤 새가 와도 문제 없음
}

// 날 수 있는 새들만을 위한 함수
function makeBirdFly(flyableBird) {
    if ('fly' in flyableBird) {
        flyableBird.fly();
    } else {
        console.log('이 새는 날 수 없습니다');
    }
}

// 테스트
console.log('=== 참새 테스트 ===');
feedBird(sparrow);  // "새가 먹이를 먹습니다"
makeBirdFly(sparrow);  // "참새가 날고 있습니다"

console.log('\n=== 펭귄 테스트 ===');
feedBird(penguin);  // "새가 먹이를 먹습니다"
makeBirdFly(penguin);  // "이 새는 날 수 없습니다"
penguin.swim();  // "펭귄이 수영합니다"




이 예제를 통해 LSP를 설명하면:

기본 원칙:

    const createBird = () => ({
    eat: () => {
        console.log('새가 먹이를 먹습니다');
    }
});



모든 새는 eat() 능력을 가집니다
이는 모든 새가 지켜야 할 기본 계약입니다


인터페이스 구현:

    const Flyable = {
    fly: () => {
        throw new Error('fly 메서드를 구현해야 합니다');
    }
};


날 수 있는 새들이 구현해야 할 인터페이스입니다


LSP 적용:

    // 어떤 새가 와도 문제 없이 동작합니다
function feedBird(bird) {
    bird.eat();
}


sparrow든 penguin이든 feedBird 함수에서 문제 없이 동작합니다
이것이 바로 LSP의 핵심입니다!


안전한 기능 확장:


    const createPenguin = () => {
    const bird = createBird();
    const swim = () => {
        console.log('펭귄이 수영합니다');
    };
    return { ...bird, swim };
};



펭귄은 날지는 못하지만 수영할 수 있는 기능이 추가되었습니다
기본 새의 기능(eat)은 그대로 유지됩니다


LSP의 장점:


*코드의 유연성이 증가합니다
* 새로운 종류의 새를 추가해도 기존 코드를 수정할 필요가 없습니다
* 각 새들의 특성에 맞는 기능을 자유롭게 구현할 수 있습니다

이렇게 함수형으로 구현하면:

* 상속 대신 합성을 사용합니다
* 더 유연한 구조를 만들 수 있습니다
* 코드의 재사용성이 높아집니다

LSP를 지킴으로써 얻는 이점은:

* 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다
* 코드의 안정성이 높아집니다
* 유지보수가 쉬워집니다

 


 

 

4. Interface segregation principle (ISP, 인터페이스 분리법칙)


*클래스는 자신이 사용하지 않을 메소드를 구현하도록 강요 받지 말아야한다.
*인터페이스는 책임에 따라 나눠야한다.

 

 


 

 

5. Dependency inversion principle (DIP, 의존성 역전 법칙)


*고수준 모듈이 저수준 모듈에 의존하면 안된다.


 

이건 예시가 애매해서 그런가 와닿지가 않는다...

 

<DIP 예시>

// 전자기기 인터페이스 (추상화)
const createSwitchableDevice = () => ({
    turnOn: () => {
        throw new Error('turnOn 메서드를 구현해야 합니다');
    },
    turnOff: () => {
        throw new Error('turnOff 메서드를 구현해야 합니다');
    }
});

// 저수준 모듈: 선풍기
const createFan = () => {
    // 인터페이스 구현
    return {
        turnOn: () => {
            console.log('선풍기가 돌아갑니다');
        },
        turnOff: () => {
            console.log('선풍기가 멈춥니다');
        }
    };
};

// 저수준 모듈: LED 전구
const createLightBulb = () => {
    return {
        turnOn: () => {
            console.log('전구가 켜집니다');
        },
        turnOff: () => {
            console.log('전구가 꺼집니다');
        }
    };
};

// 고수준 모듈: 스위치
const createSwitch = (device) => {
    // device가 필요한 메서드를 가지고 있는지 검증
    if (!device.turnOn || !device.turnOff) {
        throw new Error('유효한 디바이스가 아닙니다');
    }

    return {
        turnOn: () => device.turnOn(),
        turnOff: () => device.turnOff()
    };
};

// 사용 예시
const fan = createFan();
const lightBulb = createLightBulb();

// 스위치에 각각의 기기를 연결
const fanSwitch = createSwitch(fan);
const lightSwitch = createSwitch(lightBulb);

// 테스트
console.log('=== 선풍기 테스트 ===');
fanSwitch.turnOn();  // "선풍기가 돌아갑니다"
fanSwitch.turnOff(); // "선풍기가 멈춥니다"

console.log('\n=== 전구 테스트 ===');
lightSwitch.turnOn();  // "전구가 켜집니다"
lightSwitch.turnOff(); // "전구가 꺼집니다"

// 새로운 기기 추가도 쉽게 가능
const createAircon = () => ({
    turnOn: () => console.log('에어컨이 켜집니다'),
    turnOff: () => console.log('에어컨이 꺼집니다')
});

const aircon = createAircon();
const airconSwitch = createSwitch(aircon);

console.log('\n=== 에어컨 테스트 ===');
airconSwitch.turnOn();  // "에어컨이 켜집니다"
airconSwitch.turnOff(); // "에어컨이 꺼집니다"





추상화 정의:

const createSwitchableDevice = () => ({
    turnOn: () => { throw new Error('turnOn 메서드를 구현해야 합니다') },
    turnOff: () => { throw new Error('turnOff 메서드를 구현해야 합니다') }
});



이것이 우리의 추상화(인터페이스)입니다
모든 전자기기가 따라야 할 계약을 정의합니다


저수준 모듈 (구체적인 구현):

const createFan = () => ({
    turnOn: () => { console.log('선풍기가 돌아갑니다') },
    turnOff: () => { console.log('선풍기가 멈춥니다') }
});



추상화를 실제로 구현하는 부분입니다
구체적인 기기들(선풍기, 전구 등)을 만듭니다


고수준 모듈:

const createSwitch = (device) => ({
    turnOn: () => device.turnOn(),
    turnOff: () => device.turnOff()
});



추상화에 의존하는 부분입니다
구체적인 구현(선풍기인지 전구인지)을 알 필요가 없습니다


의존성 주입:

const fan = createFan();
const fanSwitch = createSwitch(fan);



구체적인 구현을 외부에서 주입받습니다
이를 통해 결합도가 낮아집니다

DIP의 장점:

*유연성: 새로운 기기를 쉽게 추가할 수 있습니다
* 테스트 용이성: 모의 객체(mock)를 쉽게 만들 수 있습니다
* 재사용성: 코드 재사용이 쉬워집니다
* 유지보수성: 변경이 다른 부분에 영향을 미치지 않습니다

이렇게 구현하면:

* 고수준 모듈(스위치)은 저수준 모듈(선풍기, 전구)에 직접 의존하지 않고
* 둘 다 추상화(전자기기 인터페이스)에 의존하게 됩니다
* 이것이 바로 "의존성 역전" 입니다!

 

 

 

 

 

 

 

 

 

아직 애매하게 이해한 부분이 많은데, 마냥 무지성으로 소스 짤때보단 뭔가 깨닫는게 있었다 ㅋㅋㅋㅋㅋ

디자인 패턴 좀 더 파보도록 하자!

 

 

728x90
반응형

'개발 > script' 카테고리의 다른 글

[script] 객체 리터럴 문법  (0) 2025.01.12
[script] 전개구문 ...  (0) 2025.01.11
null vs undefined 차이  (0) 2024.12.31
데이터 타입의 종류  (0) 2024.12.30
vue3 스크롤 페이지네이션  (0) 2024.12.29