본문 바로가기
Dev/Spring Boot

[실전! 스프링 부트와 JPA 활용1] 7. 웹 계층 개발 정리

by ulmu 2024. 3. 12.

스프링 부트 타임리프 기본 설정

spring:
 thymeleaf:
 	prefix: classpath:/templates/
 	suffix: .html
  • resources: templates/ + {ViewName} + .html
  • resources: templates/home.html

 

 

 

Model 문법

@GetMapping(value = "/members/new")
 public String createForm(Model model) {
 	model.addAttribute("memberForm", new MemberForm());
 	return "members/createMemberForm";
 }
  • Model: 스프링 MVC에서 컨트롤러와 뷰 사이의 데이터 전송을 담당하는 인터페이스
  • model.addAttribute("memberForm", new MemberForm())
    : MemberForm 객체 생성 - 모델에 "memberForm"이라는 이름으로 추가 - 모델에 추가된 데이터는 뷰에서 사용 가능
  • 여기서 memberForm은 사용자 입력을 받기 위한 폼 데이터를 임시로 저장하는 객체
//createMemberForm.html
<form role="form" action="/members/new" th:object="${memberForm}" method="post">
  • <form> 태그는 웹 페이지에서 사용자 입력을 수집하기 위해 사용
  • submit(제출)시, 앞서 생성된 memberForm에 맞게 사용자가 입력한 데이터를 /members/new URLdmf  Post 방식으로 요청 

 

 

 

BindingResult

@PostMapping(value = "/members/new")
 public String create(@Valid MemberForm form, BindingResult result) {
 	if (result.hasErrors()) {
 	return "members/createMemberForm";
 }
  • @Valid 주석을 통해 검증 -> 결과(오류 포함)는 BindingResult 객체에 저장
  • 메소드 파라미터에서 @Valid로 검증된 객체 바로 뒤에 위치해야 합니다.

 

 

 

폼 객체를 사용하는 이유

  • 엔티티에 화면을 위한 로직까지 전부 포함시키면 유지보수 hard
  • 엔티티는 반드시 핵심 비즈니스 로직만 O
  • 화면을 위한 로직은 X -> 폼 객체/DTO로 대체

 

 

 

상품 수정 페이지 API

@GetMapping("/{itemId}/edit")
    public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){
       Optional<Item> item = itemService.findOne(itemId);

       if(item.isPresent()){
           Book book = (Book) item.get();

           BookForm form = new BookForm();
           form.setId(book.getId());
           form.setAuthor(book.getAuthor());
           form.setIsbn(book.getIsbn());
           form.setName(book.getName());
           form.setStockQuantity(book.getStockQuantity());
           form.setPrice(book.getPrice());

           model.addAttribute("form", form);
           return "items/updateItemForm";
       }
        return "redirect:/items";
    }
  • 우리가 일상적으로 사용하는 수정 페이지를 보면, 이전에 적었던 정보가 노출되어있다.
  • 위 코드도 이전에 작성한 정보를 노출하고자 form(Bookform 객체)를 새로 만드록 model.addAttribute()하는 거임

 

 

 

엔티티 생명주기

1) 비영속(new/transient): 영속성 컨텍스트와 전혀 관계없는 새로운 상태

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

 

2) 영속(managed): 영속성 컨텍스트에 관리되는 상태 - 주로 entity 저장이나 조회를 통해

....
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction()
tx.begin();

em.persist(member);

 

3) 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태

  • em.detach(entity): 특정 엔티티만 준영속 상태로 전환
  • em.clear(): 영속성 컨텍스트 완전 초기화 - 테스트 케이스 작성시 쿼리 보고싶을 때 활용
  • em.close(): 영속성 컨텍스트 종료

4) 삭제(removed) - 실제 DB에서 지우겠어!

 

 

 

준영속 엔티티의 수정 방법

@PostMapping("/{itemId}/edit")
    public String updateItem(@PathVariable("itemId") Long itemId, @ModelAttribute("form") BookForm form){
       
       Book book = new Book();
        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);

        return "redirect:/items";
    }
  • 여기서 book은 준영속 엔티티(영속성 컨텍스트에 저장되었다가 분리된 상태)
  • 준영속 엔티티를 수정하는 2가지 방법: 변경 감지 기능 사용/병합 -> BUT 엔티티 변경 시, 항상 변경 감지 사용
  • 변경 감지 기능: 특정 속성(필드)만 변경 가능 / 식별자를 활용해 DB에 있는 엔티티를 가져온 다음 해당 필드만 변경
  • 병합(merge): 모든 속성(필드) 변경 / 식별자를 활용해 DB에 있는 해당 엔티티를 통으로 수정 후 영속화
  • 실무에서는 변경 감지 기능을 활용 -> 실무에서 모든 필드를 다 변경하게 두지 않기 때문 & 병합시 값 없으면 null로 업데이트 되버림 

 

 

