스프링 인프런 김영한

52. HTTP 응답 - HTTP API, 메시지 바디에 직접 입력

HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로
HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

RestController REST API에 대한 이야기이다.

참고
HTML이나 뷰 템플릿을 사용해도 HTTP 응답 메시지 바디에 HTML 데이터가 담겨서 전달된다.
여기서 설명하는 내용은 정적 리소스나 뷰 템플릿을 거치지 않고
직접 HTTP 응답 메시지를 전달하는 경우를 말한다

52.1 HttpServletResponse을

가장간단한 방식은 HttpServletResponse을 사용하는 것이다.

@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
    response.getWriter().write("ok");
}

52.2 ResponseEntity

다음은 HttpEntity, ResponseEntity(Http Status 추가)를 사용하는 것이다.

@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
    return new ResponseEntity<>("ok", HttpStatus.OK);
}

52.3 @ResponseBody

@ResponseBody를 보내면 그냥 문자만 보낼 수 있다.

@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
    return "ok";
}

52.4 json처리

ResponseEntity에 객체를 담아서 보내면 json데이터로 보낼 수 있다.

@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1() {
    HelloData helloData = new HelloData();
    helloData.setUsername("userA");
    helloData.setAge(20);
    return new ResponseEntity<>(helloData, HttpStatus.OK);
}

52.5 발전

@ResponseStatus() 어노테이션을 활용해서 상태코드를 보낼 수도 있다.
@ResponseBody로 객체를 보내면 된다.

프로젝트를 할때 ResponseEntity에 map객체를 담아서 보냈었는데
이 방식대로 그냥 보낼 수 도 있다는 생각이 들게 되었다.

그런데 이부분은 상태코드를 여러개 지정하고 하는 부분에서 사람마다
선택권이 다르다고 생각되는 것 같다.

@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
    HelloData helloData = new HelloData();
    helloData.setUsername("userA");
    helloData.setAge(20);
    return helloData;
}

responseBodyV1
서블릿을 직접 다룰 때 처럼
HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달한다.
response.getWriter().write("ok")

responseBodyV2
ResponseEntity 엔티티는 HttpEntity 를 상속 받았는데, HttpEntity는 HTTP 메시지의 헤더, 바디정보를 가지고 있다.
ResponseEntity 는 여기에 더해서 HTTP 응답 코드를 설정할 수 있다.
HttpStatus.CREATED 로 변경하면 201 응답이 나가는 것을 확인할 수 있다.

responseBodyV3
@ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있다.
ResponseEntity 도 동일한 방식으로 동작한다.

responseBodyJsonV1
ResponseEntity 를 반환한다. HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환된다.

responseBodyJsonV2
ResponseEntity 는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody 를 사용하면 이런 것을
설정하기 까다롭다.

@ResponseStatus(HttpStatus.OK) 어노테이션을 사용하면 응답 코드도 설정할 수 있다.
물론 어노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없다.
프로그램 조건에 따라서 동적으로 변경하려면 ResponseEntity 를 사용하면 된다.

@RestController
@Controller 대신에 @RestController 어노테이션을 사용하면, 해당 컨트롤러에 모두
@ResponseBody 가 적용되는 효과가 있다.
따라서 뷰 템플릿을 사용하는 것이 아니라 HTTP 메시지 바디에 직접 데이터를 입력한다.
이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 컨트롤러이다.
참고로 @ResponseBody 는 클래스 레벨에 두면 전체 메소드에 적용되는데
@RestController 어노테이션 안에 @ResponseBody 가 적용되어 있다.

53. HTTP 메시지 컨버터

뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라
HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

HTTP 메시지 컨버터를 설명하기 전에 잠깐 과거로 돌아가서 스프링 입문 강의에서 설명했던 내용을 살펴보자.

@ResponseBody 사용 원리
@ResponseBody를 사용하면 HTTP의 BODY에 문자 내용을 직접 반환한다.
viewResolver 대신에 HttpMessageConverter가 동작한다.

기본 문자처리: StringHttpMessageConverter
기본 객체처리: MappingJackson2HttpMessageConverter
byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있다.

참고할점
응답의 경우 클라이언트의 HTTP Accept 해더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해서 HttpMessageConverter 가 선택된다.

스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다.
HTTP 요청: @RequestBody , HttpEntity(RequestEntity)
HTTP 응답: @ResponseBody , HttpEntity(ResponseEntity)
HTTP 메시지 컨버터 인터페이스
org.springframework.http.converter.HttpMessageConverter

public interface HttpMessageConverter<T> {
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    List<MediaType> getSupportedMediaTypes();
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;
}

HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용된다.
canRead() canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크한다.
read() write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능이다.

스프링 부트 기본 메시지 컨버터(일부 생략)

0순위 ByteArrayHttpMessageConverter
1순위 StringHttpMessageConverter
2순위 MappingJackson2HttpMessageConverter

스프링 부트는 다양한 메시지 컨버터를 제공하는데
대상 클래스 타입과 미디어 타입 둘을 체크해서 사용여부를 결정한다.
만약 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다.

