IT recording...

[스프링 MVC1] 7. 웹 페이지 만들기 본문

Spring

[스프링 MVC1] 7. 웹 페이지 만들기

I-one 2022. 2. 7. 11:46

https://adorable-aspen-d23.notion.site/MVC1-7-63af45a2d41f4629a976dcb31da15a3a

 

[스프링 MVC1] 7. 웹 페이지 만들기

1. 요구사항 분석

adorable-aspen-d23.notion.site

김영한님의 [스프링 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
** //타임리프 사용 선언**
    

상품 목록


****
ID 상품명 가격 수량
회원id 상품명 10000 10
 


  • 타임리프
    • 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는 쿼리파라미터의 형태로 넘어간다.
Comments