(실전적인 전투! Spring Boot와 JPA 1 활용 – 웹 애플리케이션 개발) 4. 웹 레이어 개발

관행!
Spring Boot 및 JPA1 사용 – 웹 애플리케이션 개발 – 인프라 | 프레젠테이션

실용적인 예로 Spring Boot 및 JPA를 사용하여 웹 애플리케이션을 설계하고 개발합니다.

이 과정을 통해 Spring Boot와 JPA를 실제로 사용하는 방법을 배울 수 있습니다.

www.inflearn.com


홈 화면 및 레이아웃

홈 컨트롤러 등록

@Controller
@Slf4j
public class HomeController {

    @RequestMapping("/")
    public String home() {
        log.info("home controller");
        return "home";
    }
}


홈 화면

가입하기

화면과 서비스 레이어를 명확하게 구분하기 위해 폼 객체를 사용합니다.

회원 등록 양식 객체

@Getter @Setter
public class MemberForm {

    @NotEmpty(message = "회원 이름은 필수입니다.

") private String name; private String city; private String street; private String zipcode; }

회원 등록 관리자

@Controller
@RequiredArgsConstructor
public class MemberController {
	
	private final MemberService memberService;
	
	@GetMapping("/members/new")
	public String createForm(Model model) {
		model.addAttribute("memberForm", new MemberForm());
		
		return "members/createMemberForm";
	}
	
	@PostMapping("/members/new")
	public String create(@Valid MemberForm 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);
		
		return "redirect:/";
	}
}


회원가입 화면

회원 목록 쿼리

구성원 목록 컨트롤러 추가

@Controller
@RequiredArgsConstructor
public class MemberController {

    //...

	@GetMapping("/members")
	public String list(Model model) {
		List<Member> members = memberService.findMembers();
		model.addAttribute("members", members);
		
		return "members/memberList";
	}
}
  • 검색된 상품을 Spring MVC에서 제공하는 Model 객체에 저장하여 View에 전달합니다.

  • 실행할 보기의 이름을 반환합니다.


회원 목록 화면

참고: 양식 개체 및 엔터티를 직접 사용
참고: 요구 사항이 매우 간단한 경우 등록 및 편집 화면에서 양식 개체( MemberForm ) 없이 엔터티( Member )를 직접 사용할 수 있습니다.

그러나 화면 요구 사항이 더 복잡해짐에 따라 엔터티는 화면을 처리할 수 있는 기능이 점점 더 많아집니다.

이렇게 하면 엔티티가 화면에 점점 더 의존하게 되고 화면 기능이 지저분해지는 엔티티는 결국 유지 관리할 수 없게 됩니다.


실제로 엔터티에는 핵심 비즈니스 논리만 있어야 하며 화면 논리는 없어야 합니다.

화면이나 API에 맞는 양식 개체 또는 DTO를 사용하십시오. 따라서 이를 사용하여 화면 또는 API 요구 사항을 처리하고 엔터티를 가능한 한 순수하게 유지하십시오.

제품 등록

제품 등록 컨트롤러

@Controller
@RequiredArgsConstructor
public class ItemController {
	
	private final ItemService itemService;
	
	@GetMapping("/items/new")
	public String createForm(Model model) {
		model.addAttribute("form", new Book());
		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);
        
        return "redirect:/items";
	}
}
  • 제품 등록 양식에 데이터를 입력하고 제출 버튼을 클릭하면 POST 방식으로 /items/new가 요청됩니다.

  • 상품 저장 후 상품 목록 화면으로 이동 ( redirect:/items )


제품 등록 화면

상품 목록

@Controller
@RequiredArgsConstructor
public class ItemController {

    //...

    @GetMapping("/items")
    public String list(Model model) {
        List<Item> items = itemService.findItems();
        model.addAttribute("items", items);
        return "items/itemList";
    }
}
  • 아이템을 꺼내서 상품 정보를 출력, 아이템은 모델에 저장된 상품 목록입니다.