몇가지 주요한 메시지 컨버터를 알아보자.

53.1 ByteArrayHttpMessageConverter

byte[] 데이터를 처리한다.
클래스 타입: byte[] , 미디어타입: */*(아무거나)
요청 예) @RequestBody byte[] data
응답 예) @ResponseBody return byte[] 쓰기 미디어타입 application/octet-stream

53.2 StringHttpMessageConverter

String 문자로 데이터를 처리한다.
클래스 타입: String , 미디어타입: */*(아무거나)
요청 예) @RequestBody String data
응답 예) @ResponseBody return "ok" 쓰기 미디어타입 text/plain

content-type: application/json
@RequestMapping
void hello(@RequestBody String data) {}

53.3 MappingJackson2HttpMessageConverter

application/json
클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
요청 예) @RequestBody HelloData data
응답 예) @ResponseBody return helloData 쓰기 미디어타입 application/json 관련

content-type: application/json
@RequestMapping
void hello(@RequestBody HelloData data) {}

안되는 케이스
객체타입은 맞는데 미디어타입이 json이아니라서 탈락한다.

content-type: text/html
@RequestMapping
void hello(@RequestBody HelloData data) {}

53.4 HTTP 요청 데이터 읽기

HTTP 요청이 오고 컨트롤러에서 @RequestBody, HttpEntity 파라미터를 사용한다.
메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead() 를 호출한다.

대상 클래스 타입을 지원하는가.
예) @RequestBody 의 대상 클래스 ( byte[] , String , HelloData )

HTTP 요청의 Content-Type 미디어 타입을 지원하는가.
예) text/plain , application/json , */*

canRead() 조건을 만족하면 read() 를 호출해서 객체 생성하고 반환한다.

53.5 HTTP 응답 데이터 생성

컨트롤러에서 @ResponseBody, HttpEntity 로 값이 반환된다.
메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 를 호출한다.

대상 클래스 타입을 지원하는가.
예) return의 대상 클래스 ( byte[] , String , HelloData )

HTTP 요청의 Accept 미디어 타입을 지원하는가.(더 정확히는 @RequestMapping 의 produces )
예) text/plain , application/json , */*

canWrite() 조건을 만족하면 write() 를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성한다.

54. 요청 매핑 헨들러 어뎁터 구조

그렇다면 HTTP 메시지 컨버터는 스프링 MVC 어디쯤에서 사용되는 것일까

모든 비밀은 어노테이션 기반의 컨트롤러 @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter (요청 매핑 헨들러 어뎁터)에 있다.

54.1 ArgumentResolver

어노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다.
HttpServletRequest , Model 은 물론이고, @RequestParam , @ModelAttribute 같은 어노테이션
그리고 @RequestBody , HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여주었다.

이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.

어노테이션 기반 컨트롤러를 처리하는
RequestMappingHandlerAdapter는 바로 이 ArgumentResolver 를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다.
이렇게 파리미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.

스프링은 30개가 넘는 ArgumentResolver 를 기본으로 제공한다.

가능한 파라미터 목록은 다음 공식 메뉴얼에서 확인할 수 있다.
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-annarguments

정확히는 HandlerMethodArgumentResolver 인데 줄여서 ArgumentResolver 라고 부른다.

public interface HandlerMethodArgumentResolver {
        boolean supportsParameter(MethodParameter parameter);

        @Nullable
        Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
    }
}

동작 방식
ArgumentResolver 의 supportsParameter()를 호출해서 해당 파라미터를 지원하는지 체크하고 지원하면 resolveArgument() 를 호출해서 실제 객체를 생성한다.
그리고 이렇게 생성된 객체가 컨트롤러 호출시 넘어가는 것이다.
원한다면 직접 이 인터페이스를 확장해서 원하는 ArgumentResolver 를 만들 수도 있다.

54.2 ReturnValueHandler

HandlerMethodReturnValueHandler 를 줄여서 ReturnValueHandler라 부른다.
ArgumentResolver 와 비슷한데 이것은 응답 값을 변환하고 처리한다.
컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 ReturnValueHandler 덕분이다.

스프링은 10여개가 넘는 ReturnValueHandler 를 지원한다.
예) ModelAndView , @ResponseBody , HttpEntity , String

가능한 응답 값 목록은 다음 공식 메뉴얼에서 확인할 수 있다.
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-annreturn-types

54.3 HTTP 메시지 컨버터

HTTP 메시지 컨버터를 사용하는 @RequestBody 도 컨트롤러가 필요로 하는 파라미터의 값에 사용된다.
@ResponseBody 의 경우도 컨트롤러의 반환 값을 이용한다.

요청의 경우 @RequestBody 를 처리하는 ArgumentResolver 가 있고
HttpEntity 를 처리하는 ArgumentResolver 가 있다.
이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.

응답의 경우 @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있다.
그리고 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.

스프링 MVC는 @RequestBody @ResponseBody 가 있으면
RequestResponseBodyMethodProcessor (ArgumentResolver)
HttpEntity 가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용한다.

