본문 바로가기
개발관련 이것저것

SOLID

by 이상한나라의개발자 2024. 5. 23.

단일 책임 원칙 (SRP : Single Responsibility Principle)

단일 책임 원칙 이란 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화 해야한다. 클래스가 제공하는 모든 기능은 이 책임과 주의 깊게 부합해야합니다. 로버트 마튼은 책임을 변경하려는 이유로 정의하고, 어떤 클래스나 모듈은 변경하려는 단 하나의 이유만 가져야 한다고 합니다.

 

* 코드 라인이 100줄 이상이라면 책임이 많은건 아닌지 의심해야봐 합니다.

 

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

개방-폐쇄 원칙은 소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다. 는 프로그래밍 원칙입니다. 이 원칙을 잘 적용하면 기능이 추가되거나 변경해야 할 때 제대로 동작하고 있던 원래 코드를 변경하지 않아도 기존의 코드에 새로운 코드를 추가함으로써 기능의 추가나 변경이 가능합니다. 이 원칙을 무시하고 프로그래밍 한다면, 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 결코 얻을 수 없습니다.

 

* 추상화(인터페이스)가 부족한 경우

 

리스코프 치환 원칙(LSP : Liskov Substitution Principle)

컴퓨터 프로그램에서 자료형 S가 자료형 T의 하위형이라면 필요한 프로그램 속성(정확성, 수행하는 업무 등)의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체(치환)할 수 있어야 한다는 원칙입니다.

 

쉽게 말해서 하위 자료형이 상위 자료형의 모든 동작을 완전히 대체 가능해야 한다

LSP는 상위 클래스의 객체를 하위 클래스의 객체로 치환하더라도 프로그램의 동작이 변하지 않아야 한다는 원칙

즉, 하위 클래스는 상위 클래스의 계약을 위반하지 않고, 상위 클래스의 기능을 확장할 수 있어야 한다.

 

상위 클래스와 하위 클래스 사이의 계약이 깨지는 경우로 Square 클래스가 Rectangle 클래스의 동작을 위반하고 있습니다.

  • Rectangle 클래스는 너비와 높이를 독립적으로 설정할 수 있는 사각형을 나타냅니다. 
  • Square 클래스는 Rectangle을 상속받아, 너비와 높이가 항상 동일한 정사각형을 나타냅니다.
  • Square 객체를 Rectangle 타입으로 사용하게 되면 Rectangle의 세터 메서드를 사용하여 높이를 독립적으로 변경할 수 있는 가능성을 열어주게 됩니다. 이는 Square 의 동작과 상충됩니다.
  • Rectangle square = new Square(10);로 정사각형 객체를 생성합니다.
  • square.setHeight(5);를 호출하면, 정사각형의 높이만 5로 변경될 것으로 기대하지만, 정사각형의 너비도 5로 변경되어야 합니다.
  • 이는 Rectangle 객체가 Square 객체로 치환될 때 동작이 일관되지 않음을 의미합니다.
@Getter
@Setter
@AllArgsConstructor
public class Rectangle {

    protected double width;
    protected double height;
}

class Square extends Rectangle {

    public Square(double size) {
        super(size, size);
    }

    public static void main(String[] args) {
        Rectangle square = new Square(10);
        square.setHeight(5);
    }
}

 

 

이 예제에서는 FlyingBird와 NonFlyingBird 객체를 생성하고 makeFlyingBird 객체를 치환하여 사용할 수 있으며, 이는 리스코프 치환의원칙을 준수하고 있습니다.

// 상위 클래스
abstract class Bird {
    public abstract String fly();
}

// 하위 클래스 1
class FlyingBird extends Bird {
    @Override
    public String fly() {
        return "I can fly!";
    }
}

// 하위 클래스 2
class NonFlyingBird extends Bird {
    @Override
    public String fly() {
        return "I cannot fly.";
    }
}

// 메인 클래스
public class Main {
    // 함수: 새가 날 수 있는지 확인
    public static void makeBirdFly(Bird bird) {
        System.out.println(bird.fly());
    }

    // 메인 메서드
    public static void main(String[] args) {
        Bird sparrow = new FlyingBird();
        Bird penguin = new NonFlyingBird();

        makeBirdFly(sparrow);  // 출력: I can fly!
        makeBirdFly(penguin);  // 출력: I cannot fly.
    }
}

 

위 와 같이 리스코프 치환 원칙을 지키지 못하는 경우는 개발하다 보면 종종 발생합니다. 그러므로 상속보다는 컴포지션을 활용해라고 합니다. 또는 인터페이스로 분리하여 개발하는게 좋아 보입니다.

 

인터페이스 분리 원칙 (ISP : Interface Segregation Principle) 

