안녕하세요, Devloo입니다. 🙂 이번 시간에는 서비스 이름을 키로 하고 실제 서비스 인스턴스를 값으로 가지는 Map을 활용한 의존관계 자동 주입 방법을 알아보려고 합니다. 이 방법은 표준 팩토리 패턴을 대체하여 조회 목적에 매우 유용합니다. 예제를 통해 이 기법을 더 명확하게 이해할 수 있으니, 바로 시작해 보겠습니다. 🙂
이 기법의 사용 사례는 공통 추상 클래스를 구현하는 여러 구체 클래스들이 있는 설정입니다. 클래스 계층 구조는 다음과 같습니다.
각 구체 클래스는 특정 기능을 구현합니다. 이 클래스들의 진입점은 기본 서비스에서 추상 메서드로 정의된 메서드 집합입니다. 따라서 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 인스턴스를 얻을 수 있습니다.
이 방법은 효과적이며 원하는 대로 작동합니다. 그렇다면 왜 이 글에서 대안적인 방법을 논의하는 것일까요? 새로운 서비스 클래스인 TypeDService
를 구현하려는 경우를 예로 들어보겠습니다.
TypeDService
라는 새로운 클래스와 관련 기능을 구현하고,util/ServiceConstants.java
에 서비스 이름에 대한 상수 값을 추가합니다.- 서비스 팩토리에 새로운 서비스를 자동으로 주입하는 줄을 추가합니다.
- 서비스 팩토리의
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을 활용한 의존관계 자동 주입의 방법에 대해 알아보았습니다.
혹시 궁금하신 점은 편하게 댓글 남겨주세요 🙂 끝까지 읽어주셔서 감사합니다. (_ _)