[Spring] 좋은 객체 지향 설계의 5가지 원칙(SOLID)

soonhankwon·2023년 9월 14일
0
post-thumbnail

SOLID 원칙은 국내에서 클린시리즈의 저자로 유명한 로버트 C 마틴(엉클밥)이 2000년 논문에서 소개했습니다. SOLID 라는 약어는 마이클 패터스에 의해 소개되었습니다.

이 디자인 원칙은 약어 그대로 견고한 원칙입니다. 이 원칙을 지킨다면 우리는 보다 유지 보수가 쉽고, 확장과 변경에 유연한 소프트웨어을 만들 수 있습니다.

결과적으로 애플리케이션의 크기가 커짐에 따라 복잡성을 줄이고 많은 골치 아픈 일들을 줄일 수 있습니다.

아래에서는 스프링에 주로 초점을 맞춰 SOLID를 하나씩 살펴보겠습니다.

SOLID - 로버트 마틴

아래의 5가지 개념이 SOLID 원칙입니다.

  • SRP, OCP, LSP, ISP, DIP

SRP 단일 책임 원칙

  • Single Responsibility Principle
  • 한 클래스는 하나의 책임만 가져야 한다.
    • 하지만 이는 실제 개발을 하다보면 분명 모호해지는 상황을 마주합니다.
    • 책임은 클 수 있고, 작을 수도 있습니다.
  • 중요한 기준은 변경입니다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것입니다.

장점

  • 테스트 용이 : 책임이 하나인 클래스에는 테스트 케이스가 적어 테스트가 용이합니다.
  • 낮은 결합 : 단일 클래스의 기능이 적으면 결합도가 낮습니다.
  • 조직 : 더 작고 잘 구성된 클래스는 모놀리식 클래스보다 검색하기가 쉽습니다.

Example

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters

    // 북 클래스의 properties 에 직접 연관된 메서드(상태변경)
    public String replaceWordInText(String word, String replacementWord){
        return text.replaceAll(word, replacementWord);
    }

    public boolean isWordInText(String word){
        return text.contains(word);
    }
		
		// SRP 위반
		void printTextToConsole(){
        // our code for formatting and printing the text
    }
}
  • Book 의 인쇄 업무를 덜어내주어 SRP를 지키도록 해보자
public class BookPrinter {

    // methods for outputting text
    void printTextToConsole(String text){
        //our code for formatting and printing the text
    }

    void printTextToAnotherMedium(String text){
        // code for writing to any other location..
    }
}

OCP 개방-폐쇄 원칙

  • Open-Closed Principle
  • 소프트웨어 요소는 확장(구현)에는 열려 있으나 변경(역할)에는 닫혀 있어야 한다.
  • 다형성을 활용합니다.
  • 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현합니다.
  • 단순히 다형성을 사용했다고 지킬수 없다. (클라이언트에서 코드 변경)
    • 객체를 생성하고 연관관계를 맺어주는 별도의 조립 생성자가 필요하다.
    • ex) 스프링 컨테이너 DI & Configuration

Example

  • 다형성을 사용해서 서로 다른 가속방식을 가지는 WeriedRocketCar 와 NormalCar를 구현해주었습니다.
  • 하지만 객체를 생성하고 사용하려면 클라이언트에서 코드 변경이 불가피 합니다.
  • 스프링은 이것을 스프링 컨테이너의 DI 와 Configuration을 통해서 해결하고 있습니다.
public class WeiredRocketCar implements Car {
		
		@Override
		void accelerate(){
        if(this.speed >= 100) {
			this.speed *= 2;
		}
    }
}

public class NormalCar implements Car {
		
		@Override
		void accelerate(){
        if(this.speed >= 100) {
			this.speed += 2;
		}
    }
}

public class CarApp {
      public static void main (String[] args) {
          // 어쨋든 코드를 변경해야하네.......
          //Car car1 = new WeiredRocketCar();
          Car car1 = new NoramalCar();
          car1.accelerate();
      }
}

LSP 리스코프 치환 원칙

Liskov substitution priciple

  • 프로그램의 객체는 프로그램의 정확성 을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀수 있어야 한다.
  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체를 믿고 사용하려면, 이 원칙이 필요합니다.
  • 컴파일에 성공하는 것을 넘어서는 이야기입니다.

Example

  • Car 는 자동차가 수행할 책임을 정의한 인터페이스 (역할) 입니다.
  • MotorCar 는 일반적인 차의 역할을 수행하고 있습니다.
  • WeiredRocketCar 는 프로그램의 정확성 즉 자동차 역할을 깨뜨리고 있습니다. 즉 리스코프 치환 원칙을 위반하고 있습니다.
public interface Car {
    void turnOnEngine();
    void accelerate();
}

public class MotorCar implements Car {

    private Engine engine;

    //Constructors, getters + setters

    public void turnOnEngine() {
        //turn on the engine!
        engine.on();
    }

    public void accelerate() {
        //move forward!
        engine.powerOn(1000);
    }
}

public class WeiredRocketCar implements Car {
    public void turnOnEngine() {
        engline.off(); //???????
    }

    public void accelerate() {
				engline.powerOn(-10000); //??????
    }
}

ISP 인터페이스 분리 원칙

Interface segregation principle

  • 더 큰 인터페이스를 더 작은 인터페이스로 분할해야 함을 의미합니다.
  • 분리하면 인터페이스 자체가 변해도 다른 클라이언트에 영향을 주지 않는다.
  • 인터페이스가 명확해지고, 대체 가능성이 높아집니다.

Example

public interface TigerKeeper {
	void washTheTiger();
	void feedTheTiger();
	void petTheTiger();
}
  • 인터페이스 분할
public interface TigerCleaner {
    void washTheTiger();
}

public interface TigerFeeder {
    void feedTheTiger();
}

public interface TigerPetter {
    void petTheTiger();
}
  • 인터페이스 분리를 해서 우리는 중요한 메서드만 자유롭게 구현할 수 있습니다.
public class TigerCarer implements TigerCleaner, TigerFeeder {
	public void washTheTiger() {
		//wash hard.....
	}

	public void feedTheTiger() {
		//meat Monday....
	}
}

DIP 의존관계 역전 원칙

Dependency Inversion Principle

  • 프로그래머는 추상화에 의존해야지 구체화에 의존하면 안된다.
  • 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻
    • 역할에 의존하게 해야 한다는 것입니다.

Summary.

  • 객체지향의 핵심은 다형성입니다.
    • 역할(추상화), 구현(확장)을 분리
  • 하지만 다형성 만으로는 OCP, DIP를 지킬 수 없다.
  • 스프링은 이를 해결!
  • 스프링은 다음 기술로 다형성 + OCP, DIP 를 가능하게 지원합니다.
    • DI(Dependency Injection) : 의존관계, 의존성 주입
    • DI 컨테이너 제공
  • 클라이언트 코드의 변경 없이 기능 확장
    • 쉽게 부품을 교체하듯이 개발할 수 있습니다.
  • 모든 설계에 역할구현을 분리하자!
  • 이상적으로는 모든 설계에 인터페이스를 부여하자!
    • 추상화라는 비용이 발생 (장점, 단점)
    • 기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고, 향후 꼭 필요할 때 리팩터링해서 인터페이스를 도입하는 것도 방법입니다.

Reference.

profile
ProblemOverFlow

0개의 댓글