Spring MVC 과제

오젼·2024년 5월 12일
1

+) 이제 막 스프링을 배워가는 단계라 제가 이해한 게 맞는지 확신이 없는 상태입니다..ㅠㅠ
잘못된 점이 있으면 알려주시면 감사하겠습니다..!!

1단계

힌트: 웹 관련 의존성

현재 프로젝트는 웹 관련 gradle 의존성이 추가되어있지 않습니다.
필요한 의존성을 찾아서 추가해주세요.

Thymeleaf 추가

templates 폴더 아래의 home.html 문서를 보면 네임스페이스에 대한 선언이 되어 있음.

xmlns:는 XML 네임스페이스에 대한 선언.
="http://www.thymeleaf.org"는 Thymeleaf 네임스페이스를 사용하겠다는 것임.

이걸 th=으로 해놨으니 th 접두어를 사용해서 Thymeleaf 템플릿에서 사용되는 속성들을 사용할 수 있게 되는 것.

저런다고 바로 사용가능한 게 아니라 build.gradle에 의존성을 추가시켜줘야 한다. Spring Boot에 포함된 라이브러리가 아닌 외부 라이브러리이기 때문에.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

그럼 Thymeleaf는 뭐냐?

Thymeleaf는 템플릿 엔진. 템플릿 엔진은 웹 애플리케이션에서 동적으로 HTML을 생성하는 도구.

그니까 템플릿 HTML 파일은 일반적으로 정적이 아닌 동적 파일. 템플릿 엔진을 사용하여 동적으로 생성되는 부분을 포함하는 것이 템플릿의 주요 목적이기 때문.

그걸 처리해서 정적 파일로 바꿔주는 것이 템플릿 엔진.

힌트: 화면 응답

현재 프로젝트 상태에서는 localhost:8080 요청 시 아무런 페이지를 응답하고 있지 않습니다.
스프링부트 프로젝트에서 welcome page를 응답하는 방법을 확인해보세요.
아니면 템플릿엔진을 활용하여 페이지를 응답하는 방법을 확인해보세요.

HomeController 생성

요청에 대한 처리를 하기 위해 controller를 생성해야 함.
보통 다음과 같은 패키지 구조를 사용한다고 함.

답변대로 웹 요청을 처리하는 컨트롤러 클래스를 구현해야 할 것이기 때문에 controller 패키지를 생성하고 그 아래 HomeController 클래스 생성.

package roomescape.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }
}

지금 이 코드가 어떻게 동작하냐면
/로 GET 요청이 오면
home()이 실행되고 home()은 "home" 문자열을 반환하고
src/main/resources/templates/home.html을 찾고
해당 문서를 찾으면 타임리프가 해당 문서를 처리해서 반환.

왜 "home"만 반환하는데 src/main/resource/templates/home.html이 되냐?

스프링부트에서 Thymeleaf를 사용할 때, src/main/resources/templates/가 기본 템플릿 디렉토리로 설정되어 있음

자동으로 prefix가 src/main/resources/templates/, suffix가 .html로 설정 됨.

이거 바꿔주려면 application.properties 또는 application.yml 파일에서 수정하면 됨.

ex)

spring.thymeleaf.prefix=classpath:/views/
spring.thymeleaf.suffix=.xhtml

이럼 이제 "home" 문자열에 설정한 prefix, suffix가 붙어서 src/main/resources/views/home.xhtml 을 찾게 되는 거.

결과

됐다..........

+) main 쪽에 하위 패키지를 import 안 했는데도 왜 돌아감?

보면 controller는 하위 패키지인데 따로 import 하지 않았는데도 잘 돌아감
왜냐면 @SpringBootApplication 어노테이션 덕분에~

이렇게 컴포넌트 스캔을 사용하면 개발자가 일일이 빈을 등록하는 작업을 하지 않아도 되므로 편리하게 스프링 애플리케이션을 구성할 수 있다.

근데 하위 패키지만 자동 스캔이 되고 다른 패키지는 따로 추가 시켜줘야 함.

