개요
Spring 기반의 프로젝트를 살펴보면, Service와 ServiceImpl로 나누어진 구조를 자주 접하게 됩니다.
그런데 저는 개인 프로젝트나 팀 프로젝트를 진행할 때에는 별도로 ServiceImpl을 두지 않고 하나의 Service 클래스에서 비즈니스 로직을 구현하곤 했습니다.
하지만 실제 업무에서 코드를 분석하다 보니, 많은 프로젝트들이 Service와 ServiceImpl을 분리하는 방식을 택하고 있었습니다.
이 글에서는 왜 Service와 ServiceImpl을 분리하는지, 이러한 구조를 채택한 이유에 대해서 정리하고자 합니다.
분리하는 이유는❓
1. 유지보수성
Service는 인터페이스로 정의하고, ServiceImpl에서 실제 구현을 담당하는 방식은 결합도를 낮추고 변경에 유연하게 대응할 수 있도록 도와줍니다.
즉, 느슨한 결합으로 인해 수정이 용이합니다.
예를 들어, UserService 인터페이스 가 있고, 이를 구현한 UserServiceImpl 이 있다고 가정해봅시다.
public interface UserService {
User findUserById(Long id);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public User findUserById(Long id) {
// 실제 로직
}
}
만약 기존 UserServiceImpl을 변경하지 않고 새로운 구현체(NewUserServiceImpl)로 교체하고 싶다면, 단순히 스프링 빈(Bean) 설정을 변경하면 됩니다.
이는 특히 다양한 비즈니스 로직이 필요하거나, 구현체를 변경해야 할 가능성이 높은 경우 유리합니다.
2. 테스트 용이성
인터페이스를 사용하면 단위 테스트 시 실제 ServiceImpl을 사용하지 않고, Mock 객체를 주입하여 테스트를 수행할 수 있습니다.
이렇게 하면 불필요한 의존성을 제거하고 테스트 속도를 향상시킬 수 있습니다.
@Mock
private UserService userService;
@Test
void testFindUser() {
when(userService.findUserById(1L)).thenReturn(new User(1L, "Test User"));
User user = userService.findUserById(1L);
assertEquals("Test User", user.getName());
}
예를 들어, Mockito를 사용하여 UserService의 Mock 객체를 생성하면, UserServiceImpl의 내부 구현과 무관하게 UserService를 사용하는 로직을 테스트할 수 있습니다.
3. AOP 적용의 유연성
인터페이스 기반 구조는 AOP 적용을 더 유연하게 할 수 있습니다.
과거 Spring 2.0에서는 JDK Dynamic Proxy를 사용하여 AOP Proxy를 생성했으며, 이는 인터페이스를 반드시 구현해야만 AOP 적용이 가능한 방식이었습니다.
즉,
@Transactional(트랜잭션 관리), @Cacheable(캐싱), @Logging(로깅) 등 과 같은 AOP 기능을 사용하려면
인터페이스-구현체 구조가 필수적이었으며, 자연스럽게 Service와 ServiceImpl로 나누는 패턴이 일반화되었습니다.
그러나 Spring 3.2 / Spring Boot 1.4 이후부터는 CGLIB가 기본적으로 포함되면서, 클래스 기반으로도 AOP 프록시를 생성할 수 있게 되었습니다.
덕분에 이제는 인터페이스 없이도 AOP 적용이 가능하며, 과거처럼 인터페이스-구현체 구조를 강제할 필요는 없어졌습니다.
4. 의존성 역전(DIP)의 실현
- 고수준 모듈(컨트롤러)이 저수준 모듈(서비스 구현체)에 의존하지 않고, 둘 다 추상화(인터페이스)에 의존하게 합니다.
- 이를 통해 "구현보다 인터페이스에 의존하라"는 객체지향 설계 원칙을 따릅니다.
분리하는 이유는❓
1. 불필요한 추상화
인터페이스는 여러 개의 구현체가 존재할 가능성이 있을 때 의미가 있습니다. 그러나 하지만 대부분의 서비스 계층에서는 하나의 구현체만 존재하는 경우가 많습니다.
public interface UserService {
User findUserById(Long id);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public User findUserById(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
위처럼 구현체가 하나뿐이라면, 굳이 인터페이스를 분리할 필요가 없습니다. 이런 경우, 직접 클래스를 사용하면 코드가 간결해지고 유지보수도 쉬워집니다.
@Service
public class UserService {
public User findUserById(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
즉,1:1로 구현할 것이라면 굳이인터페이스를 분리할 필요가 없다고 생각합니다.
2. 코드의 가독성과 유지보수성 저하
인터페이스를 분리하면 코드의 가독성이 오히려 떨어질 수 있습니다.
- 인터페이스와 구현체를 왔다 갔다 하면서 코드를 확인해야 함.
- 단순한 비즈니스 로직에도 불필요한 추상화가 추가됨.
- 작은 규모의 프로젝트에서는 오버엔지니어링이 될 가능성이 높음
3. 테스트 시 복잡성 증가
Mocking을 활용한 단위 테스트는 Service-Impl 구조 없이도 충분히 가능합니다.
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService; // 인터페이스 없이도 Mock 주입 가능
@Test
void testFindUser() {
when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "Test User")));
User user = userService.findUserById(1L);
assertEquals("Test User", user.getName());
}
인터페이스 없이도 테스트는 문제없이 수행될 수 있으며, 오히려 단순한 구조를 유지할 수 있습니다.
4. DI(의존성 주입)와 충돌❌
Spring의 @Service는 기본적으로 클래스 기반의 빈 등록을 지원하기 때문에, 인터페이스 없이도 DI(의존성 주입)에는 아무런 문제가 없습니다.
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
즉, 인터페이스 없이도 Spring의 IoC 컨테이너는 정상적으로 동작합니다.
결론
당연한 이야기지만, 프로젝트의 규모나 상황을 고려하여 선택하는 것이 최선입니다.
객체지향프로그래밍(OOP) 관점에서 보았을 땐 인터페이스는 다형성 또는 SOLID원칙의 OCP(계방폐쇄원칙)때문에 사용합니다. 그러나 대부분의 경우 Service는 1:1 구조를 가지며, 하나의 Service가 여러 개의 ServiceImpl을 가지는 1:N 관계가 아닙니다.
즉, 다형성을 적극적으로 활용할 필요가 없는 경우가 많으므로, 불필요한 추상화는 오히려 복잡도를 증가시킬 수 있습니다.
따라서 특별한 이유가 없다면 단순한 구조를 유지하는 것이 바람직하며, 필요할 때만 인터페이스를 도입하는 것이 합리적인 접근이라고 생각이 듭니다.
'Framework > Spring\Spring boot' 카테고리의 다른 글
[SpringBoot] ObjectMapper와 직렬화/역직렬화 (0) | 2025.02.27 |
---|---|
[Spring boot] DTO와 Entity 변환 위치에 대한 고찰 - Controller vs Service Layer (0) | 2024.12.25 |
[Spring boot] Cache Manager와 @Cacheable 어노테이션 이해하기 (0) | 2024.09.02 |
[Spring boot] @Pattern 어노테이션: 정규 표현식으로 입력 검증 간편하게 하기 (0) | 2024.08.27 |
[Spring boot] @PostConstruct : 빈의 안전한 초기화 콜백 (0) | 2024.08.25 |