서비스 구현 시 인터페이스를 구현하는 형태로 하는 이유 (spring AOP)

ssongkim·2022년 5월 17일
8
post-custom-banner

우리는 스프링으로 개발하며 관행적으로 서비스를 구현할 때 인터페이스를 구현하는 구조로 개발을 해왔습니다.
왜 그렇게 해왔을까요, 의문점을 해결해보려 그 이유를 공부해보았으며 크게 2가지 이유가 있음을 알게되었습니다.

1. spring AOP 관점

이는 spring AOP와 관련이 깊습니다.

Spring AOP는 빈 등록 시 사용자의 특정 메서드 호출 시점에 AOP를 수행하는 Proxy Bean을 생성해주며 크게 2가지 프록시 객체 생성 방법이 존재합니다.

JDK Dinamic Proxy는 프록시 객체 생성 시 인터페이스 존재가 필수적이고 CGLib은 프록시 객체 생성 시 인터페이스가 존재하지 않아도 클래스 기반으로 프록시 객체를 생성할 수 있습니다. 두 가지의 자세한 차이점은 이전 블로그 게시글을 참고해주세요. (자세히 알아보기: JDK Dynamic Proxy와 CGLib를 알아보자 #2)

옛날옛적 스프링

옛날옛적 spring framework에서는 spring AOP 사용 시 CGLib의 여러 문제점으로 인해 JDK Dinamic Proxy을 사용하는 것을 권장하였다고 합니다. (spring.aop.proxy-target-class=false)

당시 CGLib 문제점

  • net.sf.cglib.proxy.Enhancer 의존성 추가
  • default 생성자가 무조건 필요함
  • 타깃의 생성자 두 번 호출

JDK Dinamic Proxy는 인터페이스를 기반으로 프록시 객체를 생성합니다.
반드시 인터페이스가 존재해야 프록시 객체를 정상적으로 만들어 낼 수 있으므로 인터페이스를 구현한 서비스 형태가 관행적으로 내려왔다고 생각합니다.

하지만 요즘 spring boot는 인터페이스를 구현한 클래스임에도 프록시 객체 생성 시 CGLib를 사용합니다.

spring 3.2

spring 3.2 이후부터는 CGLib의 위에서 언급한 문제점이 외부 라이브러리의 도움을 받아 개선이 이루어졌다고 판단되어 spring core 패키지에 포함되었고 spring boot 사용 시 인터페이스를 구현한 클래스여도 JDK Dynamic Proxy보다 성능이 좋은 CGLib를 디폴트(spring.aop.proxy-target-class=true)로 사용하여 프록시 객체를 생성합니다.

@Autowired
private UserService userService;

@Test
void printClass() {
	System.out.println(userService.getClass());
}
class com.example.aop.cglib.test.service.UserServiceImpl$$EnhancerBySpringCGLIB$$ade10628

UserServiceImpl는 인터페이스를 구현하는 형태로 구현하였음에도 CGLib을 사용하는 것을 확인할 수 있습니다.

만약 직접 JDK Dynamic ProxyCGLib를 활용한 프록시 객체 생성을 눈으로 확인해보고 싶다면 application.propertiesspring.aop.proxy-target-class=false(CGLib 사용하지 않음), spring.aop.proxy-target-class=true(CGLib 사용 - 디폴트)로 확인해주세요.

그래서 현 시점에는 spring boot를 사용한다면 디폴트로 CGLib를 사용하므로 spring AOP 관점에서 서비스 구현 시 인터페이스를 사용하는 이유는 없어졌다고 생각합니다.

2. OOP 관점

스프링을 배제하고 OOP관점에서 인터페이스를 구현하는 방식으로 서비스를 작성하는 것이 맞다고 보는 사람들도 많습니다.

개방 폐쇄의 원칙(OCP)

소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려있고, 변경에는 닫혀있어야 한다.

SOLID 5원칙 중 하나인 OCP는 특정 소스코드의 변경으로 인해 다른 클래스가 수정되는 일이 일어나는 일에 대해서 폐쇄적으로 대응해야 한다고 말합니다.

의존관계 역전의 원칙(DIP)

저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 하고 그 반대가 되면 안된다

Robert C. Martin - Clean Architecture

ServiceImplService 인터페이스의 구현체이므로 Service에 대해 Compile 의존성을 가지지만 인터페이스인 Service는 반대로 ServiceImpl에 대해 Compile 의존성을 가지지 않습니다. 구현체가 어떻게 변경되어도 인터페이스는 그것을 신경쓰지 않아도 됩니다.

로버트 마틴의 Clean Arichitecture 18장을 보면 그림에서 경계를 기준으로 왼쪽을 고수준(High level) 컴포넌트, 오른쪽을 저수준(Low level) 세부구현이라고 표현합니다.
고수준 컴포넌트는 잦은 변경에 노출되어 있는 저수준 세부사항에 의존해서는 안되며 저수준 세부사항이 고수준 컴포넌트에 의존성을 가져야 한다고 설명합니다.

만약 고수준 컴포넌트가 저수준 컴포넌트를 의존하는 경우, 자주 변경이 일어나는 저수준 컴포넌트의 변경으로 인해 고수준 컴포넌트가 변경이 일어나므로 개방 폐쇄의 원칙을 어기게 됩니다.

OCP와 DIP를 지키지 않은 예제

일단 OCP와 DIP 객체지향원칙을 지키지 않은, 인터페이스를 구현하지 않는 형태의 예제를 보도록 하겠습니다.


@Service
public class UserServiceA {
	...
    
	@Transactional
    public void signup(..) {
    	...
	}
}
@Service
public class UserServiceB {
	...
    
	@Transactional
    public void signup(..) {
    	...
	}
}

@RestController
public class UserController {
	private final UserServiceA userService;
    ...
}

현재 UserServiceAUserController는 매우 강한 결합도를 가지고 있습니다. 만약 UserServiceAUserServiceB라는 것으로 바꿔 끼우면 UserController도 마찬가지로 수정해주어야 합니다. 이는 개방 폐쇄의 원칙에 어긋나며 고수준인 UserController가 저수준인 UserServiceA를 의존하므로 의존관계 역전의 원칙에도 어긋납니다.

OCP와 DIP를 지킨 예제

결합도가 높았던 구조를 다음과 같은 구조로 변경하면 UserService 구현체가 바뀌어도 개방 폐쇄의 원칙과 의존관계 역전의 원칙을 지킬 수 있습니다.

interface UserService {
	void signup(..);
}

@Service
public class UserServiceImplA implements UserService {
	...
    
	@Transactional
    @Override
    public void signup(..) {
    	...
	}
}
@RestController
public class UserController {
	private final UserService userService;
    ...//생성자주입
{

이런 구조를 가지면 userService는 컴파일 시점에 어떤 클래스를 담을지 결정하지 않고 런타임 시점에 스프링 컨테이너에 존재하는 UserService 구현체 빈 중 하나를 주입받게 됩니다.

가져다 쓰는 UserController 입장에서는 UserService가 구현체가 런타임 시점에 지정되므로 UserServiceUserController는 느슨한 결합도를 가지게 되어 변경 없이 가져다 쓰고(변경최소화), 재활용성을 향상시켰습니다.

만약 구현체가 1개라면 컨트롤러는 그것을 주입받습니다.(UserServiceImplA)

만약 새로 만든 구현체로 바꿔 끼워야한다면 UserService인터페이스 구현체를 하나 더 만들고(UserServiceImplB) @Primary 어노테이션을 사용하여 어떤 클래스를 컨트롤러에 주입할지 결정해주면 됩니다. 컨트롤러쪽에서는 수정해줄 필요가 없습니다.

즉 스프링을 사용하며 객체의 생성 제어를 IoC컨테이너에게 위임하고 컨트롤러에서 런타임 중에 어떤 자바빈을 주입할 지 인터페이스에게 위임하여 느슨한 결합도를 가지도록 하였습니다.

사실 스프링을 통해 개발해오며 저렇게 서비스 구현체를 여러개 구현해볼 일이 없었어서 꼭 이런 구조를 가져야하나 의문이지만 OOP 관점에서 보면 인터페이스를 구현하도록 하는 쪽이 맞는듯 합니다.

보면 좋은 것

https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html

https://dahye-jeong.gitbook.io/spring/spring/2020-04-10-aop-dynamicproxy

https://kim-solshar.tistory.com/75#%EC%B-%B-%EA%B-%A-

profile
鈍筆勝聰✍️
post-custom-banner

3개의 댓글

comment-user-thumbnail
2023년 2월 15일

좋은 글 잘 읽었습니다.

1개의 답글
comment-user-thumbnail
2024년 4월 16일

덕분에 궁금증이 잘 해소되었습니다. 감사합니다!

답글 달기