삽질....

  1. 타임리프 implementation 블로그에만 적어놓고 프로젝트에 안 넣고 코드 돌렸음;; 로그 보면 "/" path에 해당하는 파일 자체를 못 찾고 있는 거고 controller에서 타임리프로 "/" GET mapping에 대한 리턴값이 처리 되게 했는데 그게 안 된 거니까 타임리프가 제대로 implementation 돼있는지 확인해 봤어야 했던 것🤔

  1. main 코드 돌려서 서버 실행중인 상태에서 테스트 코드로 또 한 번 localhost:8080을 사용하려니까 오류;;
    이건 그나마 로그 보고 바로 해결

2단계

힌트: 예약 페이지

Thymeleaf 템플릿엔진을 활용하여 페이지를 응답하세요.
/reservation 요청 시 templates/reservation.html을 응답하도록 구현해주세요.
templates/reservation.html 파일을 이동하거나 수정할 필요는 없습니다.

Reservation 메서드 생성

먼저 ReservationController를 만들고
메서드를 하나 만드는데 아까 한 것처럼 타임리프를 적용한 상태니까
@GetMapping("/reservation") return "reservation" 하면 된다.

@Controller
public class ReservationController {
    private List<ReservationDTO> reservations = new ArrayList<>();

    public ReservationController() {
        reservations.add(new ReservationDTO(1L, "안녕", "2024-05-13", "04:18"));
        reservations.add(new ReservationDTO(2L, "하세요", "2024-05-14", "04:18"));
        reservations.add(new ReservationDTO(3L, "감사해요", "2024-05-15", "04:18"));
    }

    @GetMapping("/reservation")
    public String Reservation(Model model) {
        model.addAttribute("reservations", reservations);
        return "reservation";
    }
    
    ...

Model 객체

Reservation 메서드에서 Model 객체를 인자로 받는데,
Model 객체가 뭐냐면

Model 객체를 사용하면 스프링에서 컨트롤러와 뷰 사이에서 데이터를 전달해준다!

지금 뷰에 해당하는 부분이 statics, templates 의 html 파일들.
MVC 패턴에서 뷰에서 모델의 값을 사용하고 싶을 때는 컨트롤러를 거쳐야 함.
이걸 Spring에선 Model 객체로 손쉽게 사용하도록 하고 있음.

힌트: 예약 목록 조회 API

예약 페이지 요청과 예약 목록 조회 요청을 처리하는 메서드를 구현하세요.
예약 생성 로직이 아직 없으니 정상 동작 확인을 위해 임의 데이터를 넣어서 확인해볼 수 있습니다.

ReservationDTO

예약 데이터에 관련된 값을 관리하기 위해 레코드 클래스로 DTO 클래스를 구현해준다.

https://velog.io/@zhy2on/record-class

@ResponseBody

API니까 뷰에 표현될 필요 없음.
@ResponseBody를 사용해서 뷰 템플릿을 렌더링하지 않고
메서드의 리턴 값 자체를 HTTP response body로 전송.

리스트가 직렬화(serialize) 되어 JSON 형태로 반환됨.
시리얼라이즈는 객체를 바이트스트림으로 변환하는 것.
스프링에선 jackson이 담당.

