TIL 2023/11/02 Spring MVC

YEONGDO·2023년 11월 2일

Spring MVC

  1. Spring MVC

1) MVC 디자인 패턴

  • MVC란 Model-View-Controller의 약자로, 소프트웨어 디자인 패턴 중 하나이다.
  • MVC 패턴은 소프트웨어를 구성하는 요소들을 Model, View, Controller로 구분하여 각각의 역할을 분리함.

Model

  • 데이터와 비즈니스 로직을 담당한다.
  • 데이터베이스와 연동하여 데이터를 저장하고 불러오는 등의 작업을 수행

View

  • 사용자 인터페이스를 담당한다.
  • 사용자가 보는 화면과 버튼, 폼 등을 디자인하고 구현

Controller

  • Model과 View 사이의 상호작용을 조정하고 제어
  • 사용자의 입력을 받아 Model에 전달하고, Model의 결과를 바탕으로 View를 업데이트한다.

@ MVC 패턴은 소프트웨어를 구성하는 요소들을 분리함으로써 코드의 재사용성과 유지보수성을 높이고, 개발자들 간의 협업을 용이하게 한다. 따라서 소프트웨어를 개발할 때, MVC 패턴을 적용하여 구조를 잘 설계하는 것이 중요해

2) MVC란?

Spring Web MVC는 Servlet API를 기반으로 구축된 독창적인 웹 프레임워크로, 처음부터 Spring Framework에 포함되어 왔으며, 정식 명칭인 "Spring Web MVC"는 소스 모듈(spring-webmvc)의 이름에서 따왔으나, "Spring MVC"로 더 일반적으로 알려져 있다.

Spring MVC는 중앙에 있는 DispatcherServlet이 요청을 처리하기 위한 공유 알고리즘을 제공하는 Front Controller 패턴을 중심으로 설계되어 있으며 이 모델은 유연하고 다양한 워크 플로우를 지원

https://docs.spring.io/spring-framework/reference/web/webmvc.html

  • Spring 공식 문서에서 Spring MVC에 대한 설명으로 ‘DispatcherServlet이 중앙에서 HTTP 요청을 처리해주는데 이는 Front Controller 패턴으로 설계되어있다’라고 설명하고 있다.
  • 쉽게 표현해보자면 ‘Spring에서 MVC 디자인 패턴을 적용하여 HTTP 요청을 효율적으로 처리하고 있다’ 라고 이해하면 좋다!

Servlet (서블릿)
자바를 사용하여 웹 페이지를 동적으로 생성하는 서버 측 프로그램 혹은 그 사양

  1. 사용자가 Client(브라우저)를 통해 서버에 HTTP Request 즉, API 요청
  2. 요청을 받은 Servlet 컨테이너는 HttpServletRequest, HttpServletResponse 객체를 생성합니다.
    (약속된 HTTP의 규격을 맞추면서 쉽게 HTTP에 담긴 데이터를 사용하기 위한 객체)
  3. 설정된 정보를 통해 어떠한 Servlet에 대한 요청인지 찾는다.
  4. 해당 Servlet에서 service 메서드를 호출한 뒤 브라우저의 요청 Method에 따라 doGet 혹은 doPost 등의 메서드를 호출
  5. 호출한 메서드들의 결과를 그대로 반환하거나 동적 페이지를 생성한 뒤 HttpServletResponse 객체에 응답을 담아 Client(브라우저)에 반환한다.
  6. 응답이 완료되면 생성한 HttpServletRequest, HttpServletResponse 객체를 소멸

Front Controller

  • 모든 API 요청을 앞서 살펴본 서블릿의 동작 방식에 맞춰 코드를 구현한다면 무수히 많은 Servlet 클래스를 구현해야한다.
  • 따라서 Spring은 DispatcherServlet을 사용하여 Front Controller 패턴 방식으로 API 요청을 효율적으로 처리

  1. Client(브라우저)에서 HTTP 요청이 들어오면 DispatcherServlet 객체가 요청을 분석

  2. DispatcherServlet 객체는 분석한 데이터를 토대로 Handler mapping을 통해 Controller를 찾아 요청을 전달

  3. Controller → DispathcerServlet
    (해당 Controller는 요청에 대한 처리를 완료 후 처리에 대한 결과 즉, 데이터('Model')와 'View' 정보를 전달)

  4. DispatcherServlet → Client
    (ViewResolver 통해 View에 Model을 적용하여 View를 Client에게 응답으로 전달)

  5. Controller

@Controller
public class HelloController {
    @GetMapping("/api/hello")
    @ResponseBody
    public String hello() {
        return "Hello World!";
    }
}
  1. @GET, @POST, @PUT, @DELETE
  • @GET
