개요
Spring MVC를 사용하면서 자연스럽게 사용하고 있는 것이 바로 @Controller, @GetMapping, @PostMapping과 같은 애너테이션 기반 컨트롤러 매핑이다. 그러나 내부적으로 이 컨트롤러들이 실제로 어떻게 등록되고 매핑되는지 이해하는 개발자는 많지 않다.
이번 글에서는 Spring MVC가 내부적으로 어떻게 컨트롤러를 등록하고, 요청이 왔을 때 어떻게 적절한 컨트롤러 메서드를 찾아 실행하는지 살펴본다.
DispatcherServlet 이란?
Spring MVC에서 가장 중요한 클래스는 바로 DispatcherServlet이다. DispatcherServlet은 프론트 컨트롤러 역할을 하며, 모든 요청의 진입점이다.
이해를 하기 위해서는 구동 시점에 따라서 어떤 일이 발생하는지를 확인하는것이 중요하다고 생각한다.
스프링이 실행될때의 ApplicationContext 초기화 흐름을 한번 살펴보자, [어플리케이션의 구동 시점]
1. ApplicationContext(WebApplicationContext) 초기화 시작
└ 빈(Bean) 생성 및 의존성 주입 (DI)
└ @Controller, @Service 등 Component 스캔 (클래스패스 스캔)
└ 리플렉션(Reflection)을 사용하여 클래스 및 메서드 분석
└ 컨트롤러(@Controller)의 메서드(@GetMapping, @PostMapping) 정보를 추출
└ 추출된 URL과 메서드 정보를 RequestMappingHandlerMapping에 등록
2. ApplicationContext 초기화 완료
[클라이언트의 요청 처리 시점]에서도 살펴 보자
클라이언트 요청
→ DispatcherServlet이 요청을 받음
→ HandlerMapping(RequestMappingHandlerMapping)을 통해 미리 등록된 URL ↔ Controller 메서드 정보에서 요청에 맞는 메서드 탐색
→ HandlerAdapter가 Controller 메서드를 실행
→ Controller 메서드가 비즈니스 로직을 처리하고 View 이름을 반환
→ DispatcherServlet이 ViewResolver를 통해 View 객체를 찾아 반환
→ 클라이언트에 응답 전송
컨트롤러는 언제, 어떻게 등록되는가? (어플리케이션 시점)
예를 들어 아래와 같은 컨트롤러가 있다면
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
Spring은 다음과 같은 과정으로 컨트롤러를 등록한다
1. 클래스 스캔 중 @Controller가 붙은 클래스를 찾는다.
https://www.baeldung.com/spring-component-scanning
2. 리플렉션 API(Class.getDeclaredMethods())를 사용하여 해당 클래스의 모든 메서드를 읽는다.
자바의 Reflection(리플렉션) 이란, 객체를 통해 클래스의 정보를 분석하거나 런타임(runtime)에 클래스의 동작을 검사하고 조작할 수 있게 해주는 기술이다.
리플렉션을 사용하면 클래스 이름만 알고 있어도 다음과 같은 작업을 수행할 수 있다.
- 클래스의 메서드, 변수, 생성자와 같은 정보 조회
- 클래스의 객체를 동적으로 생성
- 객체의 메서드를 동적으로 호출
즉, 리플렉션은 컴파일 시점에서 클래스에 대한 명확한 타입 정보를 알지 못하더라도, 실행 시점(runtime)에 동적으로 클래스를 활용할 수 있게 해준다.
3. 메서드에 붙은 애너테이션(@GetMapping, @PostMapping 등)을 확인하고, URL 패턴과 HTTP 메서드 등을 추출한다.
4. 이렇게 얻은 정보를 바탕으로 RequestMappingHandlerMapping에 등록하여 URL → 메서드 매핑 테이블을 만든다.
* 매핑 테이블을 만드는 과정 - Spring MVC 기준
- 클래스 후보 찾기
ClassPathScanningCandidateComponentProvider가 ASM으로 클래스 파일을 읽어
@Controller, @RestController 등을 가진 빈 후보를 모읍니다.
이때는 클래스 로딩·리플렉션 없이 바이트코드만 훑음.
https://minkukjo.github.io/framework/2020/07/09/Spring-133/ - 빈 로딩 후 메서드 조사
RequestMappingHandlerMapping.initHandlerMethods() →
MethodIntrospector.selectMethods()- 실제 클래스를 로딩하고
- 리플렉션으로 모든 메서드·애노테이션을 순회하여
@RequestMapping, @GetMapping 등 매핑 정보를 수집.
- 매핑 테이블 구축
- 각 메서드를 HandlerMethod로 래핑
- Map<RequestMappingInfo, HandlerMethod>에 저장
- 이후 DispatcherServlet이 URL 패턴으로 O(1) 조회.
ComponentScan Annotation이 처리되는 과정
Component Scan 실질적으로 처리하는 클래스와 메소드
minkukjo.github.io
리플렉션은 성능에 문제를 주지 않을까?
리플렉션은 일반적으로 성능이 좋지 않다고 알려져 있다.
https://blogs.oracle.com/javamagazine/post/java-reflection-performance
The performance implications of Java reflection
Reflection slows down your Java code. Why is that?
blogs.oracle.com
- 요약 : 리플렉션은 컴파일 시점에 구체적인 타입을 모르더라도, 런타임에 클래스‧메서드‧필드 정보를 조회하고 조작하는 기능이다.
즉, 유연성 대가로 JNI 전환, 인라이닝 불가, 박싱, 접근 제어 등 의 특징을 가진다.
호출 빈도를 감지해 바이트코드를 동적 생성해 완화하지만, 여전히 직접 호출보다 느리다고 한다.
그렇다면 Spring은 리플렉션으로 인한 성능 문제를 어떻게 해결할까?
결론부터 말하자면, Spring은 리플렉션을 초기 한 번만 사용한다. (하단은 BeanUtils의 코드)

