- 우리는 늘상 스프링으로 개발하며 관행적으로 서비스를 구현할 때 인터페이스를 구현하는 구조로 개발을 해왔습니다
- 왜 그렇게 하였을까? 무슨 이유에서 였을까? 함께 알아보자
Spring AOP 관점
- 이는 Spring AOP와 관련이 깊습니다
- Spring AOP는 빈 등록시 사용자의 특정 메서드 호출 시점에 AOP를 수행하는 Proxy Bean을 생성하며, 크게 2자기 프록시 객체 생성 방법이 존재합니다
- "JDK Dinamic Proxy"는 프록시 객체 생성시 인터페이스 존재가 필수적이고
"CGLib"은 프록시 객체 생성시 인터페이스가 존재하지않아도, 클래스 기반으로 프록시 객체를 생성 할 수 있습니다
- (이전 시리즈의 JDK Dynamic Proxy, CGLib를 알아보자 글에서 확인 가능)
호랑이 담배피던 시절 Spring
- 옛날옛적 Spring Framework 에서는 Spring AOP 사용시 CGLib의 여러 문제점으로 인하여 Dynamic Proxy를 사용하는것을 권장하였다고 하빈다
당시 CGLib의 문제점
- net.sf.cglib.proxy.Enhancer 의존성추가
- default 생성자 필요
- 타겟의 생성자 2번 호출
- JDK Dynamic Proxy는 인터페이스를 기반으로 프록시 객체를 생성합니다
- 반드시 인터페이스가 존재해야 프록시 객체를 정상적으로 만들어 낼 수 있으므로, 인터페이스를 구현한 서비스 형태가 관행적으로 내려왔다고 생각이 듭니다
- 그런데 띠용? 요즘 Spring Boot에서는 " 인터페이스를 구현한 클래스임에도 불구하고, 프록시 객체 생서이 CGLib을 사용" 한다고 한다
- 어째서일까?
Spring 3.2
- Spring 3.2 이후부터는 CGLib이 위에서 언급한 문제점이 외부 라이브러리의 도움을 받아 개선이 이루어졌다고 판단되어 Spring core 패키지에 포함되었다
- Spring Boot 사용시 인터페이스를 구현한 클래스여도 JDK Dynamic Proxy보다 성능이 좋은 CGLib을 디폴트로 사용하여 프록시 객체를 생성합니다 (그렇군)
package org.terror.codeplaygroundspring.testService;
import org.springframework.stereotype.Service;
import org.terror.codeplaygroundspring.annotation.PrintLog;
import org.terror.codeplaygroundspring.proxy.Rabbit;
@Service
public class TestService implements TestServiceImpl {
@Override
@PrintLog
public void test() {
Rabbit rabbit = new Rabbit();
rabbit.eat();
}
}
package org.terror.codeplaygroundspring;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.terror.codeplaygroundspring.testService.TestService;
@SpringBootTest
public class PrintServiceTest {
@Autowired
TestService testService;
@Test
@DisplayName("런타임 위빙 테스트 하기")
void test1(){
System.out.println(testService.getClass());
}
}
- 분명 인터페이스를 오버라이딩 하여 구현한 형태임에도 불구하고, CGLib으로 표기되는 모습 !
- 그래서 현 시점에서는 Spring Boot를 사용한다면 디폴트로 CGLib을 사용하므로, Spring AOP관점에서 서비스 구현시 인터페이스를 사용하는 이유가 희미해졌다고 보인다
OOP 관점
- Spring을 배제하고, OOP 관점에서 인터페이스를 구현하는 방식으로 서비스를 작성하는 것이 맞다고 보는 사람들도 많습니다
개방 폐쇄의 원칙
소프트웨어의 구성요소 (컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려있고, 변경에는 닫혀있어야 한다
- SOLID 원칙중 하나인 OCP(개방 폐쇄의 원칙)은 특정 소스코드의 변경으로 인해 다른 클래스가 수정되는 일에 대해서 폐쇄적으로 대응해야 한다고 말합니다
의존관계 역전의 원칙 (DIP)
저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야하고, 그 반대가 되면 안된다
- ServiceImpl은 Service의 구현체이므로 Service에 대해 Compile 의존성을 가지지만, 인터페이스인 Service는 반대로 ServiceImpl에 대해 컴파일 의존성을 가지지 않습니다.
- 즉 Service는 ServiceImpl의 구현체가 변경되어도, 그것을 신경쓰지 않아도됩니다
- Service(고수준 모듈 - 인터페이스), ServiceImpl(저수준 모듈 - 구현체)이기 때문입니다
컴파일 의존성
저수준 모듈인 고수준 모듈의 값들을 참조하여 가지지만 (관심 있음 - 의존성을 가지고 있음), 고수준 모듈은 저수준 모듈의 값들을 참조하여 가지고있지않음 (관심없음 - 의존성이 없음) 이다
- 그림에서 경계를 기준으로 왼쪽을 고수준 컴포넌트, 오른쪽을 저수준 세부구현 이라고 표현합니다
- 고수준 컴포넌트는 잦은 변경에 노출되어 있는 저수준 세부사항에 의존해서는 안되며, 저수준 세부사항이 고수준 컴포넌트에 의존성을 가져야 한다고 말합니다
- 만약 고수준 컴포넌트가 저수준 컴포넌트를 의존하는 경우, 자주 변경이 일어나는 저수준 컴포넌트의 변경으로 인해 컴포넌트가 변경이 일어나므로 OCP(개방-폐쇄-원칙)를 어기게 됩니다
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;
...
}
- UserServiceA와 UserController는 매우 강한 결합도를 가지고 있습니다
- 만약 UserServiceA -> UserServiceB 라는 것으로 바꿔 끼우러면 UserController의 변경이 가히 필수적이라고 볼 수 있으며, 이는 OCP를 준수하였다고 보기 어려우며
- 고수준 모듈이라고 할 수 있는 (컨트롤러)가 저수준 모듈인 (서비스)에 의존서을 갖고있으니 (저수준 모듈의 변경에 의한 고수준 모듈 변동) DIP에도 어긋난다고 볼 수 있습니다
OCP와 DIP를 지킨 예제
- 결합도가 높았던 구조를 다음과 같은 구조로 변경하면, UserService가 변경되어도 OCP와, DIP를 지킬 수 있습니다
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는 느슨한 결합도를 가지게 되어 변경없이 가져다 쓰고, 재활용성을 향상 시켰습니다
- 만약 새로운 UserService의 구현체를 가져다 써야한다면 @Primary 어노테이션으로 지정해주면 끝이고, 컨트롤러에서는 수정할 필요가없어 OCP를 준수 할 수 있습니다
- Spring을 통해 개발해오며 서비스 구현체를 여러개 구현해볼 일이 없었어서, 이런 구조를 가져야하나 의문이지만 OOP 관점에서보면 확실히 OCP,DIP를 준수할 수 있다는 관점에서 미루어보아도 하는것이 맞다고 생각이 듭니다
한줄요약
- 서비스에 인터페이스를 구현하는이유는, 이전에는 AOP가 CGLib을 지양하고 다이나믹 프록시 객체를 지향하였기 때문인데, 현대에 이르러서는 인터페이스로 구현해도 CGLib을 사용하기 떄문에 그 이유가 불분명하다
- 인터페이스를 사용함으로써 고수준 모듈(상위 수준 로직 정의)이 저수준 모듈(상위 수준 로직을 구현한것)을 의존하지 않게 됨으로써 DIP를 준수 할 수 있다
- 따라서 직접 해당 객체를 주입해서 사용하는것이 아닌 인터페이스를 통해 사용하기 때문에 직접적인 영향이 아닌, 간접적으로 사용 가능하다 이에 따라 저수준 모듈에서의 구체적인 구현변경이 고수준 모듈에 영향을 줄일 수 있다 (이 부분은 조금 이해가안되서, 다음 블로그에 정리해 보아야 겠다)
- OCP준수
- 저수준 모듈의 구현체의 변경에 고수준 모듈은 전혀 신경쓰지않아도 된다 (새로운 기능을 추가할때는 그저 인터페이스를 활용하여 추가적으로 작업하면되고 변경할 필요가 전혀없다)
- DIP 준수
- 고수준 모듈은 저수준 모듈에 의존하면안되고, 둘다 인터페이스에 의존 해야한다
참조블로그
https://velog.io/@suhongkim98/%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B5%AC%ED%98%84-%EC%8B%9C-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-spring-AOP