
안녕하세요! 최근에 본 한 유튜브 영상을 계기로 정리해보고 싶은 이야기가 있어 글을 쓰게 되었어요.
아래 영상은 제가 개인적으로 존경하는 개발자 제미니님 채널의 콘텐츠인데요.
영상을 보고 나서 “아… 나는 지금까지 너무 관성적으로 개발해온 건 아닐까?” 하는 생각이 강하게 들더라고요.
그래서 이번 글에서는 ServiceImpl 패턴을 당연하게 사용하는 관행을 한 번 돌아보고자 해요.
가볍게 읽어주세요. (영상을 함께 보시면 더 좋아요!)
이 글에서 이야기할 내용
- ServiceImpl 패턴이 무엇이며, 왜 스프링에서 거의 국룰처럼 쓰이게 되었는지
- 이 패턴이 실제로 제공하는 장점은 무엇인지
- 그런데도 왜 “무조건 쓰지 말자”는 말이 나오는지
- 마지막으로, 언제 인터페이스를 도입하는 것이 진짜 가치 있는 선택인지
결론부터 말씀드리면, 이 글은 ServiceImpl을 쓰지 마세요가 아니라 서비스 인터페이스는 정말 필요한 순간에만 쓰자에 더 가까워요.
ServiceImpl 패턴이란?
스프링으로 프로젝트를 만들다 보면 자연스럽게 다음과 같은 계층 구조를 떠올리게 돼요.
- Controller: 사용자 요청 처리
- Service: 비즈니스 로직
- Repository: DB 접근
그리고 Service 계층은 보통 아래와 같은 형태로 작성하곤 하죠.
public interface UserService {
User getUserById(Long id);
void createUser(User user);
void updateUser(User user);
void deleteUser(Long id);
}
그리고 이를 다음처럼 구현합니다.
@Service
public class UserServiceImpl implements UserService {
...
}
여기서 Impl은 Implementation의 약자로, 말 그대로 “이 인터페이스의 구현체”라는 의미를 담고 있어요.
이 구조는 스프링 개발 경험이 조금이라도 있다면 너무 익숙할 만큼 널리 쓰이고 있는데요.
그렇다면 왜 이렇게까지 흔해졌을까요?
ServiceImpl 패턴이 주는 실제 장점
인터페이스가 있으면 비즈니스 로직이 더 잘 읽혀요
Service 클래스를 인터페이스 없이 곧바로 구현하면, 클래스가 커질수록 전체 기능을 한눈에 파악하기 어려워져요.
예를 들어 아래처럼 구현 클래스만 존재한다고 가정해볼게요.
@Service
public class UserService {
...
}
이 경우 서비스가 어떤 기능을 제공하는지 확인하려면 내부 코드를 계속 스크롤해야 하죠.
반대로 인터페이스가 하나 있으면 이야기가 달라져요.
public interface UserService {
User getUserById(Long id);
void createUser(User user);
void updateUser(User user);
void deleteUser(Long id);
}
인터페이스만 봐도 이 서비스의 책임과 기능이 빠르게 드러나요.
특히 팀 단위 개발이나 규모가 큰 프로젝트일수록 이 차이는 더 크게 느껴져요.
구현체만 바꾸면 새로운 요구사항을 반영할 수 있어요
인터페이스가 있으면 새로운 요구사항이 생겼을 때 구현체를 교체하는 방식으로 기능 확장이 가능해요.
예를 들어 기존 UserService는 예외 처리가 단순했다고 가정해볼게요.
이제 더 정교한 처리가 필요해져서 새로운 구현체를 도입하고 싶다면 이렇게 작성할 수 있어요.
public class UserServiceV2Impl implements UserService {
@Override
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
}
}
클라이언트 코드는 여전히 UserService 인터페이스만 바라보기 때문에, 큰 수정 없이 새 구현체로 교체할 수 있어요.
이런 점들은 분명 ServiceImpl 패턴의 강점이에요.
여기까지 보면 확실히 괜찮은 패턴처럼 보이죠.
그런데도 “굳이 쓰지 말자”는 의견이 나오는 이유는 무엇일까요?
그런데, 왜 ServiceImpl을 쓰지 말자는 얘기가 나올까요?

