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();
  • 랜덤한 itemCodeprice를 가진 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에서 itemCodeNOT 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);
  • nameitemCode를 같게 해서 테스트 용이.
  • 저장해서 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