엔티티 변경할 때 유의점

  • Controller 계층에서 어설프게 엔티티 생성X
  • 트랜잭션이 있는 서비스 계층에서 식별자(id)와 변경할 데이터를 parmeter나 dto 형태로 명확하게 전달
  • 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티 조회, 엔티티 데이터 직접 변경
  • 트랜젝션 커밋 시점에 변경 감지가 실행

 

 

주문 조회(Repository 계층)

 public List<Order> findAllByString(OrderSearch orderSearch) {

        String jpql = "select o from Order o where o.member m";
        Boolean isFirstCondition = true;


        if(orderSearch.getOrderStatus() != null){
            if(isFirstCondition){
                jpql += "where ";
                isFirstCondition = false;
            } else {
                jpql += " and";
            }
            jpql += " o.status = :status";
        }

        if(StringUtils.hasText(orderSearch.getMemberName())){
            if(isFirstCondition){
                jpql += " where";
                isFirstCondition = false;
            } else {
                jpql += " and";
            }
            jpql += " m.name like :name";
        }

        TypedQuery<Order> query = em.createQuery(jpql, Order.class)
                .setMaxResults(1000);

        if(orderSearch.getOrderStatus() != null) {
            query.setParameter("status", orderSearch.getOrderStatus());
        }

        if(orderSearch.getMemberName() != null){
            query.setParameter("name", orderSearch.getMemberName());
        }

        return query.getResultList();
    }
  • StringUtils.hasText(): 주어진 문자열(String)이 실제 텍스트 포함하는지 확인
    -> 문자열이
    null이 아니며, 길이가 0보다 크고, 하나 이상의 비공백 문자를 포함하고 있을 때 true 반환
  • em.createQuery : 쿼리 만들기
  • .setParameter() : parameter 바인딩하기
  • .getResultList() : 해당 쿼리 날려서 결과 반환하기

 

 

header & footer 설정(feat.thymeleaf 기초 문법)

<head th:replace="fragments/header :: header">
...
<div th:replace="fragments/footer :: footer" />
  • th:replace 속성: 지정된 템플릿의 일부를 현재 템플릿의 해당 부분으로 대체합니다.
  • fragments/header: 대체할 템플릿 파일의 경로 지정
  • :: header: 대체할 템플릿 파일 내에서 특정 이름의 템플릿 조각을 참조

 

 

 

상품 수정 페이지 HTML

<form th:object="${form}" method="post">
  • action 속성이 생략되어 있을 때, 폼은 현재 페이지의 URL로 데이터를 제출
  • GetMapping과 PostMapping의 URI:/{itemId}/edit 가 같음

 

 

 

Thymeleaf 문법

<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요">
  • <label> : 특정 폼 필드와 연관되어 있음을 나타냄
  • th:for="name" : "name"이라는 필드와 연관되어있다
  • th:field="*{name}" : 폼 필드와 객체의 속성(name)을 바인딩

 

 

 

회원 목록 뷰

<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>
  • <tr th:each="member : ${members}">: members 컬렉션(예: List, Set 등)에서 각 member 객체를 반복하여 표의 행 생성
  • ?. 연산자(Safe Navigation Operator 또는 Elvis Operator) : address null인 경우 NullPointerException을 방지

 

 

 

Redirect

@PostMapping("/cancel/{orderId}")
public String cancel(@PathVariable("orderId") Long orderId){
    orderService.cancelOrder(orderId);
    return "redirect:/orders";
}
  • "redirect:/orders" 에서 /orders는 html 파일이 아니라 "/orders를 처리하는 다른 컨트롤러 메서드로 연결"

 

 

HomeController 관련 주의점

@Controller("/order")
@RequiredArgsConstructor
public class OrderController {

	...

    @GetMapping
    public String createForm(Model model){
        List<Member> memberList = memberService.findMemberList();
        List<Item> itemList = itemService.findAll();

        model.addAttribute("members", memberList);
        model.addAttribute("items", itemList);

        return "order/orderForm";
    }
  • 이렇게 작성하면 Spring이 homeController랑 헷갈려서 잘못 매핑할 수 있으므로 주의
  • 위와 같은 경우는 맨 위 @Controller("/order")가 아니라, 각 API 마다 @GetMapping("/order")를 붙이는 게 좋음

'Dev > Spring Boot' 카테고리의 다른 글

[실전! 스프링 데이터 JPA] ch 1 ~ ch 4요약정리  (0) 2024.03.14

댓글