Service 대신 UseCase를 선택하게 된 이유
✈️ 서론
Spring Boot 프로젝트를 진행하거나, 신규로 설계하면서 가장 먼저 고민했던 것은 아키텍처 자체라기보다는 Service의 책임을 어디까지 가져가야 하는가였습니다.
처음부터 UseCase 구조를 의도한 것은 아니었고, 실서비스를 만들고 운영하면서 반복적으로 겪은 문제를 그때그때 해결하다 보니 자연스럽게 선택하게 된 구조에 가깝습니다만 UseCase를 택하게 된 이유를 정리해보고자 합니다.
⚠️ 문제의 시작: 하나의 흐름이라는 명목
이전 프로젝트에서 결제를 하나의 흐름으로 처리한다는 이유로 검증, 계산, 저장, 알림까지 모든 로직이 하나의 Handler에 모여 있던 적이 있습니다. 의존성은 계속 늘어났고, 결과적으로 메서드 하나가 500줄을 넘겼습니다.구조적으로 이해는 됐습니다.
결제 프로세스라는 맥락은 분명했으니까요.
다만 문제는,
이 코드가 바뀔 때마다 어디까지가 영향 범위인지 판단하기 어려웠다는 점이었습니다.
⚠️ 나누려 했지만 더 흐려졌던 책임
이를 개선하기 위해 Service 대신 Handler, Helper, Provider, Validator 등으로 역할을 나누는 시도도 했습니다.
하지만 시간이 지나면서 이런 고민이 반복됐습니다.
- 이 로직은 FileService에 넣는 게 맞을까
- 아니면 FileHandler일까
- URL 조회는 Provider일까, Resolver일까
이름은 늘어났지만 책임을 나누는 기준은 여전히 코드 안에만 존재했고, 결국 클래스를 열어봐야만 역할을 알 수 있었습니다.
⚠️ Service 구조 안에서의 한계
Service를 더 잘 나누는 방향도 고민했습니다.
도메인별로 Service를 쪼개고, 메서드 이름과 주석으로 의도를 보완하는 방식도 시도해봤습니다.
하지만 Service라는 경계 안에서는 기능이 계속 안쪽으로 쌓였고, 수정의 단위는 여전히 Service 전체로 남아 있었습니다. 특히나 공용으로 사용하는 도메인의 서비스라던지, 중요한 비즈니스 역할을 하는 도메인의 서비스는 아무리 잘개 나눠도 한계가 보였습니다.
⚙️ 선택: 기능 하나를 구조로 고정하기
그래서 접근을 바꿨습니다.
Service에 메서드를 추가하는 대신,
클린 아키텍처에 나오는 개념 중 하나인 UseCase을 일부 차용해 기능 하나를 클래스 하나로 고정하기로 했습니다.
예를 들어 파일 도메인의 경우, 기존에는 다음과 같은 형태였습니다.
@Service
public class FileService {
public void uploadFiles(...) { }
public void deleteFile(...) { }
public void sortFiles(...) { }
public String getFileUrl(...) { }
// 이후 계속 기능이 추가됨
}파일 관련이라는 이유로 기능이 계속 추가되면서 이 Service는 점점 비대해졌고,
어떤 수정이 어디까지 영향을 미치는지 판단하기 어려워졌습니다.
이를 다음과 같이 바꿨습니다.
@Service
public class UploadTempFilesUseCase {
// 파일 업로드만 담당
}
@Service
public class DeleteTempFileUseCase {
// 파일 삭제만 담당
}
@Service
public class GetFileAccessUrlResolverUseCase {
// 파일 접근 URL 조회만 담당
}기능 하나를 클래스 하나로 고정하면서, 이 클래스는 무엇을 하는가를 코드를 열지 않아도 알 수 있도록 의도했습니다.
❓왜 UseCase였나
UseCase라는 이름 자체가 중요했던 것은 아닙니다.
클린 아키텍처에서 많이 언급되는 UseCase를 따르려고 한 것은 맞습니다
중요했던 것은 책임을 합의가 아니라 구조로 강제할 수 있느냐였습니다.
무엇을 고칠 때 어디를 봐야 하는지
이 변경이 다른 기능에 영향을 주는지
테스트 범위를 어디까지 가져가야 하는지
이 기준을 코드 구조만 보고도 판단할 수 있어야 했고, 그 역할을 가장 잘 해주던 단위가 UseCase였습니다.
📃 정리
Service 구조가 틀렸다는 이야기를 하려는 것은 아닙니다.
규모가 작거나 단순한 도메인에서는 Service 기반 구조가 훨씬 빠르고 효율적일 수 있습니다.
UseCase로 하기에 적합한 것은 복잡한 도메인의 기능을 단위 별로 나누기 위함, 또는 공용으로 사용되는 기능을 UseCase로 나누므로써 각 기능 별로 독립적으로 클래스로 존재하게 하기 위함이라고 볼 수 있을 것 같습니다.
(이름은 Service, UseCase, Resolver 등 무엇이든 상관없습니다. 중요한 것은 명칭이 아니라, 기능 단위를 최소화해 각 역할의 책임을 명확히 하는 데 있다고 생각합니다.)
다만 Service가 감당하기 어려워지는 시점에서, 어떤 문제를 겪었고
그 문제를 제가 어떻게 구조적으로 풀어보려 했는지 하나의 사례를 정리해보고 싶었습니다.
감사합니다.
실제 프로젝트에 적용 된 코드 구조, UseCase를 어디까지 적용했고 어디서 타협했는지는 블로그에 정리해 두었습니다.
https://nive-codes.tistory.com/entry/Service-대신-UseCase를-선택한-이유