참고
HttpMessageConverter 를 구현한 클래스를 한번 확인해보자.

확장
스프링은 다음을 모두 인터페이스로 제공한다.
따라서 필요하면 언제든지 기능을 확장할 수 있다.

HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler
HttpMessageConverter

스프링이 필요한 대부분의 기능을 제공하기 때문에 실제 기능을 확장할 일이 많지는 않다.
기능 확장은 WebMvcConfigurer 를 상속 받아서 스프링 빈으로 등록하면 된다.
실제 자주 사용하지는 않으니 실제 기능 확장이 필요할 때 WebMvcConfigurer 를 검색해보자.

@Bean
public WebMvcConfigurer webMvcConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
            // ...
        }

        @Override
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
            // ...
        }
    };
}

스프링 MVC - 웹 페이지 만들기

55. 프로젝트 생성

타임리프 springweb 롬복 꼭 넣어주기
welcome page

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
    <ul>
        <li>
            상품 관리
            <ul>
                <li>
                    <a href="/basic/items">상품 관리 - 기본</a>
                </li>
            </ul>
        </li>
    </ul>
</body>
</html>

56. 요구사항 분석

요구사항 분석
상품을 관리할 수 있는 서비스를 만들어보자.
상품 도메인 모델
상품 ID
상품명
가격
수량

상품 관리 기능
상품 목록
상품 상세
상품 등록
상품 수정

서비스 제공 흐름
클라이이언트 상품목록 -> 상품등록폼 -> 상품저장(con) -> 내부호출 상품상세
상품목록 -> 내부호출 상품상세 -> 상품수정폼 -> 상품수정(con) redirect -> 상품상세

모든 서비스 앞에는 컨트롤러들이 있다.

요구사항이 정리되면 디자이너 웹 퍼블리셔 백엔드 개발자가 업무를 나누어 진행한다.

디자이너: 요구사항에 맞도록 디자인하고 디자인 결과물을 웹 퍼블리셔에게 넘겨준다.

웹 퍼블리셔: 다자이너에서 받은 디자인을 기반으로 HTML, CSS를 만들어 개발자에게 제공한다.

백엔드 개발자: 디자이너 웹 퍼블리셔를 통해서 HTML 화면이 나오기 전까지 시스템을 설계하고
핵심 비즈니스 모델을 개발한다.
이후 HTML이 나오면 이 HTML을 뷰 템플릿으로 변환해서 동적으로 화면을 그리고 또 웹 화면의 흐름을 제어한다.

참고
React, Vue.js 같은 웹 클라이언트 기술을 사용하고 웹 프론트엔드 개발자가 별도로 있으면
웹 프론트엔드 개발자가 웹 퍼블리셔 역할까지 포함해서 하는 경우도 있다.

웹 클라이언트 기술을 사용하면 웹 프론트엔드 개발자가 HTML을 동적으로 만드는 역할과 웹 화면의 흐름을 담당한다.
이 경우 백엔드 개발자는 HTML 뷰 템플릿을 직접 만지는 대신에 HTTP API를 통해 웹클라이언트가 필요로 하는 데이터와 기능을 제공하면 된다

전자인 경우가 많다.
그래서 백엔드 개발자도 view템플릿을 하나정도는 다룰줄 알아야한다.

57. 상품 도메인 개발

@Data는 getter setter등등을 만들어주는데 @getter @setter정도만 사용하고 필요한것은 넣어주는게 좋다.
예측하지 못하게 작동할 수도 있다.
일반적인 DTO는 그냥써도 되긴하지만 확인해서 사용하는게 좋다.

57.1 Item도메인

@Data
public class Item {
    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

57.2 ItemRepository

@Repository를 붙여서 저장소라는 것을 의미하고 컴포넌트 스캔에 자동으로 걸리게 해주자.

실제 웹어플리케이션이면 그냥 해시맵을 사용하면안된다.!!
싱글톤인데 멀티스레드에서 접근하게 되면 문제가 생길 수 있기 때문이다.
싱글톤이기 때문에 static안붙여줘도되는데 일단 붙여줬다.

findAll()에서는 ArrayList로 감싸서 리턴하는데 원래는 감싸지 않아도된다.
그래도 감싸서 반환하게 되면 list에 뭔가를 추가하게 되어도 store의 값이 변화하지 않기 때문에 안전하게 감싼것이다.

update일때 중복일지라도 명확성을 위해 새로운 객체를 만드는게 낫지만
작은 프로그램이니 일단은 그냥 같은 도메인을 쓸것이다.

@Repository
public class ItemRepository {
    private static final Map<Long, Item> store = new HashMap<>(); // static 사용
    private static long sequence = 0L; // static 사용

    public Item save(Item item) {
        item.setId(++sequence);
        store.put(item.getId(), item);
        return item;
    }

    public Item findById(Long id) {
        return store.get(id);
    }

    public List<Item> findAll() {
        return new ArrayList<>(store.values());
    }

