Spring/[인프런 김영한 실전 스프링 부트와 JPA 활용 1]

[인프런 김영한 실전 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 웹 계층 개발

h2boom 2024. 9. 3. 21:56

웹 계층 개발

  • @NotEmpty() : jakarta를 통해서 값을 확인해주는 역할로 값이 비어있으면 알려주는 역할 
  • @Valid : @NotEmpty, @NotNull과 같은 어노테이션이 붙은 항목을 validation 해준다. 

 

  • Spring Model : key와 value로 이뤄져있는 HashMap이다.
    • addAttribute()를 호출함으로 view에 전달할 데이터를 담아서 전달할 수 있다.
    • Servlet의 request.setAttribute()와 유사한 역할을 한다.

 

  • BindingResult : 스프링에서 제공하는 검증 오류 처리 방법이다.
    • BindingResult가 없으면 오류 발생 시 Controller가 정상 호출되지 못하고 오류가 발생해 Error Page로 이동한다.
    • @Valid 이후에 사용 시 오류가 발생하면 BindingResult 객체 담기고 Controller가 정상 호출된다.

 

  • 엔티티는 최대한 순수하게 유지하고 다른 곳에서 필요로 하는 경우 DTO를 별도로 만들어서 사용해야한다.
    • API를 만들 때는 엔티티를 절대 외부로 반환하면 안된다.
      => 엔티티에 로직 추가 시 API 스펙이 변한다, 패스워드 노출 가능성

JPA 데이터 수정

  • JPA에서 데이터 수정 시 변경 감지, 병합(merge) 방식을 사용함으로 데이터를 반영할 수 있다.

 

  • ID를 URI에 넘기는 경우 조심해야 한다.
    • 다른 사람이 인위적으로 다른 ID 값을 넘기면 다른 데이터가 수정될 수 있다.
      ex) ID = 1 인 상품 정보를 수정해야하지만 ID = 2로 넘겨주면 ID = 2 인 상품의 정보가 수정된다.
    • 해결 방법 -> ex) 서비스 계층에서 유저가 상품에 대한 권한이 있는지 체크하는 로직이 필요

 

 

  • 준영속 엔티티 : 영속성 컨텍스트가 더는 관리하지 않는 엔티티를 의미한다.
    • 준영속 상태는 식별자를 기준으로 영속상태가 되어서 DB에 저장된 적이 있는지의 여부이다.
    • DB에 한번 저장되서 식별자(ID)가 존재하는 경우 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.
    • 준영속 엔티티의 경우 JPA가 관리하지 않기 때문에 변경 감지가 일어나지 않는다.
//준영속 엔티티 예제
@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:/";
}
  • 예제에서 book 객체를 만들었지만 form 객체를 통해 기존 식별자를 가지고 있기에 새로 만든 book 객체는 준영속 엔티티이다.

 

//변경 감지 예제
@Transactional
public void updateItem(Long itemId, Book param) {
	Item findItem = itemRepository.findOne(itemId);
	findItem.setPrice(param.getPrice());
	findItem.setName(param.getName());
	findItem.setStockQuantity(param.getStockQuantity());
    ...
}

//병합(merge) 예제
public void save(Item item) {
	...
    
	if (item.getId() != null) {
		Item mergeItem = em.merge(item); //update
	}
}

//병합(merge) 동작 방식과 동일
@Transactional
public Item updateItem(Long itemId, Book param) {
	Item findItem = itemRepository.findOne(itemId);
	findItem.setPrice(param.getPrice());
	findItem.setName(param.getName());
	findItem.setStockQuantity(param.getStockQuantity());
	...
    
	return findItem;
}
  • 준영속 엔티티를 수정하는 방법
    1. 변경 감지 기능 사용
      • 변경 감지 (Dirty Checking) : 트랜잭션 커밋시 영속화된 엔티티에서 가지고 있었던 정보와 바뀐 엔티티 정보를 비교해서 바뀐 부분을 데이터베이스에 자동으로 저장해주는 기능이다.
        • 영속성 컨텍스트에서 관리하는 엔티티 => 즉, 영속화된 엔티티만 변경감지가 일어난다.
      • 영속성 컨텍스트에서 엔티티를 다시 조회(find)한 후 데이터를 수정하는 방법
        => em.persist(), em.find()를 하는 경우 엔티티는 영속 상태가 되기 때문에
    2. 병합 (merge) 사용
      • 병합 : 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능이다.
      • em.merge()로 변경 감지 예제 코드와 유사한 기능을 JPA가 제공한다.
      • 예제에서 파라미터 값인 item이 영속 상태로 변하는 것이 아닌 mergeItem이 영속 상태이기에 별도로 사용할 일이 있다면 item이 아닌 mergeItem을 사용해야 한다.

 

  • 변경 감지 과정
    1. 1차 캐시의 엔티티와 스냅샷을 비교한다.
    2. 비교해서 다른 경우 UPDATE SQL을 생성해서 쓰기 지연 SQL 저장소에 담아둔다.
    3. commit() 호출 시점에 UPDATE SQL을 DB로 날린다.
    4. 실제 DB 트랜잭션이 commit 된다.

 

  • 병합(merge) 동작 방식
    1. merge()를 호출
    2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
      1. 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고 1차 캐시에 저장한다.
    3. 조회한 영속 엔티티(mergeMember)에 수정할 엔티티(member)로 값을 채워 넣는다.
    4. 영속 상태인 mergeMember를 반환한다.

 

  • 변경 감지 기능과 병합(merge) 차이점
    • 변경 감지는 원하는 속성만 선택해서 변경할 수 있다.
    • 병합은 모든 속성이 변경된다.
      • 병합 시 값이 없으면 null로 업데이트한다. (병합은 모든 필드를 교체한다)

 

  • 실무에서는 변경가능한 데이터만 노출하기 때문에 병합(merge)을 사용하는 것은 번거롭고 사용하기 어렵기에 변경 감지 기능을 사용해야 한다.
    • 엔티티 변경 시 항상 변경 감지를 사용할 것!!!!!!

 

//잘못된 예시
item.setName(...);
item.setPrice(...);

//옳은 예시
item.change(name,price);
  • 업데이트 시 Setter가 아닌 엔티티에서 의미있는 메소드를 통해 업데이트를 해야 한다.
    • 그래야 변경 지점이 흩어져있지 않고 엔티티에 모일 수 있게 된다.

 

  • 컨트롤러에서 어설프게 엔티티를 생성하지 말 것!!
    • 필요한 필드만 각각 보내주는 것이 좋다.
    • 혹은 별도의 필요한 필드로 구성된 DTO를 만들어서 보내주는 것이 좋다.
  • 트랜잭션이 있는 서비스 계층에 식별자(ID)와 변경할 데이터를 명확하게 전달할 것!! 
    • 트랜잭션 안에서 조회를 해야 엔티티가 영속 상태로 조회가 되고 그 때 값을 변경해야 트랜잭션 커밋 시점에 변경 감지가 가능해진다.

 

  • 서비스 계층에 별다른 비지니스 로직이 없고 레포지토리만 호출한다면 컨트롤러에서 바로 해도 괜찮다.

 

  • @RequestParam : 사용자가 요청 시 전달하는 값을 매개 변수로 1:1 매핑할 때 사용하는 어노테이션
  • @ModelAttribute : 메소드 레벨, 메소드 파라미터에서 적용이 가능하며 사용자가 전달하는 값을 오브젝트 형태로 매핑해주는 어노테이션

출처: [인프런 김영한 실전 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1