본문 바로가기
  • 개발하는 곰돌이
Development/Spring & Spring Boot

[Spring/Spring Boot] Service와 ServiceImpl 구조에 대하여

by 개발하는 곰돌이 2023. 1. 26.

일반적으로 사용되는 Service와 ServiceImpl 구조

목차

    문제 인식

    Spring 또는 Spring Boot 예제들을 보면 거의 모든 예제가 위와 같이 Service 인터페이스와 각 서비스 인터페이스에 대한 구현체 클래스로 구성되어 있다. 이러한 구조 자체는 다형성적인 측면에서 보면 절대 잘못된 것이 아니지만, 온라인 상의 거의 모든 예제들, 나아가 회사에서 진행하는 프로젝트에서도 이러한 구조를 무분별하게 사용하는 모습을 보면서 한가지 의문이 생겼다.

    어차피 인터페이스와 구현체 클래스를 1:1로 구성할거면 인터페이스 없이 서비스 클래스를 바로 작성하면 되지 않나?

    이러한 의문으로 인해 서비스 인터페이스 - 구현체 클래스의 구조가 탄생한 계기에 대해 찾아보게 되었다.

    Service 인터페이스와 ServiceImpl 구현체 클래스를 나누는 이유

    기본적으로 Service 인터페이스와 ServiceImpl 구현체를 나누게 된 이유는 두 가지가 있다.

     

    다형성과 OCP(Open Closed Principle)

    이론상으로 인터페이스와 구현체가 나눠져있으면 구현체는 외부로부터 독립된다. 이로 인해 구현체의 수정이나 확장이 자유로워지고, 이를 사용하는 클라이언트의 코드에는 영향을 주지 않는다. 예를 들어, 사용자의 동작을 정의하는 서비스가 존재할 때 관리자와 일반 사용자의 경우와 같이 사용자의 권한에 따라 동작이 다른 경우가 있다. 이 경우에는 구현체만 추가하면 추가적인 코드 수정 없이 손쉽게 동작을 추가할 수 있다.

     

    AOP Proxy

    과거에는 Spring에서 AOP를 구현할 때 JDK Dynamic Proxy를 사용했다. 이 JDK Dynamic Proxy는 프록시 객체를 생성하려면 인터페이스가 필수적이다. 즉, AOP를 사용하여 구현되는 @Transactional 어노테이션 등이 동작하기 위해서는 반드시 인터페이스 - 구현체 관계로 작성되어 있어야 했다는 것이다. 그러나 시간이 지나면서 Spring 3.2 / Spring Boot 1.4 버전부터는 CGLIB를 기본적으로 포함하여 클래스를 기반으로 프록시 객체를 생성할 수 있게 되었다.

     

    결론적으로 2번의 이유는 사실상 사라져버린지 오래고, 1번의 이유 또는 과거부터 이어진 관습적인 사용이 주된 이유라고 볼 수 있다.

    이러한 추상화를 꼭 적용해야 할까?

    이러한 추상화를 적용해야 하는가에 대한 의견은 개인마다 다르지만 개인적으로는 거의 모든 프로젝트에 적용되는 1:1로 구성된 인터페이스 - 구현체 관계는 이런 추상화를 적용하는 것은 불필요한 계층 구조만 만들게 될 뿐이라고 생각한다. 따라서 인터페이스 - 구현체 관계가 1:1로 구성된다면 그냥 Service 클래스로 작성했다가, 나중에 어떤 Service의 기능을 확장하는 경우가 생긴다면 그 때 해당 Service 클래스를 인터페이스 - 구현체 관계로 마이그레이션해도 괜찮다고 생각한다. 혹시나 미래의 확장 가능성을 대비하여 인터페이스 - 구현체 관계를 미리 적용한다고 하더라도 그 이유는 알고 적용해야 한다고 생각한다.

     

    예를 들어 글 최상단 사진의 예시나 대다수의 예제와 같이 인터페이스는 ~~Service로 명명하고 구현체는 단순히 인터페이스의 이름 뒤에 Impl만 붙이는 방식은 다형성과 OCP를 고려하여 이러한 구조를 채택한 것이라고 보기 어렵지 않나 싶다. 정말로 미래의 확장 가능성을 대비해서 인터페이스 - 구현체 관계의 추상화를 적용한다면 더욱 직관적인 작명법을 사용하는 것이 하나의 방법이라고 볼 수 있을 것이다.

     

    직관적인 작명법의 예시로 Java의 List, Map, Set 등을 들 수 있다. List, Map, Set은 모두 인터페이스이지만 이들의 구현체는 LinkedList, ArrayList / HashMap, TreeMap, LinkedHashMap / HashSet, TreeSet, LinkedHashSet 등과 같이 각각의 구현체의 특성에 맞는 이름을 갖고 있다. 최상단 사진과 같은 구조에서는 아래와 같은 방식으로 작명할 수 있을 것이다.

    public interface MemberService	// 인터페이스
    
    public class AdminMemberService implements MemberService	// 관리자 동작 구현체
    public class CommonMemberService implements MemberService	// 일반회원 동작 구현체

    결론

    결론적으로는 남들 다 하니까 관습적으로 따라하는 Service - ServiceImpl 구조는 지양해야 한다고 생각한다. 인터페이스 - 구현체 구조로 코드를 작성한다면 그 목적을 알고 목적에 맞게 작성하는 것이 바람직할 것이다.

    댓글