    public void update(Long itemId, Item updateParam) {
        Item findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    public void clearStore() {
        store.clear();
    }
}

57.3 테스트해보기

스프링이랑 연결이 되어 있지 않으니 직접 객체를 생성해서 해주자.

class ItemRepositoryTest {

    ItemRepository itemRepository = new ItemRepository();

    @AfterEach
    void afterEach() {
        itemRepository.clearStore();
    }

    @Test
    void save() {
        // given
        Item item = new Item("itemA", 10000, 10);
        // when
        Item savedItem = itemRepository.save(item);
        // then
        Item findItem = itemRepository.findById(item.getId());
        assertThat(findItem).isEqualTo(savedItem);
    }

    @Test
    void findAll() {
        // given
        Item item1 = new Item("item1", 10000, 10);
        Item item2 = new Item("item2", 20000, 20);
        itemRepository.save(item1);
        itemRepository.save(item2);
        // when
        List<Item> result = itemRepository.findAll();
        // then
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(item1, item2);
    }

    @Test
    void updateItem() {
        // given
        Item item = new Item("item1", 10000, 10);
        Item savedItem = itemRepository.save(item);
        Long itemId = savedItem.getId();
        // when
        Item updateParam = new Item("item2", 20000, 30);
        itemRepository.update(itemId, updateParam);
        Item findItem = itemRepository.findById(itemId);
        // then

        assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
        assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());

        assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
    }
}

58. 상품 서비스 HTML

핵심로직을 만들었고 웹퍼블리셔가 markup하는 것을 마크업했다고 가정해보자.
마크업이란 디자인된것을 html로만드는것을 의미한다.

부트스트랩
참고로 HTML을 편리하게 개발하기 위해 부트스트랩 사용했다.

먼저 필요한 부트스트랩 파일을 설치해줘야한다.
부트스트랩 공식 사이트: https://getbootstrap.com

먼저 다운을 해줘야한다.
https://getbootstrap.com/docs/5.0/getting-started/download/
Compiled CSS and JS 항목을 다운로드

압축을 출고 bootstrap.min.css 를 복사해서 다음 폴더에 추가
resources/static/css/bootstrap.min.css

참고
부트스트랩(Bootstrap)은 웹사이트를 쉽게 만들 수 있게 도와주는 HTML, CSS, JS 프레임워크이다.
하나의 CSS로 휴대폰, 태블릿, 데스크탑까지 다양한 기기에서 작동한다.
다양한 기능을 제공하여 사용자가 쉽게 웹사이트를 제작, 유지, 보수할 수 있도록 도와준다.

HTML, css 파일
/resources/static/css/bootstrap.min.css 부트스트랩 다운로드

/resources/static/html/items.html 아래 참조
/resources/static/html/item.html
/resources/static/html/addForm.html
/resources/static/html/editForm.html

참고로 /resources/static 에 넣어두었기 때문에 스프링 부트가 정적 리소스를 제공한다.
http://localhost:8080/html/items.html
정적 리소스여서 해당 파일을 탐색기를 통해 직접 열어도 동작하는 것을 확인할 수 있다.

참고
이렇게 정적 리소스가 공개되는 /resources/static 폴더에 HTML을 넣어두면
실제 서비스에서도 공개된다.
서비스를 운영한다면 지금처럼 공개할 필요없는 HTML을 두는 것은 주의하자.

war의 경우에는 web-inf에서 파일을 관리했었는데 이런폴더에 넣어두는 것 자체를 하지 말아야한다.

59. 상품 목록 - 타임리프

59.1 컨트롤러

ItemRepository를 생성자로 autowired해준다.

모든 목록을 찾아서 뿌려줄것이다.

@PostConstruct를 붙이면 스프링이 시작될때 실행되는데
일단 테스트용 데이터를 알고싶다.

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {

    private final ItemRepository itemRepository;

    @GetMapping
    public String items(Model model) {
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items);
        return "basic/items";
    }

    /**
    * 테스트용 데이터 추가
    */
    @PostConstruct
    public void init() {
        itemRepository.save(new Item("testA", 10000, 10));
        itemRepository.save(new Item("testB", 20000, 20));
    }
}

59.2 view

view가 잇어야된다. 만들기 위해서 정적인 화면을 타임리프로 만들기위해서
resources.templates 에 넣어주면된다.

순수한 html인데 타임리프를 이용해서 사용하자.
조금 복잡하지만 매뉴얼에서 찾아서사용하면된다.

html태그에 xmlns을 추가해줘서 타임리프인 것을 명시해줘야한다.
th태그를 사용할 수 있게 된다.

<html xmlns:th="http://www.thymeleaf.org">

css파일이 변경되면 문제가 생길 수 있으니 상대경로에서 타임리프로 절대경로를 지정해줄 수 있다.

<link th:href="@{/css/bootstrap.min.css}" href="../css/bootstrap.min.css" rel="stylesheet">

기본링크가 되는데 탬플릿이 실행되면 th(타임리프)가 있는 것이 덮어버리게 된다.

th:onclick="|location.href='@{/basic/items/add}'|"

