본문 바로가기
backend/Spring

[Spring] 스프링의 DI를 이용한 전략패턴 도입

by seongju.lee 2023. 6. 8.
프로젝트를 진행하다 보니, 여러 가지 상황을 겪게 된다.

이번에 경험한 것은 제목과 같이 프로젝트에 "전략패턴"을 구현함으로써 유연성을 향상시킬 수 있었던 이야기다.

 

상황은 아래와 같다.

 

문제 A: 그냥 읽기만 하면 되는 문제

문제 B: 객관식

위와 같이 두 문제가 있고, 각 문제의 유형은 다르다.

또한 각 문제마다 채점 방식이 다른데, 채점 방식은 아래와 같다.

- 문제 A는 읽기만 하고 [제출]을 누르면 Solve 처리가 된다.

- 문제 B는 읽고, 정확하게 번호를 고른 후 [제출]을 눌러야 Solve 처리가 된다.

 

이러한 상황에서 전략 패턴을 도입하기 전과 후 어떤 차이가 있고, 어떤 식으로 Spring에서 전략패턴을 구현할 수 있는지에 대해 정리해보고자 한다.

 

 

전략패턴이란?
알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 한다.
전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.
- "헤드퍼스트 디자인패턴"

 

전략 패턴 도입 전

- 문제 유형 별, 정답 유무를 체크하는 로직

- 위 코드는 문제 유형 별(problemType) 별로 정답 로직을 처리하는 부분이다.

만약 현재 요구사항에서 문제유형이 서술형, 단답형 등 다양하게 추가된다면 이 부분을 수정(조건문을 더 추가하는 등) 해야 한다.

 

 

- Client 코드

 

위 코드가 client 코드라고 가정해 보면, 현재 정답 확인을 위해서 모든 유형별 채점 방식이 구현되어 있는 SolveLogic에 의존적인 것을 확인할 수 있다.

(여기서 SolveLogic이라는 한 클래스에 채점 로직을 다 때려 박아놨지만,
만약 유형별로 클래스를 나눴다면 클라이언트 코드에서 여러 채점 로직 클래스에 의존하게 될 것이다.)

 


전략 패턴 도입 후

- 문제 유형 별, 정답 유무를 체크하는 로직

SolveLogic 이란 인터페이스
문제 A: 그냥 읽기만 하면 되는 문제의 정답체크 로직
문제 B 객관식: 정확히 답을 맞춰야 하는 정답체크 로직

- 가장 위 이미지처럼 SolveLogic이란 인터페이스를 두고, 각 문제유형에 따라 구현가능하게끔 checkAnswer라는 추상 메서드를 두었다.

- 그 아래 이미지처럼 문제 A, 문제 B에 대해서 로직을 구현.

 

 

 

- SolveLogic이란 전략(인터페이스)을 사용

(service 계층) - 뒤쪽에 코드에 대한 자세한 설명

- Client 코드

뒤쪽에 설명할 interface 구현체에 대한 의존성 주입을 위해 test 코드로 실행

  • client 코드를 잘 살펴보면 "전략패턴 전"의 코드와 다른 점이 보일 것이다.
    • Before 
      • 전략패턴 전에 구현한 코드는 SolveLogic 클래스 내부에 ProblemType에 따른 모든 로직이 들어있었다.
      • 그렇기 때문에 ProblemType을 추가하고, 그에 맞는 로직을 구현하려면 SolveLogic 클래스 내부를 수정했어야 했다.
    • After
      • 바뀐 현재 코드에서는 ServiceLayer 클래스에서 SolveLogic이란 추상화에 의존하여 ProblemType만 전달해주고,
        ProblemType에 따른 SolveLogic을 동적으로 수행한다.
      • 그러므로 앞으로 변경사항이 생긴다면 client로부터 각 로직(SolveLogic의 구현체)을 캡슐화한 상태로 수정해 나아갈 수 있게 된 것이다.

 

  • 정리하자면 아래와 같다.
    • ServiceLayer는 SolveLogic이란 인터페이스 의존함으로써, DIP를 준수한다.
    • 문제 유형에 따른 해답로직을 수정하거나, 추가하려면 client코드는 전혀 건드릴 필요 없이
      해답로직에 대한 구현 부분만 수정하거나 새로운 로직을 추가하면 된다.
      (아래 문단에서 코드에 대한 자세한 설명)

 

  • 그럼, 여기서 한 가지 의문이 든다.
    • 어떻게 클라이언트는 어떻게 각 문제에 따른 해답로직을 실행시킬 수 있는 것일까?
    • 인터페이스의 구현체를 동적으로 선택하기 위해서는 몇 가지 방법이 있는데,
      스프링은 Map으로 인터페이스의 모든 구현체를 주입받을 수 있다. 자세한 설명은 아래 섹션과 같다.

Spring의 DI를 이용하여 인터페이스의 모든 구현체를 주입받는 방법

위에서 작성한 ServiceLayer 클래스는 Map으로 모든 SolveLogic의 구현체를 주입받은 것이다.

스프링은 Set, List, Map을 통해서 관련된 인터페이스를 주입해 주곤 하는데 자세히 살펴보자.

 

 

  • 위 두 구현체들을 보면, @Component를 통해 자동으로 빈 등록을 해주고 있다.

 

 

  • 그리고, 서비스계층에 필드로 존재하는 Map을 보면 @RequiredArgsConstructor를 통해 자동으로 의존성 주입이 되고 있다.
  • 여기서 중요한 것은 현재 ServiceLayer는 Map으로 모든 SolveLogic을 주입받고 있다는 점인데,
    Map<String, SolveLogic>이 바로, Map의 키에 스프링 빈의 이름을 넣어주고 / 값에는 빈이 담긴다.
  • 바로 이 부분에 의해서 client 단에서는 동적으로 ProblemType에 따라 그에 맞는 해결로직을 실행할 수 있는 것이다.

 

 

여기서, 한 가지 더 보태어 설명하자면 client 단은 ProblemType에 맞는 해결로직을 선택하기 위해서 방안이 있어야 한다.

이때 enum을 통해 그 방안을 세울 수 있다.

enum을 통해 정책을 세울 때, logicName이라는 필드를 두어 ProblemType에 맞는 해결로직을 실행할 수 있게끔 하면 된다.

여기서 logicName의 값은 앞서 살펴본 Map의 value값인 Bean의 이름으로 하면 아래와 같이 client는 동적으로 해결로직을 선택하여 수행할 수 있게 된다.

 

 

  1. 현재 문제에 대한 유형인 problemType에 대응하는 logicName을 들고 온다. (=> problemType.getLogicName())
  2. Map에서 logicName에 대응하는 Bean을 조회한다. (=> solveLogicMap.get()
  3. 각 타입에 맞게 구현된 구체화 로직을 통해 정/오답을 판단한다. (=> solveLogic.checkAnswer())