RicoCheesethe studio log · v2.0
Live · KRRead posts
목록으로
개발PUBLISHED · 2026년 7월 14일·10 MIN READ

[스프링 요청 파이프라인] 요청이 실제로 처리되고 돌아나가기까지, Controller와 그 너머 (5)

Controller가 요청을 처리하고 Service·Repository로 일을 나누는 과정과, 응답이 역순으로 빠져나가는 흐름을 정리하며 5부작 시리즈를 마무리한다.

#spring#java#backend#web#request-lifecycle
Sleek corporate building lobby with marble walls and glass windows

접수창구를 지나 드디어 만난 사람 #

4화에서 손님은 부서 접수창구(Interceptor)를 통과했다. 신분증을 한 번 더 확인받고 방문증을 받아 든 채였다. 이제 손님은 복도를 따라 걸어가 마침내 그 사람 앞에 앉는다. 담당 직원이다. 여기까지 오는 데 문을 세 개나 거쳤지만, 정작 손님이 온 이유는 지금부터 시작이다.

이 담당 직원이 기술 용어로는 컨트롤러(@RestController 혹은 @Controller)다. 안내데스크(DispatcherServlet)가 "이 방문은 몇 층 몇 번 방으로"라고 정확히 지목해서 보낸 사람이 바로 이 직원이다. 손님이 들고 온 신청서(이름, 날짜, 첨부 서류 같은 것들)를 직원이 굳이 한 글자씩 손으로 옮겨 적지는 않는다. 스프링이 신청서 양식을 미리 보고 필요한 값을 알아서 꺼내 직원 손에 쥐여준다. 개발자들은 이걸 파라미터 바인딩이라고 부른다.

담당 직원은 혼자 다 하지 않는다 #

여기서 흔한 오해가 하나 있다. 담당 직원이 신청서를 받았으니 그 자리에서 혼자 다 처리할 거라는 오해다. 실제로는 안 그렇다. 직원은 신청서 내용을 보고 판단이 필요한 부분을 업무 규칙을 훤히 아는 동료에게 넘긴다. 이 동료가 서비스(Service)다. "이 사람이 이 서류를 받을 자격이 있는지", "포인트를 얼마나 깎아줘야 하는지" 같은 판단은 전부 이 동료 몫이다.

동료도 혼자 다 하지 않는다. 판단에 필요한 자료가 있으면 자료 보관실에 요청서를 보낸다. 이게 리포지토리(Repository)고, 그 뒤에 실제 서류함이 데이터베이스다. 자료 보관실 담당자는 요청받은 서류만 찾아서 건네줄 뿐, 그 서류로 뭘 판단할지는 관여하지 않는다.

  • 담당 직원(Controller) — 손님을 맞고, 결과물을 어떤 형태로 돌려줄지 정한다
  • 업무 규칙 동료(Service) — 실제 판단과 처리, 여러 자료를 종합하는 일을 맡는다
  • 자료 보관실(Repository) — 서류(데이터)를 찾아오거나 새로 채워 넣는다

이렇게 나뉘어 있는 이유는 1화에서 나온 그 이유와 같다. 한 사람이 접수도 하고 판단도 하고 서류도 뒤진다면, 업무 규칙 하나만 바뀌어도 그 사람의 하루 전체가 흔들린다. 판단 로직(Service)이 따로 있으면 그 부분만 고치면 되고, 다른 담당 직원(다른 Controller)도 같은 동료에게 판단을 맡길 수 있다. 데이터를 다루다 문제가 생겼을 때 되돌리는 작업(@Transactional)도 보통 이 동료 선에서 처리한다.

예를 들면 이런 식이다 #

말로만 설명하면 감이 안 잡히니 아주 짧게만 코드로 옮겨본다.

MemberController.java
java
@RestController
class MemberController {
    @GetMapping("/mypage")
    Member getMypage(Long memberId) {
        return memberService.findMember(memberId); // 판단은 동료(Service)에게
    }
}