@GetMapping("/api/get")
@ResponseBody
public String get() {
    return "GET Method 요청";
}
  • @POST
@PostMapping("/api/post")
@ResponseBody
public String post() {
    return "POST Method 요청";
}
  • @PUT
@PutMapping("/api/put")
@ResponseBody
public String put() {
    return "PUT Method 요청";
}
  • @DELETE
@DeleteMapping("/api/delete")
@ResponseBody
public String delete() {
    return "DELETE Method 요청";
}
  1. @RequestMapping
@Controller
@RequestMapping("/api")
public class HelloController {
    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello World!";
    }

    @GetMapping("/get")
    @ResponseBody
    public String get() {
        return "GET Method 요청";
    }

    @PostMapping("/post")
    @ResponseBody
    public String post() {
        return "POST Method 요청";
    }

    @PutMapping("/put")
    @ResponseBody
    public String put() {
        return "PUT Method 요청";
    }

    @DeleteMapping("/delete")
    @ResponseBody
    public String delete() {
        return "DELETE Method 요청";
    }
}
  1. 정적 페이지와 동적 페이지

  2. 데이터를 Client에 반환하는 방법

1) Response 트렌드 변화

과거에는 서버가 주로 요청을 받아 html/css/js 파일을 반환

최근엔 웹 생태계가 고도화 되는 과정중에 상대적으로 프론트엔드와 백엔드가 각각 따로 발전하게 되면서, 느슨하게 결합하는 방식을 더 많이 채택

그래서 최근에는 서버가 직접 뷰(html/css/js)를 반환하기 보다는 요청에 맞는 특정한 정보만 반환하는 것을 조금 더 선호하기도 한다.

서버에서는 데이터 교환 포맷 중 주로 JSON 형태로 데이터를 반환한다.

  • html 반환하는 API 코드는 없고 html을 관리하는 서버는 프론트엔드에서 따로 운영을 한다.
  • 백엔드에서는 JSON 데이터 요청을 받고 데이터를 가공해서 JSON형태로 바꾼 다음 Client에 반환하는 서버만 개발한다.
  1. JSON 데이터 반환 방법
  • 템플릿 엔진이 적용된 SpringBoot에서는 Controller에서 문자열을 반환하면 templates 폴더에서 해당 문자열의 .html 파일을 찾아서 반환해준다.
  • 따라서 html 파일이 아닌 JSON 데이터를 브라우저에 반환하고 싶다면 해당 메서드에 @ResponseBody 애너테이션을 추가해줘야한다!
  1. @RestController

= @Controller + @ResponseBody

  • @RestController를 사용하면 해당 클래스의 모든 메서드에 @ResponseBody 애너테이션이 추가되는 효과를 부여할 수 있습니다.
  1. Jackson

Jackson은 JSON 데이터 구조를 처리해주는 라이브러리

  • ObjectJSON 타입의 String으로 변환해줄 수 있다.
  • JSON 타입의 StringObject로 변환해줄 수 있다.

Spring은 3.0버전 이후로 Jacskon과 관련된 API를 제공함으로써, 우리가 직접 소스 코드를 작성하여 JSON 데이터를 처리하지 않아도 자동으로 처리

1) Object To JSON

@Test
@DisplayName("Object To JSON : get Method 필요")
void test1() throws JsonProcessingException {
    Star star = new Star("Robbie", 95);

    ObjectMapper objectMapper = new ObjectMapper(); // Jackson 라이브러리의 ObjectMapper
    String json = objectMapper.writeValueAsString(star);

    System.out.println("json = " + json);
}
  • objectMapper의 writeValueAsString 메서드를 사용하여 변환할 수 있다.
  • 파라미터에 JSON으로 변환시킬 Object의 객체를 주면 된다.
  • ObjectJSON 타입의 String으로 변환하기 위해서는 해당 Object에 get Method가 필요

2) JSON To Object

@Test
@DisplayName("JSON To Object : 기본 생성자 & (get OR set) Method 필요")
void test2() throws JsonProcessingException {
    String json = "{\"name\":\"Robbie\",\"age\":95}"; // JSON 타입의 String

    ObjectMapper objectMapper = new ObjectMapper(); // Jackson 라이브러리의 ObjectMapper

    Star star = objectMapper.readValue(json, Star.class);
    System.out.println("star.getName() = " + star.getName());
}
  • objectMapper의 readValue 메서드를 사용하여 변환할 수 있다.
  • 첫 번째 파라미터는 JSON 타입의 String, 두 번째 파라미터에는 변환할 Object의 class 타입을 주면 된다..
  • JSON 타입의 StringObject로 변환하기 위해서는 해당 Object에 기본 생성자와 get 혹은 set 메서드가 필요
  1. Path Variable 과 Request Param

