IT recording...
[스프링 MVC1] 7. 웹 페이지 만들기 본문
https://adorable-aspen-d23.notion.site/MVC1-7-63af45a2d41f4629a976dcb31da15a3a
김영한님의 [스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 강의를 듣고 작성한 글입니다.
1. 요구사항 분석
- 상품을 관리한다.
- Domain
- 상품 ID
- 상품명
- 가격
- 수량
- 상품 관리 기능
- 상품 목록
- 상품 상세
- 상품 등록
- 상품 수정
2. 도메인 , repository구현
- Item 도메인
@Data
//Data를 쓰면 위험하다. Getter,Setter, toString, 등등 다 만들어주기 때문에 -> 핵심 도메인 모델에 쓰기에는 적절하지 않다.
//실제 개발에서는 Getter,Setter정도만 사용하자
//Dto(단순하게 데이터 왔다갔다할 때는 사용 가능)
public class Item {
private Long id;
private String itemName;
private Integer price;
private int quantity;
public Item(){
}
public Item(String itemName, Integer price, int quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- ItemRepository
@Repository
//component스캔의 대상이 된다.
public class ItemRepository {
private static final Map<Long,Item> store = new HashMap<>(); //static
//실무에서는 동시에 여러 쓰레드가 접근할 수 있기 때문에 Hashmap을 사용하지 않고 Cuncurrent Hashmap을 사용해야 한다.
private static long sequence = 0L; //static -> 싱글톤을 유지하기 위해
//얘도 automic long 같은거를 사용해야 함
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());
}
**//중복이냐 명확성이냐? -> 명확성을 따르자!
//지금 update에서 id빼고 다 사용되니까 원래는 따로 객체를 만드는 것이 맞다.**
public void clearStore(){
store.clear();
}
}
- ItemRepositoryTest
class ItemRepositoryTest {
ItemRepository itemRepository = new ItemRepository();
**//깔끔한 테스트를 위해 사용, 테스트 끝날 때마다 리포지토리를 초기화한다.
@AfterEach
void afterEach(){
itemRepository.clearStore();
}**
@Test
void save() {
//given
Item item = new Item("itemA",10000,10);
//when
Item savedItem = itemRepository.save(item);
//then
Item findItem = itemRepository.findById(item.getId());
assertThat(findItem).isEqualTo(savedItem);
}
@Test
void findAll() {
//given
Item item1 = new Item("item2",10000,10);
Item item2 = new Item("item1",20000,20);
itemRepository.save(item1);
itemRepository.save(item2);
//when
List<Item> result = itemRepository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
assertThat(result).contains(item1,item2);
}
@Test
void update() {
//given
Item item = new Item("itemA",10000,10);
Item savedItem = itemRepository.save(item);
Long itemId = savedItem.getId();
//when
Item updateParam = new Item("item2", 20000, 30);
itemRepository.update(itemId,updateParam);
//then
Item findItem = itemRepository.findById(itemId);
assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
}
}
3. 상품 목록
3-1. 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 필드나, @NonNull이 붙은 필드에 대해 생성자를 생성해 준다.
- 주로 의존성 주입(Dependency Injection)편의성을 위해 사용된다.
- 어떠한 빈(Bean)에 생성자가 오직 하나만 있고, 생성자의 파라미터 타입이 빈으로 등록 가능한 존재라면 이 빈은 @Autowired 어노테이션 없이도 의존성 주입이 가능하다.
//@RequiredArgsConstructor
public class BasicItemController {
//private final ItemRepository itemRepository;
@RequiredArgsConstructor면 아래를 자동으로 생성해준다.
@Autowired
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
@Service
@RequiredArgsConstructor
public class RequiredArgsConstructorDIServiceExample {
private final FirstRepository firstRepository;
private final SecondRepository secondRepository;
private final ThirdRepository thirdRepository;
// ...
}
- PostConstruct
- 해당 빈의 의존관계가 모두 주입되고 나면 호출된다.
- 테스트용 데이터를 넣기 위해 사용 가능하다.
/**
* 테스트용 데이터 추가
*/
@PostConstruct
public void init(){
itemRepository.save(new Item("itemA",10000,10));
itemRepository.save(new Item("itemB",20000,20));
}
4. 타임리프
- /resources/templates/basic/items.html
** //타임리프 사용 선언**
- 타임리프
- th:xxx 가 붙은 부분은 서버 사이드에서 렌더링 되고, 기존 것을 대체한다.
- th:xxx가 없으면 기존 html의 xxx 속성이 그대로 사용된다.
- URL 링크 표현식
- @{...}
- 변수 표현식
- ${...}
**th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test') //쿼리 파라미터 생성**
>> <http://localhost:8080/basic/items/1?query=test>
- 리터럴 대체
- |...|
th:onclick="'location.href=' + '\\'' + @{/basic/items/add} + '\\''"
>>>
**th:onclick = "|location.href='@{/basic/items/add}'|"**
- 반복 출력
- th:each
**<tr th:each="item: ${items}">**
- 내용 변경
- th:text
<td **th:text="${item.price}"**>10000</td>
5. 상품 상세
- 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로 상품을 조회하고, 모델에 담아 뷰 템플릿을 호출한다.
6. 상품 수정
- 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}"**; //기존 경로를 지우고 리다이렉트 시키기
}
- Redirect
- 스프링은 redirect:/... 를 이용하여 편리하게 리다이렉트를 지원한다.
- 컨트롤러에 매핑된 @PathVariable의 값을 사용할 수도 있다.
7. 상품 등록
- BasicItemController
- 같은 경로를 사용하지만 Get, Post로 기능을 나누어 사용할 수 있다.
@GetMapping("/add")
public String addForm(){
return "basic/addForm";
} //단순 뷰 템플릿 호출
1. 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";
}
2. ModelAttibute 사용
//@PostMapping("/add")
//ModelAttribute -> 자동으로 객체를 만들어주고, 뷰에서 사용하는 model에 넣어주는 역할까지 수행함
public String addItemV2(@ModelAttribute("item") Item item, Model model){
itemRepository.save(item);
//model.addAttribute("item", item); //ModelAttribute가 자동으로 추가하기 때문에 생략 가능
return "basic/item";
}
//@PostMapping("/add")
**public String addItemV3(@ModelAttribute Item item){
itemRepository.save(item);
return "basic/item";
}**
//@PostMapping("/add")
public String addItemV4(Item item){
itemRepository.save(item);
return "basic/item";
}
3. 리다이렉트
//@PostMapping("/add")
public String addItemV5(Item item){
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId(); //PRG (post후 데이터 중복 저장을 막기 위해 get으로 리다이렉트 한다.)
}
**@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}"; //PRG (post후 데이터 중복 저장을 막기 위해 get으로 리다이렉트 한다.)
}**
- ModelAttribute
- RequestParam을 이용하여 Item객체를 생성하는 것은 불편하므로 한 번에 객체를 생성해준다.
- 기능
- 요청 파라미터 처리 : Item객체를 생성하고, 요청 파라미터의 값을 프로퍼티 접근법(setXXX)으로 입력해준다.
- Model추가: Model에 지정한 객체(Item)을 자동으로 넣어준다. → 따로 model.attribute(”item”,item);을 해줄 필요가 없다.
- Redirect
- 리다이렉트를 해주지 않고 내부 호출을 진행하면 새로고침 시 마지막에 서버에 전송한 데이터를 다시 전송한다.
→ PRG Post/Redirect/Get
- Post 후 Redirect를 통해 Get을 호출하면 문제가 해결된다.
- RedirectAttributes
- 기타 다른 정보들을 함께 보내고 싶을 때 RedirectAttriubutes를 사용하여 보낼 수 있다.
- ex) 저장이 완료 되었으면 “저장되었습니다"라는 메시지를 보여줬음 좋겠음
- → status=true를 함께 보내기
/**
* 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}";
}
→ http://localhost:8080/basic/items/3?status=true
- redirect:/... 에서 사용하지 않은 attribute는 쿼리파라미터의 형태로 넘어간다.
'Spring' 카테고리의 다른 글
[Spring] 개발 중 마주한 오류들 - 1 (0) | 2022.02.15 |
---|---|
[스프링 JPA1] 1. 요구사항 분석 및 도메인 셜계 (0) | 2022.02.07 |
[스프링 MVC1] 6. MVC 기본 기능 (0) | 2022.01.15 |
[스프링 MVC1] 5. MVC 패턴 (0) | 2022.01.14 |
[스프링 MVC1] 4. MVC 프론트 컨트롤러 패턴 (0) | 2022.01.14 |
Comments