상품 목록 화면

제품 수정

제품 수정 관련 컨트롤러 코드

@Controller
@RequiredArgsConstructor
public class ItemController {
	
	// ...
    
    @GetMapping("/items/{itemId}/edit")
    public String uodateItemForm(@PathVariable("itemId") Long itemId, Model model) {
    	Book item = (Book) itemService.findOne(itemId);
    	
    	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);
        
        return "items/updateItemForm";
    }
    
    @PostMapping("items/{itemId}/edit")
    public String updateItem(@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";
    }
}

  1. “편집” 버튼을 선택하면 GET 방식으로 /items/{itemId}/edit URL을 요청합니다.

  2. 따라서 updateItemForm() 메서드가 실행되어 itemService.findOne(itemId)를 호출하여 수정할 제품을 검색합니다.

  3. 검색 결과를 모델 객체에 넣고 뷰에 전달합니다( items/updateItemForm ).

제품 수정 실행

상품 편집 양식 HTML에는 상품 ID(숨김), 상품명, 가격 및 수량 정보가 포함되어 있습니다.

  1. 상품 수정 양식의 정보를 수정하려면 제출 버튼을 클릭하십시오.
  2. POST 메서드에서 /items/{itemId}/edit URL을 요청하고 updateItem() 메서드를 실행합니다.

  3. 이때 컨트롤러에 매개변수로 전달된 아이템 엔터티 인스턴스는 현재 반영구적인 상태입니다.

    따라서 지속성 컨텍스트를 지원할 수 없으며 데이터가 수정되더라도 변경 감지가 작동하지 않습니다.

변경 감지 및 병합

참고: 이것은 정말 중요하므로 반드시 읽어야 합니다!
완전히 이해해야 합니다.

반영구적 존재?

지속성 컨텍스트에서 더 이상 관리하지 않는 엔티티입니다.

(여기서 itemService.saveItem(book)은 Book 객체를 수정하려고 시도한다.

Book 객체는 DB에 한 번 저장되어 식별자를 가진다.

이렇게 임의로 생성된 개체도 반영구적 개체로 볼 수 있다.

기존 식별자.)

반영구 항목을 수정하는 두 가지 방법

  • 변경 감지 사용
  • 병합 사용

변경된 기능 감지 활성화

@Transactional
void update(Item itemParam) { // itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
	Item findItem = em.find(Item.class, itemParam.getId()); // 같은 엔티티를 조회한다.

findItem.setPrice(itemParam.getPrice()); // 데이터를 수정한다.

}

지속성 컨텍스트에서 엔터티를 다시 쿼리한 후 데이터를 수정하는 방법

트랜잭션 내에서 엔터티를 다시 검색하여 변경할 값 선택 → 트랜잭션 커밋 시 변경 감지(dirty check)가 실행되고 데이터베이스에서 UPDATE SQL 실행

병합 사용

머지(Merge)는 반영구적 상태의 엔터티를 영속적 상태로 바꾸는 기능입니다.

@Transactional
void update(Item itemParam) { // itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
	Item mergeItem = em.merge(item);
}

병합: 기존 엔터티


합병 작동 방식

  1. 병합() 실행
  2. 인수로 전달된 반영구 엔터티의 식별자 값을 사용하여 기본 캐시에서 엔터티를 검색합니다.

  3. 검색된 영구 엔터티(mergeMember)에 구성원 엔터티의 값을 입력합니다.

  4. 영구 상태인 mergeMember를 반환합니다.

합체시 조작방법 간단 요약

  1. 반영구적 엔터티의 식별자 값을 이용하여 영속적 엔터티를 검색합니다.

  2. 모든 영구 엔터티 값은 반영구적 엔터티 값으로 대체(병합)됩니다.

  3. 트랜잭션이 커밋되면 변경 감지가 활성화되고 데이터베이스에서 UPDATE SQL이 실행됩니다.

