_

Always be tactful

멋쟁이사자처럼/정기 세션

[🦁2] Web App & Spring MVC #2

funczun 2025. 3. 23. 03:23
이번 포스트에서는 `스프링 MVC 동작 원리와 구현`을 다룹니다.

학습 순서
  1. 웹 애플리케이션 기본 원리
  2. 스프링 MVC 동작 원리와 구현 📍
  3. 객체 지향 설계 원칙 (다음 주 예정)

📚 세부 학습 순서

  1. Spring MVC 동작 원리
    • DispatcherServlet의 역할 & 동작 방식
    • Controller, View Resolver와의 관계
    • Spring MVC 요청 처리 흐름 (Controller → Service → Repository)
  2. Spring을 활용한 REST API 구현
    • REST의 특징 (HTTP 메서드, REST 제약 조건)
    • @RestControllerREST API 구현

Spring MVC 동작 원리

DispatcherServlet의 역할 & 동작 방식

 

 Spring MVC의 핵심 컴포넌트라고 할 수 있는 DispatcherServlet은 `프론트 컨트롤러 패턴`을 구현한 서블릿이다. 애플리케이션의 모든 HTTP 요청을 가로채서 적절한 컨트롤러로 전달하고, 최종적으로 응답을 생성하는 역할을 한다. (*컴포넌트는 각각의 독립된 모듈 정도로 생각하자.)

더보기

DispatcherServlet의 주요 역할

 

1. 요청을 받아 적절한 컨트롤러로 라우팅

2. 핸들러(컨트롤러) 실행 및 처리 결과를 반환

3. 뷰 선택 및 렌더링

    ▼ DispatcherServlet 흐름 ▼    

더보기
[클라이언트 요청]
        ↓
[DispatcherServlet] → [HandlerMapping] (컨트롤러 찾기)
        ↓
[HandlerAdapter] (컨트롤러 실행)
        ↓
[컨트롤러] (비즈니스 로직 처리, ModelAndView 반환)
        ↓
[ViewResolver] (뷰 선택)
        ↓
[View] (HTML 생성)
        ↓
[응답 반환]

DispatcherServelt,
상세 과정 알아보기

 

1. 클라이언트가 `http://example.com/reservations`와 같은 요청을 보낸다.

 

2. DispatcherServlet이 모든 요청을 가로채고 처리하기 시작한다.

*스프링 부트에서는 DispatcherServlet이 자동 설정되므로, 따로 등록할 필요가 없다.

@WebServlet(name = "dispatcherServlet", urlPatterns = "/")
public class DispatcherServlet extends FrameworkServlet {
    ...
}

 

3. DispatcherServlet이 `HandlerMapping`을 사용해 요청을 처리할 컨트롤러를 찾는다.

