실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 강의 - 인프런
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., 스프
www.inflearn.com
해당 강의는 Inflearn에 등록된 김영한님의 Spring Boot 강의입니다.
이번 시간에는 실제로 웹계층에 필요한 기능들을 개발하겠다.
1. 홈화면과 레이아웃
1. 홈 컨트럴로 등록
Controller
패키지에 HomeController
추가
@Controller
@Slf4j //롬복의 로그 기능
public class HomeController {
@RequestMapping("/") //첫번째화면
public String home(){
log.info("home controller");
return "home"; //home.html 파일로 찾아가서 타임리프 파일을 찾아가게된다.
}
}
@Slf4j -> 롬복의 로그 기능: log가 찍혔는지 확인
2. 타임리프 템플릿 등록
home.html
<!DOCTYPE HTML>
<!-- 타임리프 th -->
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header">
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<div class="jumbotron">
<h1>HELLO SHOP</h1>
<p class="lead">회원 기능</p>
<p>
<a class="btn btn-lg btn-secondary" href="/members/new">회원 가입</a>
<a class="btn btn-lg btn-secondary" href="/members">회원 목록</a>
</p>
<p class="lead">상품 기능</p>
<p>
<a class="btn btn-lg btn-dark" href="/items/new">상품 등록</a>
<a class="btn btn-lg btn-dark" href="/items">상품 목록</a>
</p>
<p class="lead">주문 기능</p>
<p>
<a class="btn btn-lg btn-info" href="/order">상품 주문</a>
<a class="btn btn-lg btn-info" href="/orders">주문 내역</a>
</p>
</div>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
<html 추가 설명>
btn,btn-lg,btn-secondary 클래스
: bootstrap css 프레임워크의 버튼 스타일
href=”/member/new”
: 해당 링크가 /member/new 경로로 이동함을 의미함
fragments/header.html
<!DOCTYPE html>
<!-- -->
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="header">
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink
to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/css/bootstrap.min.css" integrity="sha384
ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="/css/jumbotron-narrow.css" rel="stylesheet">
<title>Hello, world!</title>
</head>
fragments/bodyHeader.html
<!DOCTYPE html>
<!-- -->
<html xmlns:th="http://www.thymeleaf.org">
<div class="header" th:fragment="bodyHeader">
<ul class="nav nav-pills pull-right">
<li><a href="/">Home</a></li>
</ul>
<a href="/"><h3 class="text-muted">HELLO SHOP</h3></a>
</div>
fragments/footer.html
<!DOCTYPE html>
<!-- -->
<html xmlns:th="http://www.thymeleaf.org">
<div class="footer" th:fragment="footer">
<p>© Hello Shop V2</p>
</div>
main함수 실행 결과
3. Bootstrap으로 cs,js 다운받고, Css,js 적용
https://getbootstrap.com/docs/5.3/getting-started/download/
Download
Download Bootstrap to get the compiled CSS and JavaScript, source code, or include it with your favorite package managers like npm, RubyGems, and more.
getbootstrap.com
위의 사이트를 방문해서 다운로드(버전 상광없이 그냥 하시면됩니다.)
해당 파일 두개를 복붙해서 resources/static
하위에 css
, js
추가
안되면 직접 소스코드 파일안의 리소스파일의 static까지 가서 복붙 하면 된다.
jumbotron-narrow.css 파일
css에추가
/* Space out content a bit */
body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
padding-left: 15px;
padding-right: 15px;
}
/* Custom page header */
.header {
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
padding-bottom: 19px;
}
/* Custom page footer */
.footer {
padding-top: 19px;
color: #777; border-top: 1px solid #e5e5e5;
}
/* Customize container */
@media (min-width: 768px) {
.container {
max-width: 730px;
}
}
.container-narrow > hr {
margin: 30px 0;
}
/* Main marketing message and sign up button */
.jumbotron {
text-align: center;
border-bottom: 1px solid #e5e5e5;
}
.jumbotron .btn {
font-size: 21px;
padding: 14px 24px;
}
/* Supporting marketing content */
.marketing {
margin: 40px 0;
}
.marketing p + h4 {
margin-top: 28px;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
/* Remove the padding we set earlier */
.header,
.marketing,
.footer {
padding-left: 0;
padding-right: 0; }
/* Space out the masthead */
.header {
margin-bottom: 30px;
}
/* Remove the bottom border on the jumbotron for visual effect */
.jumbotron {
border-bottom: 0;
}
}
2. 회원 등록
폼 객체를 사용해서 화면 계층
과 서비스 계층
을 명확하게 분리
한다.
회원 등록 폼 객체
@Getter @Setter
public class MemberForm {
@NotEmpty(message ="회원 이름은 필수 입니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
Validation-> 검증 (NotEmpty annotation)
회원 등록 컨트롤러
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/members/new")
public String createForm(Model model)
{
//memberForm이라는 이름으로 MemberForm의 데이터를 넘긴다.
model.addAttribute("memberForm",new MemberForm());
return "members/createMemberForm";
}
@PostMapping("/members/new")
public String create(@Valid MemberForm form, BindingResult result) //사용자가 입력한 form 객체를 전달받음.
{
//BindingResult result 을 통해 에러을 받아서 처리함
if(result.hasErrors())
{
return "members/createMemberForm"; //해당 폼으로 다시 제출
}
Address address=new Address(form.getCity(),form.getStreet(),form.getZipcode());
//회원 정보 저장 객체 생성
Member member=new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member); //멤버 아이디
//★★재로딩을 막기위해 redirect로 폼에 보냄★★
return "redirect:/"; //첫번째로 페이지로 넘어감
}
}
회원 등록 폼 화면( html templates/members/createMemberForm.html)
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<style>
.fieldError {
border-color: #bd2130;
}
</style>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/> <!--바디 include-->
<!--members/new에 memberForm을 post-->
<form role="form" action="/members/new" th:object="${memberForm}" method="post">
<div class="form-group">
<label th:for="name">이름</label>
<!-- 이름 입력 필드 -->
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요"
th:class="${#fields.hasErrors('name')} ? 'form-control fieldError' : 'form-control'">
<!-- ★★★이름 입력 필드의 오류 메시지 ★★★-->
<p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Incorrect date</p>
</div>
<div class="form-group">
<label th:for="city">도시</label>
<!-- 도시 입력 필드 th:field="*{city}"-> id="city",name="city"로 자동 랜더링을 함-->
<input type="text" th:field="*{city}" class="form-control" placeholder="도시를 입력하세요">
</div>
<div class="form-group">
<label th:for="street">거리</label>
<!-- 거리 입력 필드 -->
<input type="text" th:field="*{street}" class="form-control" placeholder="거리를 입력하세요">
</div>
<div class="form-group">
<label th:for="zipcode">우편번호</label>
<!-- 우편번호 입력 필드 -->
<input type="text" th:field="*{zipcode}" class="form-control" placeholder="우편번호를 입력하세요">
</div>
<!-- 회원 가입 버튼 -->
<br/>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br/>
<!-- 페이지 하단의 푸터 부분 -->
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
Members/new 실행 결과
회원 등록 데베에 저장되는지 확인
회원 이름 없이 저장시 에러(Validation
기능)
BindingResult
**를 통해 에러처리(MemberController
)**
빨간색으로 테두리 처리되며 오류 처리
3. 회원 목록 조회
회원 목록을 보도록 회원 컨트롤러에 추가
//form 객체가 아닌 단순 엔티티를 받아서 모델에 넣음
@GetMapping("/members")
public String list(Model model)
{
List<Member> members= memberService.findAllmembers(); //데베에서 모든 멤버들을 List형식으로 갖고옴
model.addAttribute("members",members); //model에 위의 회원 리스트를 넣음
return "members/memberList"; //meberList.html 파일로 처리
}
회원 목록 뷰(html templates/members/memberList.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>이름</th>
<th>도시</th>
<th>주소</th>
<th>우편번호</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
<td th:text="${member.address?.city}"></td> <!--?: 널이면 처리-->
<td th:text="${member.address?.street}"></td>
<td th:text="${member.address?.zipcode}"></td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
+a 타임리프에서 ?를 사용하면 null 을 무시한다.
요구사항이 정말 단순할 때는 폼 객체( MemberForm
) 없이 엔티티( Member
)를 직접 등록과 수정 화면에 서 사용해도 된다. 하지만 화면 요구사항이 복잡해지기 시작하면, 엔티티에 화면을 처리하기 위한 기능이 점점 증가한다. 결과적으로 엔티티는 점점 화면에 종속적으로 변하고, 이렇게 화면 기능 때문에 지저분해진 엔티티는 결 국 유지보수하기 어려워진다. 실무에서 엔티티는 핵심 비즈니스 로직만 가지고 있고, 화면을 위한 로직은 없어야 한다.
화면이나 API에 맞는 폼 객체나 DTO를 사용하자. 그래서 화면이나 API 요구사항을 이것들로 처리하고, 엔티티는 최대한 순수하게 유지 하자. (DTO는 JPA 활용편2에서 설명하겠습니다.)
단, API를 만들때는 엔티티를 웹으로 반환하지 말 것. DTO형식으로 제출할것. 타임리프는 서버사이드에서 랜더링하는 것이므로 괜찮다.
실행결과
4. 상품 등록
ItemService
@Service
@Transactional(readOnly = true) //기본으로 읽기 전용 트랜잭션으로 두기
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
//비즈니스 로직 기능: 상품 등록,상품 단건,목록 조회
//1. 상품 등록
@Transactional
public void saveItem(Item item){
itemRepository.save(item);
}
//변경감지 기능 사용(transaction안의 flush를 통해 dirty checking 함으로써 업데이트 쿼리를 전송함)
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item findItem = itemRepository.findOne(itemId);//같은 엔티티를 조회한다.
findItem.setPrice(price); //데이터를 수정한다.
findItem.setName(name); //데이터를 수정한다.
findItem.setStockQuantity(stockQuantity); //데이터를 수정한다.
//findItem.change(price,name,stockQuantity); ->setter사용보단 이걸로 하는게 낫긴하다.
}
//2, 상품 단건 조회
public Item findOne(Long itemId){
return itemRepository.findOne(itemId);
}
//3, 상품 목록 조회
public List<Item> findItems()
{
return itemRepository.findItems();
}
}
ItemRepository
@Repository
@RequiredArgsConstructor
public class ItemRepository {
//★★스프링이 엔티티 매니저 생성 후 영속성 컨테스트(PersistenceContext)에 주입
private final EntityManager em;
//기능: 상품 등록, 상품 목록 조회, 상품 수정(이것은 도메인에 넣어놓았음...setter와 비슷하므로)
//상품 등록
public void save(Item item)
{
if(item.getId()==null)
{
//저장해논게 없으면 저장
em.persist(item);
}
else {
//★저장한게 있으면 병합해서 업데이트
em.merge(item);
}
}
//상품 단건 조회
public Item findOne(Long id){
return em.find(Item.class,id);
}
//상품 목록(전체) 조회
public List<Item> findItems(){
return em.createQuery("select i from Item as i",Item.class)
.getResultList();
}
}
상품 등록 폼
@Getter @Setter
public class BookForm {
private Long id;
//상품의 공통 속성
private String name;
private int price;
private int stockQuantity;
//책의 특별 속성
private String author;
private String isbn;
}
강의에서는 책만 상품으로 등록하게 하였다.
상품 등록 컨트롤러
package jpabook.jpashop.controller;
import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("/items/new")
public String createForm(Model model) {
model.addAttribute("form", new BookForm());
return "items/createItemForm";
}
@PostMapping("/items/new")
public String create(BookForm form) {
Book book = new Book();
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book); // 아이템 저장(merge를 사용함.)
return "redirect:/items"; //책목록으로 리다이렉트
}
}
상품 등록 뷰
(html items/createItemForm.html )
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<form th:action="@{/items/new}" th:object="${form}" method="post">
<div class="form-group">
<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요">
</div>
<div class="form-group">
<label th:for="price">가격</label>
<input type="number" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
</div>
<div class="form-group">
<label th:for="stockQuantity">수량</label>
<input type="number" th:field="*{stockQuantity}" class="form-control" placeholder="수량을 입력하세요">
</div>
<div class="form-group">
<label th:for="author">저자</label>
<input type="text" th:field="*{author}" class="form-control" placeholder="저자를 입력하세요">
</div>
<div class="form-group">
<label th:for="isbn">ISBN</label>
<input type="text" th:field="*{isbn}" class="form-control" placeholder="ISBN을 입력하세요">
</div>
<br/>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br/>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
상품등록 화면
Db 저장확인
5. 상품 목록
상품 컨트롤러
에 목록 추가
//상품 목록
@GetMapping("/items")
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "/items/itemList";
}
상품 목록 뷰(html items/itemList.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>상품명</th>
<th>가격</th>
<th>재고수량</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td th:text="${item.id}"></td>
<td th:text="${item.name}"></td>
<td th:text="${item.price}"></td>
<td th:text="${item.stockQuantity}"></td>
<td>
<!--수정 버튼: items/{아이템의 id#}/edit->수정 폼으로 이동 -->
<a href="#" th:href="@{/items/{id}/edit (id=${item.id})}"
class="btn btn-primary" role="button">수정</a>
</td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
</html>
model 에 담아둔 상품 목록인 items
를 꺼내서 상품 정보를 출력
결과
6. 상품 수정
상품 컨트롤러
에 수정 로직 추가
//★★★★상품 수정★★★★
//1. 상품 수정 폼(get 처리)
@GetMapping("items/{itemId}/edit") //Items의 수정할 itemId를 받아서 사용(@PathVariable)
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model) {
//수정 할(넘겨 받은 itemId) 책 객체 생성
Book item = (Book) itemService.findOne(itemId);
//Form에 세팅한 후 이 form을 추후에 post함.
BookForm form = new BookForm();
form.setId(item.getId());
form.setName(item.getName());
form.setPrice(item.getPrice());
form.setStockQuantity(item.getStockQuantity());
form.setAuthor(item.getAuthor());
form.setIsbn(item.getIsbn());
//모델에 폼 속성 추가
model.addAttribute("form", form); //BookForm
return "items/updateItemForm";
}
/**
* 2. 상품 수정 (Post 처리)..merge 방식으로 수정
@PostMapping("items/{itemId}/edit")
//@ModelAttribute("form"):요청 바디에서 "form" 속성을 추출하여 BookForm 객체로 바인딩.
public String updateItem(@ModelAttribute("form") BookForm form) {
//form을 book으로 바꿈.
Book book = new Book(); //준영속성 상태-> db에 갔다온 객체..식별자가 존재
book.setId(form.getId());
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book); //form의 값들을 book 객체에 다시 담고, DB에 재저장
return "redirect:/items"; // 수정 후 아이템 목록으로 다시이동
}
*/
상품 수정 폼 화면(html items/updateItemForm)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<form th:object="${form}" method="post">
<!-- id -->
<input type="hidden" th:field="*{id}" />
<div class="form-group">
<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요" />
</div>
<div class="form-group">
<label th:for="price">가격</label>
<input type="number" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요" />
</div>
<div class="form-group">
<label th:for="stockQuantity">수량</label>
<input type="number" th:field="*{stockQuantity}" class="form-control" placeholder="수량을 입력하세요" />
</div>
<div class="form-group">
<label th:for="author">저자</label>
<input type="text" th:field="*{author}" class="form-control" placeholder="저자를 입력하세요" />
</div>
<div class="form-group">
<label th:for="isbn">ISBN</label>
<input type="text" th:field="*{isbn}" class="form-control" placeholder="ISBN을 입력하세요" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
위의 콘트롤러에 수정 코드는 get,post 둘중에 하나의 방식으로 처리했다.
GET,POST
<**수정 결과>**
1. 상품 목록에서 수정 버튼 클릭 시 해당 상품 아이디의 속성들을 불러옴
2. 상품 수정
3. 수정 확인
4. db**에서 수정됐는지 확인**
수정시, 사용자가 수정가능한 권한이 있는지 코드를 넣는게 정석이다.
즉, 로그인시 관리자 회원으로 로그인으로 하면 된다.
7. 변경 감지와 병합(merge)
준영속 엔티티를 수정하는 2가지 방법
1. 변경 감지 기능 사용
2. 병합(merge) 사용
1. 변경 감지 기능 사용(ItemController)
위에서 수정하는 방법 2가지를 사용했다. 다음은 변경감지기능을 이용하여 수정하는 방식이다.
/**
* 3. 상품 수정 (Post 처리)..변경감지 방식으로 수정
*/
@PostMapping("items/{itemId}/edit")
//@ModelAttribute("form"):요청 바디에서 "form" 속성을 추출하여 BookForm 객체로 바인딩.
public String updateItem(@PathVariable("itemId") Long itemId, @ModelAttribute("form") BookForm form) {
itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());
return "redirect:/items"; // 수정 후 아이템 목록으로 다시이동
}
itemService
에 다음 코드 추가
//변경감지 기능 사용(transaction안의 flush를 통해 dirty checking 함으로써 업데이트 쿼리를 전송함)
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item findItem = itemRepository.findOne(itemId);//같은 엔티티를 조회한다.
findItem.setPrice(price); //데이터를 수정한다.
findItem.setName(name); //데이터를 수정한다.
findItem.setStockQuantity(stockQuantity); //데이터를 수정한다.
//findItem.change(price,name,stockQuantity); ->setter사용보단 이걸로 하는게 낫긴하다.
}
영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법
트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택 트랜잭션 커밋 시점에 변경 감지(Dirty Checking)이 동작해서 데이터베이스에 UPDATE SQL 실행.
병합 동작 방식을 간단히 정리
병합 주의 할 점.
사용자가 실수로 null값을 입력해도 그 값으로 업데이트 가능함.
따라서, 병합 보단 변경감지(dirty-checking)로 setting을 하는게 좋다.
8. 상품 주문
OrderController
package jpabook.jpashop.controller;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.repository.OrderSearch;
import jpabook.jpashop.service.ItemService;
import jpabook.jpashop.service.MemberService;
import jpabook.jpashop.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final MemberService memberService;
private final ItemService itemService;
//orderForm 생성
@GetMapping("/order")
public String createForm(Model model)
{
List<Member> members=memberService.findAllmembers();
List<Item> items=itemService.findItems();
model.addAttribute("members",members);
model.addAttribute("items",items);
return "order/orderForm";
}
//@RequestParam : 넘겨 받은 {값}
@PostMapping("/order")
public String order(@RequestParam("memberId") Long memberId ,
@RequestParam("itemId") Long itemId ,
@RequestParam("count") int count)
{
orderService.order(memberId,itemId,count);
return "redirect:/orders";
}
}
코드 설명
주문 폼으로 이동
메인 화면에서 상품 주문을 선택하면 /order
를 GET
방식으로 호출
OrderController
의 createForm()
메서드 실행
주문 화면에는 주문할 고객정보와 상품 정보가 필요하므로 model
객체에 담아서 뷰
에 넘겨줌.
주문 실행
주문할 회원과 상품 그리고 수량을 선택해서 Submit
버튼을 누르면 URL
을 POST
방식으로 호출.
컨트롤러의 order()
메서드를 실행
주문이 끝나면 상품 내역이 있는 /orders
URL로 리다이렉트
(url 변경)
주문 수량이 더 많으면 에러가 나야한다. 다음은 주문 수량 초과에 대한 오류 처리 확인이다.
(따로 NotEnoughStockException 클래스를 만들어서 처리하였음.)
상품 주문 폼
order/orderForm
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<form role="form" action="/order" method="post">
<div class="form-group">
<label for="member">주문회원</label>
<select name="memberId" id="member" class="form-control">
<option value="">회원선택</option>
<option th:each="member : ${members}"
th:value="${member.id}"
th:text="${member.name}" />
</select>
</div>
<div class="form-group">
<label for="item">상품명</label>
<select name="itemId" id="item" class="form-control">
<option value="">상품선택</option>
<option th:each="item : ${items}"
th:value="${item.id}"
th:text="${item.name}" />
</select>
</div>
<div class="form-group">
<label for="count">주문수량</label>
<input type="number" name="count" class="form-control" id="count" placeholder="주문 수량을 입력하세요">
</div>
<br/>
<button type="submit" class="btn btn-primary">Submit</button>
<br/>
</form>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
8.1 주문 실행시 GET 방식이 아닌 POST 방식으로 호출 하는 이유
- 보안: POST 방식은 데이터를 HTTP 메시지의 본문(Body)에 담아서 전송_한다. 이 방식은 GET 방식처럼 URL에 데이터를 포함시키지 않기 때문에, 민감한 정보가 _URL에 노출되지 않아 보안성이 더 높다. 예를 들어, 패스워드나 개인 정보를 전송할 때 POST 방식을 사용하는 것이 좋다.
- 데이터 크기: POST 방식은 전송할 수 있는 데이터의 크기에 거의 제한이 없다. 이는 큰 데이터를 서버로 전송해야 할 경우, 예를 들어 파일 업로드나 큰 양의 폼 데이터를 처리할 때 유리하다. 반면, GET 방식은 URL 길이에 제한이 있어서 큰 데이터를 전송하기에는 부적합하다.
- 서버 상태 변화: 일반적으로 웹에서 어떤 작업을 수행하여 _서버의 데이터나 상태를 변경해야 할 때 POST 방식을 사용_해. 예를 들어, 새로운 데이터를 생성하거나 기존 데이터를 수정하는 경우에 해당한다. POST는 이러한 작업을 안전하게 처리하기 위한 목적으로 설계되었기 때문에, 서버의 상태를 변경하는 모든 작업에 적합하디.
- 캐싱 방지: POST 요청은 일반적으로 캐싱되지 않다. 이는 서버의 상태를 변경할 수 있는 요청이 캐시되는 것을 방지하여, 의도치 않은 데이터 변경이나 보안 문제를 예방해준다. (캐싱은 자주 사용되는 데이터를 임시 저장소에 보관해두는 기술, 서버로부터 데이터를 매번 받아오는 대신, 캐시에서 빠르게 데이터를 가져올 수 있다. 하지만 민감함 정보는 캐싱을 하면 누군가가 도용가능하기에 캐싱 기술로 적용하지않는다. 간단하게 POST 요청으로 처리하자. )
9. 주문 목록 검색 및 취소
**OrderController
에 주문 목록 보기,주문 취소 메서드 추가**
//주문 목록(OrderSearch에서 검색할 변수들 선언)
@GetMapping("/orders")
public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model)
{
List<Order> orders = orderService.findOrders(orderSearch);
model.addAttribute("orders",orders);
return "order/orderList"; //orderList 파일(view)에 뿌림
}
//주문 취소
@PostMapping("/orders/{orderId}/cancel")
public String cancelOrder(@PathVariable("orderId") Long orderId)
{
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
OrderService
에 주문 내역 조회 메서드 추가
// 3. 주문 내역 조회....추후 강의 내용
public List<Order> findOrders(OrderSearch orderSearch)
{
return orderRepository.findAllByCriteria(orderSearch);
}
**OrderSearch**
회원명,회원 상태를 repository 패키지에 추가
@Getter @Setter
public class OrderSearch {
//주문 검색...회원명, 회원상태를 통해 검색
private String memberName;
private OrderStatus orderStatus;
}
주문 목록 검색 화면
( order/orderList
)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<div>
<div>
<!--콘트롤러에 modelAttribute에 담은 오브젝트 orderSearch-->
<form th:object="${orderSearch}" class="form-inline">
<div class="form-group mb-2">
<input type="text" th:field="*{memberName}" class="form-control" placeholder="회원명"/>
</div>
<div class="form-group mx-sm-1 mb-2">
<select th:field="*{orderStatus}" class="form-control">
<option value="">주문상태</option>
<option th:each="status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
th:value="${status}"
th:text="${status}">option
</option>
</select>
</div>
<button type="submit" class="btn btn-primary mb-2">검색</button>
</form>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>회원명</th>
<th>대표상품 이름</th>
<th>대표상품 주문가격</th>
<th>대표상품 주문수량</th>
<th>상태</th>
<th>일시</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${orders}">
<td th:text="${item.id}"></td>
<td th:text="${item.member.name}"></td>
<td th:text="${item.orderItems[0].item.name}"></td>
<td th:text="${item.orderItems[0].orderPrice}"></td>
<td th:text="${item.orderItems[0].count}"></td>
<td th:text="${item.status}"></td>
<td th:text="${item.orderDate}"></td>
<td>
<a th:if="${item.status.name() == 'ORDER'}" href="#"
th:href="'javascript:cancel('+${item.id}+')'" class="btn btn-danger">CANCEL</a>
</td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
<script>
function cancel(id) {
var form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", "/orders/" + id + "/cancel");
document.body.appendChild(form);
form.submit();
}
</script>
</html>
9.1 orderList.html 오류
이것은 제가 처음 작성했을때부터 패키지를 jpabook.jpabook으로 잘못 설정해서 난 오류입니다. 제가 포스팅 내용들은 정상적으로 했으니 걱정하지마시고 참고만 해주시길 바랍니다.
ParseException 에러
패키지를 잘못 적어서 오류가 났음을 확인했다.
Orderstatus**의 패키지위치 확인 후 고침**
10. 최종 프로젝트 작동 확인
회원등록
회원 목록
상품 등록
상품 목록
상품 수정
주문 목록
주문 검색
주문 취소
주문 취소시 재고수량 증가 확인
DB에서 확인
11. 페이지 이동 및 리디렉션 실행 예시
초기 상태: http://localhost:8080/
이동: http://localhost:8080/members
이동: http://localhost:8080/members/new
이동: http://localhost:8080/members/login
이 상태에서 뒤로가기를 하면 다음과 같이 진행됩니다:
뒤로가기: http://localhost:8080/members/new
뒤로가기: http://localhost:8080/members
뒤로가기: http://localhost:8080/
그렇다면 http://localhost:8080/members/login에서 http://localhost:8080/members로 리디렉션이 발생했을 때 어떻게 되는지 살펴보겠습니다.
리디렉션이 발생하면 브라우저는 새로운 페이지로 이동하며, 이 새로운 페이지도 탐색 스택에 추가됩니다.
예를 들어, http://localhost:8080/members/login에서 http://localhost:8080/members로 리디렉션된 경우:
이동: http://localhost:8080/members/login
리디렉션: http://localhost:8080/members
이 상태에서 뒤로가기를 하면 다음과 같이 진행됩니다:
<탐색 스택>
초기 상태: http://localhost:8080/
이동: http://localhost:8080/members
이동: http://localhost:8080/members/new
이동: http://localhost:8080/members/login
리디렉션: http://localhost:8080/members
뒤로가기: http://localhost:8080/members/login
뒤로가기: http://localhost:8080/members/new
뒤로가기: http://localhost:8080/members
뒤로가기: http://localhost:8080/
'Spring Boot > Spring Boot JPA-활용편1 강의 정리' 카테고리의 다른 글
[SpringBoot-JPA 활용편1] 자바 ORM 표준 JPA 프로그래밍 - 활용편1: 6. 주문 도메인 개발 (1) | 2024.02.27 |
---|---|
[SpringBoot-JPA 활용편1] 자바 ORM 표준 JPA 프로그래밍 - 활용편1: 5. 상품 도메인 개발 (1) | 2024.02.26 |
[SpringBoot-JPA 활용편1] 자바 ORM 표준 JPA 프로그래밍 - 활용편1: 4. 회원 도메인 개발 (1) | 2024.02.26 |
[SpringBoot-JPA 활용편1] 자바 ORM 표준 JPA 프로그래밍 - 활용편1: 3. 애플리케이션 구현 준비 (0) | 2024.02.26 |
[SpringBoot-JPA 활용편1] 자바 ORM 표준 JPA 프로그래밍 - 활용편1: 2. 도메인 분석 설계 (1) | 2024.02.26 |