ISP는 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙입니다. 즉, 특정 클라이언트에 맞는 작고, 구체적인 인터페이스를 여러 개 만드는 것이 좋습니다.

 

  • 클라이언트는 자신이 사용하지 않는 인터페이스에 의존해서는 안됩니다.
  • 큰 인터페이스를 여러 개 작은 인터페이스로 분리하여, 특정 클라이언트가 자신에 필요한 메서드만 알도록 합니다.
// 작은 인터페이스들
interface Printer {
    void print(Document document);
}

interface Fax {
    void fax(Document document);
}

interface Scanner {
    void scan(Document document);
}

class Document {
    // 문서 내용
}

// 프린터만 필요한 클라이언트
class OldPrinter implements Printer {
    @Override
    public void print(Document document) {
        // 프린트 기능 구현
    }
}

// 팩스만 필요한 클라이언트
class FaxMachine implements Fax {
    @Override
    public void fax(Document document) {
        // 팩스 기능 구현
    }
}

// 프린트와 스캔이 모두 필요한 클라이언트
class MultifunctionPrinter implements Printer, Scanner {
    @Override
    public void print(Document document) {
        // 프린트 기능 구현
    }

    @Override
    public void scan(Document document) {
        // 스캔 기능 구현
    }
}

 

 

이렇게 인터페이스를 분리함으로써 각 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않게 됩니다.

 

인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 큰 인터페이스를 작은 인터페이스로 분리하는 것을 권장합니다. 

 

* 인터페이스를 분리해서 조립하듯 개발

 

의존성 역전 원칙 (DIP : Dependency Inversion Principle)

DIP는 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다는 원칙입니다. 이 원칙은 소프트웨어 시스템의 유연성과 확장성을 높이고, 변경에 대한 영향을 최소화 합니다.

 

  • 고수준 컴포넌트는 저수준 컴포넌트에 의존해서는 안된다. 둘 다 추상화에 의존해야 한다. (인터페이스 또는 추상클래스)
  • 추상화는 세부 사항에 의존해서는 안 된다. 세부 사항이 추상화에 의존해야 한다.

DIP를 위반하는 예시

class LightBulb {
    public void turnOn() {
        System.out.println("LightBulb: turned on...");
    }

    public void turnOff() {
        System.out.println("LightBulb: turned off...");
    }
}

class Switch {
    private LightBulb lightBulb;

    public Switch() {
        this.lightBulb = new LightBulb();
    }

    public void operate() {
        lightBulb.turnOn();
    }
}

 

DIP를 준수하는 예시

// 추상화된 인터페이스
interface Switchable {
    void turnOn();
    void turnOff();
}

class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("LightBulb: turned on...");
    }

    @Override
    public void turnOff() {
        System.out.println("LightBulb: turned off...");
    }
}

class Fan implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Fan: turned on...");
    }

    @Override
    public void turnOff() {
        System.out.println("Fan: turned off...");
    }
}

// 고수준 모듈
class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void operate() {
        device.turnOn();
    }
}

public class Main {
    public static void main(String[] args) {
        Switchable bulb = new LightBulb();
        Switchable fan = new Fan();

        Switch bulbSwitch = new Switch(bulb);
        Switch fanSwitch = new Switch(fan);

        bulbSwitch.operate();  // 출력: LightBulb: turned on...
        fanSwitch.operate();   // 출력: Fan: turned on...
    }
}

 

이와 같이 DIP를 적용하면 고수준 모듈(Switch)이 저수준 모듈 (LightBulb, Fan)에 직접 의존하지 않고 추상화된 (Switchable) 에 의존하게 됩니다. 이를 통해 시스템의 유연성과 확정성을 높을 수 있습니다.

 

* 의존성 주입과 의존 역전은 완전히 다르다 , 용어가 DI, DIP 로 혼란이 되지만 엄연히 다른 개념입니다. 

의존성 주입은 실제로 의존을 주입 받는 상태를 말합니다. 이때, 보통 세 가지 방법을 사용합니다.

 

  1. 생성자 주입 :  의존성을 생성자를 통해 주입받는 방식
  2. 세터 주입 : 의존성을 세터 메서드를 통해 주입 받는 방식
  3. 필드 주입 : 의존성 필드를 통해 직접 주입 받는 방식(스프링에서 권장되지 않음)

그리고 만약, 생성자 주입이 7개 이상 넘어가거나, 파라미터 의존성 주입이 4개이상 넘어간다면 클래스 분할이나 메서드 분할을 해야하는는 신호 입니다. 

 

'개발관련 이것저것' 카테고리의 다른 글

Springboot + Redis + Kafka 설치부터 설정, 실행 과정  (2) 2024.10.16
고수준 & 저수준  (0) 2024.05.24
VO & DTO  (0) 2024.05.23
get vs find  (0) 2024.05.23
제네릭 명명 관례  (0) 2024.05.08