김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
@Getter @Setter public class Item { private Long id; private String itemName; private Integer price; private Integer quantity; }
@Data
를 핵심 도메인 모델에 사용하는 것은 좋지 않다
@Getter
, @Setter
정도만 사용하는 것이 좋음
DTO의 경우에는 @Data
를 사용해도 괜찮다
int 대신 Integer를 사용한 이유
int는 값이 0이라도 들어가야함
가격이나 수량이 아직 없을 수도 있기 때문에 null을 받을 수 있도록 Integer로 작성
@Repository public class ItemRepository { private static final Map<Long, Item> store = new HashMap<>(); private static long sequence = 0L; }
@Repository
가 붙어있으므로 컴포넌트 스캔의 대상이 된다
동시성 문제 때문에 HashMap<>을 사용하면 안되고 ConcurrentHashMap<>
을 사용하는 것이 좋다
위와 같은 이유로 long 대신 AtomicLong
을 사용하는 것이 좋다
public class Item { public void update(Long itemId, Item updateParam) { Item findItem = findById(itemId); findItem.setItemName(updateParam.getItemName()); findItem.setPrice(updateParam.getPrice()); findItem.setQuantity(updateParam.getQuantity()); } }
updateParam으로 Item객체를 사용하는 것보다 DTO를 만들어서 사용하는 것이 좋음
DTO를 id를 제외한 나머지 3개의 필드를 갖도록 만든다
<html xmlns:th="http://www.thymeleaf.org">
thymeleaf 사용 선언
<link th:href="@{/css/bootstrap.min.css}" href="../css/bootstrap.min.css" rel="stylesheet">
thymeleaf view template을 거치게 되면 thymeleaf가 XXX
를 th가 붙은 th:XXX
로 변경
th:XXX
가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체
th:XXX
가 없는 경우, 기존 html의 xxx 속성이 그대로 사용
th:XXX
만 존재하고, html의 XXX
가 없는 경우, XXX를 생성해서 값을 넣는다
대부분의 HTML 속성을 th:XXX
로 변경할 수 있다
기본은 href
의 경로가 실행되지만 view template을 거치면 th:href
이 지정된 경로로 변경
<td th:text="${모델명.필드}">기존내용</td> <td th:text="${반복변수.필드}">기존내용</td>
${ }
: 변수 표현식
기존내용을 ${반복변수.필드}
의 값으로 변경
<tr th:each="반복변수 : ${모델명}">
모델에 포함된 데이터가 반복변수 하나씩 포함된다
반복문 안에서 ( 태그 내부에서 ) 반복변수를 사용할 수 있다
데이터 수 만큼 <tr> .. </tr>
이 하위 태그를 포함해서 생성된다
자바의 for-each문과 동일한 형식인듯
<button onclick="location.href='addForm.html'" th:onclick="|location.href='@{/basic/items/add}'|"> </button>
리터럴 대체 : | |
thymeleaf에서 문자와 표현식은 분리되어 있어서 +
를 사용해야 하는데 | |
를 사용하면 분리해서 사용하지 않아도 된다
자바스크립트의 ` `
기능인듯 하다
ex>
사용 전 : <span th:text="'Welcome to our application, ' + ${user.name} + '!'">
사용 후 : <span th:text="|Welcome to our application, ${user.name}!|">
<a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">상품id</a>
@{ }
: URL 링크 표현식 ( thymeleaf에서 링크를 걸 때 사용 )
링크 안에 모델의 값으로 경로 변수를 사용하려면 ( )
안에 지정해줘야함
"@{/basic/items/{itemId}(itemId=${item.id})}"
query 속성을 사용해 쿼리 파라미터 생성 가능
ex> th:href="@{/items/{itemId}(itemId=${item.id}, query='test')}"
생성 링크 : http://localhost:8080/items/1?query=test
리터럴 대체 문법을 활용해서 아래처럼 작성 가능
th:href="@{|/basic/items/${item.id}|}"
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
th:if
: 조건이 참이면 실행
${param.파라미터 이름}
: 쿼리 파라미터를 조회하는 기능
위의 경우, ?status=true
와 같이 쿼리 파라미터로 값이 넘어온 경우에 사용하고 status 값이 true이면 실행된다
@Controller @RequestMapping("/basic/items") @RequiredArgsConstructor public class BasicItemController { private final ItemRepository itemRepository; }
생성자가 하나만 있는 경우, 생성자에 붙은 @Autowired
생략 가능
@RequiredArgsConstructor
를 사용하면 final 이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다
@PostConstruct public void init() { itemRepository.save(new Item("itemA", 10000, 10)); itemRepository.save(new Item("itemB", 20000, 20)); }
@PostConstruct
때문에 해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출된다@PostMapping("/add") public String addItemV1(@RequestParam String itemName, @RequestParam int price, @RequestParam Integer quantity, Model model) { ... }
form 태그 내부 input 태그의 name 속성으로 넘어옴
<input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">
@PostMapping("/add") public String addItemV2(@ModelAttribute("item") Item item) { ... }
@ModelAttribute
의 기능
요청 파라미터 처리 : @ModelAttribute
가 Item 객체를 생성해 요청 파라미터로 받은 값들을 넣어준다
Model 추가 : @ModelAttribute
로 지정한 객체를 Model에 자동으로 넣어준다 ( "item"이라는 이름으로 )
model.addAttribute("item", item)
을 자동으로 처리
그래서 파라미터에 Model을 선언할 필요도 없고 model.addAttribute()
를 사용할 필요도 없다
( ) 안에 이름을 생략하면 클래스명의 첫 글자를 소문자로 변경해서 model에 등록한다 ( Item → item )
@ModelAttribute
생략 가능
@RequestParam
이 적용되고, 객체 타입인 경우 @ModelAttribute
가 적용됨@PostMapping("/{itemId}/edit") public String edit(@PathVariable Long itemId, @ModelAttribute Item item) { itemRepository.update(itemId, item); return "redirect:/basic/items/{itemId}"; }
@Pathvariable
로 넘어온 itemId, 넘어온 요청 파라미터와 @ModelAttribute
로 생성된 model로 기존 item을 수정
스프링은 redirect:/
로 편리하게 리다이렉트 지원
리다이렉트 시, @PathVariable Long itemId
의 값을 그대로 사용해서 경로 지정 가능
상태 코드를 확인하면 302, Location 헤더 정보는 /basic/items/{itemId}
@PostMapping("/add") public String addItemV3(@ModelAttribute Item item) { ... return basic/item }
Post 요청의 결과로 단순히 다른 화면으로 전환하도록 했을 때 새로고침을 누르면 중복으로 등록되는 문제가 발생
새로고침은 마지막으로 수행한 요청이 다시 수행하는데 웹 브라우저 기준에서 마지막으로 한 요청이 Post 방식
즉, 마지막에 서버에 전송한 데이터를 다시 전송하기 때문에 중복 등록 문제 발생
➡️ 리다이렉트를 사용해서 해결
리다이렉트를 하면 웹 브라우저가 url이 바뀌면서 Get 방식으로 다시 요청
리다이렉트 후 새로고침을 눌러도 마지막 요청이 Get 방식이기 때문에 아무런 문제가 발생하지 않는다
@PostMapping("/add") public String addItemV5(@ModelAttribute("item") Item item, RedirectAttributes redirectAttributes) { Item savedItem = itemRepository.save(item); //return "redirect:/basic/items/" + item.getId(); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true); return "redirect:/basic/items/{itemId}"; }
return "redirect:/basic/items/" + item.getId();
그러나 위의 방식은 위험한 방식
URL에 변수를 더해서 사용하는 것은 URL 인코딩이 되지 않기 때문
이런 문제를 해결하기 위해 RedirectAttributes
를 사용
RedirectAttributes
: 리다이렉트할 때 파라미터를 붙여서 보낼 수 있음
지정한 속성 이름과 return 에서 사용한 경로변수와 이름이 같은 경우, 경로 변수에 값이 들어가게 된다
경로에 이름이 없는 경우, ?status=true
처럼 쿼리 파라미터 형식으로 전달된다
즉, RedirectAttributes는 URL 인코딩, pathVarible , 쿼리 파라미터까지 처리해준다