직원(Controller)이 하는 일은 딱 두 줄이다. 넘어온 값을 동료에게 그대로 넘기고, 동료가 찾아준 결과를 돌려주는 것. 동료 안에서 무슨 판단이 벌어지는지, 자료 보관실에서 서류를 어떻게 찾아오는지는 이 직원이 알 필요가 없다. 간혹 자료 보관실에서 서류를 못 찾거나 동료가 이상한 값을 만나 처리를 못 하는 경우도 있는데, 이럴 땐 총무팀 격인 전역 예외 처리(@ControllerAdvice)가 대신 나서서 정중하게 사유를 안내한다. 존재만 짚고 넘어간다.

나갈 때는 정확히 반대 순서로 #

담당 직원이 결과물을 손님에게 건넸다고 해서 손님이 그 자리에서 바로 문밖으로 나가는 건 아니다. 들어올 때 지났던 문들을 그대로 정확히 반대 순서로 되짚어 나간다. 들어올 때 마지막으로 지난 문이 접수창구였다면, 나갈 때는 그 접수창구를 제일 먼저 다시 지난다는 뜻이다.

  1. 담당 직원(Controller)이 결과물을 반환한다
  2. 접수창구(Interceptor)가 postHandle로 한 번 더 확인한다 — "이 손님, 나가는 것도 기록해뒀나?"
  3. 안내데스크(DispatcherServlet)가 결과물을 손님이 알아볼 수 있는 형식(JSON이나 화면)으로 바꾼다
  4. 접수창구가 afterCompletion으로 "이 손님 용무 끝, 기록 마감"을 처리한다
  5. 경비실(Filter)을 다시 지나 건물 밖으로 나간다

들어올 때와 나갈 때가 대칭을 이루는 이유는 단순하다. 접수창구에서 방문 기록을 남겼다면, 그 기록을 마감하는 것도 결국 접수창구 몫이다. 경비실에서 신원을 확인했다면, 나갈 때 인원수를 세는 것도 경비실 몫이다. 문 하나가 들어올 때 한 일과 나갈 때 하는 일은 항상 짝을 이룬다.

그래서 이게 왜 중요한데 #

하나는 계층을 나누는 실무적 이유다. 신청서 검증 로직이 바뀌었는데 그 로직이 컨트롤러 안에 파묻혀 있으면, 그 컨트롤러를 쓰는 다른 화면까지 전부 손대야 할 수도 있다. Service로 분리해두면 그 부분만 고치면 되고, 여러 컨트롤러가 같은 동료에게 판단을 맡기고 있었다면 한 번 고친 게 전부에 반영된다.

다른 하나는 역순 흐름을 알아야 디버깅과 응답 가공이 쉬워진다는 점이다. 응답 헤더에 값 하나를 추가하고 싶은데 "어느 관문에서 추가해야 하나" 고민될 때, 나가는 순서가 접수창구 → 안내데스크 → 경비실이라는 걸 알면 답이 바로 나온다. 로그를 봤는데 손님이 나가는 기록(afterCompletion)만 있고 들어오는 기록이 없다면, 그건 애초에 손님이 접수창구까지 오지도 못했다는 뜻이다. 순서를 모르면 이런 단서들이 그냥 잡음으로 지나간다.

시리즈를 닫으며 #

다섯 화에 걸쳐 손님 한 명이 건물에 들어와 나가기까지의 동선을 따라왔다. 경비실(Filter)에서 신원을 거르고, 안내데스크(DispatcherServlet)가 담당자를 지목하고, 접수창구(Interceptor)가 한 번 더 확인하고, 담당 직원(Controller)이 동료(Service)와 자료 보관실(Repository)의 도움을 받아 볼일을 처리한 다음, 왔던 길을 그대로 되짚어 나간다.

버튼 하나 눌렀을 뿐인 그 0.2초 동안 이 정도 절차가 순서대로 돌아간다는 걸 알면, 다음에 "응답이 왜 이상하게 나오지"라는 질문을 받았을 때 어디부터 들여다봐야 할지 감이 잡힐 것이다. 관문 하나하나의 이름을 다 외울 필요는 없다. 손님이 지나는 순서, 그리고 그 순서가 나갈 때 그대로 뒤집힌다는 것. 이 지도 하나면 충분하다.