
지난 글에서는 웹 서버 아키텍처를 공부했습니다. 이번에는 Spring을 처음 배우면서 새로운 개념들을 접했고, 아직 부족한 점도 많다는 것을 느꼈습니다. 이번 글에서는 기본 개념을 확실히 다지고, 좀 더 깊이 있게 Spring을 이해하는 데 집중하려고 합니다.
Spring Boot Application을 실행하면, 제일 먼저 눈에 띄는 코드입니다.
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args); // Spring 컨테이너(ApplicationContext) 생성
}
}
이때 호출되는 SpringApplication.run()은 내부적으로 Spring Container, 즉 ApplicationContext를 생성합니다.
이 컨테이너는 애플리케이션 전반에 걸쳐 모든 Bean들을 생성하고 관리하는 핵심 역할을 합니다.
✅ Bean이란?
- Spring 컨테이너가 생성하고 관리하는 객체를 의미하며, 보통
@Component,@Service,@Repository,@Controller등의 어노테이션이 붙은 클래스가 자동으로 빈으로 등록됩니다.
- 빈은 싱글톤 스코프로 기본 관리되며, 애플리케이션 전반에서 공유되어 사용됩니다.

@Controller, @Service, @Repository 등이 붙은 클래스들은 컴포넌트 스캔을 통해 자동으로 감지되고, Spring Container에 빈으로 등록됩니다.
등록된 빈들은 Spring 컨테이너에 의해 생성, 초기화, 의존성 주입, 소멸까지 생명주기가 관리됩니다.
DispatcherServlet은 Spring MVC의 Front Controller로서, 모든 HTTP 요청의 진입점 역할을 합니다.
DispatcherServlet 인스턴스는 Spring Container에 의해 하나의 Bean으로 생성 및 관리됩니다.
이 Bean은 서블릿 컨테이너의 서블릿 목록에 "DispatcherServlet 역할을 하는 서블릿"으로 등록됩니다.
이 등록 과정은 ServletContainerInitializer와 같은 서블릿 컨테이너 초기화 메커니즘을 통해 자동으로 수행되며, 개발자가 별도로 DispatcherServlet을 등록하지 않아도 웹 애플리케이션 구동 시 자동으로 서블릿 컨테이너에 등록됩니다.
DispatcherServlet은 내부적으로 Spring 컨테이너에서 HandlerMapping, HandlerAdapter, ViewResolver 등 여러 Spring MVC 핵심 컴포넌트들을 주입받아, HTTP 요청 처리 파이프라인을 구성하고 실행합니다.
예를 들어 사용자가 웹 브라우저에서 /members 같은 URL로 요청을 보낸다고 하겠습니다.
이 요청은 다음과 같은 과정을 통해 처리됩니다.
요청은 먼저 웹 서버(Web Server)에 도착합니다. Spring Boot에서는 기본적으로 내장 톰캣(Tomcat)을 사용하므로, 별도 설정 없이도 서버가 자동 실행됩니다.
요청이 정적 리소스(예: style.css, index.html)에 해당한다면, resources/static 폴더에서 바로 파일을 찾아 응답합니다.
반면에 동적인 요청이라면, 웹 서버는 이 요청을 서블릿 컨테이너로 넘기고, 컨테이너는 등록된 DispatcherServlet에게 전달합니다.
이때부터 Spring MVC의 핵심인 DispatcherServlet이 본격적으로 요청을 처리하기 시작합니다.

DispatcherServlet이 모든 요청 처리를 직접 수행하는 것은 아닙니다.
요청이 들어오면 어떤 컨트롤러가 이 요청을 처리할지 결정하는 과정은 다음과 같습니다:
이렇게 찾은 HandlerMethod는 DispatcherServlet이 직접 실행하지 않고, Spring 컨테이너로부터 주입받은 HandlerAdapter를 통해 HandlerMethod를 실행합니다.
이때, HandlerAdapter는 요청 파라미터를 알맞은 매개변수에 바인딩한 뒤 컨트롤러 메서드를 실행해줍니다.
이러한 과정 덕분에 @GetMapping("/members") 같은 선언만 해도 복잡한 매핑과 호출, 바인딩 과정이 자동으로 이루어집니다.
또한, HandlerAdapter는 어노테이션 기반, 인터페이스 기반 등 다양한 형태의 컨트롤러를 일관되게 실행할 수 있도록 중간에서 조율하는 역할도 합니다.
💡 이 모든 흐름은 Spring이 실행될 때, 스프링 컨테이너가 DispatcherServlet에 필요한 HandlerMapping과 HandlerAdapter 등을 자동으로 주입해주기 때문에 가능한 일입니다.
즉, DispatcherServlet을 직접 생성하거나 복잡하게 설정하지 않아도, 요청 처리를 위한 핵심 컴포넌트들이 자동으로 준비되고 작동하게 됩니다.

