SOLID 5원칙

Minsang Yu·2023년 4월 10일
0

1. 단일 책임 원칙 (Single Responsibility Principle)

SRP 적용 전

SRP 적용 후

  • 객체는 단 하나의 책임만을 가져야 한다. 책임이 많아지면 클래스 내부의 함수끼리 강한 결합을 발생할 가능성이 높아지며 이는 유지보수에 비용이 증가하게 되므로 책임을 분리시킬 필요가 있다.
  • 하나의 클래스에서 여러 기능을 수행하게 되면 기능 변경이 일어났을떄 파급효과가 커진다. 한 객체에서 책임이 많이잘수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아진다.
  • 예를 들어 A를 고쳤더니 B를 수정해야하고 또 C를 수정해야하고, C를 수정했더니 다시 A로 돌아가서 수정해야 하는, 마치 책임이 순환되는 형태를 들 수 있다.
  • 단일 책임 원칙을 잘 따르면 한 책임의 변경으로부터 다른 책임의 변경으로으 연쇄작용 에서 자유로울 수 있다.
  • 코드의 가독성 향상, 유지보수용이 라는 이점이 있다.

2. 개방 폐쇠 원칙 ( Open Closed Principle)

  • 기존의 코드를 변경하지 않고 기능을 수정하거나 추가할 수 있도록 설계해야한다. OCP를 만족한 설계는 변경이 유연하므로 유지보수 비용을 줄여주고 코드의 가독성 또한 높아지는 효과를 얻을 수있다.
  • 확장에 대해서 개방적(Open)이고 수정에 대해서는 폐쇠적(closed)이어야 한다는 의미
  • 손쉽게 이야기하면 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화 하도록 프로그램을 작성해야한다.

OCP 원칙이 적용된 JDBC의 관계

확장

  • 새로운 타입을 추가함으로써 기능을 추가(구현)할 수 있다.
  • 따라서 확장이 열려있다는 말은 새로운 타입(클래스, 모듈, 함수)을 추가함으로써 기능을 확장시키는 것을 말한다.
    변경
  • 확장이 발생했을 떄 상위 레벨이 영향을 미치지 않아야 하는 것을 말한다.
  • 따라서 변경에는 닫혀있어야 한다는 말은 확장이 발생했을 떄 해당 코드를 호출 하는 쪽에서 변경이 발생하지 않는 것을 말한다.

이 원칙을 지키기 위한 구현방법
1. 확장 될 것과 변경을 엄격히 구분한다.
2. 이 두 모듈이 만나는 지점에 인터페이스를 정의 한다.
3. 구현에 대한 의존보다는 정의된 인터페이스를 의존 하도록 코드를 작성한다.
4. 변경이 발생하는 부분을 추상화하여 분리한다.

3. 리스코프 치환 원칙(Liskov Substitution Principle)

  • 식 클래스는 부모클래스에서 가능한 행위를 수행할 수 있어야 한다. 상속 관계에서는 일반화 관계(IS-A)가 성립해야한다. 일반화 관계에 있다는 것은 일관성이 있다는 것이다.
  • 서브 타입은 언제든 자신의 기반(부모) 타입으로 교체 할 수 있어야 한다는 원칙을 말한다. 이 말은 즉 객체의 상속관계에서 자식 클래스는 언제든 부모 타입으로 교체할 수 있다는 말을 말한다.
  • 상속 관계에서 최소한 자식 클래스는 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다는 개념

예시

public interface Animal{
	void eat(String food);
    void sleep();
}
public class Whale implements Animal{
	@Override
    public eat(String food){
    	System.out.println(food + "을/를 먹다");
    }
    @Override
    public sleep(){
    	System.out.println("잔다");
    }
 }

고래(Whale)은 동물(Animal)을 상속받아 사용하는 것이 위의 예시와 같다고 보면 된다. 상위 클래스에서 동물이라면 해야하는 부분을 지정해주고 그 동물에 속하는 종들은 해당 행동(메서드)들을 반드시 이행 해야한다.

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

  • 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.
  • 하나의 일반적인 인터페이스 보다는 여러개의 구체적인 인터페이스가 낫다.
  • 자신이 사용하지 않는 기능에 영향을 받지 않아야 한다.
  • 객체에 대한 책임을 덜려고 하는것이 목표이고 다시 말해 인터페이스를 쪼개고 쪼개서 클래스가 단 하나의 책임(SRP)을 지니게 하는것을 도와준다.

