본문 바로가기
Spring

[Spring] ServiceLocatorFactoryBean을 활용하여 분기 처리하기

by 흑시바 2023. 2. 19.

🤜 문제점

개발하다 보면 분기 처리를 통해 특정한 경우에 따라 다르게 처리해야 하는 경우가 굉장히 많다.  🤔

예를 들면, 서비스 기획자가 이러한 요청을 한다.

 

" Color가 BLACK이라면 A 시바견을,

Color가 WHITE 라면 B 시바견을,

Color가 YELLOW 라면 C 시바견을,

그 외에는 D 시바견을 활용해서 처리해 주세요! "

 

이런 요청을 일반적인 조건문을 사용한 분기 처리를 통해 해결하면 다음과 같을 것이다. 

(실제로 프로젝트에 이런 코드가 많이 존재한다.)

public class ShibaService {

    public void shibaTalk(Color color) {
        if(color == Color.BLACK) {
            new ShibaA().talk();
        } else if(color == Color.WHITE) {
            new ShibaB().talk();
        } else if(color == Color.YELLOW) {
            new ShibaC().talk();
        } else {
            new ShibaD().talk();
        }
    }
}

 

이 코드의 문제점은 추가적인 요청이 있거나 변경이 있다면 관련 코드를 찾아서 수정해야 된다는 점이다.

예를 들어, Color가 추가되면서 ShibaZ()까지 확장된다고 하면 아마 계속해서 if문이 늘어나게 될 것이다.

 

이런 방식은 확장에는 열려있고 변경에는 닫혀있어야 한다는 OCP 원칙을 어기게 되며, 시스템이 확장되었을 때 유지보수를 어렵게 한다.

 

글의 예시는 단순한 형태지만 실제로 접했던 코드는 분기에 따라 다양한 Repository에 접근해서 여러 가지를 처리해야 하는 복잡한 구조였다. 그래서 'Repository를 주입받는 형태로 별도의 클래스로 분리하고 나중에 추가 요청이 있을 때 쉽게 확장하는 방법은 없을까?' 고민하게 되었다.

🤜 해결

필자가 찾은 해답은 ServiceLocatorFactoryBean이었다.

ServiceLocatorFactoryBean을 사용하면 특정 인터페이스에 해당하는 관련 클래스를 쉽게 찾아낼 수 있다.

해당 방식을 사용하면, 분기 처리하는 부분을 수정할 필요가 없고 쉽게 확장이 가능하다.

 

1. 모든 클래스를 하나의 인터페이스로 통일한다.

 

public interface Shiba {
    void talk();
}

 

2. 분류할 Enum 클래스를 생성한다.

 

public enum Color {
    WHITE, BLACK, YELLOW, OTHER;
}

 

3. 구현 클래스들을 모두 Bean으로 등록한다.

 

@Component("BLACK")
public class ShibaA implements Shiba {

    @Override
    public void talk() {
        System.out.println("Shiba A 입니다.");
    }
}
@Component("WHITE")
public class ShibaB implements Shiba {

    @Override
    public void talk() {
        System.out.println("Shiba B 입니다.");
    }
}
@Component("YELLOW")
public class ShibaC implements Shiba {

    @Override
    public void talk() {
        System.out.println("Shiba C 입니다.");
    }
}
@Component("OTHER")
public class ShibaD implements Shiba {

    @Override
    public void talk() {
        System.out.println("Shiba D 입니다.");
    }
}

 

@Component 어노테이션의 value 값은 Enum과 동일하게 작성한다.

 

4. Locator 전용 인터페이스를 생성하고 Bean으로 등록한다.

 

public interface ShibaLocator {
    Shiba findShiba(Color color);
}

 

Locator 전용 인터페이스의 메서드 이름은 어떠한 이름을 지어도 상관없다.

다만, 다른 사람이 코드를 봤을 때 최대한 이해하기 쉬운 이름으로 짓는 게 좋다.

 

@Bean
public ServiceLocatorFactoryBean shibaServiceLocator() {
    ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();
    factoryBean.setServiceLocatorInterface(ShibaLocator.class);
    return factoryBean;
}

 

ServiceLocatorFactoryBean을 신규 Bean으로 등록한다.

setServiceLocatorInterface에는 생성한 Locator 전용 인터페이스를 추가한다.

 

5. 테스트

 

@SpringBootTest
class ShibaLocatorTest {

    @Autowired
    ShibaLocator shibaLocator;

    @Test
    void findShiba() {
        Shiba shiba = shibaLocator.findShiba(Color.BLACK);
        shiba.talk();
    }
}

 

테스트를 해보면 Color Enum에 따라 해당하는 객체를 가져오게 된다.

 

 

6. 결과

 

@Service
@RequiredArgsConstructor
public class ShibaService {
    
    private final ShibaLocator shibaLocator;

    public void shibaTalk(Color color) {
        Shiba shiba = shibaLocator.findShiba(color);
        shiba.talk();
    }
}

 

처음에 if 문으로 분기처리 했던 코드와 비교해 보자.

훨씬 깔끔해졌고, 추가적인 요청이 있더라도 더 이상 해당 코드를 수정할 필요가 없어졌다!

앞으로 새로운 Shiba가 추가된다고 해도 Enum과 Shiba 인터페이스의 구현체만 추가하면 된다.

 

7. 주의사항

 

만약 Color Enum에 신규 상수를 추가하고, 관련 Shiba 구현체를 만들지 않은 상황에서 해당 상수로 메서드를 호출하면 NoSuchBeanDefinitionException 예외가 발생한다. 그러므로 예외 처리 로직을 추가해 주는 게 좋다.

 

@Service
@RequiredArgsConstructor
public class ShibaService {

    private final ShibaLocator shibaLocator;

    public void shibaTalk(Color color) {
        try {
            Shiba shiba = shibaLocator.findShiba(color);
            shiba.talk();
        } catch (NoSuchBeanDefinitionException ex) {
            System.out.println("존재하지 않는 Color 입니다.");
        }
    }
}

댓글