Spring

Spring Boot: Map을 활용한 의존관계 자동 주입 (팩토리 패턴 대체하기)

Written by 개발자서동우 · 59 sec read >
Spring Boot Map을 활용한 의존관계 자동 주입

안녕하세요, Devloo입니다. 🙂 이번 시간에는 서비스 이름을 키로 하고 실제 서비스 인스턴스를 값으로 가지는 Map을 활용한 의존관계 자동 주입 방법을 알아보려고 합니다. 이 방법은 표준 팩토리 패턴을 대체하여 조회 목적에 매우 유용합니다. 예제를 통해 이 기법을 더 명확하게 이해할 수 있으니, 바로 시작해 보겠습니다. 🙂

이 기법의 사용 사례는 공통 추상 클래스를 구현하는 여러 구체 클래스들이 있는 설정입니다. 클래스 계층 구조는 다음과 같습니다.

Spring Boot 맵을 활용한 의존관계 자동 주입#1
Spring Boot 맵을 활용한 의존관계 자동 주입#1

각 구체 클래스는 특정 기능을 구현합니다. 이 클래스들의 진입점은 기본 서비스에서 추상 메서드로 정의된 메서드 집합입니다. 따라서 BaseService 타입의 변수를 사용할 때, 인스턴스화된 클래스가 이러한 추상 메서드를 구현했음을 알고 그 메서드를 호출할 수 있습니다.

이러한 설정이 필요한 일반적인 시나리오는 다음과 같습니다.

  • 이벤트 핸들러: 각 서비스 클래스는 특정 유형의 이벤트를 처리하는 책임을 집니다.
  • 파일 처리: 파일을 처리할 때 서비스는 파일 내의 식별자, 엔티티 또는 섹션을 처리하는 데 사용될 수 있습니다.
  • 사용자 상호작용: 클래스는 시스템과의 다양한 사용자 상호작용을 처리하도록 설정될 수 있습니다. 이는 사용자가 생성한 이벤트 핸들러의 한 형태입니다.

그렇다면 이러한 클래스의 예시는 어떻게 생겼을까요? 다음과 같은 형태일 수 있습니다.

public abstract class BaseService {

    public abstract String printAndGetGreeting(String value);

    protected void print(final String message) {
        System.out.println(message);
    }
}

이 예제에서는 printAndGetGreeting이라는 추상 메서드와 print라는 구현 메서드가 정의되어 있습니다.

이 클래스를 구현한 예제 클래스는 다음과 같습니다.

@Service(ServiceConstants.TYPEA)
public class TypeAService extends BaseService {

    @Override
    public String printAndGetGreeting(String value) {
        String message = "서비스 타입 A에서: " + value;
        print(message);
        return message;
    }
}

여기서는 @Service 애노테이션에 추가된 문자열 식별자에 주목하세요. 이를 통해 서비스 이름을 맞춤 설정할 수 있습니다. 예제에서는 두 개의 하위 클래스를 더 생성합니다.

@Service(ServiceConstants.TYPEB)
public class TypeBService extends BaseService {

    @Override
    public String printAndGetGreeting(String value) {
        String message = "서비스 타입 B에서: " + value;
        print(message);
        return message;
    }
}

구현 방식에 약간의 차이가 있습니다.

@Service(ServiceConstants.TYPEC)
public class TypeCService extends BaseService {

    @Override
    public String printAndGetGreeting(String value) {
        String message = "서비스 타입 C에서: " + value;
        print(message);
        return message;
    }
}

이로써 구현 클래스의 예제가 모두 마무리되었습니다.

전통적인 방법

위의 서비스 집합이 추상 기본 서비스를 구현한다는 점을 고려하여, 먼저 팩토리 클래스를 사용하는 전통적인 방법을 보여드리겠습니다.

@Service
@AllArgsConstructor
public class ServiceFactory {

    private final TypeAService typeAService;
    private final TypeBService typeBService;
    private final TypeCService typeCService;

    public BaseService getService(final String serviceName) {
        if (ServiceConstants.TYPEA.equalsIgnoreCase(serviceName)) {
            return typeAService;
        } else if (ServiceConstants.TYPEB.equalsIgnoreCase(serviceName)) {
            return typeBService;
        } else {
            return typeCService;
        }
    }
}

이 팩토리 클래스는 각 서비스를 자동으로 주입합니다. 이를 위해 Lombok의 @AllArgsConstructor를 추가하고, 서비스를 private final로 선언합니다. 그런 다음 getService라는 팩토리 메서드를 구현하여 서비스 이름에 따라 해당 서비스를 반환합니다. 예를 들어, “typea”를 전달하면 TypeAService 인스턴스를 얻을 수 있습니다.

Spring Boot 맵을 활용한 의존관계 자동 주입#1
Spring Boot 맵을 활용한 의존관계 자동 주입#2

이 방법은 효과적이며 원하는 대로 작동합니다. 그렇다면 왜 이 글에서 대안적인 방법을 논의하는 것일까요? 새로운 서비스 클래스인 TypeDService를 구현하려는 경우를 예로 들어보겠습니다.

  1. TypeDService라는 새로운 클래스와 관련 기능을 구현하고, util/ServiceConstants.java에 서비스 이름에 대한 상수 값을 추가합니다.
  2. 서비스 팩토리에 새로운 서비스를 자동으로 주입하는 줄을 추가합니다.
  3. 서비스 팩토리의 getService 메서드에 “typed” 문자열을 검색하고 자동 주입된 TypeDService 인스턴스를 반환하는 새로운 로직 분기를 추가합니다.

새로운 서비스를 추가한 후 팩토리 클래스는 다음과 같이 보일 것입니다.