즉, 최초 애플리케이션 구동 시점에 한 번 클래스와 메서드 분석을 완료하고, 분석된 정보는 내부적으로 캐싱한다. 이후 실제 요청이 들어올 때는 캐싱된 정보를 기반으로 처리하므로 리플렉션을 다시 사용하지 않는다.
요청이 들어왔을 때의 처리 과정
실제 요청이 들어왔을 때의 처리는 다음과 같다
1. DispatcherServlet이 요청을 받고 URL을 분석한다.
2. 미리 생성된 URL → 메서드 매핑 테이블을 기반으로 HandlerMethod를 빠르게 찾아낸다. 즉, 캐시된 Map 을 통해 가져오는것이다.
3. 찾아낸 메서드는 HandlerAdapter가 실행한다.
4. HandlerMethod가 실행되어 결과가 반환되고, ViewResolver를 통해 뷰가 결정되어 최종 응답이 클라이언트에 전달된다.
요청 처리 시점에는 리플렉션 API가 사용되지 않고, 캐싱된 정보가 사용되므로 성능이 매우 우수하다.
자바 코드로 모사해보자
컨트롤러를 만들고, 그 컨트롤러 내 메서드를 호출하기
package com.example.mvc;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Scanner;
public class MethodCall {
public static void main(String[] args) throws Exception {
HashMap<String, String> map = new HashMap<>();
System.out.println("before: " + map);
MyController controller = new MyController();
String viewName = controller.main(map);
System.out.println("after: " + map);
render(map, viewName);
}
static void render(HashMap<String, String> map, String viewName) throws IOException {
StringBuilder result = new StringBuilder();
Scanner sc = new Scanner(new File(viewName + ".txt"));
while (sc.hasNextLine()) {
result.append(sc.nextLine()).append(System.lineSeparator());
}
for (String key : map.keySet()) {
result = new StringBuilder(result.toString().replace("${" + key + "}", map.get(key)));
}
System.out.println(result);
}
}
class MyController {
public String main(HashMap<String, String> map) {
map.put("id", "user01");
map.put("pwd", "1234");
return "viewTemplate";
}
}
리플렉션을 활용해서 메서드 매핑 테이블을 만든다.
package com.example.mvc;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import org.springframework.ui.Model;
import org.springframework.validation.support.BindingAwareModelMap;
public class MethodCallWithReflection {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("com.example.mvc.MyMvcController");
Object obj = clazz.getDeclaredConstructor().newInstance();
// 리플렉션이 적용된 부분.
Method method = clazz.getDeclaredMethod("main", int.class, int.class, int.class, Model.class);
Model model = new BindingAwareModelMap();
System.out.println("[before] model=" + model);
String viewName = (String) method.invoke(obj, 2021, 10, 1, model);
System.out.println("viewName=" + viewName);
System.out.println("[after] model=" + model);
render(model, viewName);
}
static void render(Model model, String viewName) throws IOException {
String result = "";
Scanner sc = new Scanner(new File("src/views/" + viewName + ".jsp"), "utf-8");
while (sc.hasNextLine()) {
result += sc.nextLine() + System.lineSeparator();
}
Map<String, Object> map = model.asMap();
for (String key : map.keySet()) {
result = result.replace("${" + key + "}", map.get(key) + "");
}
System.out.println(result);
}
}
정리하자면 Spring MVC는 애플리케이션 초기 구동 시 리플렉션을 통해 컨트롤러의 메서드 정보를 분석하고, 이후 실제 요청 처리 시에는 빠른 캐싱 정보를 활용한다.
'Spring' 카테고리의 다른 글
[Spring-Mock] Vanilla Java로 Custom HTTP Server 구축하기 [1] (0) | 2025.02.24 |
---|---|
[SecurityContextHolder] Authentication 분석하기, principal 객체로 유저 이름 가져오기 (0) | 2023.06.29 |
JPA관계매핑시 @JoinColumn 옵션 그리고 JPA명명전략(naming strategy) (0) | 2023.06.25 |
FetchType.EAGER vs FetchType.LAZY in JPA (0) | 2023.06.23 |
SpringBoot+MySQL(JPA) 회원가입,JWT 로그 구현하기 - (2) (1) | 2023.06.23 |
개요
Spring MVC를 사용하면서 자연스럽게 사용하고 있는 것이 바로 @Controller, @GetMapping, @PostMapping과 같은 애너테이션 기반 컨트롤러 매핑이다. 그러나 내부적으로 이 컨트롤러들이 실제로 어떻게 등록되고 매핑되는지 이해하는 개발자는 많지 않다.
이번 글에서는 Spring MVC가 내부적으로 어떻게 컨트롤러를 등록하고, 요청이 왔을 때 어떻게 적절한 컨트롤러 메서드를 찾아 실행하는지 살펴본다.
DispatcherServlet 이란?
Spring MVC에서 가장 중요한 클래스는 바로 DispatcherServlet이다. DispatcherServlet은 프론트 컨트롤러 역할을 하며, 모든 요청의 진입점이다.
이해를 하기 위해서는 구동 시점에 따라서 어떤 일이 발생하는지를 확인하는것이 중요하다고 생각한다.
스프링이 실행될때의 ApplicationContext 초기화 흐름을 한번 살펴보자, [어플리케이션의 구동 시점]
1. ApplicationContext(WebApplicationContext) 초기화 시작
└ 빈(Bean) 생성 및 의존성 주입 (DI)
└ @Controller, @Service 등 Component 스캔 (클래스패스 스캔)
└ 리플렉션(Reflection)을 사용하여 클래스 및 메서드 분석
└ 컨트롤러(@Controller)의 메서드(@GetMapping, @PostMapping) 정보를 추출
└ 추출된 URL과 메서드 정보를 RequestMappingHandlerMapping에 등록
2. ApplicationContext 초기화 완료
[클라이언트의 요청 처리 시점]에서도 살펴 보자
클라이언트 요청
→ DispatcherServlet이 요청을 받음
→ HandlerMapping(RequestMappingHandlerMapping)을 통해 미리 등록된 URL ↔ Controller 메서드 정보에서 요청에 맞는 메서드 탐색
→ HandlerAdapter가 Controller 메서드를 실행
→ Controller 메서드가 비즈니스 로직을 처리하고 View 이름을 반환
→ DispatcherServlet이 ViewResolver를 통해 View 객체를 찾아 반환
→ 클라이언트에 응답 전송
컨트롤러는 언제, 어떻게 등록되는가? (어플리케이션 시점)
예를 들어 아래와 같은 컨트롤러가 있다면
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
Spring은 다음과 같은 과정으로 컨트롤러를 등록한다
1. 클래스 스캔 중 @Controller가 붙은 클래스를 찾는다.
https://www.baeldung.com/spring-component-scanning
2. 리플렉션 API(Class.getDeclaredMethods())를 사용하여 해당 클래스의 모든 메서드를 읽는다.
자바의 Reflection(리플렉션) 이란, 객체를 통해 클래스의 정보를 분석하거나 런타임(runtime)에 클래스의 동작을 검사하고 조작할 수 있게 해주는 기술이다.
리플렉션을 사용하면 클래스 이름만 알고 있어도 다음과 같은 작업을 수행할 수 있다.
- 클래스의 메서드, 변수, 생성자와 같은 정보 조회
- 클래스의 객체를 동적으로 생성
- 객체의 메서드를 동적으로 호출
즉, 리플렉션은 컴파일 시점에서 클래스에 대한 명확한 타입 정보를 알지 못하더라도, 실행 시점(runtime)에 동적으로 클래스를 활용할 수 있게 해준다.
3. 메서드에 붙은 애너테이션(@GetMapping, @PostMapping 등)을 확인하고, URL 패턴과 HTTP 메서드 등을 추출한다.
4. 이렇게 얻은 정보를 바탕으로 RequestMappingHandlerMapping에 등록하여 URL → 메서드 매핑 테이블을 만든다.
* 매핑 테이블을 만드는 과정 - Spring MVC 기준
- 클래스 후보 찾기
ClassPathScanningCandidateComponentProvider가 ASM으로 클래스 파일을 읽어
@Controller, @RestController 등을 가진 빈 후보를 모읍니다.
이때는 클래스 로딩·리플렉션 없이 바이트코드만 훑음.
https://minkukjo.github.io/framework/2020/07/09/Spring-133/ - 빈 로딩 후 메서드 조사
RequestMappingHandlerMapping.initHandlerMethods() →
MethodIntrospector.selectMethods()- 실제 클래스를 로딩하고
- 리플렉션으로 모든 메서드·애노테이션을 순회하여
@RequestMapping, @GetMapping 등 매핑 정보를 수집.
- 매핑 테이블 구축
- 각 메서드를 HandlerMethod로 래핑
- Map<RequestMappingInfo, HandlerMethod>에 저장
- 이후 DispatcherServlet이 URL 패턴으로 O(1) 조회.
ComponentScan Annotation이 처리되는 과정
Component Scan 실질적으로 처리하는 클래스와 메소드
minkukjo.github.io
리플렉션은 성능에 문제를 주지 않을까?
리플렉션은 일반적으로 성능이 좋지 않다고 알려져 있다.
https://blogs.oracle.com/javamagazine/post/java-reflection-performance
The performance implications of Java reflection
Reflection slows down your Java code. Why is that?
blogs.oracle.com
- 요약 : 리플렉션은 컴파일 시점에 구체적인 타입을 모르더라도, 런타임에 클래스‧메서드‧필드 정보를 조회하고 조작하는 기능이다.
즉, 유연성 대가로 JNI 전환, 인라이닝 불가, 박싱, 접근 제어 등 의 특징을 가진다.
호출 빈도를 감지해 바이트코드를 동적 생성해 완화하지만, 여전히 직접 호출보다 느리다고 한다.
그렇다면 Spring은 리플렉션으로 인한 성능 문제를 어떻게 해결할까?
결론부터 말하자면, Spring은 리플렉션을 초기 한 번만 사용한다. (하단은 BeanUtils의 코드)

