스프링을 활용한 전략 패턴

이상민·2022년 1월 30일
2
post-thumbnail

스프링 앱에 디자인 패턴 적용하기

  • 스프링 백엔드 개발자로서 디자인 패턴을 처음 공부하면 "그래서 이걸 어떻게 써야하지?"라는 고민이 가장 먼저 드는것 같다.

  • 이론적으로는 어떤게 구현해야하고, 무엇을 위한 패턴인지 이해하지만 어떻게 스프링에서 구현해야할지는 항상 고민이된다.

  • 오늘은 전략 패턴을 적용 해볼만한 상황을 겪게 되어 스프링에서 해당 패턴을 구현하는 방법을 정리한다.


전략 패턴 (Strategy Pattern)

하나의 메시지와 책임을 정의하고, 이를 수행할 수 있는 다양한 전략을 만든 후, 다형성을 통해 전략을 선택해 구현을 실행하는 패턴.


스프링을 활용한 전략 패턴 구현

예시 상황

  • 어플리케이션은 여러 리소스 타입을 가지고 있다.
  • 각 타입의 리스트를 조회하는 api를 만든다
GET /api/resources/{resourceType}
  • 간단한 예시를 위해서 리소스 타입은 사진과 영상만 있는 것으로 한다
public enum ResourceType {
    PICTURE, VIDEO;
}

1. 스프링을 활용하지 않은 전략 패턴

  • 스프링 컨텍스트를 활용하지 않는다면 리소스 타입에 따라 처리가 다른 코드를 다음처럼 구현할 수 있다
@RestController
public class ResourceController {

    @GetMapping("/api/resources/{resourceType}")
    public ResponseEntity<String> getResource(@PathParam ResourceType resourceType) {
        // ResourceService는 전략 인터페이스
        // PictureService와 VideoService는 전략 구현체이다
        ResourceService service = findResourceService(resourceType);
        service.getAll();
    }
    
    private ResourceService findResourceService(ResourceType type) {
        if (ResourceType.PICTURE.equals(type))
            return new PictureService();
        if (ResourceType.VIDEO.equals(type))
            return new VideoService();
            
        throw new IllegalArgumentException("서비스가 존재하지 않습니다");
    }
    
}
  • 서비스 객체들이 스프링 컨택스트에 의해 관리되지 않고 직접적으로 생성된다. ResourceService의 구현체가 다른 스프링 빈을 주입받을 수 없는 등 스프링을 쓰는 의미가 없다...

  • 객체를 컨트롤러에서 생성하므로 테스트도 매우 어려워진다

  • 객체는 이미 스프링이 만들어 주었으므로, 잘 골라서 사용하게만 해주자

2. 스프링 Bean List를 주입받기

  • 스프링은 개별 객체 뿐만 아니라 컴포넌트의 리스트를 Autowire하여 주입 해줄 수도 있다
@RestController
public class ResourceController {

    // PictureService와 VideoService 객체가 들어간다 
    private List<ResourceService> services;
    
    public ResourceController(List<ResourceService> services) {
        this.services = services;
    }

    @GetMapping("/api/resources/{resourceType}")
    public ResponseEntity<String> getResource(@PathParam ResourceType resourceType) {
        ResourceService service = findResourceService(resourceType);
        service.getAll();
    }
    
    private ResourceService findResourceService(ResourceType type) {
        // Bean 이름을 통해서 선택할 수도 있겠지만, 
        // 일단 서비스에 리소스 타입 필드가 있다고 가정
        return this.services.stream()
            .filter(service -> type.equals(service.getType))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("서비스가 존재하지 않습니다"))
    }
    
}
  • 스프링 컨텍스트를 활용할 뿐만 아니라 if-else문이 없어져 새로운 타입 추가시에도 ResourceController를 수정할 필요가 없다.

3. 전략을 별로 컴포넌트로 분리하기

  • 만약 다른 곳에서도 동적으로 ResourceService를 선택한다면 위 예시의 findResourceService() 메소드를 중복으로 작성해야한다.

  • 별도 컴포넌트로 분리해 재사용성과 의미 전달력을 높이자

@Component
public class ResourceServiceFactory {

    private List<ResourceService> services;
    
    public ResourceServiceFactory(List<ResourceService> services) {
        this.services = services;
    }
    
    public ResourceService findResourceService(ResourceType type) {
        return this.services.stream()
            .filter(service -> type.equals(service.getType))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("서비스가 존재하지 않습니다"))
    }
    
}

@RestController
public class ResourceController {

    private ResourceServiceFactory resourceServiceFactory;
    
    public ResourceController(ResourceServiceFactory resourceServiceFactory) {
        this.resourceServiceFactory = resourceServiceFactory;
    }

    @GetMapping("/api/resources/{resourceType}")
    public ResponseEntity<String> getResource(@PathParam ResourceType resourceType) {
        ResourceService service = resourceServiceFactory.findResourceService(resourceType);
        service.getAll();
    }
   
}

전략 패턴 주의 사항

  • 설명의 편의를 위해 리소스 타입을 2가지로 하는 예시를 작성했으나, 사실 2가지 밖에 안되는데 전략 패턴을 사용하는 것은 좋지 못하다

  • 클래스의 분리로 인한 프로젝트 구조의 복잡도 상승은 물론, 아무리 별로 컴포넌트로 빼 의미전달력을 높였다지만, 타입이 2가지 밖에 되지 않아 단순한 if-else문 보다는 가독성이 떨어진다

  • 뭔가 똑똑해 보이는것, 어려워 보이는것을 마구 적용하지 말고 실제로 적용 시 이득이 있는지를 먼저 따져보자. 엔지니어는 공수 대비 효용을 항상 생각해야한다.

profile
편하게 읽기 좋은 단위의 포스트를 추구하는 개발자입니다

0개의 댓글