|안에 넣으면 타임리프가 그대로 인식해서 넣어준다. 자바스크립트 백틱같은 느낌인것 같다.

th:each를하면 반복문을 사용하는 것이다.
${}안에 있는 것을 꺼내서 앞의 변수에 넣는다.

jsp에서 el로 꺼내듯이 작성하면된다. 평소에는 기본값이 들어가는데 렌더링되면 th에 작성한 값이 들어가게 된다.
타임리프에서 url은 @{}안에 작성해야한다. 이걸 사용하면 변수를 집어넣을수있는데 ()를 사용하면 그 값을 해당변수에 값을 넣을 수 있다.
마치 스프링의 @PathVariable처럼 사용하면된다.

<tr th:each="item : ${items}">
    <td>
        <a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a>
    </td>
    <td>
        <a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a>
    </td>
    <td th:text="${item.price}">10000</td>
    <td th:text="${item.quantity}">10</td>
</tr>

타임리프를 왜 natual 탬플릿이라고 하면 코드를 넣으면 보통 html은 깨진다.
jsp를 htlm에 그냥 넣으면깨진다.
그런데 기본 html태그를 그대로 사용하고 타임리프는 값을 넣을 수 있다.

그냥열면 th부분은 무시하고 열린다. 그래서 natural template라고 한다.
그래서 마크업작업하는사람이 가지고 마크업을 하더라도 문제가 생기지 않는다.

타임리프 간단히 알아보기

59.3 타임리프 사용 선언

<html xmlns:th="http://www.thymeleaf.org">

59.4 속성 변경 - th:href

th:href="@{/css/bootstrap.min.css}"
href="value1" 을 th:href="value2" 의 값으로 변경한다.
타임리프 뷰 템플릿을 거치게 되면 원래 값을 th:xxx 값으로 변경한다.
만약 값이 없다면 새로 생성한다.
HTML을 그대로 볼 때는 href 속성이 사용되고 뷰 템플릿을 거치면
th:href의 값이 href로 대체되면서 동적으로 변경할 수 있다.
대부분의 HTML 속성을 th:xxx 로 변경할 수 있다.

59.5 타임리프 핵심

핵심은 th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고 기존 것을 대체한다.
th:xxx 이 없으면 기존 html의 xxx 속성이 그대로 사용된다.

HTML을 파일로 직접 열었을 때 th:xxx 가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시한다.
따라서 HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.

59.6 URL 링크 표현식 - @{...},

th:href="@{/css/bootstrap.min.css}"
@{...} : 타임리프는 URL 링크를 사용하는 경우 @{...} 를 사용한다.
이것을 URL 링크 표현식이라 한다.
URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.
과거엔 서블릿 컨텍스트를 넣어줫엇는데 (보통 프로젝트이름) 요즘엔 스프링부트가 알아서 없애준다.

59.7 상품 등록 폼으로 이동

속성 변경 - th:onclick

onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
여기에는 다음에 설명하는 리터럴 대체 문법이 사용되었다.

자세히 알아보자.
리터럴 대체 - |...|
|...| :이렇게 사용한다.
타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.

<span th:text="'Welcome to our application, ' + ${user.name} + '!'">

다음과 같이 리터럴 대체 문법을 사용하면 더하기 없이 편리하게 사용할 수 있다.

<span th:text="|Welcome to our application, ${user.name}!|">

결과를 다음과 같이 만들어야 하는데 location.href='/basic/items/add'

그냥 사용하면 문자와 표현식을 각각 따로 더해서 사용해야 하므로 다음과 같이 복잡해진다.

th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"

리터럴 대체 문법을 사용하면 다음과 같이 편리하게 사용할 수 있다.

th:onclick="|location.href='@{/basic/items/add}'|"

자바스크립트의 백틱같은 느낌이다.

59.8 반복 출력 - th:each

<tr th:each="item : ${items}">

반복은 th:each 를 사용한다.
이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고 반복문 안에서 item 변수를 사용할 수 있다.
컬렉션의 수 만큼 <tr>..</tr> 이 하위 태그를 포함해서 생성된다.

59.9 변수 표현식 - ${...}

<td th:text="${item.price}">10000</td>

모델에 포함된 값이나 타임리프 변수로 선언한 값을 조회할 수 있다.
프로퍼티 접근법을 사용한다. ( item.getPrice() )

59.10 내용 변경 - th:text

<td th:text="${item.price}">10000</td>

내용의 값을 th:text 의 값으로 변경한다.
여기서는 10000을 ${item.price} 의 값으로 변경한다.

59.11 URL 링크 표현식2 - @{...}

th:href="@{/basic/items/{itemId}(itemId=${item.id})}"

상품 ID를 선택하는 링크를 확인해보자.
URL 링크 표현식을 사용하면 경로를 템플릿처럼 편리하게 사용할 수 있다.
경로 변수( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성한다.

예) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
생성 링크: http://localhost:8080/basic/items/1?query=test

59.12 URL 링크 간단히

th:href="@{|/basic/items/${item.id}|}"

상품 이름을 선택하는 링크를 확인해보자.
리터럴 대체 문법을 활용해서 간단히 사용할 수도 있다.

참고 할점
타임리프는 순수 HTML 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고
서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.
JSP를 생각해보면 JSP 파일은 웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 뒤죽박죽 되어서 정상적인 확인이 불가능하다.
오직 서버를 통해서 JSP를 열어야 한다.

이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿(natural templates)이라 한다.

익숙해지는데 좀 걸릴 것 같은데 그래도 기본적으로 용어?자체는 기존과 많이 다르지 않아서
공부를 하면 사용을 할 수 있을 것으로 보인다.

60. 상품 상세

60.1 컨트롤러

PathVariable로 값을 받아서 만들었다.
id로 하나를 찾아서 반환해주면된다.

@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model model) {
    Item item = itemRepository.findById(itemId);
    model.addAttribute("item", item);
    return "basic/item";
}

60.2 view

<div class="container">
    <div class="py-5 text-center">
        <h2>상품 상세</h2>
    </div>
    <div>
        <label for="itemId">상품 ID</label>
        <input type="text" id="itemId" name="itemId" class="form-control" th:value="${item.id}" value="1" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" name="itemName" class="form-control" value="상품A" th:value="${item.itemName}" readonly>
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}" readonly>
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}" readonly>
    </div>
    <hr class="my-4">
    <div class="row">
        <div class="col">
            <button class="w-100 btn btn-primary btn-lg" 
            onclick="location.href='editForm.html'" 
            th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
            type="button">상품 수정</button>
        </div>
        <div class="col">
            <button class="w-100 btn btn-secondary btn-lg" 
            onclick="location.href='items.html'"
            th:onclick="|location.href='@{/basic/items}'|" 
            type="button">목록으로</button>
        </div>
    </div>
</div>

속성 변경 - th:value
th:value="${item.id}"
모델에 있는 item 정보를 획득하고 프로퍼티 접근법으로 출력한다. item.getId()
value 속성을 th:value 속성으로 변경한다.

상품수정 링크
th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"

목록으로 링크
th:onclick="|location.href='@{/basic/items}'|"

그냥 html파일이기때문에 직접 루트를 들어가서 고치면 서버 새로고침안하고도 볼수 있는 장점도 있다.

61. 상품 등록 폼

61.1 컨트롤러

@GetMapping("/add")
public String addForm() {
 return "basic/addForm";
}

61.2 view

나중가면 공통되는 부분은 include로 넣을 수 있으니 html의 include를 사용하자.
폼 액션태그를 같은 링크로 가게 해준다.
같은 url이지만 get요청과 post요청에 따라서 다른일을 하게 되는 것이다.
action에 아무것도 값이 없게 되면 같은 url이되게된다.

action태그에 경로를 넣을때는 꼭 경로표현식으로 넣어줘야한다.

<div class="container">
    <div class="py-5 text-center">
        <h2>상품 등록 폼</h2>
    </div>
    <h4 class="mb-3">상품 입력</h4>
    <form action="item.html" th:action="@{/basic/items/add}" method="post">
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="formcontrol" placeholder="이름을 입력하세요">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control" placeholder="가격을 입력하세요">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="formcontrol" placeholder="수량을 입력하세요">
        </div>
        <hr class="my-4">
        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">상품 등록</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg" onclick="location.href='items.html'" th:onclick="|location.href='@{/basic/items}'|" type="button">취소</button>
            </div>
        </div>
    </form>
</div>

속성 변경 - th:action
th:action="@{/basic/items/add}"
HTML form에서 action에 값이 없으면 현재 URL에 데이터를 전송한다.
상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메소드로 두 기능을 구분한다.

상품 등록 폼: GET /basic/items/add
상품 등록 처리: POST /basic/items/add

이렇게 하면 하나의 URL로 등록 폼과 등록 처리를 깔끔하게 처리할 수 있다.

취소
취소시 상품 목록으로 이동한다.
th:onclick="|location.href='@{/basic/items}'|"

62. 상품 등록 처리 - @ModelAttribute

이제 상품 등록 폼에서 전달된 데이터로 실제 상품을 등록 처리해보자.
상품 등록 폼은 다음 방식으로 서버에 데이터를 전달한다.

POST - HTML Form
content-type: application/x-www-form-urlencoded

메시지 바디에 쿼리 파리미터 형식으로 전달 itemName=itemA&price=10000&quantity=10
예) 회원 가입, 상품 주문, HTML Form 사용

62.1 @RequestParam

요청 파라미터 형식을 처리해야 하므로 @RequestParam 을 사용해보자.

@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
        @RequestParam int price,
        @RequestParam Integer quantity,
        Model model) {
    Item item = new Item();
    item.setItemName(itemName);
    item.setPrice(price);
    item.setQuantity(quantity);
    itemRepository.save(item);
    model.addAttribute("item", item);
    return "basic/item";
}