@Service
@AllArgsConstructor
public class ServiceFactory {

    private final TypeAService typeAService;
    private final TypeBService typeBService;
    private final TypeCService typeCService;
    private final TypeDService typeDService;

    public BaseService getService(final String serviceName) {
        if (ServiceConstants.TYPEA.equalsIgnoreCase(serviceName)) {
            return typeAService;
        } else if (ServiceConstants.TYPEB.equalsIgnoreCase(serviceName)) {
            return typeBService;
        } else if (ServiceConstants.TYPEC.equalsIgnoreCase(serviceName)) {
            return typeCService;
        } else {
            return typeDService;
        }
    }
}

이 접근 방식의 단점은 새로운 서비스 클래스를 구현할 때마다 동일한 세 가지 단계를 반복해야 한다는 점입니다. 또한, 20개 또는 30개의 서비스 클래스가 있다면, ServiceFactory는 다소 비대해질 수 있습니다.

새로운 서비스를 구현하는 단계를 하나로 줄일 수 있다면 어떨까요? 그 방법을 알아보겠습니다.

Map을 활용한 자동 주입

다음과 같은 새로운 버전의 팩토리로 시작해보겠습니다.

@Service
@AllArgsConstructor
public class MainService {

    // 이 맵은 Spring에 의해 자동으로 주입됩니다.
    private final Map<String, BaseService> serviceMap;

    public BaseService getService(final String serviceName) {
        return serviceMap.get(serviceName);
    }
}

이 클래스는 문자열을 키로 하고 BaseService 타입의 값을 가지는 맵을 포함하고 있습니다.

이 클래스가 스프링에 의해 생성될 때 매우 유용한 일이 발생합니다. serviceMap 변수는 BaseService를 구현하는 모든 클래스로 자동으로 채워집니다. 키는 자동으로 서비스 이름으로 설정됩니다. 따라서 우리의 맵은 다음과 같은 형태가 됩니다.

여기서 키는 우리가 @Service 어노테이션에 추가한 서비스 이름으로 설정됩니다. 그러나 지금까지의 코드는 전통적인 방법과 크게 다르지 않습니다. 서비스 획득을 위한 일련의 if/else 문 대신 맵 조회를 사용한다는 점만 다릅니다. 그렇다면 새로운 서비스인 TypeDService를 구현하려면 어떤 단계를 거쳐야 할까요? 필요한 것은 단 한 가지뿐입니다.

TypeDService라는 새로운 클래스와 관련 기능을 구현하고, util/ServiceConstants.java에 서비스 이름에 대한 상수 값을 추가합니다.

이것이 전부입니다. 새로운 클래스를 구현하고 코드를 사용할 수 있게 만드는 데 필요한 단계는 이 한 가지뿐입니다. 이 단계는 실제 새로운 로직 구현과 관련이 있으며, 서비스 이름으로 조회하는 것과는 아무런 관련이 없습니다.

위의 단계를 따라 새로운 TypeDService를 추가하면, 생성 시 맵에 자동으로 새 서비스가 포함됩니다.

이제 우리의 맵에 새로운 서비스가 추가되어 서비스 이름으로 조회할 수 있게 되었습니다.

이 기법은 이벤트 핸들러나 태그 핸들러 시나리오에서 유용할 수 있습니다. 이벤트를 수신할 때 해당 이벤트를 처리하는 서비스를 구현합니다. 새로운 이벤트가 정의되면 새로운 핸들러 서비스를 구현하고, 이 방식으로 서비스를 명명하면 자동으로 이벤트 이름에 매핑됩니다. 이는 유용한 시나리오의 한 예에 불과합니다.

이 기술은 요구 사항이 변경될 가능성이 있을 때 매우 유용할 수 있습니다. 예를 들어, 이벤트 처리, XML, JSON 또는 기타 유형의 파일에서 다양한 태그 처리, 애플리케이션 활동 처리 등이 있습니다. 이러한 상황에서는 서비스의 빈번한 또는 주기적인 추가가 필요할 수 있습니다.

첨부된 소스를 다운로드하여 직접 시도해 볼 수 있습니다. 샘플 애플리케이션에는 전통적인 방법과 자동 주입 방법을 비교하기 위한 두 개의 REST 컨트롤러가 있습니다.

전통적인 엔드포인트는 다음과 같습니다.

http://localhost:8080/api/old/map?service=typea&message=hello%20from%20A

자동 주입 엔드포인트는 다음과 같이 구현됩니다.

http://localhost:8080/api/map?service=typea&message=hello%20from%20A

이 엔드포인트들을 통해 각 옵션을 테스트하고 결과를 확인할 수 있습니다. 실제 효과를 확인하려면 예제에서 보여준 대로 새로운 서비스를 추가하고, 추가적인 코드 수정 없이 맵에 자동으로 추가되는 것을 확인해 보세요.

요약

이 글에서는 자동으로 주입 가능한 서비스 맵을 생성하는 유용한 스프링 기법을 소개했습니다. 전통적인 팩토리 방법과 서비스 맵의 자동 주입 방법을 비교해 보았고, 이 기법이 유용할 수 있는 몇 가지 시나리오를 다루었습니다. 이 기법은 적절한 요구 사항이 있을 때 사용할 수 있는 바람직한 구현 옵션으로 확실히 자리 잡았습니다.

이번 시간에는 Map을 활용한 의존관계 자동 주입의 방법에 대해 알아보았습니다.

혹시 궁금하신 점은 편하게 댓글 남겨주세요 🙂 끝까지 읽어주셔서 감사합니다. (_ _)

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *