Web/Spring

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

now0204 2023. 10. 15. 16:15

 

1. 프로젝트 생성 

 

스프링 부트 2.7, packaging Jar, dependencies: Spring Web, Thymeleaf,Lombok

 

1.1 /resources/static/index.html -> 웰컴페이지 추가함 

 

 

2. 요구사항 분석 

 

상품을 관리할 수 있는 서비스 만들기 

 

상품 도메인 : 상품 ID, 상품명, 가격, 수량

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

출처: 스프링MVC(김영한) - 인프런

 

3. 상품 도메인 개발 

@Data
public class Item {

 private Long id;
 private String itemName;
 private Integer price;
 private Integer quantity;
 public Item() {
 
 }
 	//..
 }

 > price와 quantity는 Integer로 선언하여, 0이거나 null일 때도 값을 처리할 수 있도록 함 

 

- 상품저장소 

@repository
public class ItemRepository{

	private static final Map<Long, Item< store = new HashMap<>();
    private static final sequence = 0L;
    
    public Item save(Item item){
    	item.setId(++sequence);
        stroe.put(item.getId(), item);
        return item;
    }
    
    //..
    
    public void update(Long itemId, Item updateParam){
    	Item findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        //..
    }
}

- update 부분에서 item 파라미터 DTO같은 것 따로 만들어서 넣는 게 좋다. 

  그냥 item으로 받으면, 수정할 정보 외에도 너무 많은 걸 담고 있다. (명확성이 떨어짐) 

 

* 실무에서는 hashmap 쓰면 안된다. (싱글톤 + 멀티스레드)

* 싱글톤으로 관리되기 때문에 사실 static 안써도 상관 없긴함! 

  다만 따로 생성해서 테스트 하거나 할때 확인하려면, static으로 해야함 

* Long도 그냥 쓰면 안되고 아토믹 롱? 같은 것 써야함 

 

 

4. 상품서비스 HTML 

  

-> HTML파일 따로 다운받음 (CSS 포함) 

-> 정적 파일은 직접 열어도 잘 열림 

 

5. 상품목록 - 타임리프 

 

BasicItemController 

@Controller 
@RequestMapping("/basic/items")
@RequiredArgsConstructor // final붙은애들 사용해서 생성자 만들어준다.
public class BasicItemController {

	private final ItemRepository itemRepository; //생성자가 하나면, Autowired생략해도 됨
    
    
    @GetMapping
    public String item(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));
 }
}

- 컨트롤러 로직은 모든 상품 조회해서 뷰 템플릿에 담음 

- items.html 정적 HTML을 뷰 템플릿 영역으로 복사해서 수정하자 

<html xmlns:th="http://www.thymeleaf.org">
<head>
 <meta charset="utf-8">
 <link href="../css/bootstrap.min.css"
 th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> 
 <!-- 타임리프를 사용하면, 속성을 덮어씌움 (정적페이지는 기본속성우선) -->
</head>

<!-- -->
<button class="btn btn-primary float-end" onclick="location.href='addForm.html'" 
th:onclick="|location.href='@{/basic/items/add}'|"
<!-- ||는 문자열을 이어주는 타임리프 문법 -->
type="button">상품 등록</button>

<!-- -->
<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>

 

- 타임리프의 장점은 html태그에 코드를 때려박아도, 기존 html을 최대한 유지한다는 것

   추후에 마크업 작업을 해도 html코드가 깨지지 않는다.

 

> 타임리프 사용 선언 

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

> 속성 변경 - th:href 등 

  타임리프 뷰 템플릿을 거치게 되면, 원래 값을 변경함 (값이 없다면 새로 생성)

  HTML을 그대로 볼 때는 href속성이 사용되고, 뷰 템플릿을 거치면, th:href의 값으로 치환

  대부분의 HTML 속성을 th:xxx로 변경할 수 있음 

 

> 타임리프 핵심 

   핵심은 th:xxx가 붙은 부분은 서버사이드 랜더링, 나머지는 기존 코드 사용 

   HTML 파일을 직접 열면 th:xxx가 있어도 속성을 알지 못하므로 무시함 

 

> URL 링크 표현식 @(..)

   타임리프의 URL링크 표현식이다. (서블릿 컨텍스트를 자동으로 포함한다)

 

> 리터럴 대체 |...|

   타임리프는 문자와 표현식을 나누어 관리하므로, 더해서 사용해야한다.

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

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

 

> 반복 출력: -th:each

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

items의 데이터가 item 변수에 담기고, 반복문 안에서 사용할 수 있다.

 

> 변수 표현식 ${} 

    프러퍼티 접근법을 사용하여, 변수에 접근가능 

    ${} -> 변수.getxxx와 동일 

 

> 내용 변경 - th:text

   HTML 태그 사이 내용을 대체한다.

 

> 링크 표현식 2 

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

URL 링크 표현식으로 경로를 템플릿 처럼 편리하게 사용가능 

경로변수 + 쿼리 파라미터도 생성 가능하다.

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

 

 

*정적페이지에서 css경로를 절대경로로 바꾸고 파일을 열면 

  /css > C:css로 먹음 !! 상대경로로 잘 쓰자.. 웹 프로젝트의 경우 

  root가 보통 프로젝트 최상위 (webapp / 혹은 resources 임)

[웹 프로젝트는 웹 페이지를 보여주는게 목적!]

 

6. 상품 상세 

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

- PathVariable로 넘어온 상품 ID 조회하고 모델에 담은 후 뷰 템플릿 호출

 

6. 상품 등록

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

- 단순히 From으로 이동 

- th:action HTML form에서 action에 값이 없으면, 현재 URL에 데이터를 전송

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

 

7. 상품 등록 처리 - @ModelAttribute

 

- 이제 상품 등록 폼에서 전달된 데이터를 실제 상품 등록을 처리해보자 

- 상품 등록 폼은 다음 방식으로 서버에 데이터를 전달함 

 

POST - HTML Form 

            content-type : application/x-www-form-urlencoded

            메시지 바디에 쿼리 파라미터 형식으로 전달 

            요청 파라미터를 처리해야하므로, @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";
}


@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
 
 	itemRepository.save(item);
 	//model.addAttribute("item", item);
 
 return "basic/item";
 
 @PostMapping("/add")
public String addItemV3(Item item) {
 
 	itemRepository.save(item);
 	//model.addAttribute("item", item);
 
 return "basic/item";
}

- V1 요청 파라미터로 하나씩 다 받은 후에 데이터를 토대로 Item객체를 생성 

- V2 @ModelAttribute로 요청 파라미터 처리 (프로퍼티 접근법 사용 setXXX로 생성해줌)

         Model추가 : 모델에 @ModelAttribute로 지정한 객체 자동으로 넣어줌 

         모델에 데이터를 담을 때는 이름이 필요함, 이름을 지정하지 않으면 클래스명 사용

- V3 ModelAttribute생략 (요청 파라미터의 경우 기본 자료형 아니면, 자동으로 붙여줌)

 

*어차피 Model은 컨트롤러 밖에서 생성되어 주입되는 것!! (어뎁터 + Argument Resolver)

 

8. 상품 수정 

 

@GetMappint("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model){

	Item item = itemRepository.findById(itemId);
    model.addAttribute("item", item);
    return "basic/editForm";
}

 

> 상품 수정 폼 뷰 (등록폼과 유사) 

> Get /item/{itemId}/edit : 상품 수정 폼 

> Post /item/{itemId}/edit : 상품 수정 처리 

 

- 리다이렉트 

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

스프링은 redirect:/..로 지원 

> 컨트롤러에 매핑된 @PathVariable은 redirect에도 사용할 수 있다. 

   (redirect: /basic/items/{itemId} -> @PathVariable Long itemId 그대로 사용

 

9 PRG: Post/Redirect/Get

 

 - POST 등록 후 새로고침 

출처: 스프링MVC (인프런) - 김영한

웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송

상품 등록 폼에서 데이터 입력하고 저장하면, POST/add + 상품데이터 서버 전송

새로 고침하면, 또 마지막에 전송한 POST/add가 전송되게 됨 

 

출처: 스프링MVC (인프런) - 김영한

 

- 새로 고침 문제를 해결하기 위해 상품 저장 후 뷰 템플릿으로 이동하는 것이 아니라

  상품 상세 화면으로 리다이렉트 호출! (컨트롤러 호출 -> 뷰템플릿은 포워딩처리해서 url변화 없음)

  리다이렉트는 GET 방식으로 컨트롤러 호출함 물리적 URL이 변화 -> 새로고침해도 상세화면만 나온다.

 

@PostMapping("/add")
public String addItemV5(Item item){
	itemRepository.save(item);
    return "redirect:/basic/item/"+ item.getId(); //URL 인코딩 문제 있음 사용x
    
}

 

10 RedirectAttributes

 

리다이렉트 후 -> 고객입장에서 저장이 잘 된건지 확인받고 싶음 

저장되었다는 메시지 보여달라는 요구사항이 들어옴 

 

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

- RedirectAttriute를 사용하면, URL인코딩, pathVariable,쿼리 파라미터 처리 다해준다.

- 위 객체에 값을 add시 

- redirect를 확인해서 {} < 바인딩 가능한건 꺼내서 바인딩하고, 나머지는 전부 쿼리 스트링 처리 

 

<div class="container">
 <div class="py-5 text-center">
 <h2>상품 상세</h2>
 </div>
 <!-- 추가 -->
 <h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>

- ${param.status}: 타임리프 쿼리 파라미터 간편 조회 기능 

                              원래는 모델에 담고 값을 꺼내야하는데, 쿼리파라미터는 자주 사용해서 타임리프에서 지원해줌