먼저 @RequestParam String itemName : itemName 요청 파라미터 데이터를 해당 변수에 받는다.
Item 객체를 생성하고 itemRepository 를 통해서 저장한다.
저장된 item 을 모델에 담아서 뷰에 전달한다.

62.2 상품 등록 처리 - @ModelAttribute

@RequestParam 으로 변수를 하나하나 받아서 Item 을 생성하는 과정은 불편하다.
@ModelAttribute 를 사용해서 한번에 처리할 수 있다.

@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
    itemRepository.save(item);
    // model.addAttribute("item", item); //자동 추가, 생략 가능
    return "basic/item";
}

@ModelAttribute - 요청 파라미터 처리
@ModelAttribute는 Item 객체를 생성하고 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해준다.

@ModelAttribute - Model 추가
@ModelAttribute 는 중요한 한가지 기능이 더 있는데
바로 모델(Model)에 @ModelAttribute로 지정한 객체를 자동으로 넣어준다.
model.addAttribute("item", item) 가 주석처리 되어 있어도 잘 동작하는 것을 확인할 수 있다.
모델에 데이터를 담을 때는 이름이 필요하다.
이름은 @ModelAttribute 에 지정한 name(value) 속성을 사용한다.
만약 다음과 같이 @ModelAttribute 의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.

@ModelAttribute("hello") Item item 이름을 hello 로 지정
model.addAttribute("hello", item); 모델에 hello 이름으로 저장

주의
@ModelAttribute 의 이름을 생략하면 모델에 저장될 때 클래스명을 사용한다.
이때 클래스의 첫글자만 소문자로 변경해서 등록한다.
예) @ModelAttribute 클래스명 모델에 자동 추가되는 이름
Item -> item
HelloWorld -> helloWorld

@PostMapping("/add")
public String addItemV4(Item item) {
    itemRepository.save(item);
    return "basic/item";
}

63. 상품 수정

63.1 컨트롤러

id를 받아와서 id로 값을 찾아서 모델에 넣어서 수정폼을 보여준다.

@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
    Item item = itemRepository.findById(itemId);
    model.addAttribute("item", item);
    return "basic/editForm";
}

63.2 view

<div class="container">
    <div class="py-5 text-center">
        <h2>상품 수정 폼</h2>
    </div>
    <form action="item.html" th:action="@{/basic/items/{itemId}/edit(itemId=${item.id})}" method="post">
        <div>
            <label for="id">상품 ID</label>
            <input type="text" id="id" name="id" class="form-control" value="1" th:value="${item.id}" readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="formcontrol" value="상품A" th:value="${item.itemName}">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control" th:value="${item.price}">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="formcontrol" th:value="${item.quantity}">
        </div>
        <hr class="my-4">
        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">저장</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg" onclick="location.href='item.html'" th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|" type="button">취소</button>
            </div>
        </div>
    </form>
</div>

63.3 수정처리

같은 페이지에서 post요청을 받아서 처리해준다.
받은 id값을 이용해서 상품상세페이지로 이동해준다.
원래는 서비스단을 거쳐서 리포지터리단을 거치는데
일단 리포지터리에서 업데이트 처리까지 해준다.

@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
    itemRepository.update(itemId, item);
    return "redirect:/basic/items/{itemId}";
}

상품 수정은 상품 등록과 전체 프로세스가 유사하다.

GET /items/{itemId}/edit : 상품 수정 폼
POST /items/{itemId}/edit : 상품 수정 처리

리다이렉트
상품 수정은 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트를 호출한다.

스프링은 redirect:/... 으로 편리하게 리다이렉트를 지원한다.

redirect:/basic/items/{itemId}

컨트롤러에 매핑된 @PathVariable 의 값은 redirect 에도 사용 할 수 있다.
redirect:/basic/items/{itemId} {itemId} 는 @PathVariable Long itemId 의 값을
그대로 사용한다

참고
HTML Form 전송은 PUT, PATCH를 지원하지 않는다.
GET, POST만 사용할 수 있다.
PUT, PATCH는 HTTP API 전송시에 사용한다.
스프링에서 HTTP POST로 Form 요청할 때 히든 필드를 통해서 PUT, PATCH 매핑을 사용하는 방법이, HTTP 요청상 POST 요청이다.

64. PRG Post/Redirect/Get

사실 지금까지 진행한 상품 등록 처리 컨트롤러는 심각한 문제가 있다. (addItemV1 ~ addItemV4)
상품 등록을 완료하고 웹 브라우저의 새로고침 버튼을 클릭하면
상품이 계속해서 중복 등록되는 것을 확인할 수 있다.

POST 등록 후 새로 고침
웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다.
상품 등록 폼에서 데이터를 입력하고 저장을 선택하면 POST /add + 상품 데이터를 서버로 전송한다.
이 상태에서 새로 고침을 또 선택하면 마지막에 전송한 POST /add + 상품 데이터를 서버로 다시 전송하게 된다.
그래서 내용은 같고 ID만 다른 상품 데이터가 계속 쌓이게 된다.

이문제를 해결하기 위해서는 다음과 같은 일을 해줘야한다.

