본문 바로가기
Java 웹 프로그래밍

Service - ServiceImpl 쓰지 말라고만 하고 사라지면 어떡해요!

by irerin07 2024. 2. 26.
728x90

아아...익숙한 구조다...

요즘은 많이 줄어들었지만 예전에는 개발을 하다 보면 Service - ServiceImpl 구조를 굉장히 많이 만나볼 수 있었다.

 

나 조차도 사실 이걸 왜 써야하는지, 근본적인 이유가 무엇인지에 대한 의문을 품지 않고 그저 이런 구조를 사용해서 개발을 하는 것이 회사 개발팀의 컨벤션이기도 했고, 사실 큰 불편함도 없기도 해서 사용하기도 했다. 

 

어느 날 개발을 하던 도중에 의문이 생겼다. 아무리 봐도 대부분의 Service와 ServiceImpl의 관계는 1:1에서 더 이상 바뀔 일이 없을 것 같아 보였고, 동료 개발자에게 질문을 했다. 왜 이런 구조를 채택했냐고.

 

돌아온 대답은 우리가 알고 있는 이유와 비슷했다. 인터페이스와 구현체를 분리함으로써 얻을 수 있는 이점이 많다는 이유였다.

 

틀린 말은 아니다. 인터페이스와 구현체가 나눠져 있으면 구현체는 외부로부터 독립된다. 객체간 결합도를 낮춰 변화에 유연한 개발을 할 수 있게 되고 구현체 클래스가 변경되거나 확장되어도 이를 사용하는 클라이언트에는 영향을 주지 않게 된다. SOLID 5원칙 중 하나인 OCP(개방 폐쇄 원칙)를 지향하게 되어 우린 변경에는 닫혀있고, 확장에는 열려있는 상태를 유지할 수 있게 된다.

 

하지만 납득하기엔 조금 애매했다. 아무리 봐도 하나의 인터페이스를 구현하는 여러 구현체가 생길 것 같지도 않았고, 그렇기에 다형성을 위해서라는 답변이 좀 이상하게 느껴졌다.

 

그래도 사람일은 한치 앞도 모르는 것이라고, 언젠가는 필요하겠지.라고 생각하며 다시 Service - ServiceImpl 구조를 사용해 개발을 해왔다.

 

하지만 주변 많은 개발자들과 이야기를 하고 검색을 통해 조금은 생각과 시야가 트여 이를 공유하고자 한다.

왜 인터페이스를 고집하는가

Spring AOP

예전 스프링에서는 Spring AOP 구현 시 JDK Dynamic Proxy를 사용했습니다. 이는 당시에 CGLib에 존재했던 여러 문제점들 때문이었습니다.

CGLib에 존재했던 문제점들

  • 추가적인 의존성 추가가 필요
  • default 생성자가 필수적으로 필요
  • 타겟 클래스의 생성자를 두 번 호출

JDK Dynamic Proxy는 인터페이스 기반으로 프록시 객체를 생성합니다. 그렇기 때문에 AOP를 기반으로 구현되는 어노테이션(예 : @Transactional) 같은 것들이 동작하기 위해서는 반드시 Interface와  구현체 관계로 작성되어야만 했던 것입니다.

그렇기 때문에 그 당시 스프링을 제대로 사용하기 위해서는 Interface가 필수였던 것입니다.

 

그러다 시간이 흘러 스프링도 버전 업이 되고 CGLib 역시 많은 부분에서 개선이 이루어졌습니다.

위에 명시한 문제점들도 해결이 되었고, 스프링에서는 CGLib을 다시 core  패키지에 포함 시켰습니다.

 

CGLib(Code Generator Library)은 클래스의 바이트코드를 조작해 Proxy 객체를 생성해 주는 라이브러리입니다.

Spring은 CGLib을 사용하여 인터페이스가 아닌 타깃의 클래스에 대해서도 Proxy를 생성해 주는데, CGLib은 Enhancer라는 클래스를 통해 Proxy를 생성할 수 있습니다. 

CGlib이 Proxy를 생성하는 방법은 프록시로 만들고자 하는 타깃의 클래스를 상속받아 해당 클래스의 Proxy를 생성해줍니다. 즉, 우리는 CGLib을 통해 인터페이스 없이도 프록시를 만들어 낼 수 있게 되는 것입니다.

 

하지만 CGLib에도 한계가 있는데, private 메서드와 final 키워드가 붙은 메서드나 클래스들은 프록시 생성 때 포함되지 않는다는 것입니다. 

CGLib은 타깃 클래스에 포함된 모든 메서드를 override 하여 프록시를 생성하는데, Final 메서드나 클래스들은 재정의할 수 없기 때문에 Proxy를 생성할 수 없습니다. 

private 메서드 역시 proxy 대상에 포함되지 않는데, CGLib은 상속을 사용해 proxy를 생성하기 때문에 private 메서드는 상속 대상에 포함되지 않기 때문입니다. 하지만 그럼에도 불구하고 CGLib을 사용하는 것은 바이트코드 조작을 통해 Proxy를 생성해 주기 때문에 성능상 이점이 더 많기 때문이죠.

 

OCP, 다형성

맨 처음에도 언급했지만 OCP와 다형성은 객체지향에 있어 중요한 포인트입니다. 그렇기 때문에 스프링 AOP보다도 OOP관점에서 인터페이스를 구현하는 방식으로 서비스를 작성하는 것이 맞다고 보기도 합니다.

 

그럼 어떡하나요?

우리는 이제 선택의 기로에 서있습니다. 관습적인 추상화 방식에서 벗어날 것인가, 아니면 객체지향적인 개발을 위해 남을 것인가.

 

개인적인 생각으로는 반드시 Service - ServiceImpl 구조를 지켜야 할 필요는 없다고 생각합니다. 

더욱이 위 구조처럼 Interface의 구현체에 '~Impl'만 붙이는 방식은 사실 OCP와 다형성을 염두에 두고 채택한 네이밍과 구조라고 보기도 힘들다고 보기 때문에 이런 무조건적인 Service - ServiceImpl은 이제 불필요하다고 생각합니다.

 

물론 지금은 1:1 관계로 Service - ServiceImpl가 구성되어 있을지라도 미래에는 또 어떻게 될지 모른다고 할 수 있습니다. 하지만 그 시기가 찾아오면 그때 Interface와 그 구현체의 구성으로 리팩터링해도 괜찮을 것이라 생각합니다.

 

지금으로서는 무엇이 정답이다라고 할 수는 없지만, 우리가 사용하면서 적어도 이것을 왜 사용하는지, 적용하는지 알고 쓰는 것과 모르고 쓰는 것의 차이는 크다고 생각합니다.

 

 

출처 : 

https://cheershennah.tistory.com/226

https://colabear754.tistory.com/109

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

https://docs.spring.io/spring-framework/reference/core/aop/proxying.html

https://devroach.tistory.com/86

https://velog.io/@gmtmoney2357/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EB%8F%99%EC%A0%81-%ED%94%84%EB%A1%9D%EC%8B%9C-%EA%B8%B0%EC%88%A0CGLIB-ProxyFactory

728x90