1) Path Variable

  • Client 즉, 브라우저에서 서버로 HTTP 요청을 보낼 때 데이터를 함께 보낼 수 있다.
  • 서버에서는 이 데이터를 받아서 사용해야하는데 데이터를 보내는 방식이 한 가지가 아니라 여러 가지가 있기 때문에 모든 방식에 대한 처리 방법을 학습
GET http://localhost:8080/hello/request/star/Robbie/age/95
- 서버에 보내려는 데이터를 URL 경로에 추가할 수 있다.
// [Request sample]
// GET http://localhost:8080/hello/request/star/Robbie/age/95
@GetMapping("/star/{name}/age/{age}")
@ResponseBody
public String helloRequestPath(@PathVariable String name, @PathVariable int age)
{
    return String.format("Hello, @PathVariable.<br> name = %s, age = %d", name, age);
}
- 데이터를 받기 위해서는 /star/{name}/age/{age}  이처럼 URL 경로에서 데이터를 받고자 하는 위치의 경로에 {data} 중괄호를 사용
- 그리고 해당 요청 메서드 파라미터에 @PathVariable 애너테이션과 함께 {name} 중괄호에 선언한 변수명과 변수타입을 선언하면 해당 경로의 데이터를 받아올 수 있다.

2) Request Param

GET http://localhost:8080/hello/request/form/param?name=Robbie&age=95
- 서버에 보내려는 데이터를 URL 경로 마지막에 ? 와 & 를 사용하여 추가할 수 있습니다.
// [Request sample]
// GET http://localhost:8080/hello/request/form/param?name=Robbie&age=95
@GetMapping("/form/param")
@ResponseBody
public String helloGetRequestParam(@RequestParam String name, @RequestParam int age) {
    return String.format("Hello, @RequestParam.<br> name = %s, age = %d", name, age);
}
- 데이터를 받기 위해서는 ?name=Robbie&age=95 에서 key 부분에 선언한 name과 age를 사용하여 value에 선언된 Robbie, 95 데이터를 받아올 수 있다.
- 해당 요청 메서드 파라미터에 @RequestParam 애너테이션과 함께 key 부분에 선언한 변수명과 변수타입을 선언하면 데이터를 받아올 수 있다.
  1. HTTP 데이터를 객체로 처리하는 방법

1) @ModelAttribute

@ form 태그 POST


// [Request sample]
// POST http://localhost:8080/hello/request/form/model
// Header
//  Content type: application/x-www-form-urlencoded
// Body
//  name=Robbie&age=95
@PostMapping("/form/model")
@ResponseBody
public String helloRequestBodyForm(@ModelAttribute Star star) {
    return String.format("Hello, @ModelAttribute.<br> (name = %s, age = %d) ", star.name, star.age);
}
  • HTML의 form 태그를 사용하여 POST 방식으로 HTTP 요청을 보낼 수 있다.
  • 이때 해당 데이터는 HTTP Body에 name=Robbie&age=95 형태로 담겨져서 서버로 전달된다.
  • 해당 데이터를 Java의 객체 형태로 받는 방법은 @ModelAttribute 애너테이션을 사용한 후 Body 데이터를 Star star 받아올 객체를 선언

@ Query String 방식

GET http://localhost:8080/hello/request/form/param/model?name=Robbie&age=95
// [Request sample]
// GET http://localhost:8080/hello/request/form/param/model?name=Robbie&age=95
@GetMapping("/form/param/model")
@ResponseBody
public String helloRequestParam(@ModelAttribute Star star) {
    return String.format("Hello, @ModelAttribute.<br> (name = %s, age = %d) ", star.name, star.age);
}
  • ?name=Robbie&age=95 처럼 데이터가 두 개만 있다면 괜찮지만 여러 개 있다면 @RequestParam 애너테이션으로 하나 씩 받아오기 힘들 수 있다.
  • 이때 @ModelAttribute 애너테이션을 사용하면 Java의 객체로 데이터를 받아올 수 있다.
  • 파라미터에 선언한 Star 객체가 생성되고, 오버로딩된 생성자 혹은 Setter 메서드를 통해 요청된 name & age 의 값이 담겨진다.
  • 주의할 점
    @ModelAttribute는 생략이 가능, 이때, 생각해볼 문제!
    Spring에서는 @ModelAttribute뿐만 아니라 @RequestParam도 생략이 가능
    그렇다면 Spring은 이를 어떻게 구분할까? 간단하게 설명하자면 Spring은 해당 파라미터(매개변수)가 SimpleValueType이라면 @RequestParam으로 간주하고 아니라면 @ModelAttribute가 생략되어있다 판단한다.
    SimpleValueType은 원시타입(int), Wrapper타입(Integer), Date등의 타입을 의미한다.

