Service의 관점에서 바라봤을 때, 백엔드 Layer는 다음과 같이 두 가지 구조로 구현될 수 있다.
- Controller - Service - ServiceImpl - DB
- Controller - Service - DB
이 두 구조의 차이는 ServiceImpl의 사용 여부에 따른 것이다.
ServiceImpl를 사용하는 이유는 무엇일까?
ServiceImpl을 사용하는 경우, Service Interface와 ServiceImpl Class 구조를 사용하게 된다.
기본적으로 Service 인터페이스와 ServiceImpl 구현체를 나누게 된 이유는 두 가지가 있다.
1. 다형성과 OCP(Open Closed Principle)
이론적으로 인터페이스와 구현체를 분리하면 구현체는 외부로부터 독립적이 된다. 이로 인해 구현체를 수정하거나 확장할 때, 클라이언트 코드에 영향을 미치지 않고 자유롭게 변경할 수 있다. 즉, Loose Coupling을 통해 객체 간의 결합도를 낮추어 변화에 유연하게 대응할 수 있다. 여러 구현체가 동일한 인터페이스를 구현하고, 기능에 따라 적절한 구현체를 선택함으로써 다형성을 제공하며, 하나의 인터페이스에만 의존함으로써 의존 관계를 줄일 수 있다.
2. AOP Proxy
과거에는 Spring에서 AOP를 구현할 때 JDK Dynamic Proxy를 사용했다. JDK Dynamic Proxy는 프록시 객체를 생성하기 위해 인터페이스가 필수적이었다. 따라서 @Transactional 과 같은 AOP 어노테이션이 동작하기 위해서는 인터페이스와 구현체의 관계가 반드시 필요했다. 그러나 Spring 3.2와 Spring Boot 1.4 이후부터는 기본적으로 CGLIB(Code Generation Library)를 사용하여 클래스 기반으로 프록시 객체를 생성할 수 있게 되었다.
결론적으로, 2번의 이유는 현재 거의 적용되지 않으며, 1번의 이유 또한 과거의 관습적인 사용이 주된 원인이라고 할 수 있다.
이러한 추상화를 꼭 적용해야 할까?
실무에서 경험해보고, 또 개발 표준을 정립해보는 과정에서, 무엇이 Best Practice 인가에 대한 고민을 많이 했다. 책과 다양한 자료들을 참고해 보았지만, 결국 결론은 “프로젝트 특성을 정확히 이해하고, 그 필요성을 신중히 고려하여 결정하라”는 것이다. 많은 사람들이 사용한다고 해서, 또는 단순히 관습에 따라 무조건 그 구조를 채택하는 것은 바람직하지 않다. 특정 구조를 선택했다면, 그 목적과 필요성을 충분히 이해하고, 그 목적에 부합하도록 설계하는 것이 올바른 접근 방식이다.
그래서 언제, 어느 구조를 채택해야 한다고?
몇 가지 예시를 통해, 어떤 상황에서 어떤 구조를 채택하는 것이 바람직할지 생각해보자.
1. ServiceImpl Class - Service Interface
구현체를 분리하여 구현하는 Service Interface - ServiceImpl Class 구조는, 복잡한 비스니스 로직을 처리하거나, 확장성과 유지보수성이 중요한 경우에 적합하다고 할 수 있다.
예시1) 대규모 전자상거래 플랫폼
- 상황 : 대규모 전자상거래 플랫폼을 구축하고 있으며, 주문 처리, 결제, 재고 관리 등의 복잡한 비즈니스 로직이 존재한다.
- 설명
- 이 경우 OrderService 인터페이스와 OrderServiceImpl 구현체를 분리하여 설계한다. 이렇게 하면, 주문 처리 로직이 복잡해지더라도 다양한 비즈니스 요구 사항을 충족하기 위해 OrderServiceImpl 을 확장하거나 다른 구현체를 추가하는 것이 용이하다.
- 또한, 시스템이 확장됨에 따라 새로운 기능(예: 프로모션, 할인 코드 적용 등)을 추가할 때 기존 인터페이스를 변경하지 않고 새로운 구현체를 추가할 수 있다. 이는 코드의 일관성을 유지하면서 확장성을 극대화할 수 있다.
예시2) 여러 가지 구현체가 필요한 프로젝트
- 상황 : 동일한 비즈니스 로직에 대해 여러 가지 구현체가 필요하거나, 환경에 따라 구현체가 달라져야 하는 경우
- 설명
- 예를 들어, PaymentService 가 있고, PaymentServiceImpl 구현체 외에도 PayPalPaymentServiceImpl , CreditCardPaymentServiceImpl 등의 다양한 결제 방식을 처리하는 구현체가 존재할 수 있다.
- 인터페이스를 기반으로 구현체를 나누면, 사용자는 원하는 결제 방식에 따라 적절한 구현체를 쉽게 선택할 수 있다. 이렇게 하면, 확장성이 뛰어나고 유지 보수 하기도 용이하다.
2. Service Class
Service 클래스에서 바로 구현하는 구조는 간단한 비즈니스 로직을 처리하거나, 작은 규모의 애플리케이션에서 사용해볼 수 있다.
예시1) 간단한 블로그 애플리케이션
- 상황 : 블로그 글을 작성하고, 수정하고, 삭제하는 간단한 애플리케이션을 개발하는 경우
- 설명
- 이 경우 PostService 클래스 하나로 글 작성, 수정, 삭제 등의 비즈니스 로직을 처리할 수 있다. 인터페이스와 구현체를 나누지 않더라도, 로직이 간단하므로 유지보수에 큰 어려움이 없다.
- 단일 PostService 클래스는 코드 구조를 단순하게 만들어 주고, 개발 속도도 빠르다. 복잡한 설계가 필요하지 않으므로 간결한 코드가 유지된다.
예시2) 초기 스타트업 애플리케이션
- 상황 : 스타트업의 초기 단계에서 MVP(Minimum Viable Product)를 빠르게 개발해야 하는 상황
- 설명
- 애플리케이션의 요구사항이 명확하고 복잡하지 않으며, 빠르게 시장에 출시하는 것이 중요한 경우, Service Class에서 비즈니스 로직을 구현하는 것이 적합할 수 있다.
- 예를 들어, 사용자 관리 기능을 구현할 때 UserService 하나로 사용자 생성, 조회, 삭제 등의 기능을 처리한다. 인터페이스를 나누지 않으면, 초기 개발 속도가 빨라지고, 간단한 유지보수도 쉽게 수행할 수 있다.
- 나중에 애플리케이션이 성장하면서 비즈니스 로직이 복잡해지면, 그때 인터페이스와 구현체를 분리하는 방향으로 코드를 리팩토링할 수 있다.
정리하면, 1번은 확장성과 유지보수성, 다형성이 중요한 대규모 애플리케이션이나 복잡한 비즈니스 로직이 필요한 경우에 적합하다. 반면 구조 2는 간단한 애플리케이션이나 빠른 개발이 필요한 경우, 불필요한 복잡성을 빠르게 결과를 내고자 할 때 적합하다. 정해진 정답은 없으며, 두 구조 중 어떤 구조를 선택할 지는 프로젝트의 규모, 복잡성, 미래 확장성 등을 고려하여 결정하는 것이 좋다.
이때, 개발 관점만을 고려하는 것이 아니라 도메인 관점에서도 해당 도메인을 가장 잘 이해하고 있는 전문가와 충분히 논의한 후 결론을 내려야 한다.
Reference