Programming/Spring Boot

관습적인 추상화 Service, ServiceImpl 구조를 사용해야 할까?

Jan92 2021. 9. 8. 21:09

관습적인 추상화

Service interface와 ServiceImpl class 구조를 사용하는 이유?

 

 

대부분의 프로젝트는 Service를 만들 때 MemberService와 같이 서비스를 인터페이스로 설계하고, MemberServiceImpl 라는 구현체인 클래스를 생성해서 사용하는 방식으로 설계됩니다.

 

그동안 프로젝트를 하며 이유도 모른체 그냥 구조가 그런가 보다 하면서 계속 만들어서 사용하다가 이번에 그 이유에 대해 알고 싶어서 잘 정리된 몇몇 분들의 자료를 참고하여 정리하게 되었습니다.

 

내용 중 잘못된 부분은 댓글을 통해 말씀해주시면 찾아보고 수정하며 공부하겠습니다.

 

 


 

 

이론상으로 위와 같은 Service, ServiceImpl 패턴으로 설계를 해야하는 이유는 인터페이스와 구현체를 분리함으로써 구현체를 독립적으로 확장할 수 있으며, 구현체 클래스를 변경하거나 확장해도 이를 사용하는 클라이언트의 코드에 영향을 주지 않도록 하기 위함입니다.

이 같은 추상화를 통한 구현 방식은 객체지향의 특징 중 하나인 다형성과 객체지향의 다섯 가지 원칙 중 하나인 OCP 원칙을 가장 잘 실현해주는 설계 방식이라고 할 수 있습니다.

 

하지만 실제로 대부분의 프로젝트에서는 인터페이스와 구현체 클래스 사이의 관계가 1:1의 관계로 구성되어 실질적으로 인터페이스, 클래스 구조를 사용하는 것에 대한 이점을 전혀 가져가지 못함에도 불구하고 관습적으로 이러한 추상 패턴을 적용하고 있습니다.

 

 

* OCP (Open Closed Principle)

개방, 폐쇄 원칙이라고 하며 '소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.'는 프로그래밍 원칙입니다.

 

* 위 추상화를 통한 구현 방식의 단점

코드 구조가 복잡해지고, 복잡해진 구조 만큼 코드를 분석하고 확인하는 과정에서 인터페이스를 거쳐 구현체들을 확인해야 하는 번거로움이 생길 수 있습니다.

 

 


 

 

예시를 통해 다시 한번 이야기해보면,

 

public interface MainService {
    ResponseEntity<?> doAction();
}

doAction() 기능을 정의하는 Service,

 

@Service
public class MainServiceImplA implements MainService {
    @Override
    public ResponseEntity<?> doAction() {
        System.out.println("do Action A");
    }
}

MainService의 doAction 기능을 구현하는 MainServiceImplA,

 

@Service
public class MainServiceImplB implements MainService {
    @Override
    public ResponseEntity<?> doAction() {
        System.out.println("do Action B");
    }
}

MainService의 doAction 기능을 구현하는 MainServiceImplB,

 

 

doAction() 메서드를 구현하는 구현체 MainServiceImplA, MainServiceImplB 가 있을 때, 이 두 구현체는 각각의 방법으로 doAction() 메서드를 구현합니다. 이때 doAction() 메서드를 정의하는 것은 interface 인 MainService의 역할입니다.

 

이러한 구조로 프로젝트를 설계했을 때, interface에서 정의한 기능을 새로운 방식으로 구현해야 한다면 사용해야하는 곳에서 구현체만 손쉽게 바꿀 수 있기 때문에 Service를 인터페이스로 만들고, 해당 기능을 ServiceImpl 라는 class로 구현하는 것입니다.

 

이처럼 Service를 interface로 만드는 목적은 하나의 역할을 여러 방식으로 구현하는데 있습니다. 인터페이스와 구현체의 분리를 통해 독립적으로 보다 자유로운 확장을 보장하는 것입니다.

하지만 위에서 이야기했던 것처럼 대부분의 프로젝트는 인터페이스와 구현체가 1:1로 만들어져 사용되고 있습니다. 이러한 상황에서 앞으로도 이러한 관습적인 추상화를 통한 Service, ServiceImpl 설계 방식을 적용해야 할까요?

 

 


 

 

실제로 효과도 보지 못하는 이러한 관습적인 추상화를 통한 설계방식을 계속 적용해야 할까?

 

 

결국 선택은 프로젝트를 설계하는 사람의 몫이라고 생각합니다. 하지만 인터페이스와 구현체를 분리한 설계를 통한 이점들과 해당 구조를 사용하거나 하지 않는 이유와 근거에 대해서는 알고 있고, 말할 수 있어야 한다고 느꼈습니다.

 

 

(계속 적용해야 한다는 입장에서의 의견 두 가지)

 

  1. 객체에 대한 설계와 이를 구현한 코드는 언제든지 변할 수 있습니다. 그렇기 때문에 개발자는 이를 대비해야 합니다.
    지금 만들어서 사용중인 인터페이스와 구현체 클래스가 1:1 관계를 맺고 있을지 모르지만 서비스가 커지고 변화함에 따라서 얼마든지 구현체 클래스는 확장될 가능성을 가지고 있습니다. 그렇기 때문에 이러한 구조를 통해 미래의 변화에 유연하게 대처할 수 있도록 대비해야 합니다.
  2. 이러한 구조는 협업에서 이점으로 작용될 수 있습니다.
    프로젝트를 시작할 때 설계자가 프로젝트의 큰 뼈대를 구성하고, 나머지 작업자들은 그에 맞는 실제 구현을 하게 되는 경우가 있습니다. 이때 함수명, 리턴, 파라미터 등을 설계자가 만들어놓은 인터페이스에 맞춰 코딩할 수 있습니다. 이처럼 인터페이스를 누군가 작성하고 실제 구현은 다른 사람이 할 수 있는 분할의 기능도 협업에서의 이점이 될 수 있습니다.

 


 

함께 알면 이해하기 좋은 MVC 패턴에서 Service Model의 역할

 

MVC 패턴에서 view는 자신이 요청할 Controller만 알고 있으면 되고, Controller는 화면에서 넘어오는 매개변수들을 이용해 Service객체를 호출하는 역할을 합니다.

Service는 불필요하게 Http 통신을 위한 HttpServlet을 상속받을 필요 없는 순수한 자바 객체로 구성됩니다. 그렇기 때문에 Service는 어떤 컨트롤러가 호출하든 상관없이 필요한 매개 변수만 준다면 자신의 비즈니스 로직을 처리하게 됩니다. 즉, 모듈화를 통해 어디서든 재사용이 가능한 클래스 파일이라는 뜻입니다.

(Service에서 request나 response와 같은 객체를 매개변수로 받아서는 안됩니다. request, response를 사용해야하는 작업은 컨트롤러 단에서 해야 합니다.)

 

이렇게 제대로 구현된 Service라면 단순 Web 기반이 아니라 추후 native app으로 view단이 변경되더라도 Service는 view에 종속적인 코드가 없기 때문에 그대로 재사용할 수 있게 됩니다. 그리고 추가적인 요청 사항이 들어오면 기존의 소스를 수정하는 게 아니라 기존 Service 인터페이스를 구현하는 다른 클래스를 통해 그 객체를 사용하게 하는 것입니다.

(위에서 이야기한 OCP에 따른 변화에는 닫혀있고, 확장에는 열려있는 구조)

 

 

 

참고자료

https://see-one.tistory.com/1
https://multifrontgarden.tistory.com/97