1) 요구사항
- 위 상품 목록을 클릭하면 상품 상세, 상품 등록 버튼을 누르면 상품 등록 폼이 나오도록 하는 서비스를 구현하겠다. 추가적으로 수정 기능도 구현할 것이다.
- 서비스 흐름 구성도다. 검은색이 컨트롤러, 하얀색이 뷰다.
- 클라이언트가 상품 목록에 들어가려 하면, 상품 목록 컨트롤러가 상품 목록 뷰를 렌더링해 상품 목록 페이지를 띄운다.
- 상품 등록 폼에 들어가 값을 입력하고 등록 버튼을 누르면 상품 저장 컨트롤러로 이동한다. 상품 저장 컨트롤러는 상품을 저장하고 상품 상세 뷰로 이동한다(뷰 재사용).
- 상품 상세 컨트롤러를 이용해 상품 상세 뷰로 이동하고, 상품 수정 컨트롤러를 이용해 상품 수정 폼에서 수정한 뒤 버튼을 누르면, 상품 수정 컨트롤러가 상품 상세 컨트롤러로 redirect 하게 된다.
2) 상품 도메인 개발
- 회원 저장소를 만들어준다.
- 참고로 멀티 스레드 환경에서 여러개가 동시에 접근할 때는 해시맵을 쓰지 않는다. ConcurrentHashMap을 사용한다.
- 회원 저장, 조회, 목록, 수정, 삭제 메소드를 만들어준다.
- 이제 테스트 코드를 만든다. test 폴더의 하위에 같은 환경의 폴더를 생성한다.
- ItemRepository를 생성하고 테스트가 끝날 때 마다 메소드를 실행시키는 @AfterEach를 통해 테스트가 끝날 때 마다 clearstore()로 데이터를 지워주는 작업을 수행하도록 한다.
- 아이템 정보를 저장하고 저장한 정보가 리스트에 있는 정보와 같은지 테스트
- 두 개의 아이템을 저장하고 아이템 리스트 사이즈가 2인지 테스트 및 아이템 1,2가 리스트에 포함되었는지 테스트
- 아이템을 하나 저장하고 업데이트할 정보 파라미터를 생성해서 그 파라미터를 바꿀 아이템의 아이디값을 명시해 덮어씌워 수정. 초기에 저장한 ItemId를 이용해 해당 밸류값의 이름,가격,수량이 업데이트 파라미터 값과 같은지 확인
3) 상품 서비스 HTML
- 웹 퍼블리셔가 만든 html을 경로에 넣고 동작해보겠다.
- 아이템 목록을 조회할 컨트롤러다. @Autowired 어노테이션을 사용해 생성자를 만들면 BasicItemController가 스프링 빈에 등록되면서, 생성자 주입으로 ItemRepository가 스프링 빈에 주입된다. 그러나 위 사진처럼 생성자가 단 하나면 @Autowired를 생략 가능하다.
- 롬복의 @RequiredArgsConstructor 어노테이션을 쓰면, final 이 붙은 것에 대해 알아서 생성자를 만드므로 생성자를 생략할 수 있다.
- @GetMapping을 통해 아이템 전체목록을 조회하는 items 메소드를 만들고, 이를 테스트하기 위해 @PostConstruct를 통해 회원 정보를 두 개 추가해보겠다.
- 타임리프를 통해 동적으로 html을 생성해 보도록 하겠다. 타임리프는 먼저 html 태그 내에 ""방식으로 입력하여 타임리프를 사용함을 명시한다.
- href나 onclick을 통해 경로를 이동할 때는 위와 같이 "th:태그명 ="@{경로}" 를 통해 클릭시 이동하는 경로를 설정한다.
- 를 사용하면 items 목록에서 하나씩 item을 꺼내온다.
- th:text="${item.id}" 는 a 태그 안의 텍스트를 id 값으로 바꿔주는 것이다.
- th:href="주소" 를 사용하면 해당 주소값으로 왼쪽의 href 주소값을 치환해준다.
- 따라서 클릭 시 "/basic/items/{itemID}" 주소로 이동하는데 여기서 itemID는 item.id 값을 넣은 변수이다.
- URL 링크 표현식 :
@{...}, th:href="@{/css/bootstrap.min.css}"
@{...} : 타임리프는 URL 링크를 사용하는 경우 @{...} 를 사용한다. 이것을 URL 링크 표현식이라 한다. URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.
- 아래 상품명의 th:href 에는 "th:href="@{|/basic/items/${item.id}|}"" 방식으로 |...| 형태로 표현되는 리터럴 대체 문법이 사용되었다.
- 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.
ex)
- 다음과 같이 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.
ex)
- 결과를 다음과 같이 만들어야 하는데
location.href='/basic/items/add'
- 그냥 사용하면 문자와 표현식을 각각 따로 더해서 사용해야 하므로 다음과 같이 복잡해진다. th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
- 리터럴 대체 문법을 사용하면 다음과 같이 편리하게 사용할 수 있다.
th:onclick="|location.href='@{/basic/items/add}'|"
- URL링크 표현식에 쿼리 파라미터를 담아 전달할 수도 있다.
예) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
생성 링크: http://localhost:8080/basic/items/1?query=test
4. 상품 상세
- @GetMapping을 통해 itemId 값에 따라 이동하는 상세 페이지 이동 컨트롤러를 생성해준다.
- 타임리프 th:value 를 통해 item 모델의 값을 입력해준다.
- 템플릿 언어를 통해 상품수정 버튼 onclick시 해당 id/edit 경로로 이동하도록 설정한다.
5. 상품 등록 폼
- 이제 직접 상품을 등록할 수 있는 폼을 보여주는 컨트롤러와, 실제로 작성한 폼을 저장하는 컨트롤러를 작성한다. 주목할 점은 두 컨트롤러는 "/basic/items/add"라는 같은 url 주소를 사용하는데 요청 방식이 get이냐 post이냐에 따라 다른 메소드가 실행되는 것이다.
- 실제 등록이 아닌 폼만 보여주는 addForm은 @Getmapping, 실제로 저장하는 save는 @PostMapping 이다.
- form의 요청 url을 명시하는 url에 th:action을 통해 어느 url로 post 요청을 보낼 것인지 정한다. 원래는 "/basic/items/add"로 url 요청을 보내야 맞지만 지금 현재 url과 같은 주소이므로 그냥 th:action 만 적어줘도 정상 동작한다.
6. 상품 등록 처리 - @ModelAttribute
- 이제 상품 등록 버튼을 눌렀을 때, 실제로 상품 등록이 처리되도록 하겠다.
- 요청 파라미터 형식을 처리해야 하므로 @RequestParam을 사용하자.
- 위에서 만들어둔 post 요청시 회원을 저장하는 save 메소드를 수정하겠다. 먼저 파라미터 값으로 html form의 name으로 넘어온 값들을 @RequestParam을 통해 추출해 넘겨준다. 또 모델 값을 파라미터 넘겨 템플릿에 들어갈 모델 정보를 사용할 수 있도록 한다.
- setter를 통해 item 내부에 파라미터 값을 세팅하고 item저장소에 저장한다.
- model.addAttribute를 통해 "item" 이라는 이름으로 html 템플릿에서 조회 가능하게끔 한다.
- 그러나 더욱 편리한 방법이 있다. @ModelAttribute 어노테이션을 사용하고, @ModelAttribute("이름")에 아래 model."addAttribute" 의 attributeName에 넣을 값을 입력해주면, @ModelAttribute가 알아서 요청값을 Item저장소와 모델에 등록해준다.
- @ModelAttribute - 요청 파라미터 처리: @ModelAttribute 는 Item 객체를 생성하고, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해준다.
- @ModelAttribute - Model 추가 : @ModelAttribute 는 중요한 한가지 기능이 더 있는데, 바로 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다. 지금 코드를 보면 model.addAttribute("item", item) 가 주석처리 되어 있어도 잘 동작하는 것을 확인할 수 있다.
- @ModelAttribute가 model에 알아서 등록해주므로, Model 파라미터와 addAttribute는 생략 가능하다.
- 심지어 @ModelAttribute의 괄호 값도 생략 가능하다. 이 때 모델에 저장하는 클래스명(위에서 Item) 에서 첫 글자만 소문자로 바꾸어(Item->item) 모델에 addAttribute 해준다.
- 심지어 이전에 배웠듯 우리가 만든 클래스 객체(Item item) 에 대해서는 @ModelAttribute가 자동 적용된다.(int,String 등 단순 타입은 @RequestParam 자동적용)
6. 상품 수정
- 먼저 상품 수정 폼을 보여주는 컨트롤러를 만든다. 어떤 상품을 수정할 지 id값을 파라미터로 넣어 주어야 한다.
- id값을 통해 해당 아이템을 찾고, 이를 모델에 넣어 템플릿에 출력될 수 있도록 한다.
- 수정 템플릿 폼을 생성한다. 입력 폼과 마찬가지로 같은 url에 요청을 보낼 것이므로 th:action 만 명시한다.
- 인풋의 value값에 th:value를 통해 모델값을 불러온다.
- 취소시 해당 아이템의 상세페이지로 이동하는 url을 작성한다.
- Post 요청시 수정 내용을 저장하는 컨트롤러다. itemId와, @ModelAttribute가 가져온 item 객체를 파라미터로 넘겨, 아이템 저장소에서 해당 id값의 밸류를 업데이트한다.
- /basic/items/{itemId} 경로로 redirect 시켜주어 url 자체가 아예 변경되도록 한다.
- 스프링은 redirect: /... 방식으로 리다이렉트를 지원한다.
7. PRG Post/Redirect/Get
- 현재 id값이 4인 상품 상세 화면에서 새로고침을 해보자
- 목록으로 가니 새로고침 횟수만큼 상품이 추가된다. 왜 이럴까?
- 그 이유는 이미 보낸 post 요청을 다시 보내기 때문이다. 우리는 한 url에서 get을 통해 요청 폼을 띄우고, post 요청을 통해 요청을 저장했다. 그러나 post 요청을 하는 순간 같은 post+ "/add"+ 상품 데이터가 서버로 전송된다.
- 새로고침은 마지막에 했던 행위를 다시 하는 것이다. 따라서 이를 다시 새로고침 하면 다시 post+ "/add"+ 상품 데이터가 서버로 또 전송된다. post 시마다 id값이 증가하므로 id만 같고 내용은 같은 데이터가 계속 쌓인다. 그럼 어떻게 해결할까?
- 답은 redirect를 하는 것이다. 상품 등록 폼에서 상품 저장 컨트롤러를 통해 저장을 하면, redirect를 한다. redirect는 웹 브라우저 입장에서 완전히 새로운 요청을 하는 것이다.
- 웹 브라우저는 리다이렉트의 영향으로 상품 저장 후 실제 상품 상세 화면으로 이동하는 "GET"요청을 한다. 따라서 마지막 요청이 GET이 되므로 새로고침해도 POST가 날아가지 않아 새로운 데이터가 생성되지 않는다.
- 따라서 컨트롤러의 리턴값을 리다이렉트+id값으로 해주면 리턴된 url로 아예 새로운 요청을 보내게 해준다.
- 리다이렉트를 하기 이전 코드로 서버를 돌린 버전. 상품 저장 버튼을 누르면 url 주소가 "/basic/items/add"
- 리다이렉트 설정을 하고 서버를 돌린 후, 상품 저장을 누르면 "/basic/items/{id값}" 으로 요청을 보낸 것을 알 수 있다. 여기서는 새로고침해도 데이터가 추가되지 않는다.
8. RedirectAttributes
- 이제 상품 저장이 잘 되었으면 "저장 완료" 메세지를 띄우도록 하겠다.
- 상품 저장 메소드에 RedirectAttributes 객체 파라미터를 넣는다. 이제 상품 저장 정보를 변수에 담아 redirectAttributes 객체에 "itemId"이름으로 넣는다. 추가로 "status"라는 값을 true로 넣는다.
- 이후 "redirect:/basic/items/{itemId}"로 리다이렉트를 시키면 아까 넣은 itemId 키로 넣은 값(savedItem.getId())이 치환된다.
- 그리고 url에 들어가지 않은 redirectAttributes의 값들은 쿼리 파라미터 형식으로(?status=true) 들어간다.
- 다음처럼 다시 수정 시 url의 쿼리 파라미터에 status가 들어간다.
- 이제 html에서 메세지를 띄운다. 타임리프에서 param.xx를 통해 파라미터 값을 꺼낼 수 있도록 지원한다.
- 따라서 만약 param.status 값이 존재하면 "저장 완료" 메세지가 뜨는 것을 다음과 같은 코드로 구현한다.
- 다음처럼 status 쿼리 파라미터가 존재하면 메세지가 출력된다.