14. Controller → Service: 비즈니스 로직 호출
이 단계는 Spring MVC 오픈소스가 직접 서비스 클래스를 아는 구간이 아니라, 컨트롤러 메서드 본문 안에서 애플리케이션 서비스가 호출되는 경계입니다.
Spring 소스 기준으로 확인할 수 있는 것은 InvocableHandlerMethod#doInvoke()가 실제 컨트롤러 메서드를 실행한다는 점이며, 서비스 호출은 그 메서드 본문 안에서 일어납니다.
이번 단계의 역할
13번에서 컨트롤러 메서드 호출 준비가 끝나면, Spring은 method.invoke(getBean(), args)로 실제 컨트롤러 메서드를 실행합니다.
그 순간부터 서비스 호출, 도메인 로직 실행, 저장소 선택은 프레임워크가 아니라 애플리케이션 코드의 책임이 됩니다.
호출 흐름 요약
ServletInvocableHandlerMethod.invokeAndHandle()가 컨트롤러 실행을 시작합니다.InvocableHandlerMethod.invokeForRequest()가 파라미터를 준비합니다.doInvoke(args)가method.invoke(getBean(), args)를 호출합니다.- 컨트롤러 메서드 본문 안에서 서비스 객체를 호출합니다.
- 서비스 결과가 다시 컨트롤러 메서드의 반환값으로 돌아옵니다.
호출 흐름 다이어그램
핵심 코드
1) Spring이 컨트롤러 실행을 시작하는 지점
// ServletInvocableHandlerMethod.java
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
2) 실제 컨트롤러 메서드 호출
// InvocableHandlerMethod.java
public @Nullable Object invokeForRequest(...) throws Exception {
@Nullable Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
Object returnValue = doInvoke(args);
return returnValue;
}
protected @Nullable Object doInvoke(@Nullable Object... args) throws Exception {
Method method = getBridgedMethod();
return method.invoke(getBean(), args);
}
Spring이 아는 마지막 호출은 method.invoke(getBean(), args)입니다. 그 안에서 어떤 서비스를 호출하는지는 컨트롤러 구현에 달려 있습니다.
코드 해설
1) 프레임워크 관점에서 서비스 호출은 “컨트롤러 메서드 내부”
Spring MVC는 HandlerAdapter, ArgumentResolver, InvocableHandlerMethod까지는 오픈소스 프레임워크 코드로 추적할 수 있습니다.
하지만 서비스 호출 자체는 컨트롤러 메서드 본문 안에서 발생하므로, 여기서부터는 애플리케이션 코드가 주인공이 됩니다.
2) 그래서 14번은 프레임워크가 아닌 애플리케이션 경계 설명이 중요합니다
이 페이지에서 중요한 것은 “Spring이 언제 손을 놓고, 애플리케이션 코드가 언제 실행되기 시작하는가”입니다.
오픈소스 기준으로는 method.invoke(...)가 바로 그 경계입니다.
설계 의도
1) 컨트롤러 호출 전략과 비즈니스 로직을 분리하기 위해
Spring은 컨트롤러를 어떻게 호출할지에만 집중하고, 컨트롤러 내부에서 어떤 서비스를 호출할지는 애플리케이션에 맡깁니다. 이 분리 덕분에 MVC 호출 인프라는 공통화되고, 비즈니스 로직은 자유롭게 구성할 수 있습니다.
2) 컨트롤러 메서드를 일반 자바 메서드처럼 유지하기 위해
최종 호출이 method.invoke(getBean(), args)라는 것은, 컨트롤러도 결국 일반 객체 메서드라는 뜻입니다.
즉 서비스 호출은 프레임워크 특수 규칙이 아니라, 평범한 메서드 호출로 유지됩니다.
다음 단계 연결
다음 문서 15번에서는 서비스 계층이 데이터 접근으로 넘어갈 때, 왜 JPA와 MyBatis라는 두 갈래 흐름으로 나뉘는지 오픈소스 구현 기준으로 정리합니다.
← 이전: 13. DispatcherServlet → Controller: handle() | 다음: 15. Service → JPA / MyBatis 분기