1. 목표🎯
특정 카테고리에 물품을 추가하는 기능 구현하기
2. 기능 명세서📜
Name | Method | URI | Domain | AuthZ |
물품 추가 | POST | /item | item | ADMIN |
3. 1차 코드 작성💻
이전에 만들었던 '카테고리 추가' 기능이랑 똑같이 작성해보았다.
3.1 ItemController
@RestController
@RequestMapping("/item")
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
// 아이템 추가
@PreAuthorize("hasRole('ADMIN')") // ADMIN 검사
@PostMapping
public ResponseEntity<Void> createItem(@Valid @RequestBody ItemCreateRequest request) {
itemService.createItem(request);
return ResponseEntity.ok().build();
}
}
- ADMIN(관리자)만이 물품을 추가할 수 있다.
- RequestBody를 통해 추가할 물품의 정보 받아오기
3.2 ItemService
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ItemService {
private final ItemRepository itemRepository;
private final CategoryRepository categoryRepository;
public void createItem(ItemCreateRequest request) {
// Category 객체 조회
Category category = categoryRepository.findById(request.categoryId())
.orElseThrow(()->new CustomException(CATEGORY_NOT_FOUND));
Item item = Item.create(category, request.itemStatus(), request.itemReview());
category.addItem(item);
itemRepository.save(item);
}
}
- 먼저 물품을 추가하고자 하는 카테고리가 존재하는 카테고리인지 확인.
- 존재하는 카테고리일 시, 아이템 객체 생성.
- 카테고리에 추가한다.
3.3 Item
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Item {
@Id
@GeneratedValue
@Column(name = "item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
private ItemStatus itemStatus;
private Double itemReview;
public static Item create(Category category, ItemStatus itemStatus, Double itemReview) {
return Item.builder()
.category(category)
.itemStatus(itemStatus)
.itemReview(itemReview)
.build();
}
}
3.4 ItemCreateRequest
public record ItemCreateRequest (
@NotNull(message = "카테고리를 입력해주세요")
Long categoryId,
ItemStatus itemStatus,
Double itemReview
){}
4. 오류🤯
4.1 문제 상황
에러가 뜨는 건 아니고, 카테고리에 추가되지 않는다!!
4.2 원인 탐색
post한 다음 에러가 뜨지 않는 걸로 봐서는 로직에 문제가 있는 것 같지는 않았다.
그럼 뭐가 문제일까?
카테고리에 추가는 되는데 변경사항이 저장되지 않는 것 같았다. 검색했을 때 영속성이 키워드 같았다
Item 저장 후 카테고리 업데이트 누락: Item이 ItemRepository를 통해 저장될 때, JPA는 Item 엔티티에 대한 변경 사항만 데이터베이스에 반영합니다. Category 엔티티의 items 리스트가 변경된 상태가 영속성 컨텍스트에 의해 관리되지 않을 수 있습니다. 만약 Category 객체가 영속성 컨텍스트에서 분리된(detached) 상태라면, addItem 호출은 단순히 자바 객체의 리스트에 추가할 뿐, 데이터베이스에 반영되지 않을 수 있습니다.
4.3 해결책
첫번째.
categoryRepository.save(category);
- 발상 : 아이템을 save한 것처럼 카테고리도 하면 save 기능을 이용하면 되지 않을까?
- 결론 : 안 됨
save() 메서드란?
Persisting Entities :: Spring Data JPA
Persisting Entities :: Spring Data JPA
Saving an entity can be performed with the CrudRepository.save(…) method. It persists or merges the given entity by using the underlying JPA EntityManager. If the entity has not yet been persisted, Spring Data JPA saves the entity with a call to the enti
docs.spring.io
- It persists or merges the given entity by using the underlying JPA EntityManager. If the entity has not yet been persisted, Spring Data JPA saves the entity with a call to the entityManager.persist(…) method. Otherwise, it calls the entityManager.merge(…) method.
그럼 다시! persist/merge는 뭔데?
persist : 최초 생성된 entity를 영속화
merge : detached 상태의 entity 영속
두번째.
em.flush();
- 결론 : 된다!!
- category.addItem(item);을 생략해도 작동한다.
영속성 컨텍스트와 트랜잭션: JPA에서 엔티티 매니저(EntityManager)는 영속성 컨텍스트(Persistence Context)를 관리합니다. 영속성 컨텍스트는 트랜잭션 내에서 엔티티 객체를 관리하며, 트랜잭션이 종료될 때까지 변경된 객체들을 데이터베이스에 반영하지 않을 수 있습니다.
플러시(Flush)의 의미: entityManager.flush()는 영속성 컨텍스트에 있는 변경 사항들을 강제로 데이터베이스에 반영하는 역할을 합니다. 즉, 트랜잭션이 종료되기 전에 변경 사항이 데이터베이스에 기록되도록 만듭니다.
변경된 엔티티 상태: Category와 Item의 연관관계에서 발생한 변경 사항이 영속성 컨텍스트에 반영되지 않고 있다가, flush() 호출 시 강제로 반영되면서 문제가 해결된 것입니다.
5. 그러나... 의문점🤔
5.1 save와 flush는 뭐가 다른 거지?
JPA - save() 와 saveAndFlush() 의 차이 (velog.io)
JPA - save() 와 saveAndFlush() 의 차이
영속성 컨텍스트(Persistence Context) 와 DB 사이, Baeldung 문서
velog.io
- save() 메소드는 영속성 컨텍스트에 저장하는 것이고 실제로 DB 에 저장은 추후 flush 또는 commit 메소드가 실행될 때 이루어짐
- saveAndFlush() 메소드는 즉시 DB 에 데이터를 반영함
즉 flust()를 호출해야 최종적으로 DB반영이 되는 것이다!!
5.2 category이 items 리스트에 추가(add)하지 않아도 되는 이유?
1. JPA의 연관관계 관리
연관관계의 주인: JPA에서는 연관관계의 주인(owner) 개념이 있습니다. 주인이 되는 쪽이 실제로 데이터베이스에서 외래 키(Foreign Key)를 관리합니다. 예를 들어, Item 엔티티가 ManyToOne 관계의 주인이며 category_id라는 외래 키를 관리한다고 가정할 수 있습니다.
연관관계 주인의 변경 반영: 주인 엔티티(Item)의 외래 키가 변경되면, JPA는 이를 데이터베이스에 즉시 반영합니다. 따라서 Item에서 category를 설정하면, 이 정보가 영속성 컨텍스트에 의해 관리되며, 트랜잭션 종료 시 데이터베이스에 반영됩니다.
2. 왜 리스트에 추가할 필요가 없는가?
Item 객체가 category를 설정하는 것만으로도 연관관계의 주인 쪽(Item)에서 데이터베이스에 외래 키가 관리되기 때문에, category.addItem(item)이 없어도 데이터베이스에 올바르게 반영됩니다. 즉, Item에서 category 필드를 설정하는 것만으로도 외래 키가 업데이트되고, 데이터베이스에 반영되는 것입니다.
5.3 이때까지 flush를 작성하지 않아도 변경사항이 반영된 이유?
이전에 카테고리를 추가하는 기능을 구현했다. 그때는 flush를 따로 작성해주지 않았었다.
???? 하지만 왜 제대로 작동했던 거지 ????
하고 의문을 품고 있을 때쯤.... 챗gpt의 답변에서 무언가를 발견했다.
영속성 컨텍스트는 트랜잭션 내에서 엔티티 객체를 관리하며, 트랜잭션이 종료될 때까지 변경된 객체들을 데이터베이스에 반영하지 않을 수 있습니다.
그럼 트랜잭션이 종료되면 자동으로 반영되어야 한다는 거 아냐?
분명 난 @Transactional을 썼을 텐데? 그리고 다시 보는 나의 코드...
아이템을 '추가(add)' 하는 건 아무래도 read와는 거리가 멀긴 하지....
카테고리 추가API에서 수정사항이 자동으로 반영되었던 것은 @Transactional을 추가로 붙여줬기 때문이다.
즉 @Transactional을 잘 붙여주면 em.flush()를 따로 작성해 줄 필요가 없다!
6. 최종 코드🍀
ItemService만 수정했다. 그것도 @Transactional 딱 하나만 추가해줌. 이 간단한 걸..😭
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ItemService {
private final EntityManager em;
private final ItemRepository itemRepository;
private final CategoryRepository categoryRepository;
@Transactional // 변경사항을 만들어 낸다면 반드시 작성!
public void createItem(ItemCreateRequest request) {
// Category 객체 조회
Category category = categoryRepository.findById(request.categoryId())
.orElseThrow(()->new CustomException(CATEGORY_NOT_FOUND));
Item item = Item.create(category, request.itemStatus(), request.itemReview());
itemRepository.save(item);
}
}
7. 교훈💖
영속성 컨텍스트에 대해 다시 짚어보는 시간이었다. 책에서 개념만 봤을 때는 와닿지 않았는데, 이렇게 문제 상황으로 직접 맞닥뜨리니까 얼마나 중요한 개념인지 다시 한 번 느낄 수 있었다...
그리고 이때까지 관성적으로 붙이던 @Transactional이 얼마나 중요한 것인지도 깨달았다.
'PROJECT > GDSC 프로젝트 트랙' 카테고리의 다른 글
[UXUI] 이젠 멀리서도 간편하게, 물품 대여 서비스 '와우대여' (5) | 2024.08.17 |
---|---|
[SpringBoot] 특정 카테고리 정보 조회 API 구현 (0) | 2024.08.14 |
[SpringBoot] 특정 물품 정보 조회 API 구현 (0) | 2024.08.14 |
[SpringBoot] 특정 카테고리 삭제 API 구현 (0) | 2024.08.13 |