POST, Redirect GET
웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다.
새로 고침 문제를 해결하려면 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라
상품 상세 화면으로 리다이렉트를 호출해주면 된다.

웹 브라우저는 리다이렉트의 영향으로 상품 저장 후에 실제 상품 상세 화면으로 다시 이동한다.
따라서 마지막에 호출한 내용이 상품 상세 화면인 GET /items/{id} 가 되는 것이다.
이후 새로고침을 해도 상품 상세 화면으로 이동하게 되므로 새로 고침 문제를 해결할 수 있다.

@PostMapping("/add")
public String addItemV5(Item item) {
    itemRepository.save(item);
    return "basic/item";
}

@PostMapping("/add")
public String addItemV5(Item item) {
    itemRepository.save(item);
    return "redirect:/basic/items/" + item.getId();
}

이렇게 변경해줘야한다.

상품 등록 처리 이후에 뷰 템플릿이 아니라 상품 상세 화면으로 리다이렉트 하도록 코드를 작성했다.
이런 문제 해결 방식을 PRG Post/Redirect/Get 라 한다.

주의할점
"redirect:/basic/items/" + item.getId() redirect에서 +item.getId()처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다.
다음에 설명하는 RedirectAttributes를 사용하자.

65. RedirectAttributes

상품을 저장하고 상품 상세 화면으로 리다이렉트 한 것 까지는 좋았다.
그런데 고객 입장에서 저장이 잘 된 것인지 안 된 것인지 확신이 들지 않는다.
그래서 저장이 잘 되었으면 상품 상세 화면에 "저장되었습니다"라는 메시지를 보여달라는 요구사항이 왔다.

65.1 컨트롤러 수정

RedirectAttributes를 사용하면 간단하게 해결할 수 있다.

@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/basic/items/{itemId}";
}

리다이렉트 할 때 간단히 status=true 를 추가해보자.
그리고 뷰 템플릿에서 이 값이 있으면 저장되었습니다. 라는 메시지를 출력해보자.

실행해보면 다음과 같은 리다이렉트 결과가 나온다.
http://localhost:8080/basic/items/3?status=true

RedirectAttributes 를 사용하면 URL 인코딩도 해주고, pathVarible , 쿼리 파라미터까지 처리해준다.

redirect:/basic/items/{itemId}
pathVariable 바인딩: {itemId}
나머지는 쿼리 파라미터로 처리: ?status=true

65.2 view처리

<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>

th:if : 해당 조건이 참이면 실행
${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능이다.
-> 이부분은 jsp와 같다.

원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 한다.
그런데 쿼리 파라미터는 자주 사용해서 타임리프에서 직접 지원한다.

뷰 템플릿에 메시지를 추가하고 실행해보면 "저장 완료!" 라는 메시지가 나오는 것을 확인할 수 있다.
물론 상품 목록에서 상품 상세로 이동한 경우에는 해당 메시지가 출력되지 않는다.

2023.06.07

롬복
@Data는 getter setter등등을 만들어주는데 @getter @setter정도만 사용하고 필요한것은 넣어주는게 좋다고 한다.
예측하지 못하게 작동하는 문제가 발생하기도 한다고 한다.
일반적인 DTO는 그냥써도 되긴하지만 확인해서 사용하는게 좋다고 한다.
그냥 무지성으로 @Data햇엇는데 의외라고 생각된다.

action태그를 비우는 것에 대해
이전에 공부한 뉴렉처 강의에서는 오류날수잇으니 넣어주는게 좋다고 하고
여기서는 그냥 비워두는게 좋다고 한다.

같은 페이지로의 요청을 처리하는 경우에는 action 속성을 비워두든지 현재 페이지의 URL을 명시적으로 설정하든지는 개발자의 마음대로 결정할 수 있다고 한다.
기본적으로 폼 요소에서 action 속성을 생략하면 현재 페이지로 요청이 전송되지만 명시적으로 다른 URL을 설정하여 원하는 대상으로 요청을 보낼 수도 있다.
HTML5에서는 action 속성을 비워두는 것이 유효한 동작으로 명시되어 있다.
따라서 대부분의 브라우저에서는 action 속성이 비워져도 폼 데이터를 현재 페이지로 제출한다.
그러나 특정 환경이나 요구 사항에 따라 action 속성에 명시적인 URL을 설정하는 것이 더 명확하고 안정적일 수도 있다.
따라서 개발자는 자신의 상황과 선호도에 따라 action 속성을 설정하거나 비워둘 수 있으며 동작에 큰 영향을 주지 않는다고 한다.
선택은 개발자에게 달려있으며, 일관성을 유지하고 코드를 명확하게 유지하는 것이 가장 중요하다고 한다.

'기초단계 > SPRING' 카테고리의 다른 글

2023.06.10 Spring  (0) 2023.06.22
2023.06.09 Spring  (0) 2023.06.22
2023.06.05 Spring  (0) 2023.06.06
2023.06.03 Spring  (0) 2023.06.06
2023.06.01 Spring  (0) 2023.06.06

+ Recent posts