HandlerAdapter에 의해 컨트롤러 메서드가 호출되면, 본격적인 애플리케이션 로직이 시작됩니다.
하지만 대부분의 비즈니스 로직은 컨트롤러에서 직접 처리하지 않고, 아래와 같은 구조로 위임됩니다.
@RestController
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@PostMapping("/join")
public String join(@RequestBody MemberDto memberDto) {
memberService.join(memberDto);
return "회원 가입 성공";
}
}
클라이언트로부터 요청을 받고, 요청 데이터를 받아 파라미터로 변환합니다. (@RequestBody 등 사용)
이후 복잡한 비즈니스 처리는 Service 계층에 위임하고, 그 결과를 받아 응답으로 반환합니다.
로직이 단순한 경우에는 별도의 서비스 호출 없이 파라미터를 바인딩해 바로 응답을 생성하기도 합니다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void join(MemberDto memberDto) {
// 도메인 규칙 검사, 트랜잭션 처리 등
if (memberRepository.existsByEmail(memberDto.getEmail())) {
throw new IllegalStateException("이미 존재하는 이메일입니다.");
}
memberRepository.save(new Member(memberDto));
}
}
핵심 비즈니스 로직이 모여있는 곳입니다.
도메인 규칙(예: 이메일 중복 검사), 트랜잭션 관리(한 번에 작업이 완료되거나 실패하도록) 등을 처리합니다.
데이터베이스 작업이 필요하면 Repository에 작업을 위임합니다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
boolean existsByEmail(String email);
}
DB와의 실제 통신을 담당합니다.
JPA, MyBatis, JDBC 등을 통해 DB에 CRUD 작업을 수행합니다.
💡 예를 들어, 회원 가입 요청이 들어오면
Controller가 요청을 받아 →Service가 가입 관련 로직을 처리하고 →Repository가 DB에 회원 정보를 저장하는 식입니다.
이처럼 각 계층의 역할을 분리하면, 코드가 명확해지고 유지보수와 테스트가 훨씬 쉬워집니다.
컨트롤러가 응답을 반환하면, DispatcherServlet은 이를 받아서 어떻게 클라이언트에게 전달할지 결정합니다.
이때, 반환된 값의 형태에 따라 응답을 만드는 방식이 크게 두 가지로 나뉩니다:
컨트롤러가 문자열 형태로 뷰 이름(view name) 을 반환할 때 사용됩니다.
예를 들어, 컨트롤러에서 members/list 라는 뷰 이름을 반환하면, DispatcherServlet은 이 이름을 가지고 ViewResolver에게 “members/list”라는 템플릿(화면 파일)을 찾아 달라고 요청합니다.
ViewResolver는 JSP, Thymeleaf 같은 뷰 템플릿 엔진과 연결되어 있으며, 실제 템플릿 파일을 찾아서 렌더링합니다.
렌더링이란, 템플릿 파일 안에 포함된 동적 데이터를 HTML 코드로 변환하는 과정입니다.
이렇게 생성된 최종 HTML이 HTTP 응답 본문에 담겨 사용자 브라우저에 전송됩니다.
즉, 뷰 렌더링 방식은 주로 서버에서 HTML 페이지를 만들어서 보내는 전통적인 웹 애플리케이션에 적합합니다.
컨트롤러가 자바 객체나 값을 반환할 때, 그리고 그 메서드 위에 @ResponseBody나 @RestController 어노테이션이 붙어 있을 때 사용됩니다.
이 경우, DispatcherServlet은 뷰를 찾지 않고 바로 HttpMessageConverter라는 컴포넌트를 사용합니다.
HttpMessageConverter는 자바 객체를 JSON, XML, 혹은 다른 포맷으로 자동 변환해 줍니다.
가장 흔하게는 자바 객체를 JSON 문자열로 바꿔서 HTTP 응답 본문에 넣어 보냅니다.
예를 들어, 컨트롤러에서 Member 객체를 반환하면, 이 객체가 JSON 형태로 변환되어 클라이언트에게 전달됩니다.
이는 RESTful API 서버 개발에서 주로 사용되는 방식입니다.
Spring MVC의 핵심은 DispatcherServlet입니다.
모든 요청을 받아 적절한 컨트롤러로 연결하고, 컨트롤러 실행 후 결과를 뷰나 데이터로 변환해 응답합니다.
이 과정에서 HandlerMapping, HandlerAdapter, ViewResolver, HttpMessageConverter 등이 유기적으로 동작해 개발자가 복잡한 처리 없이 비즈니스 로직에 집중할 수 있게 돕습니다.
DispatcherServlet을 제대로 이해하면 Spring의 전체 흐름과 구조가 명확해집니다.
참고 문서