1. MVC 패턴
MVC(Model-View-Controller) 패턴은 사용자 요청을 처리하는 로직을 Model, View, Controller로 분리한다. 이를 통해 컴포넌트간 결합도를 낮추고, 특정 컴포넌트의 변경이 다른 컴포넌트에 영향을 주지 않도록 한다. 사용자 화면을 비즈니스 로직과 분리하므로, 기능 확장 및 변경이 편리해진다.

Model은 애플리케이션의 상태(애플리케이션이 현재 가지고 있는 데이터)를 나타내며, 비즈니스 로직을 수행한다. 모델의 상태에 변화가 있을 때 컨트롤러와 뷰에 이를 통보한다.
View는 모델이 보유한 정보를 화면에 시각적으로 나타낸다. 꼭 HTML만이 아니라 JSON, XML 등을 표시하는 것도 View로 본다.
Controller는 데이터와 비즈니스 로직 사이의 상호 동작을 관리하는 역할을 한다.

흐름도에서 살펴보면 이해가 좀 더 쉽다. 사용자가 먼저 브라우저를 통해 어떤 요청을 했을 때 Controller가 그 요청을 받는다. Controller는 Model에 받은 요청을 전달해 비즈니스 로직을 수행하게 한다. 이후 비즈니스 로직의 수행 결과를 View로 전달해 사용자가 결과 화면을 확인할 수 있게 되는 흐름 구조이다.
2. Front Controller 패턴과 DispatcherServlet
먼저 서블릿이란 용어에 대해 정리하고 시작해보자.
서블릿(Servlet)이란 자바 어플리케이션에서 클라이언트의 요청을 처리하고 응답을 반환하는 역할을 하는 오브젝트를 말한다.
전통적인 MVC에서는 모든 컨트롤러마다 클라이언트의 요청을 처리하고 응답을 반환하는 서블릿이 필요했다. 이는 컨트롤러 코드마다 서블릿 설정이 들어가줘야해서 코드가 복잡해지고, 컨트롤러마다 서블릿 코드가 중복되는 문제가 있었다. 이러한 문제를 해결하고자 Front Controller 패턴이 도입됐다.

Front Controller 패턴에서는 클라이언트의 모든 요청을 받아내는 Front Controller가 맨 앞단에 위치한다. 이후 Front Controller가 각각의 해당 요청을 실제로 처리할 개별 컨트롤러에게 요청을 나눠주는 구조를 가진다. 택배회사에서 우선 지역 물류센터에서 지역의 물량을 다 받아내고 개별 기사님들에서 분류해주는 것과 비슷하다.
이러한 패턴은 모든 컨트롤러에서 개별 서블릿이 없어도 돼서 코드 복잡도를 낮추는 장점이 있고, 개별 컨트롤러와 서블릿 컨테이너의 결합도를 낮춘다. 또한 컨트롤러가 서블릿에 대해 신경쓰지 않고, 핵심 비즈니스 로직만 구현하면 되기 때문에 개발자가 편해진다. Spring MVC에서는 DispatcherServlet이라는 특수한 서블릿이 바로 이 Front Controller 역할을 해준다.

자바 코드에서는 이런 구조를 갖게된다. 클라이언트의 요청을 먼저 Front Controller인 DispatcherServlet이 전부 받아낸다. 이후 요청의 URL을 어떤 컨트롤러가 처리해야하는지 HandlerMapping에 물어봐서 해당 요청을 처리할 수 있는 우리가 구현한 개별 컨테이너에 전달한다. 컨트롤러는 서비스, 스토어 레이어를 통해 데이터를 생성, 조회, 수정, 삭제하는 처리과정을 거친다. 이러한 흐름을 통해 사용자의 요청에 대해 응답을 하게된다.
아래는 과정에 대한 강의자료 사진이다. 참고해보도록 하자.