가장 큰 이유는 인터페이스의 본래 목적을 흐리기 때문이에요.
인터페이스는 원래 이 기능은 여러 구현체를 가질 수 있다는 사실을 드러내기 위한 장치예요.
그래서 인터페이스가 등장하는 순간 개발자는 자연스럽게 이렇게 기대하게 되죠.
아, 이 인터페이스는 구현체가 여러 개 있겠구나
상황에 따라 다른 동작을 하게 만들 수 있겠구나
하지만 현실은 조금 다릅니다.
- 구현체는 항상 단 하나뿐이고
- 단지 패키지가 깔끔해 보인다는 이유로
- 모든 서비스에 인터페이스 + Impl 구조가 붙어요.
그렇게 되면 인터페이스는 다형성을 암시하는 구조적 힌트가 아니라,
관성적으로 만들어놓는 파일 하나로 전락해 버립니다.
결과적으로 프로젝트 구조의 직관성도 떨어지고,
인터페이스가 왜 필요한지에 대한 판단 기준도 흐려지게 돼요.
다형성이 진짜 필요한 경우: 이럴 때 인터페이스가 빛나요
인터페이스가 진가를 발휘하는 순간은 분명 존재해요.
대표적인 예가 사용자 유형마다 서비스 로직이 달라지는 경우예요.
public interface UserService {
...
void performUserSpecificOperation(User user);
}
이 구조에서는 다음처럼 여러 구현체를 둘 수 있어요.
GeneralUserServiceAdminUserServiceGuestUserService
이 경우 인터페이스를 사용하는 것이 매우 타당해요.
각 역할마다 실제로 서로 다른 동작이 필요하기 때문이죠.
팩토리를 사용해 적절한 구현체를 선택하는 흐름도 자연스럽게 이어집니다.
public UserService getUserService(User user) {
switch (user.getRole()) {
case "ADMIN": return adminUserService;
case "GUEST": return guestUserService;
default: return generalUserService;
}
}
이처럼 인터페이스는 다형성이 실제로 필요한 구조에서 가장 큰 가치를 발휘해요.
그리고 이런 상황이라면, 다른 개발자도 인터페이스만 보고 전체 설계를 직관적으로 이해할 수 있어요.
관성적인 ServiceImpl 사용이 만드는 문제들
인터페이스의 의미가 사라져요
모든 서비스를 인터페이스 + Impl 구조로 만들기 시작하면,
“이 서비스는 왜 인터페이스가 필요한 거지?”라는 질문에 답하기 어려워져요.
인터페이스 파일은 계속 늘어나지만, 정작 다형성이 필요한 경우는 거의 존재하지 않는 상황이 벌어지죠.
결과적으로 인터페이스의 존재 이유가 희미해지고, 설계가 의미 없이 복잡해져요.
단일 책임 원칙(SRP)을 깨기 쉬워요
User 도메인을 예로 들면 이런 코드 구성을 흔히 보게 돼요.
- 로그인 로직
- 회원가입 로직
- 프로필 조회·수정 로직
- 유저 삭제 로직
이 모든 기능을 UserService + UserServiceImpl에 한꺼번에 몰아넣기 쉽죠.
인터페이스가 있으니 “여기에 모아도 괜찮겠지?”라는 심리가 작용하기도 하고요.
하지만 이 기능들은 각각 서로 다른 책임을 가진 독립적인 유스케이스예요.
아래처럼 서비스를 분리하는 편이 더 자연스러운 경우가 많아요.
LoginServiceSignUpServiceUserProfileService
이처럼 유스케이스 단위로 클래스를 쪼개면, 인터페이스가 없더라도 각 기능의 역할이 명확하게 드러나요.
SRP(단일 책임 원칙)를 지키기도 훨씬 쉬워지고요.
그러면 언제 인터페이스를 도입해야 할까요?
인터페이스는 있어도 없어도 그만인 구조적 장식이 아니에요.
저는 아래 질문들을 기준으로 인터페이스 도입 여부를 판단하려고 해요.
- ✔ 여러 구현체가 필요할 가능성이 있는가?
- ✔ 외부 모듈이나 다른 팀에 제공해야 하는 안정적인 계약이 필요한가?
- ✔ 리팩토링 과정에서 공통 로직을 실제로 추출해야 하는 상황이 발생했는가?
이 중 하나라도 “예”라면 인터페이스를 고려할 충분한 가치가 있어요.
하지만 대부분의 서비스는 이 조건에 해당하지 않아요.
그냥 클래스 하나로 시작하세요.
정말 필요해지는 순간이 오면, 그때 인터페이스를 도입해도 전혀 늦지 않아요.
영상에서 재민님은 인터페이스를 가장 아껴 써야 할 자원이라고 표현해요.
저는 이 말이 정말 크게 와닿았어요.
저도 예전에는
“ServiceImpl이 좋은 구조라니까, 그냥 전부 이렇게 만들면 되겠지!”
라고 생각하며 프로젝트를 진행한 적이 있었어요.
하지만 지금 돌아보면, 그건 구조를 단단하게 만든 게 아니라 불필요한 복잡도를 키운 선택에 가까웠던 것 같아요.
재민님과 기술적인 대화를 나눌 기회가 한 번 있었는데, 그때 동시성 처리를 어떻게 하느냐는 질문을 받았어요.
저는 자신 있게 “Redis로 분산 락을 잡습니다!”라고 답했죠.
요즘 많이 쓰이고 성능도 좋으니까 꽤 괜찮은 선택이라고 믿고 있었어요.
그런데 돌아온 피드백은 예상 밖이었어요.
새로운 기술을 도입하는 건 좋지만,
기존 구조를 약하게 만들 수 있다면 신중해야 합니다.
그 말을 듣고 깨달았어요.
멋져 보이는 기술을 쓰는 것보다, 구조적으로 단단한 선택을 하는 것이 더 중요하다는 것을요.
ServiceImpl 패턴도 같은 맥락이라고 생각해요.
좋아 보인다고 무조건 적용할 것이 아니라,
정말 필요할 때 사용하는 것이 구조를 더 건강하게 만들어요.
ServiceImpl 패턴 자체는 좋은 도구예요.
하지만 모든 도구가 그렇듯, 언제 사용하는 것이 가장 가치 있는지가 더 중요하죠.
인터페이스는 필요할 때만 도입해도 충분해요.
오히려 그렇게 할 때 프로젝트 구조가 더 직관적이고 유지보수하기도 쉬워지더라고요.
앞으로도 다양한 패턴과 기술을 접할 때마다
“지금 이 상황에서 정말 필요한가?”라는 질문을 계속 던지며
조금씩 더 좋은 설계를 향해 고민해보고 싶습니다!
읽어주셔서 감사합니다! 🙇♂️
'Backend > Spring 🌱' 카테고리의 다른 글
| Spring Boot에서 PostgreSQL Master–Slave Replication 적용하기 (0) | 2025.11.27 |
|---|---|
| Redis 캐싱으로 API 성능 개선하기 (0) | 2024.04.01 |
| Spring SSE와 Redis Pub/Sub으로 구현하는 실시간 알림 (다중 서버 환경까지 스케일링하기) (0) | 2024.03.19 |
| 스프링 컨테이너 이해하기: BeanFactory와 ApplicationContext (0) | 2024.03.12 |
| Spring MVC에서 HandlerMapping과 HandlerAdapter를 나눈 이유 (0) | 2024.02.07 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