	@GetMapping("/reservations")
    @ResponseBody
    public List<ReservationDTO> getReservations() {
        return reservations;
    }

결과

삽질

API 사용한다길래 @RestController 어노테이션 사용했는데
그러면 컨트롤러 자체에서 뷰가 렌더링이 안 된다.
이번 단계에선 그냥 @Controller 사용해서 ReservationController 생성하고(그래야 뷰가 렌더링 돼야 하는 메서드는 렌더링 될 수 있음)
API 부분은 @ResponseBody를 사용하면 되는 것이었음.
그럼 해당 메서드는 뷰가 렌더링 되지 않고 리턴값이 response body에 json data 형식으로 전달 되게 되는 것.

3단계

힌트: 요구사항 해결 방향성

추가/취소 API 요청과 응답을 처리하는 컨트롤러 메서드 구현을 위해서는 Spring MVC가 제공하는 Annotation을 잘 활용해야합니다.
학습 테스트를 참고하여 기능을 익히고 각 기능들이 어떤 역할을 하는지 학습하세요.

POST, DELETE 메서드

POST: 서버에 새로운 리소스를 생성할 때 사용

DELETE: 서버에서 기존 리소스를 삭제할 때 사용

status code

201 Created: 새로운 리소스가 성공적으로 생성되었음. Location 헤더에 생성된 리소스의 URI를 제공. response body에 생성된 리소스의 표현(representation)이 JSON 형식으로 포함.

204 No Content: 요청은 성공적으로 처리되었지만, response body에 포함할 내용이 없음.

예약 추가

예약 추가 API 명세를 보면 request body에 생성할 리소스가 담겨서 옴.
그래서 메서드의 인자에 @RequestBody 어노테이션을 사용하여 리퀘스트 바디를 받아올 수 있도록 함.

ResponseEntity

Spring MVC에서 @ResponseBody를 사용하여 객체를 반환하면, 기본적으로 HTTP 200 OK status code를 사용.

status code를 지정해주고 싶다면 ResponseEntity를 사용해야 함.

이번 코드 같은 경우

@PostMapping("/reservations")
public ResponseEntity<ReservationDTO> addReservation(@RequestBody ReservationDTO reservation) {
    long id = index.incrementAndGet();
    ReservationDTO newReservation = new ReservationDTO(id, reservation.name(), reservation.date(), reservation.time());
    reservations.add(newReservation);

    return ResponseEntity.created(URI.create("/reservations/" + id))
            .body(newReservation);
}

이런식으로 created 상태코드 201을 반환하기 위해 ResponseEntity를 사용해줬음. 201의 경우 Location 헤더에 새로운 리소스의 URI를 제공하기 때문에 해당 부분을 만들어 줌.

deserialize(역직렬화)

@RequestBody ReservationDTO reservation
부분을 리퀘스트 바디로 받은 json data를 ReservationDTO 객체로deserialize(역직렬화) 한다고 한다.

시리얼라이즈란 객체를 바이트스트림으로 변환하는 과정이고
디시리얼라이즈란 바이트스트림을 객체로 변환하는 과정이다.

@RequestBody 같은 스프링에서 제공하는 어노테이션을 사용하면 스프링은 Jackson 라이브러리를 사용하여 역직렬화를 한다.

예약 취소

@PathVariable

URL 경로에 포함된 값을 메서드 매개변수에 바인딩할 때 사용.

URL이 /reservations/{id}와 같은 형태라면, {id} 부분은 동적으로 변하는 값. 이 값을 메서드 인자로 받고 싶다면 @PathVariable을 사용

힌트: 식별자 처리

예약 정보의 식별자를 생성할 때 AtomicLong를 활용할 수 있습니다.

AtomicLong

Java에서 제공하는 원자적(atomic) 연산을 지원하는 클래스.

멀티스레드 환경에서 동시에 여러 스레드가 값을 변경할 때 발생할 수 있는 race condition 문제를 해결하기 위해 고안되었음.

다른 스레드가 값을 변경하는 동안에는 해당 연산이 중단되지 않고 완료된다.

--> 값의 증가/감소 연산을 수행할 때 스레드에 안전한 방식으로 동작하기 위함.

결과

4단계..

힌트: 요구사항 해결 방향성

요청에 대한 처리를 하는 도중에 Exception이 발생하는 경우가 있습니다.
모든 예외의 경우에 400 응답을 해야하는 것은 아니기 때문에 400 응답이 필요한 경우 Exception을 정하세요.
기존 exception을 사용해도 좋고 custom exception을 만들어서 사용해도 좋습니다.
해당 exception이 발생하는 경우 컨트롤러에서 400 응답을 할 수 있도록 처리해보세요.

exception package

보통 exception package에 각각의 예외를 클래스로 구현한다고 한다.

@ControllerAdvice

@ControllerAdvice는 Spring MVC에서 제공하는 어노테이션으로, 전역적인 예외 처리와 바인딩 설정 등을 공유할 수 있는 특별한 용도의 클래스를 정의할 때 사용

음 이거 전에 정리 했었는데
그니까 exception 들을 여기로 한 번에 모이게 하고
거기서 어떤 예외냐에 따라 나눠서 처리한다.

꼭 예외처리 아니더라도 전역적으로 사용돼야 하는 바인딩 관련 로직을 추가하고 싶을 때도 사용될 수 있음.

결과

0개의 댓글