Spring Interface 사용 이유

그렇다면 왜 예전부터 서비스에 인터페이스를 습관적으로 사용하였을까?

예전에는 Spring 에서 AOP Proxy 를 만드는 방식이 JDK Dynamic Proxy 를 사용하여 인터페이스 기반으로만 만들게 되어 있었다.

예를 들어, 인터페이스가 있어야지 @Transactional 이런 어노테이션이 동작이 가능하기 때문이다. (AOP Proxy 만들어서 트랜잭션을 처리하기 때문에)

특정 버전부터 CGLIB 라이브러리를 사용하여, 클래스 기반으로 AOP Proxy 를 만들도록 지원을 하게 되었다.

그래서 개발자는 AOP Proxy를 만드는 방식을 선택을 할 수 있게 되었다.

SpringBoot 에서는 디폴트로 클래스 기반으로 만들도록 아래와 같이 설정되어 있다.

  • true : CGLIB를 사용하여, 클래스를 상속받아 AOP Proxy를 만듬
  • false : JDK Dynamic Proxy를 사용하여 인터페이스를 데코레이션 해서 AOP Proxy를 만듬
spring.aop.proxy-target-class=true

결론적으로 인터페이스 사용하는 이유는?

OOP 관점에서 봤을때 인터페이스는 다형성(Polymorphism) 혹은 개방 폐쇄 원칙 때문에 사용한다. 보통 흔히 얘기하는 느슨한 결합 혹은 유연해지도록 하기 위해 사용한다고 봐야 한다.

그렇다면 Spring 에서 Service 는 다형성이 필요할까?

제 경험적으로 봤을때 다형성이 필요 없는 경우가 대부분이였던 것 같다. 사실 Service 보다 다른 클래스에서 다형성이 필요했던것 같다. 그리고 대부분의 Spring 프로젝트에서 Service 가 하나의 인터페이스에 하나의 구현체로 있는 것이 대부분이였다.

그래서 요즘은 나도 Service 에 인터페이스가 불필요 하면 사용하지 않고 있다. 다형성이 필요한 곳에만 인터페이스 사용한다.

다만 proxy-target-class=false 로 설정하면 어플리케이션이 동작하지 않는다는 찜찜함은 감수해야 한다.

* OCP (Open Closed Principle)

개방, 폐쇄 원칙이라고 하며 ‘소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.’는 프로그래밍 원칙입니다.

* 위 추상화를 통한 구현 방식의 단점

코드 구조가 복잡해지고, 복잡해진 구조 만큼 코드를 분석하고 확인하는 과정에서 인터페이스를 거쳐 구현체들을 확인해야 하는 번거로움이 생길 수 있습니다.

서비스 인터페이스

서비스 구현체 1

서비스 구현체 2

….

컨트롤러에서 특정 조건에 의해 동적으로 구현체를 선택하여 사용할 경우 팩토리 클래스를 만들어서 사용하면 된다

https://zorba91.tistory.com/306

예)

public interface FoodService {

    FoodType getFoodType();

    void deliverItem();
}
// 사탕, 과자, 라면, 초콜릿
public enum FoodType {
    CANDY, SNACK, NOODLE, CHOCOLATE
}
// 사탕 ServiceImpl
@Service
public class CandyServiceImpl implements FoodService {

    @Override
    public FoodType getFoodType() {
        return FoodType.CANDY;
    }

    @Override
    public void deliverItem() {
        System.out.println("사탕 배달 완료!");
    }
}

// 과자 ServiceImpl
@Service
public class SnackServiceImpl implements FoodService {

    @Override
    public FoodType getFoodType() {
        return FoodType.SNACK;
    }

    @Override
    public void deliverItem() {
        System.out.println("과자 배달 완료!");
    }
}

// 라면 ServiceImpl
@Service
public class NoodleServiceImpl implements FoodService {

    @Override
    public FoodType getFoodType() {
        return FoodType.NOODLE;
    }

    @Override
    public void deliverItem() {
        System.out.println("라면 배달 완료!");
    }
}

// 초콜릿 ServiceImpl
@Service
public class ChocolateServiceImpl implements FoodService {

    @Override
    public FoodType getFoodType() {
        return FoodType.CHOCOLATE;
    }

    @Override
    public void deliverItem() {
        System.out.println("초콜릿 배달 완료!");
    }
}
@Component
public class FoodServiceFactory {
    // foodService를 담고있어줄 Map
    private final Map<FoodType, FoodService> foodServices = new HashMap<>();

    // 생성자 주입으로 FoodService를 상속하고 있는 bean들을 주입받는다.
    public FoodServiceFactory(List<FoodService> foodServices) {
        // foodService를 상속받는 bean이 없을 경우 IllegalArguemntException을 던진다.
        if(CollectionUtils.isEmpty(foodServices)) {
            throw new IllegalArgumentException("존재하는 foodService가 없음");
        }

        // foodService의 구현체인 bean들을 for문을 돌리면서 key는 음식 종류의 타입, value는 해당 동일한 bean을 map에 담아준다.
        for (FoodService foodService : foodServices) {
            this.foodServices.put(foodService.getFoodType(), foodService);
        }
    }

    public FoodService getService(FoodType foodType) {
        // 인자로 넘겨준 타입에 맞는 foodService의 bean을 넘겨준다.
        return foodServices.get(foodType);
    }
}
@SpringBootTest
class FoodServiceFactoryTest {

    @Autowired
    private FoodServiceFactory foodServiceFactory;

    @Test
    void 음식_타입별로_서비스_가져오기() {
        // given
        FoodType candy = FoodType.CANDY;
        FoodType chocolate = FoodType.CHOCOLATE;
        FoodType snack = FoodType.SNACK;
        FoodType noodle = FoodType.NOODLE;

        // when
        FoodService candyService = foodServiceFactory.getService(candy);
        FoodService chocolateService = foodServiceFactory.getService(chocolate);
        FoodService snackService = foodServiceFactory.getService(snack);
        FoodService noodleService = foodServiceFactory.getService(noodle);

        // then
        assertThat(candyService.getFoodType(), is(FoodType.CANDY));
        assertThat(chocolateService.getFoodType(), is(FoodType.CHOCOLATE));
        assertThat(snackService.getFoodType(), is(FoodType.SNACK));
        assertThat(noodleService.getFoodType(), is(FoodType.NOODLE));

        // print
        candyService.deliverItem();
        chocolateService.deliverItem();
        snackService.deliverItem();
        noodleService.deliverItem();
    }
}