즉, 최초 애플리케이션 구동 시점에 한 번 클래스와 메서드 분석을 완료하고, 분석된 정보는 내부적으로 캐싱한다. 이후 실제 요청이 들어올 때는 캐싱된 정보를 기반으로 처리하므로 리플렉션을 다시 사용하지 않는다.
요청이 들어왔을 때의 처리 과정
실제 요청이 들어왔을 때의 처리는 다음과 같다
1. DispatcherServlet이 요청을 받고 URL을 분석한다.
2. 미리 생성된 URL → 메서드 매핑 테이블을 기반으로 HandlerMethod를 빠르게 찾아낸다. 즉, 캐시된 Map 을 통해 가져오는것이다.
3. 찾아낸 메서드는 HandlerAdapter가 실행한다.
4. HandlerMethod가 실행되어 결과가 반환되고, ViewResolver를 통해 뷰가 결정되어 최종 응답이 클라이언트에 전달된다.
요청 처리 시점에는 리플렉션 API가 사용되지 않고, 캐싱된 정보가 사용되므로 성능이 매우 우수하다.
자바 코드로 모사해보자
컨트롤러를 만들고, 그 컨트롤러 내 메서드를 호출하기
package com.example.mvc;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Scanner;
public class MethodCall {
public static void main(String[] args) throws Exception {
HashMap<String, String> map = new HashMap<>();
System.out.println("before: " + map);
MyController controller = new MyController();
String viewName = controller.main(map);
System.out.println("after: " + map);
render(map, viewName);
}
static void render(HashMap<String, String> map, String viewName) throws IOException {
StringBuilder result = new StringBuilder();
Scanner sc = new Scanner(new File(viewName + ".txt"));
while (sc.hasNextLine()) {
result.append(sc.nextLine()).append(System.lineSeparator());
}
for (String key : map.keySet()) {
result = new StringBuilder(result.toString().replace("${" + key + "}", map.get(key)));
}
System.out.println(result);
}
}
class MyController {
public String main(HashMap<String, String> map) {
map.put("id", "user01");
map.put("pwd", "1234");
return "viewTemplate";
}
}
리플렉션을 활용해서 메서드 매핑 테이블을 만든다.
package com.example.mvc;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import org.springframework.ui.Model;
import org.springframework.validation.support.BindingAwareModelMap;
public class MethodCallWithReflection {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("com.example.mvc.MyMvcController");
Object obj = clazz.getDeclaredConstructor().newInstance();
// 리플렉션이 적용된 부분.
Method method = clazz.getDeclaredMethod("main", int.class, int.class, int.class, Model.class);
Model model = new BindingAwareModelMap();
System.out.println("[before] model=" + model);
String viewName = (String) method.invoke(obj, 2021, 10, 1, model);
System.out.println("viewName=" + viewName);
System.out.println("[after] model=" + model);
render(model, viewName);
}
static void render(Model model, String viewName) throws IOException {
String result = "";
Scanner sc = new Scanner(new File("src/views/" + viewName + ".jsp"), "utf-8");
while (sc.hasNextLine()) {
result += sc.nextLine() + System.lineSeparator();
}
Map<String, Object> map = model.asMap();
for (String key : map.keySet()) {
result = result.replace("${" + key + "}", map.get(key) + "");
}
System.out.println(result);
}
}
정리하자면 Spring MVC는 애플리케이션 초기 구동 시 리플렉션을 통해 컨트롤러의 메서드 정보를 분석하고, 이후 실제 요청 처리 시에는 빠른 캐싱 정보를 활용한다.
'Spring' 카테고리의 다른 글
[Spring-Mock] Vanilla Java로 Custom HTTP Server 구축하기 [1] (0) | 2025.02.24 |
---|---|
[SecurityContextHolder] Authentication 분석하기, principal 객체로 유저 이름 가져오기 (0) | 2023.06.29 |
JPA관계매핑시 @JoinColumn 옵션 그리고 JPA명명전략(naming strategy) (0) | 2023.06.25 |
FetchType.EAGER vs FetchType.LAZY in JPA (0) | 2023.06.23 |
SpringBoot+MySQL(JPA) 회원가입,JWT 로그 구현하기 - (2) (1) | 2023.06.23 |