본문 바로가기
DesignPattern

Spring Bean을 이용한 팩토리 메서드 패턴으로 객체 분리하기

by 발개발자 2024. 4. 7.
반응형

일이 너무 많아 오랜만에 글을 쓰는 것 같다...

새롭게 배운 내용도 엄청 많고 정리해야할 내용도 엄청 많은데 바쁘고 귀찮다는 핑계로 미뤄두다가 오랜만에 써보게 되는 것 같다. 후후... 

 

그러면 간만에 리팩토링한 내용을 정리해보려고 한닷.

ㄱㅈㅇ!!

 

 

문제상황

기존의 코드는 아래와 같다. 

Mybatis에서 Union All을 통해 여러가지 쿼리가 조건에 따라 합쳐져서 보여지고 있는 형태이다. ㅂㄷㅂㄷ...

SELECT ... FROM Table1 
JOIN ...
WHERE ...
<IF test>
UNION ALL
SELECT ... FROM Table2 
JOIN ...
WHERE ...
</IF test>
<IF test>
UNION ALLSELECT ... Table3
JOIN ...
WHERE ...
UNION ALL ...
</IF test>

 

 

위의 코드에서 여러 문제점이 발생하게 된다. 

 

1. 너무 복잡도가 너무 높다.

한방의 쿼리에 모든 로직을 넣으려고 하니, 디버깅이 어려운건 당연하고 쿼리자체가 엄청 복잡해지고 가독성이 낮아진다. 따라서 유지보수하기 어려운 코드가 된다.

 

2. 유연성도 부족이 부족하다. 한방쿼리로 구현되어 있다보니, 요구조건에 따라 커스텀하기가 굉장히 까다롭다. 

 

3. 모든 로직을 쿼리로 작성하니 소중한 DB의 리소스를 많이 점유할 수 있고 최적화가 안되어 있는 경우, 성능이슈가 발생할 가능성도 높다.

 

눈앞에 이런 한방쿼리를 바라보니 순간 아찔해졌을 따름이다. 이 난관을 어떻게 헤쳐가야하나 고민해보니 그래도 어느정도 규칙성을 찾을 수 있었다.

 

각각의 Union All 부분은 모두 분리할 수 있고 조건에 따라 필요한 내용만 선택해서 보여주면 된다는 점이었다.

그래서 팩토리 메서드 패턴을 통해 어느정도 리팩토링이 가능하다고 생각했다.

 

팩토리 메서드 패턴은 아래 블로그에 아주 친절하고 자세하게 설명되어 있다.

💠 팩토리 메서드(Factory Method) 패턴 - 완벽 마스터하기 (tistory.com)

 

정석적인 팩토리 메서드 패턴은 아니지만, 파라미터에 따라 다른 인스턴스를 반환해주는 형태의 간단한 팩토리 메서드 패턴을 통해 리팩토링을 구현해보고자 한다.

리팩토링 코드는 기억에 의존하여 라이브로 작성되어 정확하지 않을 수 있다... -_-;;

 

 

리팩토링

Union All을 분리하여 각각의 Provider로 분리 

public interface ResultProvider {
 public Type getType(Type type);
 
 public List<DTO> search(SearchOption searchOption);
}

@Component
public class Test1ResultProvider implements ResultProvider{
 public Type getType(){
   return Enum.type;
 }
 
 public List<DTO> search(SearchOption searchOption){
   repository.find(searchOption);
   .. 로직 수행
 }
}


@Component
public class Test2ResultProvider implements ResultProvider{
 public Type getType(){
   return Enum.type2;
 }
 
 public List<DTO> search(SearchOption searchOption){
   repository.find(searchOption);
   .. 로직 수행
 }
}

..

 

 

각각의 Provider 인스턴스를 관리하고 반환해주는 Factory 객체 생성

@Component
public class ResultProviderFactory {

    private final Map<Type, ResultProvider> providerMap = new HashMap<>();

    public ResultProviderFactory(List<ResultProvider> resultProviders) {
        pointCalculateServices.forEach(s -> providerMap.put(s.getType(), s));
    }