3. Spring MVC 예시 코드
그럼 이제 실제 코드로 어떻게 클라이언트의 요청을 받고 처리해서 응답을 보내는지 살펴보자. 앞서 봤듯이 개발자는 클라이언트 요청을 받아내는 Front Controller에 대한 고민을 할 필요가 없어졌다. DispatcherServlet이 알아서 해서 우리가 구현한 Controller에 넘겨줄 것이기 때문이다. 우리는 Controller, Service, Store 부분만 구현하면 된다.
Controller (요청 제어)
클라이언트의 요청을 받아서 서비스로 전달하고 응답을 결정한다.
@RestController
@RequestMapping("/club")
public class ClubController {
private ClubService clubService;
public ClubController(ClubService clubService) {
this.clubService = clubService;
}
@PostMapping // localhost:8090/club
public String register(@RequestBody TravelClubCdo travelClubCdo) {
return clubService.registerClub(travelClubCdo); // DTO를 받아 로직 위임
}
@GetMapping("/all")
public List<TravelClub> findAll() {
return clubService.findAll();
}
@GetMapping("/{clubId}")
public TravelClub find(@PathVariable String clubId) {
return clubService.findClubById(clubId);
}
@GetMapping // localhost:8090/club?name=JavaClub
public List<TravelClub> findByName(@RequestParam String clubName) {
System.out.println(clubName);
return clubService.findClubsByName(clubName);
}
@PutMapping("/{clubId}")
public void modify(@PathVariable String clubId, @RequestBody NameValueList nameValueList) {
clubService.modify(clubId, nameValueList);
}
@DeleteMapping("/{clubId}")
public void delete(@PathVariable String clubId) {
clubService.remove(clubId);
}
}
코드에서 어려운 것은 없다. Service 레이어에 정의된 비즈니스 로직을 호출해서 리턴해주면 끝나기 때문이다. 컨트롤러에서 요청을 받아내는 어노테이션에 대해서는 살펴볼 필요가 있다.
먼저 컨트롤러에서 `@RestController` 어노테이션을 사용하게 되면, 내부적으로 `@ResponseBody`가 적용된다. `@ResponseBody`가 붙어 있으면 SpringBoot에 포함된 Jackson이라는 메시지 컨버터 라이브러리에서 자바객체를 JSON으로 자동으로 변환해준다.
HTTP 요청을 자바 메소드에 연결(매핑)해주는 어노테이션들이 있다. 먼저 `@RequestMapping`은 클래스 전체의 계층을 공통으로 나타내는 역할이다. 예를 들어 `@RequestMapping("/club")`을 클래스에 써주면 해당 클래스에 있는 모든 메소드는 자동으로 `/club/~~`으로 URL이 시작되는 것이다. 즉 공통 부분을 클래스 레벨에서 빼낸 것이라고 이해하면 된다.
그 밖에 HTTP Method에 맞게 `@GetMapping`, `@PostMapping`, `@PutMapping`, `@PatchMapping`, `@DeleteMapping`이 있으며 뒤에 `@GetMapping("club/all")`처럼 괄호 안에 요청을 받을 URL을 명시해주면 된다. @RequestMapping으로 클래스내 공통 부분은 빼낼 수 있다.
HTTP 요청에서 단순히 URL만 딸랑 오는 것이 아니라 변수, 쿼리가 URL에 포함되어있거나 해당 HTTP 요청의 Body를 활용해야할 때가 있다. 이때도 어노테이션을 활용해 쉽게 대응할 수 있다.
| 어노테이션 | 데이터 추출 위치 | 특징 |
| `@PathVariable` | URL 경로 | `/clubs/{clubId}`처럼 URL 경로에 포함된 변수 값을 추출한다. |
| `@RequestParam` | 쿼리 | `/search?name=uicheol`처럼 `?` 물음표 뒤의 파라미터 값을 이름별로 1:1 매핑한다. |
| `@RequestBody` | HTTP의 Body | JSON, XML의 HTTP 요청의 Body 부분을 HttpMessageConverter를 통해 객체로 변환한다. |
| `@ModelAttribute` | Form 데이터, 객체 | 요청 파라미터를 객체(DTO) 형태로 묶어서 바인딩하며, 자동으로 모델에 추가된다. |
Service (비즈니스 로직 - Model)
@Service
public class ClubServiceLogic implements ClubService {
private ClubStore clubStore;
public ClubServiceLogic(ClubStore clubstore) {
this.clubStore = clubStore;
}
@Override
public String registerClub(TravelClubCdo clubCdo) {
TravelClub club = new TravelClub(clubCdo.getName(), clubCdo.getIntro());
return clubStore.create(club);
}
@Override
public TravelClub findClubById(String clubId) {
return clubStore.retrieve(clubId);
}
// ... 기타 메소드 구현
}
Store (데이터 접근 - Model)
@Repository
public class ClubMapStore implements ClubStore {
private Map<String, TravelClub> clubMap = new LinkedHashMap<>();
@Override
public String create(TravelClub club) {
clubMap.put(club.getId(), club);
return club.getId();
}
@Override
public TravelClub retrieve(String clubId) {
return clubMap.get(clubId);
}
// ... CRUD 기능 수행
}
이 코드에서 Controller 클래스는 컨트롤러에 대응되고, Service, Store 클래스는 모델에 대응되고, Controller에서 리턴해주는 JSON 데이터가 뷰에 해당하게 된다.
'프로젝트 > Spring' 카테고리의 다른 글
| [Spring 5] REST API, RESTful API 이름 짓기 (0) | 2026.02.26 |
|---|---|
| [Spring 5] 프로젝트 관리 도구 - Maven (0) | 2026.02.25 |
| [Spring 5] 제어의 역전(IoC), 의존성 주입(DI) (0) | 2026.02.24 |