@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;
}
int나 Long의 원시타입을 사용하지 않은 이유는 null을 넣기 위함다.
즉, 조금 더 널널하게 개발하기 위함이다.
@Data를 사용하면 굉장히 편하긴 하나(getter, setter, etc.. 다 만들어줌) 핵심 도메인 모델에 사용하기엔 굉장히 위험할 수 있다.
왜냐하면 이게 예상치 못 하게 동작할 수 있기 때문이다.
그래서 그냥 DTO에는 써도 되나 이것도 잘 살펴보고 써야된다.
@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();
}
}
또한 앞서 자바는 멀티 쓰레드를 이용하여 유기적으로 동작할 수 있다고 하였는데
문제는 동시성 문제가 생기기 때문에 HashMap은 ConCurrentHashMap, long 타입도 atomic long같을 것을 써줘야한다.
@Respository를 들어가보면 @Component를 상속받고있기 때문에 당연 얘도 스프링이 뜰 때 컨테이너에 들어간다.
또한 위 update 코드에서는 id를 제외한 나머지를 업데이트 하고 있다면 따로 ItemParameterDTO 이런거를 만들어서 해당 메서드에서 사용하는 3개의 파라미터만 넣어둬서 다른 개발자와의 혼동을 피하는 것이 좋다.
왜냐하면 @Data로 setId() 는 만들어져 있는데 update() 메서드에서는 사용을 안 하니 "뭐지? 임의로 추가해도 되는건가?" 하고 헷갈릴 수 있기 때문이다.
중복 vs 명확성
이 둘의 대결은 언제나 명확성이 승리다.
따라서 중복을 극도로 싫어하더라고 조금이라도 애매하면 중복을 제거하여 명확성을 높히자.
템플릿을 작성할 때 bootstrap을 이용할 수도 있는데 이게 잘 적용이 되는지는 확인해봐야한다.
그래서 통상 static 밑에 css 폴더를 만들어서 거기에 min 파일을 넣는데 만약 url에 입력해서 css 파일이 정상적으로 뜨지 않는다면,
root 폴더 바로 밑에 빌드하면 나오는 out 폴더를 삭제후 다시 서버를 실행하면 될 수도 있다.
또한 정적 리소스가 공개되는 resources 파일에 .html파일을 넣어두면 안된다. 왜냐하면 실제 서비스할 때도 이 경로들이 다 보이기 때문에 누군가가 접근할 수 있기 때문이다.
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>spring-mvc-상품목록</title>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>
...
<html xmlns:th="http://www.thymeleaf.org">
이 코드로 thymeleaf를 사용할 수 있고
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
이 코드로 경로를 절대경로로 바꿔서 어디서든지 사용하도록 도와준다
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button"
>상품 등록
</button>
@{}
하고 넣어주면 된다.<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>
@Controller
@RequestMapping("/basic/items")
public class BasicItemController {
private final ItemRepository itemRepository;
// @Autowired
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
}
이때 생성자함수가 하나밖에 없다면 @Autowired를 생략할 수 있다고 하였다.
또한 lombok에서 제공하는 @RequiredArgsConstructor도 배웠는데 이를 이용하면 final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다. 그래서 생성자가 따로 필요없어진다.
@PostMapping("/add")
public String addItemV1(@RequestParam("itemName") String itemName, @RequestParam("price") int price, @RequestParam("quantity") 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";
}
@ModelAttribute 는 중요한 한가지 기능이 더 있는데, 바로 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다.
그럼 또 이제 Model model 파라미터를 생략할 수 있어서 좋다.
지금 코드를 보면 model.addAttribute("item", item) 가 주석처리 되어 있어도 잘 동작하는 것을 확인할 수 있다.
모델에 데이터를 담을 때는 이름이 필요하다. 이름은 @ModelAttribute 에 지정한 name(value) 속성을 사용한다. 만약 다음과 같이 @ModelAttribute 의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.
@ModelAttribute("hello") Item item 이름을 hello 로 지정
model.addAttribute("hello", item); 모델에 hello 이름으로 저장
/**
* @ModelAttribute name 생략 가능
* model.addAttribute(item); 자동 추가, 생략 가능
* 생략시 model에 저장되는 name은 클래스명 첫글자만 소문자로 등록 Item -> item
*/
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
itemRepository.save(item);
return "basic/item";
}
즉, @ModelAttribute 다음에 지정한 클래스 이름의 첫글자를 소문자로 변환하여 자동으로 넣어준다.
마치 스프링 빈 생성하는 규약처럼.
그리고 default로 @ModelAttribute가 들어가기 때문에 그마저도 생략할 수 있다.
/**
* @ModelAttribute 자체 생략 가능
* model.addAttribute(item) 자동 추가
*/
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
redirect:/...
으로 편리하게 리다이렉트를 지원한다.@PostMapping("/{itemId}/edit")
public String edit(@PathVariable("itemId") Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
// 새로 고침 버그 삭제를 위한 PRG 패턴 적용
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item saveItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", saveItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
url에 직접 코드를 넣을 때 만약 변수가 한들이라면 utf-8 인코딩을 하는 코드를 추가해야한다.
저장 후 reaction이 없기 때문에 사용자 입장에서는 저장이 된지 뭔지 감이 잘 안 잡힌다.
위 코드처럼 사용하면 url 인코딩은 물론이고 다 해결이 된다.
추가적으로 addAttribute에 itemId과 status를 추가해줬는데
itemId는 url에 명시되어 들어가는 걸 알지만 status는 뭐지? 할 수 있는데 queryParam으로 뒤에 알아서 붙어서 나간다.
그래서 view 단에서 th:text%{param.status} 뭐 이런식으로 꺼내서 view를 만들어주면 된다.
th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체한다.
th:xxx 이 없으면 기존 html 의 xxx 속성이 그대로 사용된다.
따라서 HTML을 파일로 직접 열었을 때, 서버로 열었을 때 처럼 똑같이 정상적으로 보이며 소스를 보면 th:xxx 가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시한다.
, HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.
th:href="@{/css/bootstrap.min.css}"
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'
원래는 이렇게 표현식과 string을 조합하여 보내야하는데
th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
리터럴 대체 문법을 사용하면 개편하게 사용할 수 있다.
th:onclick="|location.href='@{/basic/items/add}'|"
<tr th:each="item : ${items}">
<tr>..</tr>
이 하위 테그를 포함해서 생성된다.<td th:text="${item.price}"\>10000</td>
<td th:text="${item.price}"\>10000</td>
th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
동적 링크를 생성하는 코드를 봐보자.
URL 링크 표현식을 사용하면 경로를 템플릿처럼 편리하게 사용할 수 있다.
경로 변수( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성한다.
예) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
생성 링크: http://localhost:8080/basic/items/1?query=test
th:href="@{|/basic/items/${item.id}|}"
<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>
<form action="item.html" th:action method="post">
<div>
...
상품 등록 폼: GET /basic/items/add
상품 등록 처리: POST /basic/items/add
주로 다음 두 애노테이션에서 문제가 발생한다.
@RequestParam
, @PathVariable
//애노테이션에 username이라는 이름이 명확하게 있다. 문제 없이 작동한다.
@RequestMapping("/request")
public String request(@RequestParam("username") String username) {
...
}
//애노테이션에 이름이 없다. -parameters 옵션 필요
@RequestMapping("/request")
public String request(@RequestParam String username) {
...
}
//애노테이션도 없고 이름도 없다. -parameters 옵션 필요
@RequestMapping("/request")
public String request(String username) {
...
}
//애노테이션에 userId라는 이름이 명확하게 있다. 문제 없이 작동한다.
public String mappingPath(@PathVariable("userId") String userId) {
...
}
//애노테이션에 이름이 없다. -parameters 옵션 필요
@RequestMapping("/request")
public String request(@RequestParam String username) {
...
}
애노테이션에 이름을 생략하지 않고 다음과 같이 이름을 항상 적어준다.
@RequestParam("username") String username
@PathVariable("userId") String userId
IntelliJ IDEA에서 File -> Settings를 연다. (Mac은 IntelliJ IDEA -> Settings)
Build, Execution, Deployment → Compiler → Java Compiler로 이동한다.
Additional command line parameters라는 항목에 다음을 추가한다.
-parameters
자바를 컴파일할 때 매개변수 이름을 읽을 수 있도록 남겨두어야 사용할 수 있다. 컴파일 시점에 -parameters 옵션을 사용하면 매개변수 이름을 사용할 수 있게 남겨둔다.
스프링 부트 3.2 전까지는 바이트코드를 파싱해서 매개변수 이름을 추론하려고 시도했다. 하지만 스프링 부트 3.2 부터는 이런 시도를 하지 않는다.