spring - multiple implements : interface - 인터페이스의 구현체를 동적으로 선택하는 방법

동기

본 글은 interface 란? spring 주입받아 사용하기 에서 이어집니다.

동적으로 구현체를 선택하는 방법을 알아보자

여러 판매자가 판매하는 상품을 장바구니에 담고 결제를 하였다고 가정해보자.
각 판매자에게 구입 요청을 해야할것이다.

이때 판매자에게 할수 있는 행위들이 interface로 정의되어있고, 실제로 요청해야하는 판매자에 따라서 구현체를 바꾸어야한다면?? 어떻게 될까? 예시를 확인해보자

ex)

1
2
3
라면 + 노트북을 장바구니에넣고 한번에 주문 -> 내부적으로는 오뚜기 / LG전자에 구매 요청을 해야야한다
주문, 주문취소, 환불이라는 행위자체는 동일하지만 요청해야하는 도메인정보등 세부정보는 다를것이다
그리고 이공통점을 판매자라는 인터페이스로, 달라지는 세부적인 부분을 오뚜기/LG전자라는 구현체로 구현했다면?

각 판매자에 해당하는 구현체를 선택하여 요청 로직을 실행해야할것이다.

인터페이스 구현

1
2
3
interface SampleInterface {
...
}
1
2
3
4
@Repository
public class SampleInterfaceImpl implements SampleInterface {
...
}
1
2
3
4
@Repository
public class SampleInterfaceOtherImpl implements SampleInterface {
...
}

모든 인터페이스를 선언해놓고 분기처리하여 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class SampleController {
@Autowired
private SampleInterfaceImpl basic;

@Autowired
private SampleInterfaceOtherImpl other;

@RequestMapping(path = "/path/Basic", method = RequestMethod.GET)
public void basic() {
basic.sampleMethod();
}

@RequestMapping(path = "/path/Other", method = RequestMethod.GET)
public void other() {
other.sampleMethod();
}
}

인터페이스를 Map에 넣어두고 꺼내서 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
public class SampleController {
@Autowired
private SampleInterfaceImpl basic;

@Autowired
private SampleInterfaceOtherImpl other;

Map<String, SampleInterface> services;

@PostConstruct
void init() {
services = new HashMap()<>;
services.put("Basic", basic);
services.put("Other", other);
}

@RequestMapping(path = "/path/{service}", method = RequestMethod.GET)
public void method(@PathVariable("service") String service){
SampleInterface sample = services.get(service);
// remember to handle the case where there's no corresponding service
sample.sampleMethod();
}
}

ApplicationContext 를 활용하는 방법

구현체를 bean으로등록하고

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class SampleInterfaceImpl implements SampleInterface {
public void sampleMethod() {
// ...
}
}

@Component
public class SampleInterfaceOtherImpl implements SampleInterface {
public void sampleMethod() {
// ...
}
}

ApplicationContext 의 getBean() 으로 구현체를 로딩하여 사용한다.

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class SampleController {
@Autowired
private ApplicationContext appContext;

@RequestMapping(path = "/path/{service}", method = RequestMethod.GET)
public void method(@PathVariable("service") String service){
SampleInterface sample = appContext.getBean(service, SampleInterface.class);
sample.sampleMethod();
}
}

Spring의 DI의 도움을 받는 방법

set이나list, map을 통해 자동으로 등록해준다

SampleInterface를 구현한 모든 인터페이스를 리스트에 주입

1
2
@Autowired
private List<SampleInterface> SampleInterfaces;

SampleInterface를 구현한 모든 인터페이스를 맵에 주입한다. key는 구현체의 bean name.

1
2
@Autowired
private Map<String, SampleInterface> SampleInterfaceMap;

그리고 사용하는곳에서는 맵에서 bean name을통해 선택하여 사용 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void excuteSampleMethod() {
String interfaceName = ""

//enum으로 구분하거나, 조건에 맞게 수동으로분기하거나 서비스에맞게 고른다.
if (isFirst()) {
interfaceName = "SampleInterfaceImpl"
} else {
interfaceName = "SampleInterfaceOtherImpl"
}

SampleInterface impl = SampleInterfaceMap.get(interfaceName);
impl.sampleMethod();
}

enum으로 정책정하기

위처럼 분기처리하는대신 정책을 정리하는 enum을 두는것도 하나의 방법이다
먼저 동적으로 달라지는 종류를 enum으로 정의하고, 구현체의 이름을 적어준다

1
2
3
4
5
6
7
8
9
10
11
12
public enum SampleType {
SAMPLE("SampleInterfaceImpl"),
OTHERS("SampleInterfaceOtherImpl")

SampleType(String implementation) {
this.implementation = implementation;
}

public String getImplementation() {
return this.implementation;
}
}

이후 선택된 type에 맞게 구현체를 선택하여 기능을 수행한다.

1
2
3
4
5
void excuteSampleMethod(SampleType sampleType) {    

SampleInterface impl = SampleInterfaceMap.get(sampleType.getImplementation);
impl.sampleMethod();
}

내가 선택한 방법

마지막에 소개한 Spring의 DI의 도움을 받는 방법 으로 구현하였다.
실제로는 로직이 필요하지는 않고 특정 데이터의 종류에 맞게 구현체를 선택만 하면 되는 스팩이었다
enum으로 정책을 한곳에서 관리해도되지만, 특정조건을 검사하는 책임각 구현체들이 검사하는것이 더 맞다고 생각하였다.

각 구현체에서는 자신을 실행할수 있는지 여부를 검사하는 책임을 가진다

1
2
3
4
5
public class SampleInterfaceImpl implements SampleInterface {
@Overide
public boolean isAvailableType(SampleType){
}
}

그리고 중간에서 중계해주는 Router를 하나 만들어 주었다

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@RequiredArgsConstructor
public class SampleInterfaceRouter {

private final List<SampleInterface> sampleInterfaces; //의존성 List로 주입

public SampleInterface getImplemetationByType(SampleType sampleType) {
return sampleInterfaces.stream()
.filter(e -> e.isAvailableType(sampleType)) //각 구현체에서 판단
.findFirst().orElseThrow(() -> new NotSupportedTypeException());
}
}

사용은 아래와같이 하면된다

1
2
3
4
5
6
7
8
@Autowired
SampleInterfaceRouter sampleInterfaceRouter;

void excuteSampleMethod(SampleType sampleType) {

SampleInterface impl = sampleInterfaceRouter.getImplemetationByType(sampleType);
impl.sampleMethod();
}

참고자료

https://stackoverflow.com/a/19027319
https://stackoverflow.com/a/37413949
https://stackoverflow.com/a/37408117
https://stackoverflow.com/q/10534053

Comments