본문 바로가기
SPRING

[Spring] 서비스에 인터페이스를 구현하는 이유는 뭘까? (ServiceImpl)

by 킹명주 2023. 10. 13.

최근 회사에서 프로젝트를 진행한다고 글을 많이 못썼다 ㅜㅜ 그럼에도 킹명주의 백엔드 일지가 벌써 조회수가 4000회가 넘었다!! 정말 감사드립니다 ~~~

요번 주제는 프로젝트를 진행하면서 궁금했던 부분을 다루고자 한다. 레거시 코드들을 보면 진짜 100에 99는

요런식으로 서비스를 구현할 때는 인터페이스를 항상 선언해주는 패턴을 사용하고 있었다. 그래서 나도 따라쓰긴 했는데,, 왜 인터페이스를 상속하는지 궁금해졌다.


사전 지식

 

해당 내용을 확실하게 이해하기 위해서는 사전 지식이 조금 필요하다. 만약 알고 있는 내용이라면 본문으로 쭈욱 스크롤!!

 

Proxy

일단 proxy에 대해서 알고 있어야한다. proxy는 어떤 일을 대신 시키는 것! 이라고 생각하면 된다. 

약간 회사버전으로 표현해보았는데, 약간은 개념이 다르지만 어째든 대신 시키는 것을 강조하기 위해서 그림으로 나타내 보았다.

그래서, 이제 프록시 패턴에 대해서 간단하게 알아보고 가겠다.

public interface Work {
    String 일을해봅시다();
}
public class Proxy implements Work{
    private final WorkImpl work = new WorkImpl();

    @Override
    public String 일을해봅시다(){
        return work.일을해봅시다();
    }
}
public class WorkImpl implements Work {
    @Override
    public String 일을해봅시다(){
        return "일을 열심히 해서 아파트를 사봅시다.";
    }
}
public class Foo {
    public static void main(String[] args) {
        Work work = new Proxy();
        System.out.println(work.일을해봅시다());
    }
}

이런식으로 Proxy를 통해 서비스를 실행시키는 것이 Proxy 패턴이다. (이해를 쉽게하기 위해 method 이름을 한글로 했습니다 ㅎ)

 

JDK Dynamic Proxy

Java의 Reflection에 존재하는 Proxy라는 클래스를 통해 생성된 프록시 객체를 의미한다. 또한, Proxy를 생성하기 위해서는 interface가 필요하다.

import java.lang.reflect.Proxy;

Proxy.newProxyInstance(
    ClassLoader        // class loader
  , Class<?>[]       // target의 인터페이스
  , InvocationHandler // target의 정보가 포함된 handler
)

 

CGLib Proxy

Code Generator Libray의 약자로, 클래스의 바이트 코드를 조작하여 프록시 객체를 생성해주는 라이브러리이다.  Enhancer 클래스를 활용해서 인터페이스가 아닌 클래스에 대해서도 프록시 객체를 만들어줄 수 있다. 

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TargetService.class); // 타겟 클래스
enhancer.setCallback(MethodInterceptor); // Handler
Object proxy = enhancer.create(); // Proxy 생성

출처 : stack overflow


그래서 왜 우리는
서비스에 인터페이스를 선언하는가?

관습

Spring 2.0 버전에서는 AOP Proxy를 구현할 때 JDK Dynamic Proxy를 사용했다. 즉, 인터페이스를 구현하는 경우에만 프록시를 생성할 수 있었다는 것!!

 

대표적으로, AOP Proxy하면 떠오르는 예제가 무엇인가?! 바로바로 @Transactional 이다. 예제를 한번 살펴보자.

AOP Proxy를 사용하기 위해서 TestService라는 interface를 하나 만들어두고 이를 상속받아 service 로직을 구현해야 한다. 이러한 예제처럼 프록시를 생성하기 위해서는 interface의 구현이 필수적이였다.

 

즉, 예전부터 JDK Dynamic Proxy를 사용하면서 인터페이스를 구현하는 것이 관습으로 굳어져 버린 것이다. 

(관습은 개인적인 생각이 많이 많이 들어가 있습니다.. 참고 부탁드립니다~)

 

유지보수

예를 들어보자. 만약, 하나의 서비스에 10가지 기능이 들어간다면??

위 예제처럼 구현될 것이다. 즉, 너무 많은 로직이 들어 있어 스크롤이 생기고, 한 눈에 어떠한 기능이 필요한지 찾기가 힘들다.

 

만약에 이를, interface로 분리한다면?

인터페이스를 통해 유지보수가 필요한 메서드의 implementation을 클릭하면 바로 해당 함수로 이동하게 된다.

 

Loose Coupling

인터페이스를 사용하는 이유 중 하나는 느슨한 결합이 아닐까 싶다. 일단, 추상화된 인터페이스를 통해 상호작용하기 때문에 컴포넌트들의 상호 의존성을 최소화할 수 있다.

 

그 외에 다형성, SOLID 원칙 등이 있는데, 우리는 이러한 이유때문에 사용하지는 않을 것이다. ㅋㅋㅋㅋㅋ 왜냐하면, service와 interface가 1대1로 존재하기 때문에 뭔가 다형성을 추구한다? 느슨한 결합을 원한다? 이런 느낌은 없었다.

 

아마 대부분이 관습적으로 사용하는게 젤 클 것이다. (뇌피셜~)


그렇다면 서비스에 인터페이스를
선언하는 것이 정답일까?

 

사실 이부분은 패턴과 관련된 부분이기 때문에 당연하게도 정답은 없다. 그렇지만, 위에서 사용하는 이유 즉, 장점에 대해서만 다루었기 때문에 마지막으로 단점을 소개하고자 한다.

 

성능

JDK Dynamic Proxy는 앞에서 소개한 것처럼 Reflection을 활용하여 Proxy를 생성한다. Reflection은 런타임에 객체 정보를 동적으로 파악할 수 있는 것인데, 그러다보니 일반적인 호출, 정적인 호출보다는 더 많은 오버헤드를 발생시킬 수 있다. 즉, 성능 상의 문제가 있다.

 

만약에, 필자에게 사용을 할래? 말래? 라고 물어본다면 협업을 진행하는 입장에서는 당연히 사용할 것 같다. 약간, 내가 느끼는 인터페이스 패턴은 뭐랄까.. 식당에서 메뉴판을 보는 느낌??!

그래서, 한 눈에 해당 인터페이스가 어떠한 기능이 담겨져 있고, 내가 필요한 기능을 빠르게 찾아서 쓸 수 있었던 것 같다.

 

그래서 이 글을 읽으시는 분들도 상황에 맞게 적절하게 사용하면 될 것이다!! 그럼 20000~~

 

 

 

# 참고

찾아보니 @EnableAspectJAutoProxy(proxyTargetClass = true) 와 같은 옵션들이 있는데, 굳이 사용할 필요는 없다. 왜냐하면 스프링에서는

이렇게 똑똑하기 때문에 인터페이스 유무를 판단으로 알아서 프록시를 설정한다!