본문 바로가기

스프링

[Spring] 객체 지향 설계 SOLID 개념과 적용 예시

참고
1. 스프링 핵심 원리 강의(김영한)
2. https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-ISP-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EB%B6%84%EB%A6%AC-%EC%9B%90%EC%B9%99?category=967430

SOLID

클린 코드의 저자(로버트 마틴)의 객체 지향 설계 원칙 5가지

  • SRP(Single Responsibility Principle): 단일 책임 원칙
  • OCP(Open-Closed Principle): 개방-폐쇄 원칙
  • LSP(Liskov Substitution Principle): 리스코프 치환 원칙
  • ISP(Interface Segregation  Principle): 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle): 의존관계 역전 원칙

SRP: Single Responsibility Principle

  • 하나의 클래스는 하나의 책임만 가져야 함
  • 클래스의 변경이 있을 때, 파급 효과가 적으면 SRP 준수한 것 (절대적인 기준은 없으며, 스스로 기준을 잡아야 한다.)
  • 성능보다는 유지보수를 위한 원칙에 가까움

예시

아래와 같이 '공학계산기'와 '그래프계산기'가 있다.

이때, 공학계산기의 더하기 함수는 공학계산기와 그래프계산기 두 클래스의 계산 함수를 책임지고 있다.

class 공학계산기 {
    public static void 더하기(){}
    
    public void 공학계산(){
    	... 
        더하기();
        ...
    } 
}


class 그래프계산기 {
    public void 그래프계산(){
    	...
        공학계산기.더하기();
        ...
    }
}

 

만약, 아래와 같이 더하기 함수와 같은 사칙연산 전용 클래스를 만들면, SRP를 더욱 잘 준수할 수 있을 것이다.

class SimpleCaculator{
    public static void 더하기(){}
}

class 공학계산기 {
    public void 공학계산(){
        SimpleCaculator.더하기();
    } 
}

class 그래프계산기 {
    public void 그래프계산(){
        SimpleCaculator.더하기();
    }
}

OCP: Open-Closed Principle

  • 확장에는 열려 있고, 변경에는 닫혀 있어야 함
  • 다형성을 활용(인터페이스 혹은 추상클래스 ➡️ 클래스 구현)

예시

위 공학용계산기에서 인터페이스를 이용해 OCP를 적용하면 아래와 같다.

이후, SimpleCaculator의 버전에 따라 편리하게 확장할 수 있고, 인터페이스에 의해 SimpleCalculator의 요소들은 닫혀 있게 된다. 

interface SimpleCaculator {
    public void 더하기();
}

class SimpleCaculatorV1 implements SimpleCaculator{
    public void 더하기(){}
}

class SimpleCaculatorV2 implements SimpleCaculator{
    public void 더하기(){}
}

class 공학계산기 {
    // SimpleCaculator simpleCaculator = new SimpleCaculatorV1();
    SimpleCaculator simpleCaculator = new SimpleCaculatorV2();

    public void 공학계산(){
        simpleCaculator.더하기();
    } 
}

 

LSP: Liskov Substitution Principle

  • 프로그램의 정확성은 유지하며 하위 타입의 인스턴스를 변경할 수 있어야 함
  • 상위 인터페이스의 규약을 정확히 지켜야 함 (추상, 인터페이스 - 구현체 간 다형성을 잘 지키라는 의미)

예시

아래 SimpleCacluatorV2는 인터페이스의 규약을 지키고 있지 않다. 이는 LSP에 위반된다. (IDE에서 에러라인으로 알려준다.)

interface SimpleCaculator {
    public void 더하기();
}

class SimpleCaculatorV1 implements SimpleCaculator{
    public void 더하기(){}
}

class SimpleCaculatorV2 implements SimpleCaculator{
    public void 더하기(int a, int b){} //LSP 위반.
}

ISP: Interface Segregation  Principle

  • 인터페이스용 SRP
  • 하나의 범용 인터페이스보다 여러 개의 인터페이스가 좋음 (커다란 단일 인터페이스 < 여러 개의 작은 인터페이스)
  • 인터페이스가 명확해지고, 대체 가능성 ⬆️
  • 인터페이스를 분리하면, 구현체에서 필요한 기능만 받아 구현 가능 (불필요한 구현 방지)
  • 즉, 인터페이스가 복잡해지면 분리를 고려하자

예시

아래는 공학계산기에 필요한 모든 기능이 하나의 인터페이스에 모두 담겨있는 ISP를 준수하지 않는 코드이다.

interface SimpleCaculator {
    public void 더하기();
    public void 공학계산();
    public void 공학더하기();
    public void 공학계산결과출력();
}

class SimpleCaculatorV1 implements SimpleCaculator{;
    public void 더하기(){}
    public void 공학계산(){}
    public void 공학더하기(){}
    public void 공학계산결과출력(){}
}


class 공학계산기 {
    // SimpleCaculator simpleCaculator = new SimpleCaculatorV1();
    SimpleCaculator simpleCaculator = new SimpleCaculatorV1();
}

 

아래와 같이, 인터페이스를 기능/역할 별로 분리해주어야 한다.

interface SimpleCaculator {
    public void 더하기();
}

interface 공학기능 {
    public void 공학계산();
    public void 공학더하기();
}

interface 계산출력 {
    public void 계산결과출력();    
}

class SimpleCaculatorV1 implements SimpleCaculator{
    public void 더하기(){}
}

class 공학계산기 implements 공학기능, 계산출력{
    SimpleCaculator simpleCaculator = new SimpleCaculatorV1();
	
    public void 공학계산(){}
    public void 공학더하기(){}
    public void 계산결과출력(){}
}

  

DIP: Dependency Inversion Principle

  • 구현체가 아닌, 추상화에 의존해야 한다.
  • 즉, 구현 클래스가 아닌 인터페이스에 의존해야 한다는 뜻.

예시

아래는 공학계산기 클래스가 내부의 '더하기' 메서드에 의존하고 있다. 이는 DIP에 위배된다.

class 공학계산기 {
    // SimpleCaculator simpleCaculator = new SimpleCaculatorV1();
    SimpleCaculator simpleCaculator = new SimpleCaculatorV1();

    public void 더하기(){}
    public void 공학계산(){
        ...
        더하기();
        ...
    }
    public void 공학더하기(){
        ...
        더하기();
        ...
    }
}

 

아래와 같이 분리해서 DIP를 준수할 수 있다.

interface SimpleCaculator {
    public void 더하기();
}

interface 공학기능 {
    public void 공학계산();
    public void 공학더하기();
}

class SimpleCaculatorV1 implements SimpleCaculator{
    public void 더하기(){}
}

class 공학계산기 implements 공학기능{
    SimpleCaculator simpleCaculator = new SimpleCaculatorV1();
	
    public void 공학계산(){}
    public void 공학더하기(){}
}

'스프링' 카테고리의 다른 글

[Spring] 동시성 문제 - ThreadLocal로 개선  (0) 2024.04.07
[Spring]IoC(Inversion of Control)와 DI  (0) 2024.03.28
스프링 - WebSocket  (0) 2024.03.07
[WAS] 쓰레드 풀  (0) 2024.03.07
[Query DSL] Query DSL 왜 쓸까?  (0) 2024.03.07