ISP 적용전

public interface Car {
    String autoDrive();    
    String autoParking();    
    String drive();
    String break();    
}
public class Telsa implements Car {
    @Override
    public String autoDrive() {
        return "autoDrive";
    }
    @Override
    public String autoParking() {
        return "autoParking";
    }
    @Override
    public String drive() {
        return "drive";
    }
    @Override
    public String break() {
        return "break";
    }
}

Tesla 클래스는 Car라는 상위 클래스의 인터페이스를 상속받아 오버라이딩 하여서 해당 기능을 사용하고 있다. 만약 autoParing, autoDrive기능을 지원하지 않는 차가 있으면 어떻게 해야할까 ?

public class Ray implements Car {
    //해당 기능을 사용 안하고싶음!!!!!
    @Override
    public String autoDrive() {
        return "";
    }
    //해당 기능을 사용 안하고싶음!!!!!
    @Override
    public String autoParking() {
        return "autoParking";
    }
    @Override
    public String drive() {
        return "drive";
    }
    @Override
    public String break() {
        return "break";
    }
}

위와 같이 autoparking, 과 autoDrive를 사용하고 싶지 않을떄는 isp의 인터페이스 분리 원칙을 적용시키면 된다.

[ISP 적용 후]

Car (Interface)

public interface Car {
    String drive();
    String break();   
}

ElectricCar (Interface)

public interface ElectricCar {
    String autoDrive();    
    String autoParking();     
}

Tesla (Class)

public class Telsa implements Car,ElectricCar {
    @Override
    public String autoDrive() {
        return "autoDrive";
    }
    @Override
    public String autoParking() {
        return "autoParking";
    }
    @Override
    public String drive() {
        return "drive";
    } 
    @Override
    public String break() {
        return "break";
    }
}

Ray(Class)

public class Ray implements Car {
    @Override
    public String drive() {
        return "drive";
    }
    @Override
    public String break() {
        return "break";
    }
}

위의 예제와 같이 인터페이스를 클래스(객체)의 기능에 맞게 SRP의 개념을 생각해서 잘개 쪼개어 사용하는 것을 ISP라 한다.


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

고수준 모듈은 저수준 모듈에 의존하며 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야한다. 즉, 자신보다 변하기 쉬운것에 의존하지 마라

다시 말하면 의존 관계를 맺을 떄 변화하기 쉬운 것과 자주 변화하는 것 보다는 변화하기 어려운것과 거의 변화가 없는것에 의존 하라는 원칙을 말한다.

여기서 변화하기 어려운 것을 말하는 것은 보통 추상적인 객체(Interface, abstract)를 말한다.


현재는 겨울이기 떄문에 스노우 타이어를 구매하여 자동차에 끼도록 설계하였습니다. 즉, 고수준 모듈인 자동차가 저수준 모듈인 스노우 타이어에 의존하는 상태입니다.

하지만, 날씨가 따뜻해지면서 더 이상 스노우 타이어를 사용할 필요가 없어졌습니다. 그래서 일반 타이어로 교체하기로 결정하였습니다. 그런데, 단순히 스노우 타이어를 일반 타이어로 바꾼다고 코드가 끝나는 것이 아닙니다. 이것에 의존하고 있던 자동차의 코드도 연쇄적으로 영향을 끼치게 됩니다.

이것은 개방-폐쇄 원칙을 위반하는 것이므로 추상화나 다형성을 통해 문제를 고쳐야 합니다. 의존 역전 원칙은 그 중에서도 추상화를 이용합니다. 바로, 스노우 타이어나 일반 타이어를 '타이어' 자체로 추상화하는 것이죠.


고수준 모듈이 저수준 모듈에 의존했던 상황이 역전되어 저수준 모듈이 고수준 모듈에 의존하게 된다.


이렇게 SOLID 원칙에 근거하여 작성을 하게되면 클래스의 책임을 덜어주고 기능에 의존하지 않게 하는 것이 확장성과 다른 클라이언트에 미치는 영향을 최소화 하는 것을 알 수 있다.

profile
Jr. DataEngineer

0개의 댓글