○○○Service, ○○○ServiceImpl?

뾰족머리삼돌이·2024년 10월 26일
0

Spring

목록 보기
8/11

웹 개발공부를 하거나 프로젝트를 진행하다보면 Controller - Service - Repository의 형태로 구성하는 경우가 많다.

Controller는 외부 요청을 받아들이고 내부 로직진행을 위해 Service를 호출하며,
Service에는 실제 비즈니스 로직을 작성하고,
Repository는 저장소와의 상호작용을 담당한다.

Controller와 Repository는 잠시 논외로 치고, 이번에는 Service에 대해 살펴볼 예정이다.
작성자는 지금까지 Spring 환경에서 3회의 팀 프로젝트를 진행해봤는데, 처음 프로젝트를 생성하고 클래스를 구성하는 과정에서 의문점이 생겼다.

그건 바로 홀린듯이 작성하는 ServiceServiceImpl이다.
별다른 근거없이 Service를 인터페이스로 작성하고, ServiceImpl을 통해 실제 메서드를 작성하는 형식으로 구성하고 있었던 것이다.

실제로 기획단계에서 팀원들이 ServiceImpl왜 쓰는지 모르겠는데 그냥 Service만 쓰죠? 라는 의견을 냈고, 별다른 문제점이 생각나지 않아서 그대로 반영했던 프로젝트도 있었다.

따라서, 이번 포스팅에서는 ServiceServiceImpl로 나눠서 구성하는 이유에 대해 알아볼 생각이다.

인터페이스의 장점

먼저 생각나는 장점은 유지보수의 용이함이다.
동일한 인터페이스를 구현하는 클래스라면 큰 영향없이 교체하는게 가능하다.

만약, 클래스타입을 이용한다면 클래스 필드선언부터 변경해야 했을 것이다.

하지만, 이 장점은 인터페이스 타입을 구현하는 여러 클래스가 있을 때야 드러난다.
그래야 상황에따라 다른 구현클래스를 Bean으로 주입받고 사용하는 의미가 생긴다.

예를들어, 아이디 찾기를 생각해보자. 보통 아이디 찾기를 실행하면 휴대전화나 이메일처럼 여러개의 방법을 제공한다. 이는 사용자 인증이라는 큰 틀안에 묶여서 휴대전화인증과 이메일인증이라는 세부동작으로 구분되는 형식이다.

사용자 인증을 인터페이스로 볼 때, 인증() 이라는 메서드가 존재한다고 생각해보자.
사용자인증 타입의 객체를 가진 클래스는 인증()을 호출할 뿐이지만, 실제 할당된 구현클래스가 휴대전화인증이냐 이메일인증이냐에 따라 동작변경이 가능하다.

인터페이스를 사용함으로써 1. 클래스간의 결합도를 낮추고, 2. 일관적인 사용방식을 제공하는 것이다

하나의 단일 서비스 클래스만 사용하면 인터페이스를 사용안해도 되는거네?
다른방향으로 생각해보면 이런 생각이 가능하다.
틀린 말은 아니지만, 클래스 설계과정에서 추가적인 확장이 없는지 충분히 고려하고 작성해야한다.

만약, 서비스 유지보수 과정에서 구조를 변경하려고 한다면 많은 부분을 직접 수정해야하기 때문이다.

Spring 동작에서의 인터페이스

Spring의 주요 기능이라 불리는 Spring 삼각형에는 AOP(관점 지향 프로그래밍) 이 들어간다.
간단하게 말하자면 Filter나 Interceptor처럼 특정 관심사에 대해 공통적인 동작을 별도로 관리할 수 있는 기능이다.

필터, 그리고 인터셉터와 다른 점은 동작시기와 대상이 다르다는 점이 있다.
보통 Filter의 경우 WAS로 진입하는 요청을 대상으로 동작하며, Interceptor는 Controller로 진입하는 요청에 관여한다. 반면, AOP는 비즈니스 로직을 대상으로 동작한다는 차이점이 있다.

주의할 점은 AOP의 동작방식인데, Spring AOP는 Proxy를 기반으로 동작한다.

공식 문서의 내용을 살펴보면 JDK Dynamic ProxyCGLib을 이용한다는 것을 확인할 수 있다.

프록시에 관해서는 이전 포스팅에서 소개했던 내용이 있는데, 간단히 말하자면 JDK Dynamic Proxy와 CGLib는 동적 프록시 기술을 의미한다.

JDK Dynamic Proxy인터페이스 기반으로 동작하며 리플렉션을 사용하고,
CGLib클래스 기반으로 동작하며 바이트코드 조작을 이용한다.

Spring AOP는 대상 객체가 인터페이스를 구현하고 있다면 JDK Dynamic Proxy를 사용하고, 그렇지않다면 CGLib을 사용한다.


다시 본론으로 돌아와서, ServiceServiceImpl 형태로 구현하는 이유는 인터페이스 기반 프록시를 이용함으로써 관심사를 공통적으로 관리하기 위함도 있다. 클래스 기반 프록시는 대상 클래스에 한정되어 동작하지만, 인터페이스 기반 프록시는 인터페이스를 구현하는 모든 구현 클래스에 적용된다. 따라서, 관심사를 기준으로 관리한다는 AOP의 방향성에 좀 더 적합하다고 볼 수 있다.

Test 작성과 인터페이스

테스트 작성은 경험이 많지않기에 자세한 설명은 힘들지만, 테스트 코드를 작성하다보면 Mock객체라는 것을 생성한다.
이는 가짜객체를 의미하며, 실제 대상클래스의 타입과 구조를 지니지만 동작방식은 없는 객체를 말한다.

Mock객체생성에서 인터페이스는 구현클래스가 작성되지 않았더라도 테스트를 작성하게 해준다.
인터페이스를 이용해 구조를 제공함으로써, 실제 구현클래스가 없더라도 Mock객체를 생성하고 테스트를 진행할 수 있다는 말이다.

이는 실패하는 테스트를 먼저 작성하고, 테스트가 성공하도록 실제 클래스를 구현한다. 라는 TDD 접근 방식을 실천하는데 큰 도움이된다.

Spring Boot 공식문서의 테스트 작성 예시

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

@SpringBootTest
class MyTests {

	@Autowired
	private Reverser reverser;

	@MockBean
	private RemoteService remoteService;

	@Test
	void exampleTest() {
		given(this.remoteService.getValue()).willReturn("spring");
		String reverse = this.reverser.getReverseValue(); // Calls injected RemoteService
		assertThat(reverse).isEqualTo("gnirps");
	}

}

0개의 댓글