@ RequestBody 방식

  • HTTP Body에 JSON 데이터를 담아 서버에 전달할 때 해당 Body 데이터를 Java의 객체로 전달 받을 수 있다.
  • Body JSON 데이터
POST http://localhost:8080/hello/request/form/json
// [Request sample]
// POST http://localhost:8080/hello/request/form/json
// Header
//  Content type: application/json
// Body
//  {"name":"Robbie","age":"95"}
@PostMapping("/form/json")
@ResponseBody
public String helloPostRequestJson(@RequestBody Star star) {
    return String.format("Hello, @RequestBody.<br> (name = %s, age = %d) ", star.name, star.age);
}
  • HTTP Body에 {"name":"Robbie","age":"95"} JSON 형태로 데이터가 서버에 전달되었을 때 @RequestBody 애너테이션을 사용해 데이터를 객체 형태로 받을 수 있습니다.
  • 주의할 점

데이터를 Java의 객체로 받아올 때 주의할 점

  • 해당 객체의 필드에 데이터를 넣어주기 위해 set or get 메서드 또는 오버로딩된 생성자가 필요하다.
  • 예를 들어 @ModelAttribute 사용하여 데이터를 받아올 때 해당 객체에 set 메서드 혹은 오버로딩된 생성자가 없다면 받아온 데이터를 해당 객체의 필드에 담을 수 없다.
  • 이처럼 객체로 데이터를 받아올 때 데이터가 제대로 들어오지 않는다면 우선 해당 객체의 set or get 메서드 또는 오버로딩된 생성자의 유무를 확인하시면 좋다.
  1. Create, Read 구현

1) DTO

  • 이름에서도 알 수 있듯이 DTO(Data Transfer Object)는 데이터 전송 및 이동을 위해 생성되는 객체를 의미합니다.
  • Client에서 보내오는 데이터를 객체로 처리할 때 사용
  • 또한 서버의 계층간의 이동에도 사용
  • 그리고 DB와의 소통을 담당하는 Java 클래스를 그대로 Client에 반환하는 것이 아니라 DTO로 한번 변환한 후 반환할 때도 사용

(VO랑 DTO랑 같은 의미인 줄 알았는데 찾아보니까 VO는 read only 속성을 가지고 있다!)

2) Create 구현

Controller

package com.sparta.memo.controller;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import org.springframework.web.bind.annotation.*;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

    private final Map<Long, Memo> memoList = new HashMap<>();

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
        // RequestDto -> Entity
        Memo memo = new Memo(requestDto);

        // Memo Max ID Check
        Long maxId = memoList.size() > 0 ? Collections.max(memoList.keySet()) + 1 : 1;
        memo.setId(maxId);

        // DB 저장
        memoList.put(memo.getId(), memo);

        // Entity -> ResponseDto
        MemoResponseDto memoResponseDto = new MemoResponseDto(memo);

        return memoResponseDto;
    }

    @GetMapping("/memos")
    public List<MemoResponseDto> getMemos() {
        // Map To List
        List<MemoResponseDto> responseList = memoList.values().stream()
                .map(MemoResponseDto::new).toList();

        return responseList;
    }
}

ResponseDto

import lombok.Getter;

@Getter
public class MemoResponseDto {
    private Long id;
    private String username;
    private String contents;
}

RequestDto

import lombok.Getter;

@Getter
public class MemoRequestDto {
    private String username;
    private String contents;
}

데이터를 저장할 Java 컬렉션 생성

private final Map<Long, Memo> memoList = new HashMap<>();

로직 작성

@PostMapping("/memos")
public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
    // RequestDto -> Entity
    Memo memo = new Memo(requestDto);

    // Memo Max ID Check
    Long maxId = memoList.size() > 0 ? Collections.max(memoList.keySet()) + 1 : 1;
    memo.setId(maxId);

    // DB 저장
    memoList.put(memo.getId(), memo);

    // Entity -> ResponseDto
    MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
    
    return memoResponseDto;
}

3) Read 구현

@GetMapping("/memos")
public List<MemoResponseDto> getMemos() {
    // Map To List
    List<MemoResponseDto> responseList = memoList.values().stream()
            .map(MemoResponseDto::new).toList();

    return responseList;
}
  • DB 역할을 하는 memoList를 조회하여 List로 변환한 후 반환합니다.
profile
개발 블로그

0개의 댓글