Home SpringMVC 기본기능 이용한 간단한 상품 페이지
Post
Cancel

SpringMVC 기본기능 이용한 간단한 상품 페이지

요구 사항

  • 상품 도메인 모델
    • 상품 ID
    • 상품명
    • 가격
    • 수량
  • 상품 관리 기능
    • 상품 목록
    • 상품 상세
    • 상품 등록
    • 상품 수정

서비스 화면

서비스 제공 흐름

가정

  • 디자이너 : 요구사항에 맞도록 디자인하고, 디자인 결과물을 웹 퍼블리셔에게 넘겨준다.
  • 웹 퍼블리셔 : 디자이너에게 받은 디자인을 기반으로 HTML, CSS를 만들어 개발자에게 제공한다.
  • 백엔드 개발자 : 디자이너, 웹 퍼블리셔를 통해 HTML 화면이 나오기 전까지 시스템을 설계하고, 핵심 비즈니스 모델을 개발한다. 이후 HTML이 나오면 이 HTML을 뷰 템플릿으로 변환해 동적으로 화면을, 그리고 또 웹 화면의 흐름을 제어한다.

상품 도메인 개발

Item (상품 객체)

여기서는 @Data를 사용하는데 위험한 부분이 있다. @Getter, @Setter만 사용하는 것을 추천.

ItemRepository (상품 저장소)

실제로는 동시에 여러 스레드가 접근해 HashMap을 쓰게 되면 동시성 문제가 발생할 수 있다. ConcurrentHashMap을 사용하는 것이 좋다.

Long도 동시에 접근하면 값이 꼬일 수 있어 AtomicLong 등등을 사용하는 것이 좋다. (Thread-safe 하다.)

update()의 경우 updateParam의 객체를 따로 만드는 것이 원래는 더 좋다. Item의 id를 사용하지 않기 때문. (ex - ItemParameterDto) => 중복과 명확성 사이에 고민이 된다면 명확성을 우선시 해라.

얘는 테스트용.

ItemRepoTest

상품 서비스 HTML

부트스트랩을 사용한다.

../css/bootstrap.min.css 만 사용한다.

thymeleaf 템플릿으로 수정 전의 HTML을 /resources/static에 넣어둔다.

사실 이렇게 정적 리소스가 공개되는 /resources/static에 HTML을 넣어두면 실제 서비스에서도 공개된다. 서비스를 운영한다면 지금처럼 공개할 필요 없는 HTML을 두는 것은 주의해야 한다.

상품 목록 (+타임리프)

BasicItemController

  • @PostConstruct - 해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출된다.

여기서는 테스트 데이터를 넣어보기 위해 사용.

복습

@RequiredArgsConstructor는 final이 붙은 친구를 찾아 생성자로 주입시켜줌. final이 붙으면 필수로 초기화해줘야 하기 때문.

recommtoon 에서 했던 것은 필드 주입

1
2
@Autowired 
private ItemRepository itemRepository; 

이런식으로 썼는데 외부에서 변경이 불가능해 테스트가 어렵다는 단점이 있다. 따라서 사용하지 않는 것이 권장된다.

생성자 주입이 BEST

생성자 주입이 좋은 이유

타임리프의 핵심

  • ‘th:xxx’가 붙은 부분은 서버사이드에서 렌더링되고 기존의 것을 대체한다.
  • ‘th:xxx’가 없으면 기존 html의 xxx 속성이 그대로 사용된다.
  • HTML을 파일로 직접 열면 웹 브라우저는 th: 속성을 알지 못하므로 무시한다.
    • 따라서 HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.

타임리프 문법 1. 리터럴 대체

’ 이렇게 사용한다.
  • 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.
1
<span th:text = " 'Welcome to our application, ' + ${user.name} + '!'">

이러한 부분을 리터럴 대체 문법을 사용하면 아래와 같이 바꿀 수 있다.

1
<span th:text="|Welcome to our application, ${user.name}!|">
1
th:onclick = "|location.href='@{/basic/items/add}'|"

이런식으로 + 를 사용하지 않아도 쓸 수 있다.

타임리프 문법 2. 반복문

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

태그내에서 반복문을 쓸 수 있다.

타임리프 문법 3. 내용 변경 및 변수 표현식

1
<td th:text = "${item.price}"> 10000 </td>

item.price로 모델에 포함된 값을 조회하고, th:text로 내용을 변경할 수 있다.

타임리프 문법 4. 링크 표현

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

경로변수 뿐만 아니라 쿼리 파라미터도 생성 가능하다.

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

http://localhost:8080/basic/items/1?query=test

링크가 생성된다.

