스프링 MVC - 웹 페이지 만들기
요구사항 분석
상품을 관리할 수 있는 서비스를 만들어보자.
'상품 도메인 모델'
- 상품 ID
- 상품명
- 가격
- 수량
'상품 관리 기능'
- 상품 목록
- 상품 상세
- 상품 등록
- 상품 수정
요구사항이 정리되고 디자이너, 웹 퍼블리셔, 백엔드 개발자가 업무를 나누어 진행
* 디자이너 : 요구사항에 맞도록 디자인하고, 디자인 결과물을 웹 퍼블리셔에게 넘겨준다
* 웹 퍼블리셔 : 디자이너에서 받은 디자인을 기반으로 HTML, CSS를 만들어 개발자에게 제공한다
* 백엔드 개발자 : 디자이너 웹 퍼블리셔를 통해서 HTML 화면이 나오기 전까지 시스템을 설계하고
핵심 비즈니스 모델을 개발한다 이후 HTML이 나오면 HTML을 뷰 템플릿으로 변환해서 동적으로
화면을 또 웹 화면의 흐름을 제어
상품 도메인 개발
'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;
}
}
'ItemRepository - 상품 저장소'
@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();
}
}
상품 서비스 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
상품 목록 - 타임리프
컨트롤러와 뷰 템플릿을 개발해보자
'BasicItemController'
@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("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
}
컨트롤러 로직은 itemRepository에서 모든 상품을 조회한 다음에 모델에 담는다.
그리고 뷰 템플릿을 호출한다.
* @RequiredArgsConstructor
- final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
이렇게 생성자가 딱 1개만 있으면 스프링이 해당 생성자에 @Autowired로 의존관계를 주입해준다.
따라서 'final 키워드를 빼면 안된다!' 그러면 ItemRepository 의존관계 주입이 안된다.
기본편 강의 참고
'테스트용 데이터 추가'
* 테스트용 데이터가 없으면 회원 목록 기능이 정상 동작하는지 확인하기가 어렵다.
* @PostConstruct : 해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출된다
타입리프 간단히 알아보기
'타임리프 사용 선언'
<html xmlns:th="http://www.thymeleaf.org">
'속성 변경 - th:href'
th:href="@{/css/bootstrap.min.css}
- href="value1"을 th:href="value2"의 값으로 변경한다
- 타임리프 뷰 템플릿을 거치게 되면 원래 값을 th:xxx 값으로 변경한다 만약 값이 없다면 새로
생성한다.
- HTML을 그대로 볼때는 href 속성이 사용되고 뷰 템플릿을 거치면 th:href의 값이 href로
대체되면서 동적으로 변경할 수 있다.
- 대부분의 HTML 속성을 th:xxx로 변경할 수 있다.
'타임리프 핵심'
* 핵심은 th:xxx가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체한다. th:xxx이
없으면 기존 html의 xxx 속성이 그대로 사용된다.
* HTML을 파일로 직접 열었을때, th:xxx가 있어도 웹 브라우저는 th: 속성을 알지 못하므로
무시한다.
* 따라서 HTML을 파일 보기로 유지하면서 템플릿 기능도 할 수 있다.
'URL 링크 표현식 - @{...}'
th:href="@{/css/bootstrap.min.css}"
- @{...} : 타임리프는 URL 링크를 사용하는 경우 @{...}를 사용한다. 이것을 URL 링크
표현식이라 한다.
- URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.
'상품 등록 폼으로 이동 속성 변경 - th:onclick'
- onclic="location.href='addForm.html'"
- th:onclick="|location.href='@{/basic/items/add}'|"
여기에는 다음에 설명하는 리터럴 대체 문법이 사용되었다. 자세히 알아보자.
'리터럴 대체 - |...|'
|...| 이렇게 사용한다.
* 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.
- <span th:text="'Welcome to our application, ' + ${user.name} + '!'">
* 다음과 같이 리터럴 대체 문법을 사용하면 더하기 없이 편리하게 사용할 수 있다.
- <span th:text="|Welcom to our application,$user.name}!">
결과를 다음과 같이 만들어야 하는데
- location.href='/basic/items/add'
그냥 사용하면 문자와 표현식을 각각 따로 더해서 사용해야 하므로 다음과 같이 복잡해진다.
- th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
리터럴 대체 문법을 사용하면 다음과 같이 편리하게 사용할 수 있다.
- th:onclick="|location.href='@{/basic/items/add}'|"
'반복 출력 - th:each'
* <tr th:each="item : ${items}">
* 반복은 th:each를 사용한다. 이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에
하나씩 포함되고, 반복문 안에서 item 변수를 사용할 수 있다.
* 컬렉션 수만큼 <tr>...</tr>이 하이ㅜ 테그를 포함해서 생성된다.
'변수 표현식 - ${...}'
* <td th:text="${item.price}">10000</td>
* 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.
* 프로퍼티 접근법을 사용한다 (item.getPrice())
'내용 변경 - th:text'
* <td th:text="${item.price}">10000</td>
* 내용의 값을 th:text의 값으로 변경한다.
* 여기서는 10000을 ${item.price}의 값으로 변경한다.
'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
'URL 링크 간단히'
* th:href="@{|/basic/items/${item.id}|}"
* 상품 이름을 선택하는 링크를 확인해보자,
* 리터럴 대체 문법을 활용해서 간단히 사용할 수도 있다.
참고
타임리프는 순수 HTML을 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고 서버를 통해
뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다 JSP를 생각해보면 JSP 파일은
웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 뒤죽박죽 되어서 정상적인 확인이 불가능하다
오직 서버를 통해서 JSP를 열어야 한다.
이렇게 '순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿'
이라 한다.
상품 상세
상품 상세 컨트롤러와 뷰를 개발하자
'BasicItemController에 추가'
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
PathVariable로 넘어온 상품ID로 조회하고, 모델에 담아둔다 그리고 뷰 템플릿 호출
'상품 상세뷰'
'속성 변경- 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}'|
상품 등록 폼
'상품 등록 폼 BasicItemController에 추가'
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
상품 등록 폼은 단순히 뷰 템플릿만 호출한다.
'상품 등록 폼 뷰'
'속성 변경 - th-action'
* th-cation
* HTML form에서 action에 값이 없으면 현재 URL에 데이터를 전송한다.
* 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을
구분한다.
- 상품 등록 폼 : GET /basic/items/add
- 상품 등록 처리 : POST /basic/items/add
* 이렇게 하면 하나의 URL로 등록 폼과 등록 처리를 깔끔하게 처리할 수 있다.
'취소'
* 취소시 상품 목록으로 이동한다
th:onclick="|location.href='@{/basic/items}'|"
상품 등록 처리 @ModelAttribute
'상품 등록 처리 - @RequestParam'
'addItemV1 - BasicItemController에 추가'
@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";
}
* 먼저 @Request String itemName : itemName 요청 파라미터 데이터를 해당 변수에 받는다.
* Item 객체를 생성하고 itemRepository를 통해서 저장한다.
* 저장된 item을 모델에 담아서 뷰에 전달한다.
'중요' : 여기서는 상품 상세에서 사용한 item.html 뷰 템플릿을 그대로 재활용한다.
-----------------------------------------------------------------------------
'상품 등록 처리 @ModelAttribute'
@RequestParam으로 변수를 하나하나 받아서 Item을 생성하는 과정을 불편했다.
이번에는 @ModelAttribute를 사용해서 한번에 처리해보자
'addItemV2 - 상품 등록 처리 - ModelAttribute'
/**
* @ModelAttribute("item") Item item
* model.addAttribute("item", item); 자동 추가
*/
@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는 중요한 한가지 기능이 더 있는데, 바로 모델에 @ModelAttribute로
지정한 객체를 자동으로 넣어준다 지금 코드를 보면 model.addAttribute("item", item)가
주석처리 되어 있어도 잘 동작하는 것을 확인할 수 있다.
모델에 데이터를 담을 때는 이름이 필요하다. 이름은 @ModelAttribute에 지정한 name(value)
속성을 사용한다. 만약 다음과 같이 @ModelAttribute의 이름을 다르게 지정하면 다른 이름으로
모델에 포함된다.
@ModelAttribute("hello") Item item 이름을 hello 로 지정
model.addAttribute("hello", item); 모델에 hello 이름으로 저장
-----------------------------------------------------------------------------------
'addItemV3 - 상품 등록 처리 - ModelAttribute 이름 생략'
/**
* @ModelAttribute name 생략 가능
* model.addAttribute(item); 자동 추가, 생략 가능
* 생략시 model에 저장되는 name은 클래스명 첫글자만 소문자로 등록 Item -> item
*/
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
itemRepository.save(item);
return "basic/item";
}
@ModelAttribute의 이름을 생략할 수 있다.
주의!
@ModelAttribute의 이름을 생략하면 모델에 저장될 때 클래스명을 사용한다.
이때 클래스의 첫글자만 소문자로 변경해서 등록한다
예) @ModelAttribute 클래스명 -> 모델에 자동 추가되는 이름
- Item -> item
- HelloWorld -> helloWorld
------------------------------------------------------------------------------
'addItemV4 - 상품 등록 처리 - ModelAttribute 전체 생략'
/**
* @ModelAttribute 자체 생략 가능
* model.addAttribute(item) 자동 추가
*/
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
@ModelAttribute 자체도 생략가능하다. 대상 객체는 모델에 자동 등록된다
나머지 사항은 기존과 동일
상품 수정
'상품 수정 폼 컨트롤러 BasicItemController에 추가'
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
수정에 필요한 정보를 조회하고, 수정용 폼 뷰를 호출한다
'상품 수정 개발'
@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의 값을 그대로 사용한다.
PRG Post/Redirect/Get
지금까지 진행한 상품 등록 처리 컨트롤러는 심각한 문제가 있다.
상품 등록을 완료하고 웹 브라우저의 새로고침 버튼을 클릭해보자
상품이 계속해서 중복 등록되는것을 확인할 수 있다.
전체 흐름
POST 등록 후 새로 고침
웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다
상품 등록 폼에서 데이터를 입력하고 저장을 선택하면 POST /add + 상품 데이터를 서버로 전송한다.
이 상태에서 새로 고침을 또 선택하면 마지막에 전송한 POST /add + 상품 데이터를 서버로 다시
전송하게 된다. 그래서 내용은 같고 ID만 다른 상품 데이터가 계속 쌓이게 된다.
이 문제를 어떻게 해결할까?
POST,Redirect GET
웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다
새로 고침 문제를 해결하려면 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라 상품 상세
화면으로 리다이렉트 호출해주면 된다.
웹 브라우저는 리다이렉트의 영향으로 상품 저장 후에 실제 상품 상세 화면으로 다시 이동한다.
따라서 마지막에 호출한 내용이 상품 상세 화면인 GET/items/{id}가 되는 것이다.
이후 새로고침을 해도 상품 상세 화면으로 이동하게 되므로 새로 고침 문제를 해결할 수 있다.
'BasicItemController에 추가'
/**
* PRG - Post/Redirect/Get
*/
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
상품 등록 처리 이후에 뷰 템플릿이 아니라 상품 상세 화면으로 리다이렉트 하도록 코드를 작성해보자
이런 문제 해결 방식을 PRG Post/Redirect/Get 라 한다.
RedirectAttributes
상품을 저장하고 상품 상세 화면으로 리다이렉트 한 것 까지는 좋았다. 그런데 고객 입장에서
저장이 잘 된 것인지 안 된 것인지 확신이 들지 않는다. 그래서 저장이 잘 되었으면 상품 상세
화면에 "저장되었습니다"라는 메시지를 보여달라는 요구사항을 해결해보자.
'BasicItemController에 추가'
/**
* 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'
RedirectAttributes를 사용하면 URL 인코딩도 해주고, pathVariable, 쿼리 파라미터까지
처리해준다.
* redirect:/basic/items/{itemId}
- pathVariable 바인딩 : {itemId}
- 나머지는 쿼리 파라미터로 처리 : ?status=true
'뷰 템플릿에 메시지 추가'
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<!-- 추가 -->
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
* th:if : 해당 조건이 참이면 실행
* ${param:status}: 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능
- 원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 한다. 그런데 쿼리 파라미터는
자주 사용해서 타임리프에서 직접 지원한다.
뷰 템플릿에 메시지를 추가하고 실행해보면 "저장 완료!"라는 메시지가 나오는 것을 확인할수 있다.
물론 상품 목록에서 상품 상세로 이동한 경우에는 해당 메시지가 출력되지 않는다.
Author And Source
이 문제에 관하여(스프링 MVC - 웹 페이지 만들기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@gustjtmd/스프링-MVC-웹-페이지-만들기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)