SOLID 5 원칙은 객체 지향적인 설계를 위한 규칙으로, 아래 문장을 규칙화하였다고 볼 수 있다!

응집도는 높이고, 결합도는 낮추기

  • 결합도 : 클래스 간의 상호 의존성 → 결합도가 낮아야 유지보수/재사용이 편리하다.
  • 응집도 : 클래스 내부 요소들끼리의 관련성 → 응집도가 높으면 해당 기능/책임에 집중 가능하고 독립적이다. 따라서 유지보수/재사용이 편리하다.
  • 쉽게 말하면, 클래스 내부 요소들끼리는 연관성이 크지만, 클래스끼리는 독립적이어야 한다. 이는 유지보수/재사용을 편리하게 한다.

프로그램의 유지보수/재사용을 편하게 하기위해 객체 지향적인 방법이 등장했다. 객체 지향적으로 개발하기 위해서는 응집도는 높이고, 결합도는 낮추 어야 한다. 이를 구체화한 규칙이 SOLID 5 원칙이다. 각각에 대해 정리해보았다.

SRP (single responsibility principle)

  • 단일 책임 원칙
    • 역할/책임에 따라 클래스를 분리한다. 즉, 하나의 클래스는 하나의 역할/책임만 맡도록 한다.

추상화하는 단계에서 고민해볼 원칙이다. 클래스를 하나의 책임만 갖도록 분리하고 상속을 통해서 메소드를 각자 행위에 맞게 오버라이딩한다.

  • BAD 하나의 클래스 안에 여러 기능/역할이 있을 경우 만약 메소드 내부에서 if 분기문 후 instanceof 이 있다면 SRP가 위배되었을 수 있다.
// 3가지 책임(역할)이 모두 있음
public class Vehicle {
    public void printDetails() {}
    public double calculateValue() {}
    public void addVehicleToDB() {}
}
  • GOOD 역할 별로 클래스를 분리하였다.
public class print{
    public void printDetails() {}
}

public class calculate {
    public double calculateValue() {}
}

public class addToDB {
    public double addVehicleToDB() {}
}

OCP (Open/closed principle)

  • 개방 폐쇄 원칙
    • (기능) 확장에는 열려있으나 (기존 코드) 변화에는 닫혀있어야 한다.

(클래스/메소드) 추가 확장 시에, 원래 클래스의 내부 코드는 바뀌지 않으면서, 해당 클래스를 상속받는 하위 클래스를 새롭게 만들어준다. 이를 통해 확장에는 열려있으나 변화에는 닫혀있을 수 있다. 대표적으로 IoC(제어의 역전), DI(의존성 주입)이 OCP를 따른다. 후에 나오는 DIP도 OCP와 연관있는 원칙이다.

  • BAD 아래 상황에서 새로운 Vehicle 종류인 Truck을 추가하려면, 이 클래스 내부의 코드를 수정해야 한다.
