우리는 스프링으로 개발하며 관행적으로 서비스를 구현할 때 인터페이스를 구현하는 구조로 개발을 해왔습니다.
왜 그렇게 해왔을까요, 의문점을 해결해보려 그 이유를 공부해보았으며 크게 2가지 이유가 있음을 알게되었습니다.
이는 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
)
net.sf.cglib.proxy.Enhancer
의존성 추가default
생성자가 무조건 필요함JDK Dinamic Proxy
는 인터페이스를 기반으로 프록시 객체를 생성합니다.
반드시 인터페이스가 존재해야 프록시 객체를 정상적으로 만들어 낼 수 있으므로 인터페이스를 구현한 서비스 형태가 관행적으로 내려왔다고 생각합니다.
하지만 요즘 spring boot
는 인터페이스를 구현한 클래스임에도 프록시 객체 생성 시 CGLib
를 사용합니다.
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 Proxy
와 CGLib
를 활용한 프록시 객체 생성을 눈으로 확인해보고 싶다면 application.properties
에 spring.aop.proxy-target-class=false
(CGLib 사용하지 않음), spring.aop.proxy-target-class=true
(CGLib 사용 - 디폴트)로 확인해주세요.
그래서 현 시점에는 spring boot
를 사용한다면 디폴트로 CGLib
를 사용하므로 spring AOP
관점에서 서비스 구현 시 인터페이스를 사용하는 이유는 없어졌다고 생각합니다.
스프링을 배제하고 OOP관점에서 인터페이스를 구현하는 방식으로 서비스를 작성하는 것이 맞다고 보는 사람들도 많습니다.
소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려있고, 변경에는 닫혀있어야 한다.
SOLID
5원칙 중 하나인 OCP
는 특정 소스코드의 변경으로 인해 다른 클래스가 수정되는 일이 일어나는 일에 대해서 폐쇄적으로 대응해야 한다고 말합니다.
저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 하고 그 반대가 되면 안된다
ServiceImpl
은 Service
인터페이스의 구현체이므로 Service
에 대해 Compile 의존성을 가지지만 인터페이스인 Service
는 반대로 ServiceImpl
에 대해 Compile 의존성을 가지지 않습니다. 구현체가 어떻게 변경되어도 인터페이스는 그것을 신경쓰지 않아도 됩니다.
로버트 마틴의 Clean Arichitecture 18장을 보면 그림에서 경계를 기준으로 왼쪽을 고수준(High level) 컴포넌트, 오른쪽을 저수준(Low level) 세부구현이라고 표현합니다.
고수준 컴포넌트는 잦은 변경에 노출되어 있는 저수준 세부사항에 의존해서는 안되며 저수준 세부사항이 고수준 컴포넌트에 의존성을 가져야 한다고 설명합니다.
만약 고수준 컴포넌트가 저수준 컴포넌트를 의존하는 경우, 자주 변경이 일어나는 저수준 컴포넌트의 변경으로 인해 고수준 컴포넌트가 변경이 일어나므로 개방 폐쇄의 원칙을 어기게 됩니다.
일단 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;
...
}
현재 UserServiceA
와 UserController
는 매우 강한 결합도를 가지고 있습니다. 만약 UserServiceA
를 UserServiceB
라는 것으로 바꿔 끼우면 UserController
도 마찬가지로 수정해주어야 합니다. 이는 개방 폐쇄의 원칙에 어긋나며 고수준인 UserController
가 저수준인 UserServiceA
를 의존하므로 의존관계 역전의 원칙에도 어긋납니다.
결합도가 높았던 구조를 다음과 같은 구조로 변경하면 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
가 구현체가 런타임 시점에 지정되므로 UserService
와 UserController
는 느슨한 결합도를 가지게 되어 변경 없이 가져다 쓰고(변경최소화), 재활용성을 향상시켰습니다.
만약 구현체가 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
좋은 글 잘 읽었습니다.