728x90
package io.shi.dao.dao.hibernate;
import io.shi.dao.dao.mybatis.ItemMapper;
import io.shi.dao.global.entity.Items;
import jakarta.persistence.EntityManager;
import jakarta.persistence.NoResultException;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Slf4j
@Repository
@Transactional
@RequiredArgsConstructor
public class HibernateItemRepository {
private final EntityManager entityManager;
private final ItemMapper itemMapper;
public Items save(Items items) {
entityManager.persist(items);
return items;
}
public Optional<Items> findById(Long id) {
Items items = entityManager.find(Items.class, id);
return Optional.ofNullable(items);
}
public Optional<Items> findByItemCode(String itemCode) {
try {
String jpql = "select i from Items i where i.itemCode = :itemCode";
Items findItem = entityManager.createQuery(jpql, Items.class)
.setParameter("itemCode", itemCode)
.getSingleResult();
return Optional.of(findItem);
} catch (NoResultException e) {
return Optional.empty();
}
}
public void updatePrice(String itemCode, Integer price) {
Optional<Items> itemOptional = findByItemCode(itemCode);
Items item = itemOptional.orElseThrow();
item.setPrice(price);
}
// items 배열안에 있는거를 전부 저장을 하고 싶다.
public List<Items> saveAll(List<Items> items) {
// buffer
int batchSize = 50;
for (int i = 0; i < items.size(); i++) {
entityManager.persist(items.get(i));
if (i % batchSize == 0 && i > 0) {
entityManager.flush();
entityManager.clear();
log.info("FLUSH!");
}
}
entityManager.flush();
entityManager.clear();
log.info("FLUSH!");
return items;
}
}
package io.shi.dao.dao.hibernate;
import io.shi.dao.global.entity.Items;
import io.shi.dao.util.TestUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataIntegrityViolationException;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Slf4j
@SpringBootTest
class HibernateItemRepositoryTests {
@Autowired
HibernateItemRepository repository;
@Test
@DisplayName("Item 저장")
void save_item_test() throws Exception {
Items item1 = Items.builder()
.itemCode(TestUtils.genRandomItemCode())
.price(TestUtils.genRandomPrice())
.build();
Items saved = repository.save(item1);
assertThat(saved.getId()).isNotNull();
Items item2 = Items.builder()
.price(TestUtils.genRandomPrice())
.build();
assertThatThrownBy(
() -> {
repository.save(item2);
}
).isInstanceOf(DataIntegrityViolationException.class);
}
@Test
@DisplayName("item 조회")
void select_item_test() throws Exception {
String itemCode = TestUtils.genRandomItemCode();
Items item = Items.builder()
.name(itemCode)
.itemCode(itemCode)
.price(TestUtils.genRandomPrice())
.build();
repository.save(item);
Optional<Items> itemsOptional = repository.findByItemCode(itemCode);
assertThat(itemsOptional.isPresent()).isTrue();
Items findItem = itemsOptional.get();
assertThat(findItem.getItemCode()).isEqualTo(itemCode);
}
@Test
@DisplayName("item 조회 2")
void select_item_test_2() throws Exception {
String targetItemCode = "INVALID";
String itemCode = TestUtils.genRandomItemCode();
Items item = Items.builder()
.name(itemCode)
.itemCode(itemCode)
.price(TestUtils.genRandomPrice())
.build();
repository.save(item);
Optional<Items> itemsOptional = repository.findByItemCode(targetItemCode);
assertThat(itemsOptional.isPresent()).isFalse();
assertThatThrownBy(
() -> {
itemsOptional.get();
}
).isInstanceOf(NoSuchElementException.class);
}
@Test
@DisplayName("Item 수정")
void update_item_test() throws Exception {
String itemCode = TestUtils.genRandomItemCode();
Integer TARGET_PRICE = 10;
Items item1 = Items.builder()
.name(itemCode)
.itemCode(itemCode)
.price(TestUtils.genRandomPrice())
.build();
repository.save(item1);
Optional<Items> itemsOptional = repository.findByItemCode(itemCode);
assertThat(itemsOptional.isPresent()).isTrue();
Items findItem = itemsOptional.get();
repository.updatePrice(findItem.getItemCode(), TARGET_PRICE);
Optional<Items> itemsOptional2 = repository.findByItemCode(itemCode);
assertThat(itemsOptional2.isPresent()).isTrue();
Items updatedItem = itemsOptional2.get();
assertThat(updatedItem.getPrice()).isEqualTo(TARGET_PRICE);
}
// @Test
@DisplayName("save all 테스트")
void save_all_test() throws Exception {
List<Items> items = TestUtils.generateItems(150);
repository.saveAll(items);
}
}
✅ HibernateItemRepository.java 설명
@Repository
@Transactional
@RequiredArgsConstructor
public class HibernateItemRepository {
- @Repository: Spring에게 이 클래스가 데이터 접근 계층(DAO) 이라고 알려줘. 예외를 자동으로 변환해주는 기능도 있어.
- @Transactional: 이 클래스의 모든 메서드가 트랜잭션 안에서 실행됨을 의미해.
- @RequiredArgsConstructor: final로 선언된 필드를 자동으로 생성자 주입해줘.
필드
private final EntityManager entityManager;
private final ItemMapper itemMapper;
- EntityManager: JPA의 핵심 객체로, DB 작업(persist, find, query 등)을 수행함.
- ItemMapper: MyBatis용 매퍼. 현재 코드에서는 사용하지 않고 있음.
save() 메서드
public Items save(Items items) {
entityManager.persist(items);
return items;
}
- 새로운 Items 엔티티를 DB에 저장(persist).
- 저장한 객체를 그대로 리턴함.
findById()
public Optional<Items> findById(Long id) {
Items items = entityManager.find(Items.class, id);
return Optional.ofNullable(items);
}
- 기본 키(ID)로 DB에서 데이터를 조회.
- 존재하지 않을 수도 있으니까 Optional로 감쌈.
findByItemCode()
public Optional<Items> findByItemCode(String itemCode) {
try {
String jpql = "select i from Items i where i.itemCode = :itemCode";
Items findItem = entityManager.createQuery(jpql, Items.class)
.setParameter("itemCode", itemCode)
.getSingleResult();
return Optional.of(findItem);
} catch (NoResultException e) {
return Optional.empty();
}
}
- JPQL을 사용해서 itemCode 필드 값으로 조회.
- 결과가 없으면 NoResultException이 발생하므로 Optional.empty()로 처리.
updatePrice()
public void updatePrice(String itemCode, Integer price) {
Optional<Items> itemOptional = findByItemCode(itemCode);
Items item = itemOptional.orElseThrow();
item.setPrice(price);
}
- itemCode로 아이템을 찾고, 그 가격을 수정.
- 이 때 별도 persist() 안 해도 됨. 이유는?
- JPA는 영속성 컨텍스트 내 객체를 자동으로 추적해서 트랜잭션이 끝날 때 자동 반영(더티 체킹).
saveAll()
public List<Items> saveAll(List<Items> items) {
int batchSize = 50;
for (int i = 0; i < items.size(); i++) {
entityManager.persist(items.get(i));
if (i % batchSize == 0 && i > 0) {
entityManager.flush();
entityManager.clear();
log.info("FLUSH!");
}
}
entityManager.flush();
entityManager.clear();
log.info("FLUSH!");
return items;
}
- 여러 개의 Items 객체를 한꺼번에 저장.
- 50개마다 flush() & clear() 호출:
- flush(): 지금까지 영속성 컨텍스트에 모인 변경 사항을 DB에 반영
- clear(): 1차 캐시 제거 → 메모리 절약
✅ HibernateItemRepositoryTests.java 설명
이건 저장소 클래스의 기능을 검증하는 JUnit 테스트 클래스야.
save_item_test()
@Test
@DisplayName("Item 저장")
void save_item_test()
- 정상 저장 테스트 (itemCode 있는 경우)
- 실패 케이스 테스트 (itemCode 빠짐 → DB 제약조건 위반 → DataIntegrityViolationException 발생 예상)
목적: 정상 저장 확인 + 잘못된 데이터 저장 시 예외 발생 확인
Items item1 = Items.builder()
.itemCode(TestUtils.genRandomItemCode())
.price(TestUtils.genRandomPrice())
.build();
- 랜덤한 itemCode와 price를 가진 Items 객체 생성.
- @Builder 패턴을 사용해서 깔끔하게 객체 생성.
Items saved = repository.save(item1);
assertThat(saved.getId()).isNotNull();
- repository.save()는 entityManager.persist() 호출 → DB 저장됨.
- 저장되면 JPA가 자동으로 id를 채워줌. getId()로 확인.
Items item2 = Items.builder()
.price(TestUtils.genRandomPrice())
.build();
- itemCode 없이 생성 → DB에서 itemCode는 NOT NULL 제약이 걸려있다면 저장 실패할 것.
assertThatThrownBy(() -> repository.save(item2))
.isInstanceOf(DataIntegrityViolationException.class);
- 예외가 발생해야 테스트 통과.
- @Repository가 감싸서 JDBC 예외를 Spring의 DataIntegrityViolationException으로 바꿔줌.
select_item_test()
@Test
@DisplayName("item 조회")
void select_item_test()
- 먼저 데이터를 저장한 뒤, itemCode로 조회.
- 조회 성공 여부와 값 확인 (isPresent, itemCode 값 동일 확인)
목적: 저장 후 올바르게 조회되는지 확인
String itemCode = TestUtils.genRandomItemCode();
- 식별 가능한 랜덤 itemCode 생성
Items item = Items.builder()
.name(itemCode)
.itemCode(itemCode)
.price(TestUtils.genRandomPrice())
.build();
repository.save(item);
- name과 itemCode를 같게 해서 테스트 용이.
- 저장해서 DB에 들어가게 함.
Optional<Items> itemsOptional = repository.findByItemCode(itemCode);
assertThat(itemsOptional.isPresent()).isTrue();
- Optional이 비어있지 않아야 함 → 저장된 게 잘 조회됐다는 의미.
Items findItem = itemsOptional.get();
assertThat(findItem.getItemCode()).isEqualTo(itemCode);
- 실제 조회된 객체의 itemCode가 우리가 저장한 것과 동일한지 확인.
select_item_test_2()
@Test
@DisplayName("item 조회 2")
void select_item_test_2()
- 존재하지 않는 itemCode로 조회 시 어떻게 동작하는지 확인.
- Optional.get()을 억지로 호출하면 NoSuchElementException 터짐을 검증.
목적: 존재하지 않는 데이터 조회 시 동작 확인
String targetItemCode = "INVALID";
String itemCode = TestUtils.genRandomItemCode();
Items item = Items.builder()
.name(itemCode)
.itemCode(itemCode)
.price(TestUtils.genRandomPrice())
.build();
repository.save(item);
- itemCode로 저장했지만, 일부러 존재하지 않는 "INVALID" 코드로 조회 테스트함.
Optional<Items> itemsOptional = repository.findByItemCode(targetItemCode);
assertThat(itemsOptional.isPresent()).isFalse();
- 없는 itemCode이므로 결과는 없어야 한다.
assertThatThrownBy(() -> itemsOptional.get())
.isInstanceOf(NoSuchElementException.class);
- 없는 Optional에서 get() 하면 당연히 예외 발생 → 우리가 의도한 테스트 성공
update_item_test()
@Test
@DisplayName("Item 수정")
void update_item_test()
- 저장 → 조회 → 가격 변경 → 다시 조회해서 가격이 바뀌었는지 확인.
목적: itemCode로 조회한 후, 가격을 바꾼 뒤 그게 진짜로 반영됐는지 확인
String itemCode = TestUtils.genRandomItemCode();
Integer TARGET_PRICE = 10;
- 변경할 목표 가격을 미리 정함.
Items item1 = Items.builder()
.name(itemCode)
.itemCode(itemCode)
.price(TestUtils.genRandomPrice())
.build();
repository.save(item1);
- 초기 아이템을 저장함.
Optional<Items> itemsOptional = repository.findByItemCode(itemCode);
assertThat(itemsOptional.isPresent()).isTrue();
Items findItem = itemsOptional.get();
- 저장된 아이템을 다시 가져옴.
repository.updatePrice(findItem.getItemCode(), TARGET_PRICE);
- 가격을 10으로 변경하는 메서드 실행
Optional<Items> itemsOptional2 = repository.findByItemCode(itemCode);
assertThat(itemsOptional2.isPresent()).isTrue();
Items updatedItem = itemsOptional2.get();
assertThat(updatedItem.getPrice()).isEqualTo(TARGET_PRICE);
- 다시 조회해서 정말 가격이 바뀌었는지 확인
😀 여기서 중요한 포인트!
- updatePrice() 안에서는 persist() 안 쓰고 그냥 setPrice()만 했잖아?
- 왜 바뀌었냐면, JPA는 영속 상태의 엔티티를 추적하고 있다가 트랜잭션이 끝날 때 자동으로 DB에 반영해 (→ Dirty Checking)
- @Transactional이 클래스에 선언돼 있어서 이 메서드 안에서 이 변경은 하나의 트랜잭션 내에서 이루어진 거야.
save_all_test()
@DisplayName("save all 테스트")
void save_all_test() throws Exception {
List<Items> items = TestUtils.generateItems(150);
repository.saveAll(items);
}
- 150개의 아이템을 한 번에 저장하는 테스트.
- flush()/clear()가 잘 작동하는지 확인할 수 있음.
목적: 여러 개의 아이템을 한 번에 저장하고, 중간에 flush/clear가 잘 작동하는지 확인
List<Items> items = TestUtils.generateItems(150);
repository.saveAll(items);
- 랜덤한 150개의 Items 객체를 생성
- 한꺼번에 저장
✅ flush()와 clear()는 왜 썼을까?
if (i % batchSize == 0 && i > 0) {
entityManager.flush();
entityManager.clear();
}
✔️ flush()란?
- 영속성 컨텍스트에 쌓여있는 변경 내용을 DB에 반영해주는 작업
- persist()만으로는 DB에 안 들어감 → JPA 내부 버퍼(영속성 컨텍스트) 에만 저장된 상태
- flush()를 호출해야 실제 insert SQL이 나감
✔️ clear()란?
- 영속성 컨텍스트를 초기화(1차 캐시 제거)하는 작업
- 너무 많은 엔티티가 쌓이면 메모리 부담이 커짐
- → 매 batchSize마다 clear() 해서 메모리 사용량 줄임
📌 그래서 이 코드가 이렇게 구성된 이유는?
- 한꺼번에 150개 저장하면 메모리가 버거울 수 있음.
- 50개마다 flush() + clear()를 해서 메모리 부담 줄이고, 성능을 최적화하기 위함이야.
🔚 정리: flush() & clear() 실행 이유 한 문장 요약
flush()는 영속성 컨텍스트의 변경을 DB에 즉시 반영하기 위해,
clear()는 영속성 컨텍스트를 비워서 메모리 낭비를 방지하고 성능을 높이기 위해 실행되는 거야!
💡 핵심 정리 요약표
기능 | 설명 |
persist() | Entity를 영속 상태로 만들어 저장 |
find() | ID로 조회 |
JPQL | 객체 중심 쿼리. SQL 아님 |
flush() | 변경 사항을 DB에 반영 |
clear() | 영속성 컨텍스트 비움 |
@Transactional | 메서드를 트랜잭션 안에서 실행 |
728x90
'프로그래밍 > Spring' 카테고리의 다른 글
entityManager.persist()란? (0) | 2025.04.14 |
---|---|
Hibernate실습, JPQL 활용2 - 4월 14일 (0) | 2025.04.14 |
Hibernate란 - 4월 11일 (0) | 2025.04.11 |
JPA- 4월 10일 (0) | 2025.04.10 |
Mock란? - 4월 10일 (1) | 2025.04.10 |