// Vehicle 내부 코드를 수정해주어야 함!
public class Vehicle {
    public double calculateValue(Vehicle v) {
        if (v instanceof Car) {
            return v.getValue() _ 0.8;
        if (v instanceof Bike) {
            return v.getValue() _ 0.5;

        // Truck을 추가하려면 새로운 분기문을 추가하는 등
        // 기존 클래스의 코드를 변경해주어야 함
    }
}
  • GOOD 아래의 경우, Vehicle에는 필요한 공통 기능만 남겨주고 Vehicle을 상속받는 하위클래스들을 구현하였다. Truck을 추가하려면 Vehicle을 상속받아 추가해주면 된다. 또한 이러한 확장 시 Vehicle의 내부 코드는 바뀌지 않는다. 이 예시 말고도 대표적으로 Spring의 IoC도 해당된다. 이는 DIP 원칙에서 예시로 정리해두었다!
// Vehicle의 내부 코드는 변경X
public class Vehicle {
    public double calculateValue() {...}
}

// Vehicle를 상속받아 하위 클래스들 확장가능
public class Car extends Vehicle {
    public double calculateValue() {
        return this.getValue() _ 0.8;
}

public class Truck extends Vehicle{
    public double calculateValue() {
        return this.getValue() _ 0.9;
}

LSP (Liskov substitution principle)

  • 리스코프 치환 원칙
    • sub type은 언제나 자신의 base type으로 교체할 수 있어야 한다. 즉, 하위 클래스의 인스턴스가 상위 클래스 타입으로 선언되었을 경우, 상위 클래스의 인스턴스 역할을 할 수 있어야 한다.

상속이 적절하게 이루어졌을 경우, LSP를 만족한다.

  • 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류이다.
  • 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다.
  • GOOD 하위 클래스 인스턴스가 상위 클래스 메소드를 수행할 수 있다.
// 상위 클래스
public class Rectangle {
    private double height;
    private double width;
    public void setHeight(double h) { height = h; }
    public void setWidht(double w) { width = w; }
    ...
}

// 하위 클래스
public class Square extends Rectangle {

    public void setHeight(double h) {
        super.setHeight(h); // 부모 메소드를 사용하는 데 문제 X
        super.setWidth(h);
    }
    public void setWidth(double w) {
            super.setHeight(w);
            super.setWidth(w);
    }
}

ISP (Interface segregation principle)

  • 인터페이스 분리 원칙
    • 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다. 즉, 인터페이스는 자기 역할에 맞는 기능(메소드)으로만 최소한으로 구성되어야 한다. (인터페이스는 자식클래스에서 무조건 오버라이딩해야 하므로)

하나의 인터페이스는 범용 인터페이스보다 분리된 여러 인터페이스가 좋다. 인터페이스는 하위 클래스에서 모든 메소드를 필수 구현해야한다. 따라서 한 인터페이스 안에 많은 기능이 있기보다는 역할별로 분리되어 있어야 좋다.

  • BAD 한 인터페이스에 여러 기능
public interface BearKeeper {
    void washTheBear();
    void feedTheBear();
    void petTheBear();
}
  • GOOD 클래스들이 상위 클래스/인터페이스를 상속받게 한다.
    // 기능마다 인터페이스 여러개로 분리
public interface BearCleaner {
    void washTheBear();
}

public interface BearFeeder {
void feedTheBear();
}

public interface BearPetter {
void petTheBear();
}

// 필요한 인터페이스 상속받아 필요한 기능 사용
public class BearCarer implements BearCleaner, BearFeeder {

    public void washTheBear() {
        //I think we missed a spot...
    }

    public void feedTheBear() {
        //Tuna Tuesdays...
    }
}

DIP (Dependency inversion principle)

  • 의존관계 역전 원칙

    • 구체화에 의존하면 안되고 추상화에 의존해야 한다. 즉, 구체 클래스에 의존하지 말고, 상위 클래스/인터페이스에 의존해야 한다. 구현체에 의존하면 변경이 복잡해지기 때문이다.
    • 추상체에 의존하고, 실제 객체 주입시에 구현체를 주입

구체 클래스에 의존하면 변경이 어려워진다. 추상화에 의존할 경우, 확장과 변경이 쉽다. 새로운 하위클래스를 만들어 상속받게 할 수 있고, 실 구현체를 변경해주면 된다. 따라서 구체화가 아닌 추상화에 의존해야 한다. OCP와 연관되는 규칙이나, DIP가 더 구체적인 규칙이다.

  • BAD 구체 클래스를 의존하는 상황 -> Car가 Engine 변경 시 Car 클래스 내부 코드를 변경해야 한다.
// Engine 인터페이스
public class Engine {
    public void start() {...}
}

// Engine 인터페이스를 상속받은 클래스들
public class PetrolEngine implements Engine {
    public void start() {...}
}
public class DieselEngine implements Engine {
    public void start() {...}
}

// 구체화 클래스(PetrolEngine or DieselEngine)에 의존 -> Engine 변경 시, 클래스 내부 코드 변
public class Car {
    // private final Engine engine = new PetrolEngine();
    // 변경 시
    private final Engine engine = new DieselEngine();
}
  • GOOD 추상화(인터페이스)에 의존하는 상황 (아래 예시) 의존성 주입을 통해 Car 객체가 Engine 객체를 주입받는다. -> 다른 Engine 객체로 바꾸려면, Car 클래스 내부 코드를 변경하지 않고 새로 Engine 객체를 주입하면 된다.
  // Engine 인터페이스
  public interface Engine {
  public void start();
  }

// Engine 인터페이스를 상속받은 클래스들
// Car 사용시 필요한 클래스(PetrolEngine/DieselEngine)를 사용하면 된다.
public class PetrolEngine implements Engine {
public void start() {...}
}
public class DieselEngine implements Engine {
public void start() {...}
}

// 추상화(인터페이스)에 의존
public class Car {
private Engine engine;
public Car(Engine e) {
engine = e;
}
public void start() {
engine.start();
}
}

// 의존성 주입 -> 여기서(클라이언트 코) 엔진 종류 선택해서 주입
Car myCar1 = new Car(new PetrolEngine);
Car myCar2 = new Car(new DieselEngine);

정리하면

  • 객체지향적인 설계를 하려면 응집도는 높이고, 결합도는 낮추어야 한다. 즉, 클래스 내부 요소들끼리는 연관성이 크지만, 클래스끼리는 독립적이어야 한다. 이러한 객제 치향적인 설계는 유지보수/재사용을 편리하게 한다.

  • 객체지향 SOLID 5원칙은 응집도는 높이고, 결합도는 낮추기를 규칙화한 것이다.

  • SRP (단일 책임 원칙)

    • 하나의 클래스에는 하나의 역할/책임만 주자. 필요시 클래스 분리하자
  • OCP (개방 폐쇄 원칙)

    • 확장에는 Open, 변경에는 Closed되어 있어야 한다.
  • LSP (리스코프 치환 원칙)

    • 하위클래스 인스턴스가 상위 클래스의 메소드를 사용할 수 있어야 한다. 기능상 상위클래스의 규약에 맞아야 한다.
  • ISP (인터페이스 분리 원칙)

    • 인터페이스에는 해당 역할에 맞는 메소드로만 최소한으로 구성하자. 필요시 여러 인터페이스로 분리하자.
  • DIP (의존관계 역전 원칙)

    • 구체화가 아닌 추상화에 의존하자.
Reference
  • https://www.baeldung.com/java-liskov-substitution-principle
  • 책 <스프링 입문을="" 위한="" 객제치향의="" 원리와="" 이해="">
  • https://www.baeldung.com/solid-principles
  • https://www.educative.io/answers/- - - what-are-the-solid-principles-in-java