*스프링에서는 여러 종류의 HandlerMapping을 제공한다. (아래는 `/reservations/{id} 요청을 처리하는 @RequestMapping 기반 매핑 예시다.

@Controller
@RequestMapping("/reservations")
public class ReservationController {
    @GetMapping("/{id}")
    public String getReservation(@PathVariable Long id, Model model) {
        model.addAttribute("reservation", reservationService.getReservation(id));
        return "reservationDetail";
    }
}

 

4. 컨트롤러가 실행될 수 있도록 HandlerAdapter가 이를 호출한다.

*일반적으로 RequestMappingHandlerAdapter가 사용된다.

public interface HandlerAdapter {
    boolean supports(Object handler);
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler);
}

 

 5. 컨트롤러는 비즈니스 로직을 처리한 후 ModelAndView를 반환한다.

*아래 예시에서는 `reservationService.getReservation(id)`에서 데이터를 가져와 Model에 담고, View를 반환한다.

@GetMapping("/{id}")
public String getReservation(@PathVariable Long id, Model model) {
    model.addAttribute("reservation", reservationService.getReservation(id));
    return "reservationDetail"; // View 이름 반환
}

 

 6. DispatcherServlet은 `ViewResolver`를 사용하여 렌더링할 View를 찾는다.

*참고로 스프링 부트에서 기본적으로 설정된 ViewResolver는 `ThymeleafViewResolver`다. (JSP 사용 시 또 다르다.)

 

7. 최종적으로 View에서 HTML을 생성하여 클라이언트에게 반환한다.


Controller, View Resolver와의 관계

  • Controller: 클라이언트의 요청을 처리하는 역할을 하며, 주로 비즈니스 로직을 수행한다. 이때 반환되는 모델과 뷰 이름을 DispatcherServlet에 전달한다.
  • View Resolver: 컨트롤러에서 반환된 뷰 이름을 실제 뷰 템플릿(JSP, Thymeleaf 등)으로 변환하는 역할을 한다. 이 과정에서 DispatcherServlet은 뷰 리졸버를 통해 실제 뷰 파일을 찾고, 렌더링한다.

Spring MVC 요청 처리 흐름 (Controller → Service → Repository)

 

 Spring MVC에서는 요청을 처리할 때, `Controller → Service → Repository` 계층 구조로 분리하여 관리한다. 이 구조는 비즈니스 로직과 데이터 접근 로직을 분리하여 코드의 유지보수성과 확장성을 높이는 데 기여한다.

  1. Controller: 클라이언트의 요청을 받으며, 요청에 맞는 비즈니스 로직을 서비스 계층에 위임한다.
  2. Service: 비즈니스 로직을 처리하는 계층으로, 필요한 경우 여러 레포지토리로부터 데이터를 가져와서 처리한다.
  3. Repository: 데이터베이스와의 직접적인 상호작용을 담당하며, JPA나 MyBatis와 같은 ORM(Object-Relational Mapping) 기술을 사용해 데이터베이스에서 데이터를 조회하거나 수정한다.

Spring을 활용한 REST API 구현

REST의 특징 (HTTP 주요 메서드, REST 제약 조건, RESTful)

  • HTTP 메서드: REST는 HTTP 메서드를 기반으로 동작한다.
    • GET: 데이터를 조회
    • POST: 데이터를 생성
    • PUT: 데이터를 수정
    • DELETE: 데이터를 삭제
  • REST 제약 조건: 아래 조건을 모두 갖춘 것을 `RESTful`하다고 표현한다.
    1. 무상태성 (Stateless): 각 요청은 독립적이어야 하며, 서버는 클라이언트의 상태를 저장하지 않는다.
    2. 일관된 인터페이스 (Uniform Interface): 클라이언트와 서버 간의 통신은 일관된 방식으로 이루어져야 한다.
    3. 캐시 처리 가능 (Cacheable): 응답은 캐시할 수 있어야 한다.
    4. 계층화된 시스템 (Layered System): 서버는 여러 계층으로 분리될 수 있다.
    5. 클라이언트-서버 구조 (Client-Server): 클라이언트와 서버는 명확히 분리되어야 한다.
더보기

 일부 이해하기 어려운 개념만 추가로 설명하겠다.

 

    ▼ 캐시 처리 가능 ▼    

 REST 아키텍처에서는 서버와 클라이언트 간의 효율적인 데이터 전송을 위해 캐시 처리가 가능해야 한다.

캐시 처리가 가능하다는 것은 클라이언트가 서버에서 받은 데이터를 로컬에서 저장하고, 그 데이터를 재사용할 수 있도록 허용하는 방식이며, 이를 통해 불필요한 서버 요청을 줄이고 응답 시간을 단축시킬 수 있다.

 

 HTTP 헤더를 이용해 캐시를 제어함으로써 구현할 수 있다.

 

Cache-Control: 캐시의 유효 기간을 설정하거나 캐시가 가능한지 여부를 명시할 수 있다.

Expires: 특정 시간이 지나면 캐시된 데이터가 만료되도록 설정할 수 있다.

ETag: 서버에서 제공한 리소스의 버전을 식별할 수 있는 값으로, 클라이언트가 해당 리소스를 캐시할 수 있게 해 준다.

Cache-Control: public, max-age=3600

 *본 예시는 캐시된 데이터가 1시간(3600초) 동안 유효함을 의미한다.


@RestController와 REST API 구현

 

 @RestController는 스프링에서 `RESTful`한 서비스를 쉽게 구현할 수 있도록 도와주는 어노테이션이다. 이 어노테이션은 `@Controller`와 `@ResponseBody`를 결합한 것으로, 클라이언트에 데이터를 직접 반환하는 역할을 한다.

@RestController
@RequestMapping("/api")
public class MyRestController {

    @GetMapping("/greeting")
    public String greet() {
        return "Hello, World!";
    }

    @PostMapping("/create")
    public ResponseEntity<String> create(@RequestBody MyObject object) {
        // 비즈니스 로직 처리
        return ResponseEntity.status(HttpStatus.CREATED).body("Object created");
    }

    @PutMapping("/update/{id}")
    public ResponseEntity<String> update(@PathVariable Long id, @RequestBody MyObject object) {
        // 데이터 수정 로직
        return ResponseEntity.ok("Object updated");
    }

    @DeleteMapping("/delete/{id}")
    public ResponseEntity<String> delete(@PathVariable Long id) {
        // 데이터 삭제 로직
        return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
    }
}
  • @GetMapping: HTTP GET 요청 처리
  • @PostMapping: HTTP POST 요청 처리
  • @PutMapping: HTTP PUT 요청 처리
  • @DeleteMapping: HTTP DELETE 요청 처리
  • @RequestBody: 요청 본문에 있는 데이터를 객체로 변환
  • @PathVariable: URL 경로 변수에 바인딩
  • ResponseEntity: HTTP 응답을 구성할 때 사용하며, 상태 코드 및 헤더 설정
더보기

 몇 가지 낯선 개념만 추가로 설명하겠다.


 @RequestBody는 HTTP 요청의 바디에 포함된 데이터를 자바 객체로 변환하는 어노테이션이다.

 

 주로 POST, PUT, PATCH 요청에서 클라이언트가 보낸 데이터를 서버에서 객체로 받기 위해 사용한다.

 

 *클라이언트가 서버로 JSON, XML 등의 데이터를 HTTP 요청 바디(본문)에 담아서 보낼 때, 해당 어노테이션을 통해 그 데이터를 자바 객체로 자동 변환한다. 스프링은 기본적으로 JSON 변환을 위한 라이브러리(Jackson)을 사용해 변환해 준다.

@PostMapping("/users")
public ResponseEntity<String> createUser(@RequestBody User user) {
    // user는 요청 바디에 포함된 JSON 데이터로부터 변환된 User 객체
    userService.createUser(user);
    return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
}
{
  "name": "june",
  "email": "funczun@gmail.com"
}

 @PathVariable은 URL 경로에 포함된 변수를 자바 메서드의 파라미터로 바인딩하는 어노테이션이다.

 

 보통 RESTful API에서 리소스를 구분하기 위해 URL 경로에 변수를 사용하는 경우에 사용된다.

 

 *클라이언트가 보낸 URL 경로에서 값을 추출하여, 해당 값을 메서드 파라미터에 바인딩해 준다. 바인딩이라고 함은 값을 객체의 필드에 매핑하는 과정을 의미한다.

@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
    User user = userService.findUserById(id);
    if (user == null) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
    }
    return ResponseEntity.ok(user);
}

 ID 같은 자원식별자를 URL 경로에서 추출하고 처리할 때 유용하다. 위 예시에서는 `/users/{id}`로 요청이 들어오면, {id} 경로 변수의 값을 메서드의 id 파라미터로 바인딩한다. (예를 들어 `/users/1`로 요청을 보내면, id 파라미터는 1로 바인딩되고, getUserById() 메서드가 호출된다.)


 ResponseEntity는 HTTP 응답의 본문(body), 상태 코드(status code), 헤더(headers)를 모두 커스터마이즈할 수 있는 클래스로, 클라이언트에게 정확한 HTTP 응답을 반환하고자 할 때 사용된다.

 

 *빌더 패턴을 사용해 응답을 구성하며, 이를 통해 동적으로 응답을 생성할 수 있게 된다.

@PostMapping("/createUser")
public ResponseEntity<String> createUser(@RequestBody User user) {
    boolean isCreated = userService.createUser(user);
    if (isCreated) {
        return ResponseEntity.status(HttpStatus.CREATED)
                             .header("Location", "/users/" + user.getId())
                             .body("User created successfully");
    } else {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                             .body("User creation failed");
    }
}

`결합도`와 `응집도`는 OOP를 위한 기초 개념이다. 관련 내용은 다음 주 포스트에서 알아보자.

'멋쟁이사자처럼 > 정기 세션' 카테고리의 다른 글

[🦁3] 객체 지향 설계: OOP  (2) 2025.03.28
[🦁2] Web App & Spring MVC #1  (0) 2025.03.21
[🦁1] Git & GitHub  (0) 2025.03.14