스프링 백엔드 개발자로서 디자인 패턴을 처음 공부하면 "그래서 이걸 어떻게 써야하지?"라는 고민이 가장 먼저 드는것 같다.
이론적으로는 어떤게 구현해야하고, 무엇을 위한 패턴인지 이해하지만 어떻게 스프링에서 구현해야할지는 항상 고민이된다.
오늘은 전략 패턴을 적용 해볼만한 상황을 겪게 되어 스프링에서 해당 패턴을 구현하는 방법을 정리한다.
하나의 메시지와 책임을 정의하고, 이를 수행할 수 있는 다양한 전략을 만든 후, 다형성을 통해 전략을 선택해 구현을 실행하는 패턴.
GET /api/resources/{resourceType}
public enum ResourceType {
PICTURE, VIDEO;
}
@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의 구현체가 다른 스프링 빈을 주입받을 수 없는 등 스프링을 쓰는 의미가 없다...
객체를 컨트롤러에서 생성하므로 테스트도 매우 어려워진다
객체는 이미 스프링이 만들어 주었으므로, 잘 골라서 사용하게만 해주자
@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("서비스가 존재하지 않습니다"))
}
}
ResourceController
를 수정할 필요가 없다.만약 다른 곳에서도 동적으로 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문 보다는 가독성이 떨어진다
뭔가 똑똑해 보이는것, 어려워 보이는것을 마구 적용하지 말고 실제로 적용 시 이득이 있는지를 먼저 따져보자. 엔지니어는 공수 대비 효용을 항상 생각해야한다.