지침:
변경 감지를 사용하면 원하는 속성만 선택하여 변경할 수 있지만 병합을 사용하면 모든 속성이 변경됩니다.

병합할 때 값이 없으면 null로 업데이트될 위험도 있습니다(병합하면 모든 필드가 대체됨).

인용하다:
실제로 업데이트 기능은 일반적으로 매우 제한적입니다.

그러나 데이터가 없는 경우 병합은 모든 필드를 변경하고 null로 업데이트합니다.

병합을 사용할 때 이 문제를 해결하려면 항상 모든 데이터를 양식 변경 화면에 유지하십시오. 사실 병합을 사용하는 것은 일반적으로 변경 가능한 데이터만 노출하기 때문에 다소 번거롭습니다.

최상의 솔루션

엔티티를 변경할 때는 항상 변경 감지를 사용하십시오.

  • 컨트롤러에서 엔터티 생성을 방해하지 마십시오.
  • 식별자(id)와 변경할 데이터를 트랜잭션이 위치한 서비스 레이어(파라미터 또는 DTO)에 명시적으로 전달합니다.

  • 트랜잭션 서비스 계층에서 지속 상태의 엔터티를 쿼리하고 엔터티의 데이터를 직접 변경합니다.

  • 변경 감지는 트랜잭션 커밋에서 실행됩니다.

@Controller
@RequiredArgsConstructor
public class ItemController {
	
	private final ItemService itemService;
	
	// ...
    
	@PostMapping("items/{itemId}/edit")
	public String updateItem(@PathVariable("itemId") Long itemId, @ModelAttribute("form") BookForm form) {
    	
		itemService.updateItem(form.getId(), form.getName(), form.getPrice(), form.getStockQuantity());
	
		return "redirect:/items";
	}
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {

	private final ItemRepository itemRepository;
    
	// ...
	
	@Transactional
	public void updateItem(Long itemId, String name, int price, int stockQuantity) {
		Item findItem = itemRepository.findOne(itemId);
		findItem.setName(name);
		findItem.setPrice(price);
		findItem.setStockQuantity(stockQuantity);
	}
    
	// ...
}

제품 주문

제품 주문 컨트롤러

@Controller
@RequiredArgsConstructor
public class OrderController {
	private final OrderService orderService;
	private final MemberService memberService;
	private final ItemService itemService;
	
	@GetMapping("/order")
	public String createForm(Model model) {
		List<Member> members = memberService.findMembers();
		List<Item> items = itemService.findItems();
		
		model.addAttribute("members", members);
		model.addAttribute("items", items);
		
		return "order/orderForm";
	}
	
	@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() 메서드
  • 주문 화면은 주문을 하기 위해 고객 정보와 상품 정보가 필요하므로 모델 객체에 넣어 뷰에 전달합니다.

주문 실행

  • 주문할 회원, 상품, 수량을 선택하고 Submit 버튼을 클릭하면 POST 방식으로 /order URL 호출
  • 컨트롤러의 order() 메서드 실행
  • 이 메서드는 주문 서비스에서 주문을 요청하기 위해 고객 식별자(memberId), 상품 식별자(itemId) 및 수량(count) 정보를 수신합니다.

  • 주문이 완료되면 해당 제품의 주문 내역이 포함된 /orders URL로 리디렉션됩니다.


상품 주문 화면

주문 목록 조회, 취소

주문 목록 검색 컨트롤러

@Controller
@RequiredArgsConstructor
public class OrderController {

    //...

    @GetMapping("/orders")
    public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
        List<Order> orders = orderService.findOrders(orderSearch);
        model.addAttribute("orders", orders);

        return "order/orderList";
    }
}

주문 취소

@Controller
@RequiredArgsConstructor
public class OrderController {

    //...

    @PostMapping("/orders/{orderId}/cancel")
    public String cancelOrder(@PathVariable("orderId") Long orderId) {
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }
}