Deep Dive /Java Web /14

14. Controller → Service: 비즈니스 로직 호출

이 단계는 Spring MVC 오픈소스가 직접 서비스 클래스를 아는 구간이 아니라, 컨트롤러 메서드 본문 안에서 애플리케이션 서비스가 호출되는 경계입니다. Spring 소스 기준으로 확인할 수 있는 것은 InvocableHandlerMethod#doInvoke()가 실제 컨트롤러 메서드를 실행한다는 점이며, 서비스 호출은 그 메서드 본문 안에서 일어납니다.


이번 단계의 역할

13번에서 컨트롤러 메서드 호출 준비가 끝나면, Spring은 method.invoke(getBean(), args)로 실제 컨트롤러 메서드를 실행합니다. 그 순간부터 서비스 호출, 도메인 로직 실행, 저장소 선택은 프레임워크가 아니라 애플리케이션 코드의 책임이 됩니다.


호출 흐름 요약

  1. ServletInvocableHandlerMethod.invokeAndHandle()가 컨트롤러 실행을 시작합니다.
  2. InvocableHandlerMethod.invokeForRequest()가 파라미터를 준비합니다.
  3. doInvoke(args)method.invoke(getBean(), args)를 호출합니다.
  4. 컨트롤러 메서드 본문 안에서 서비스 객체를 호출합니다.
  5. 서비스 결과가 다시 컨트롤러 메서드의 반환값으로 돌아옵니다.

호출 흐름 다이어그램

sequenceDiagram participant DS as DispatcherServlet participant SIHM as ServletInvocableHandlerMethod participant IHM as InvocableHandlerMethod participant Controller participant Service DS->>SIHM: invokeAndHandle(...) SIHM->>IHM: invokeForRequest(...) IHM->>Controller: method.invoke(bean, args) Controller->>Service: business logic call Service-->>Controller: result Controller-->>IHM: returnValue

핵심 코드

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 분기