    public ResultProvider getResultProvider(Type type) {
        ResultProvider resultProvider = providerMap.get(type);
        if(resultProvider == null){
          throw new IllegalArgumentException("정의되지 않은 검색유형입니다.");
        }
        return resultProvider;
    }
}

 

 

서비스단에서 Factory 의존

@Service
public class ResultService {

  private final ResultProviderFactory ResultProviderFactory;
  
  public List<DTO> find(SearchOption searchOption){
    ResultProvider resultProvider = ResultProviderFactory.get(searchOption.getType());
    
    return resultProvider.search(searchOption);
  }
}

 

 

정리

ResultProvider 인터페이스를 생성한 후, 구현체들을 작성한다.

여기서 구현체들은 한방쿼리에서 Union All이 분리가 되어 하나하나의 조회 결과를 담당하는 객체이다.

그리고 구현체 Provider들을 Spring의 Bean으로 등록해놓고, 사용하는 인스턴스 반환을 담당하는 Factory에서 List형태로 주입받는다.

그리고 각 Provider에서 담당하는 Type을 key값으로 맵에 저장해놓고 그 Value로 객체들을 저장해놓는다.

이제 Service단에서 Factory를 의존하고 Factory에서 조건에 따라 알맞은 Provider를 반환하여 유연하게 로직수행이 가능해진다.

 

위와 같이 리팩토링을 하였을 때, 얻는 이점은 다음과 같다.

 

1. 한방쿼리를 각각 쿼리가 어떤결과를 조회하는지 클래스로 분리하고 클래스내부에서 로직을 처리하여 디버깅도 수월해지고 가독성도 좋아져 유지보수하기 수월해졌다.

 

2. 유연성이 개선되었다. 쿼리를 쪼개고 각각의 클래스에서 담당하게 변경하고 클래스내부에서 로직을 처리하니, 각각의 클래스에서 로직을 수정하기 굉장히 수월해졌다.

 

3. 쿼리를 최대한 단순화하고, 로직을 클래스에서 처리하니 DB의 부하가 훨씬 가벼워졌다.

 

즉, 한방쿼리의 문제를 모두 개선할 수 있었다. 

 

그런데 여기서 의문이 생겼다

의문상황

아래와 같이 코드를 작성할 경우, 전략패턴으로 변하는 것인가에 대한 고민이었다.

 

 

@Service
public class ResultService {

    private final List<ResultProvider> resultProviders;

    public List<DTO> find(SearchOption searchOption) {
        return resultProviders.stream()
                .filter(provider -> provider.getType() == searchOption.getType())
                .findFirst()
                .map(provider -> provider.search(searchOption.getType()))
                .orElseThrow(() -> new IllegalArgumentException());
    }
}

 

ResultService에서 ResultProvider들을 주입받고 여기서 검색 조건에 따라 원하는 응답객체의 값을 반환해주는 것이다.

결과적으로 위와 같이 작성하면 전략패턴이 맞다는 것이다.

 

팩토리 메서드 패턴은 객체의 생성을 담당하는 패턴이고 전략패턴은 객체의 행위를 담당하는 패턴이다.

즉, 위의 코드를 전략패턴으로 대입하면 아래와 같이 구성을 이룰 수 있다.

 

Strategy: ResultProvider interface

Concrete Strategy: Test1ResultProvider and Test2ResultProvider

Context: ResultService

Client: The code that interacts to ResultService.

 

ResultService가 find하는 행위에 대해 조건에 따라 다른 전략으로 행위를 바꾸기 때문에 전략패턴이 되는 것이다.

 

근데 이렇게 되면 최초에 작성한 코드도 단순 팩토리 메서드 패턴만 사용한 것이 아닌, 전략패턴도 같이 사용된 코드라는 결론이 도출된다.

 

ResultProvider객체를 가져오기 위해 팩토리 메서드 패턴이 사용되었고, find 행위에서 조회옵션에 따라 알맞은 ResultProvider 전략을 선택하여 결과를 반환하게 되어 전략패턴이 사용된 것이다.

 

각각 패턴이 어떤 행위를 하는지 집중하면 헷갈린 부분이 조금 해소되는 것 같다.

그럼 오늘은 여기까지 정리하고 마무리하려 한다.

 

끗!

반응형

댓글