본문 바로가기
인프런 워밍업 클럽

[인프런 워밍업 클럽] SOLID

by 미안한데와주겠어 2025. 3. 7.

SOLID


SRP : Single Responsibility Principle -> 단일 책임 원칙

  • 하나의 클래스가 하나의 책임(변경 이유) 만을 가져야 한다.

SRP를 지켰을 때의 이점

  • 관심사의 분리
  • 높은 응집도, 낮은 결합도
    이걸 하기 위해서는 "책임" 을 볼 수 있는 눈이 있어야 한다.
  1. MineswepperGame 클래스를 Minesweeper 클래스와 GameApplication 클래스로 분리
    -> 게임 실행에 대한 책임과 지뢰찾기 자체를 분리
  2. 사용자 입출력 부분을 ConsoleInputHandlerConsoleOutputHandler 클래스로 분리
    -> 입출력 책임을 분리
  3. BOARDGameBoard 클래스 객체로 분리
    -> Board 내부의 값에 대한 초기화, 변경 및 조회 책임을 분리

💭 : 클래스 단위로 책임을 분리한다.


OCP : Open-Closed Principle -> 개방-폐쇄 원칙

  • 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.
    -> 기존 코드의 변경 없이 기능을 확장 할 수 있어야 한다.

추상화와 다형성을 활용해 OCP를 지킬 수 있다.

지뢰 찾기에 게임의 난이도를 변경해야 하는 새로운 요구사항이 발생
-> Board의 Size가 변경 될 수 있어야 한다

  1. switch 문이나 고정된 값을 이용해 사용하던 로직을 유동적인 변수를 받아 동작하도록 변경
  2. GameLevel 인터페이스를 활용해 인터페이스를 구현하는 난이도 구현체들을 생성해 난이도 변경

💭 : 고정된 값이나 클래스를 사용하기 보다 변수나 인터페이스를 활용해 프로그램을 유동성 있게 만든다.


LSP : Liskov Substitution Principle -> 리스코프 치환 원칙

  • 상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.
    -> 자식 클래스는 부모 클래스의 책임을 준수하고 부모 클래스의 행동을 변경하면 안된다.

LSP를 위반하면 상속 클래스를 사용할 때 예외나 불필요한 타입 체크가 동반 될 수 있다.

보통 부모 자식 관계에서 자식이 더 기능을 많을텐데 부모가 쓰이던 자리에 자식이 쓰여도 동일하게 동작을 해야한다는 것이다!

  1. Cell 을 상속 구조로 변경해 지뢰 Cell , 숫자 Cell, 빈 Cell 이 자식이 되도록 변경
  2. 이때 어떤 Cell 인지 확인하기 위해 instanceof를 사용해야 하는 설계가 됨
  3. Cell 특성에 따라 메서드 구조 변경

💭 : 상속을 사용할 때에는 LSP를 신경써서 사용하자


ISP : Interface Segregation Principle -> 인터페이스 분리 원칙

  • 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안된다
    -> 인터페이스를 잘게 쪼개라

ISP를 위반하면 결합도가 높아지고 특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.

  1. run()initialize() 메서드를 가지는 Game 인터페이스를 만듬
  2. 게임 인터페이스를 구현하는 다른 클래스가 initialize() 메서드가 필요가 없을 수가 있다.
  3. 여기서 Game 인터페이스를 GameInitializable 인터페이스와 GameRunnable 인터페이스로 분리한다.

💭 : 인터페이스를 기능별로 잘게 쪼개서 사용하자


DIP : Dependency Inversion Principle -> 의존성 역전 원칙

  • 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안되고 둘 모두 추상화에 의존해야 한다.

의존성의 순방향 : 고수준 모듈이 저수준 모듈을 참조하는 것
의존성의 역방향 : 고수준, 저수준 모듈이 모두 추상화에 의존하는 것
-> 저수준 모듈이 변경되어도 고수준 모듈에는 영향이 가지 않는다.

고수준 : 추상화 레벨이 높음
저수준 : 추상화 레벨이 낮음
의존성 : 하나의 모듈이 다른 모듈을 알고 있거나, 생성하거나 사용하거나 참조할 때, 의존성이 있다고 한다.

순방향은 고수준 모듈이 직접 저수준 모듈을 가져다가 쓰는 것이다.
역방향은 추상화된 스펙 (인터페이스) 만 참조해 가져다가 쓰는 것이다. 이때 런타임 시에 구현체가 정해지게 된다.

  1. consoleInputHandlerconsoleOutputHandler 는 콘솔에 의존하게 된다.
  2. InputHanderOutputHander 인터페이스를 생성해 스펙을 정하고 구현체는 스펙에 따라 구현한다.

DI (Dependency Injection) -> 의존성 주입

필요한 의존성을 외부로 주입 받는다.
DI를 떠올리면 숫자 3을 떠올려야 한다. A가 B라는 의존성을 주입받고 싶을 때 제3자가 의존성을 맺어주게 된다. 스프링에서는 Spring Context(IoC Container)가 해주게 된다.

IoC (Inversion of Control) -> 제어의 역전

프로그램의 흐름, 제어의 주도권을 프레임워크로 넘겨주게 된다.
IoC Container가 객체를 생성해주고 생명주기를 관리해준다.


코드 리팩토링 과제

public boolean validateOrder(Order ordre) {
    if (order.getItem().size() == 0) {
        log.info("주문 항목이 없습니다.");
        return false;
    } else {
        if (order.getTotalPrice() > 0) {
            if (!order.hasCustomerInfo()) {
                log.info("사용자 정보가 없습니다.");
                return false;
            } else {
                return true;
            }
        } else if (!(order.getTotalPrice() > 0)) {
            log.info("올바르지 않은 총 가격입니다.");
            return false;
        }
    }
    return true;
}

변경 후

public boolean validateOrder(Order ordre) {  
    if (order.isEmpty()) {  
        log.info("주문 항목이 없습니다.");  
        return false;  
    }  
    if (order.isInvalidTotalPrice()) {  
        log.info("올바르지 않은 총 가격입니다.");  
        return false;  
    }  
    if (order.hasNotCustomerInfo()) {  
        log.info("사용자 정보가 없습니다.");  
        return false;  
    }  
    return true;  
}

출처 : https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95/dashboard