1
th:href="|@{/basic/items/${item.id}|}"

리터럴 대체로도 표현 가능하다.

상품 상세

BasicController에 추가.

상품 클릭하면 상세페이지로 넘어가는 구조.

상품 클릭했을 때 URL에 /[itemId] 붙음 -> 이 itemId @PathVariable 통해 가져와서 상품을 조회하고 모델이 담아 view로 전달.

상품 등록 폼

등록이 아니라 폼만 보여주는 것이기 때문에 view만 불러오면 된다.

html에서 form action의 링크를 th:action을 바꾸어 주고 PostMapping을 그에 맞게 작성하면 된다.

  • @RequestParam으로 하나하나 itemName, price 등등 받아 Model model에 넣어도 된다. 하지만 불편하다.
  • @ModelAttribute(“item”) Item item, Model model 로 사용해도 된다.
    • @ModelAttribute는 Model에 자동으로 객체를 넣어준다.
    • Model model 없애도 된다.
      • 이 모델은 view return 값인 basic/item 으로 넘어간다.
  • 그리고 @ModelAttribute에 이름 생략한 버전이 내가 쓴 코드이다.
    • 클래스 명을 사용하고, 모델에 저장될 때 클래스의 첫글자만 소문자로 변경해서 등록한다.
  • @ModelAttribute 도 생략 가능하다.

상품 수정

  • 리다이렉트
    • 수정은 마지막에 뷰 템플릿을 호출하는 대신 상품 상세 화면으로 이동하도록 리다이렉트를 호출한다.
    • 스프링은 “redirect:/…” 으로 편리하게 호출 가능하다.
    • 컨트롤러에 매핑된 @PathVariable 값은 redirect에서도 사용 가능하다.

복습

HTML Form 전송은 PUT, PATCH를 지원하지 않는다. GET, POST만 사용가능하다.

PUT, PATCH는 HTTP API 전송 시 사용한다.

PRG Post/Redirect/GET

위의 상품 등록 처리 컨트롤러는 심각한 문제가 있다.

상품 등록을 하고 계속 새로고침하면 계속 중록 등록된다.

GET으로 상품 등록 폼을 가져왔다.

그리고 값을 입력한 후 상품등록 버튼을 누르면 POST th:action /add로 간다.

그래서 연결되는 PostMapping된 컨트롤러를 보면 저장 후 “basic/item” 을 반환하는데 이는 아이템의 상세 페이지이다.

URL 뒤에 itemID가 없는데 등록한 값으로 보여지는 이유는 model을 basic/item으로 넘겼기 때문이다.

결과적으로 새로고침을 누르면 마지막에 한 행동을 다시 하는 것인데 이것이 POST이기 때문에 계속해서 등록이 되는 현상이 나타나는 것이다. 그래서 내용만 같고 ID만 ++되는 다른 상품 데이터가 계속 쌓이게 되는 것이다.

상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 리다이렉트를 호출해주면 된다. (/item/[id])

웹 브라우저는 리다이렉트의 영향으로 상품 저장 후에 실제 상품 상세화면으로 다시 이동한다.

따라서 마지막에 호출하는 내용이 추가한 상품 상세 화면인 GET /item/[id] 가 되는 것이다.

이후의 새로고침은 GET 으로 등록한 상품 상세화면이므로 문제가 없다.

주의

+item.getId()로 URL에 변수를 더해 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다.

따라서 RedirectAttributes를 사용해야 한다.

(recommtoon 했을 때 +로 했었다..)

RedirectAttributes

일단 위에서는 상품 등록을 하고 상세화면으로 리다이렉트하는 것 까지는 됐다. 하지만 고객입장에서 보면 등록을 제대로 한 건지 알 수 없다.

따라서 저장이 잘 되었다면 “저장되었습니다” 메시지를 보여주면 좋을 것 같다.

RedirectAttributes를 사용하면 URL 인코딩도 해주고, pathVariable, 쿼리파라미터까지 처리해준다.

  • redirectAttributes.addAttribute로 id값을 담아놨다. pathVariable 바인딩.
    • return redirect에 attributeName으로 직접 사용 가능하다.
  • return 값에 들어가지 못한 것은 쿼리파라미터로 처리된다.
    • 위의 링크를 보면 ?status=true로 된 것을 볼 수 있다.
1
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>

로 파라미터를 가져와 조건을 걸고 그 조건을 토대로 저장 완료를 띄울 수 있다.

당연히 리다이렉트되는 상품 상세페이지에 코드를 추가해야 한다.

물론 부트스트랩 등등 이용하면 팝업으로도 띄울 수 있다.

This post is licensed under CC BY 4.0 by the author.