728x90
Hibernate의 첫 번째 실습은 가장 기초적인 JPA 동작 원리를 이해하는 데 초점이 맞춰져 있다. 주요 학습 목표는 Entity, EntityManager, 영속성 컨텍스트, 트랜잭션, 쓰기 지연, 캐시, 준영속 상태 등을 코드 기반으로 실습해보는것!!!
📁 프로젝트 구조 및 핵심 클래스 소개
1. Member 엔티티 클래스
package io.shi.domain.eg1;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.*;
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
private String id;
@Setter
private String name;
@Builder
public Member(String id, String name) {
this.id = id;
this.name = name;
}
}
✅ 설명
- @Entity: 이 클래스가 DB 테이블과 매핑되는 JPA 엔티티임을 선언한다.
- @Id: id 필드가 테이블의 기본 키(PK)임을 지정한다.
- @Builder, @Getter, @Setter: Lombok을 이용해 생성자, Getter, Setter, 빌더 메서드를 자동 생성한다.
- @NoArgsConstructor(access = AccessLevel.PROTECTED): JPA가 사용할 수 있도록 기본 생성자를 만들되 외부에서는 호출하지 못하도록 제한한다.
※ 이 엔티티는 id와 name을 가진 단순한 회원 객체이며, 실습용으로 설계됨.
2. EntityManagerTests: Hibernate 동작 테스트 클래스
@BeforeAll
static void init() {
entityManagerFactory =
Persistence.createEntityManagerFactory("grepp-hibernate-exp1");
}
- JPA 설정 정보(persistence.xml)를 기반으로 EntityManagerFactory를 생성한다.
- persistence-unit 이름과 정확히 일치해야 동작한다.
@BeforeEach
void setUp() {
entityManager = entityManagerFactory.createEntityManager();
}
- 테스트 시작 전, 매 테스트마다 새로운 EntityManager를 생성한다. (엔티티 매니저는 쓰레드 안전하지 않기 때문에.)
@AfterEach
void close() {
entityManager.close();
}
- 테스트 종료 후 EntityManager를 반드시 닫아 자원 누수를 방지한다.
@AfterAll
static void tearDown() {
entityManagerFactory.close();
}
- 모든 테스트 종료 후 EntityManagerFactory도 종료한다.
🔧 Persistence 설정 (persistence.xml)
<persistence-unit name = "grepp-hibernate-exp1">
<class>io.shi.domain.eg1.Member</class>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/grepp_hibernate_test" />
<property name="jakarta.persistence.jdbc.user" value="happy"/>
<property name="jakarta.persistence.jdbc.password" value="shi"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
</persistence-unit>
✅ 설명
- DB 연결 정보 및 Hibernate 관련 설정이 포함된 JPA 설정이다.
- hibernate.show_sql=true : 콘솔에 SQL 로그 출력
- hibernate.format_sql=true : SQL 예쁘게 출력
🧪 테스트별 상세 설명
✅ 테스트 1 : 설정 확인
@Test
void test1() throws Exception {
Map<String, Object> properties = entityManagerFactory.getProperties();
String url = properties.get("jakarta.persistence.jdbc.url").toString();
String driver = properties.get("jakarta.persistence.jdbc.driver").toString();
log.info("url = {}", url);
log.info("driver = {}", driver);
assertThat(url).isEqualTo("jdbc:mysql://localhost:3306/grepp_hibernate_test");
assertThat(driver).isEqualTo("com.mysql.cj.jdbc.Driver");
}
- DB URL, 드라이버 설정이 제대로 되었는지 확인하는 테스트
✅ Map<String, Object> properties = entityManagerFactory.getProperties();
- 이건 Hibernate 설정 파일인 persistence.xml에서 설정한 값을 꺼내오는 것
- entityManagerFactory는 Hibernate의 엔티티 매니저들을 만드는 공장이었지? (@BeforeAll에서 생성함)
- getProperties()는 내부적으로 JDBC 드라이버, URL, 유저명, 패스워드 등의 정보들을 Map 형태로 가지고 있어.
- 예: jakarta.persistence.jdbc.url, jakarta.persistence.jdbc.driver 등
✅ String url = properties.get("jakarta.persistence.jdbc.url").toString();
- 이 줄은 위에서 꺼낸 Map에서 "jakarta.persistence.jdbc.url" 키에 해당하는 값을 문자열로 꺼낸 거야.
- 결과적으로 "jdbc:mysql://localhost:3306/grepp_hibernate_test"라는 값을 가져오게 되지.
- 이건 DB에 연결할 주소야.
✅ String driver = properties.get("jakarta.persistence.jdbc.driver").toString();
- 마찬가지로 드라이버 정보를 가져와. 보통 MySQL은 "com.mysql.cj.jdbc.Driver"를 써.
✅ log.info("url = {}", url);
✅ log.info("driver = {}", driver);
- 이건 그냥 콘솔에 출력하려고 찍는 로그야.
- 실제로 테스트 돌리면 콘솔창에 아래처럼 찍힐 거야:
url = jdbc:mysql://localhost:3306/grepp_hibernate_test
driver = com.mysql.cj.jdbc.Driver
✅ assertThat(url).isEqualTo("jdbc:mysql://localhost:3306/grepp_hibernate_test");
- 이건 우리가 설정한 값이 실제로 적용되었는지 확인하는 코드야.
- 예상한 값이랑 실제 꺼낸 값이 다르면 테스트는 실패하게 돼.
✅ assertThat(driver).isEqualTo("com.mysql.cj.jdbc.Driver");
- 위와 동일한 로직인데 드라이버 쪽 검증이야.
✅ 테스트 2: 저장(Persist)
executeCommit(entityManager, () -> {
Member member = genMember(genMemberName());
entityManager.persist(member);
});
- 새로운 객체를 만들고 persist()로 영속성 컨텍스트에 등록한다.
- 이 시점에서 객체는 Managed 상태이다.
- 트랜잭션이 커밋될 때 DB에 INSERT 쿼리가 실행된다.
✅ 테스트 3: 조회(Select) + 상태 변화 실험
// 1차 캐시 확인
entityManager.persist(member);
Member findMember = entityManager.find(Member.class, member.getId());
assertThat(findMember).isEqualTo(member); // 동일 객체
// Detached 후 변경, 재조회
entityManager.detach(findMember);
findMember.setName("MEMBER"); // DB에 반영되지 않음
// 다시 조회하면 DB 상태 유지됨
Member findAgain = entityManager.find(Member.class, member.getId());
assertThat(findAgain.getName()).isEqualTo("ADMIN");
정리
상태 | 설명 |
Transient | new로 만든 상태. 아직 JPA가 관리하지 않음 |
Managed | persist()한 상태. 영속성 컨텍스트에서 관리됨 |
Detached | detach() 호출 시, 관리에서 분리됨 |
Removed | remove()로 삭제된 상태 |
✅ 테스트 4: Write-behind
entityManager.persist(member1);
entityManager.persist(member2);
log.info("아직 쿼리가 실행되지 않았습니다!");
- Hibernate는 persist() 시점에 바로 SQL을 실행하지 않고, 트랜잭션 커밋 시점에 모아서 실행한다.
- 이를 쓰기 지연(write-behind) 전략이라고 한다.
✅ 핵심 개념 요약
🌱 영속성 컨텍스트
- JPA의 핵심 개념으로, Entity 객체를 관리하는 공간.
- 이 컨텍스트 안에 있는 객체는 상태 변화가 자동으로 감지되고, 트랜잭션 종료 시 DB에 반영함.
🧊 1차 캐시
- 영속성 컨텍스트 내부에 존재하며, find() 호출 시 먼저 캐시를 조회한다.
- 동일한 ID의 객체는 쿼리를 날리지 않고 캐시에서 반환한다.
🪓 준영속 (Detached)
- detach() 호출 시, 더 이상 JPA가 객체를 추적하지 않는다.
- 이후 값을 바꿔도 DB 반영이 되지 않음
- merge()를 사용하면 다시 영속성 컨텍스트에 병합할 수 있다.
🧩 마무리
이 실습은 Hibernate의 기초 구조와 주요 개념인 Entity, persist(), find(), detach(), flush(), commit(), 트랜잭션 등등을 직접 코드로 실험하며 이해하는 것이 핵심으로 단순히 SQL이 실행되는 것만 보지 말고 상태 변화와 영속성 컨텍스트의 작동 방식에 집중해서 학습하는 것이 매우 중요하다!!
Hibernate Enum 매핑 실습 설명
이번 실습은 Hibernate에서 Enum 타입을 어떻게 매핑하는지, 그리고 실제로 Entity에 Enum 값을 저장하고 조회하는 방식을 테스트하는 코드이다. 특히 @Enumerated 애너테이션이 어떤 식으로 작동하는지를 확인 할 수 있다.
🧱 프로젝트 파일 구조 요약
- EntityObjTests: 테스트 클래스
- GymMemberShip: Entity 클래스
- Level: Enum 클래스
1. Level Enum 클래스
public enum Level {
GOLD,
SILVER,
GENERAL
}
- Level은 헬스장 회원의 등급을 나타낸다.
- Enum 타입은 기본적으로 GOLD, SILVER, GENERAL 세 가지 상태를 가진다.
2. GymMemberShip Entity 클래스
@Getter
@Entity
public class GymMemberShip {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Setter
@Enumerated(EnumType.STRING)
private Level membershipLevel;
}
🔍 주요 포인트 설명
- @Entity: Hibernate가 이 클래스를 DB 테이블과 매핑할 수 있도록 지정한다.
- @Id: id는 기본 키.
- @GeneratedValue(strategy = GenerationType.IDENTITY) : DB에서 자동 증가되는 값을 사용한다.(MySQL에서 흔하게 사용됨)
- @Enumerated(EnumType.STRING):
- Enum 타입을 DB에 저장할 때, 이름(String)을 그대로 저장한다.
- 예: Level.GOLD -> "GOLD"
- (참고: EnumType.ORDINAL을 쓰면 0, 1, 2 숫자 형태로 저장됨 → 권장 ❌)
3. 테스트 클래스 EntityObjTests
@BeforeAll
static void init() {
entityManagerFactory =
Persistence.createEntityManagerFactory("grepp-hibernate-exp1");
}
- 테스트 전에 전체적으로 사용할 EntityManagerFactory를 초기화함
@BeforeEach
void setUp() {
entityManager = entityManagerFactory.createEntityManager();
}
- 각 테스트 시작 전에 EntityManager를 생성
@AfterEach
void close() {
entityManager.close();
}
@AfterAll
static void tearDown() {
entityManagerFactory.close();
}
- 테스트 끝나고 리소스 정리
4. ENUM TEST 상세 설명
@Test
@DisplayName("ENUM TEST")
void enum_test() throws Exception {
TestUtils.executeCommit(entityManager, () -> {
GymMemberShip memberShip1 = new GymMemberShip();
memberShip1.setMembershipLevel(Level.GOLD);
GymMemberShip memberShip2 = new GymMemberShip();
memberShip2.setMembershipLevel(Level.SILVER);
GymMemberShip memberShip3 = new GymMemberShip();
memberShip3.setMembershipLevel(Level.GENERAL);
entityManager.persist(memberShip1);
entityManager.persist(memberShip2);
entityManager.persist(memberShip3);
});
}
🥹 작동 원리
- TestUtils.executeCommit()은 트랜잭션을 시작하고 끝내주는 유틸 함수 (보통 begin → commit 또는 rollback 처리까지 포함)
- 각각의 회원권 객체를 만들고, Level enum 값을 지정한 후 persist()로 영속화한다.
- entityManager.persist()로 각각의 엔터티를 DB에 저장한다.
- 이 때 @Enumerated(EnumType.STRING) 때문에 DB에 "GOLD", "SILVER", "GENERAL"로 저장됨
5. 정리 테이블
항목 | 설명 |
@Enumerated(EnumType.STRING) | Enum 이름 그대로 DB에 저장 (GOLD, SILVER, ...) |
@Enumerated(EnumType.ORDINAL) | Enum 순서(0, 1, 2)로 저장됨 — 위험할 수 있음 |
persist() | EntityManager가 객체를 DB에 저장함 |
Enum | 자바에서 상수 집합을 표현할 때 쓰는 타입 |
✅ 결론
- Hibernate에서 Enum 타입을 다룰 때는 반드시 EnumType.STRING을 명시하는 것이 좋다.
- 이유: 나중에 enum 순서가 바뀌면 ORDINAL 방식은 데이터 무결성이 깨질 수 있음
- 이 실습은 Enum을 안전하게 저장하고 관리하는 Hibernate 설정을 익히는 데 목적이 있음
- @Enumerated(EnumType.STRING) 방식은 꼭 외우자
- 실제 서비스에서도 등급, 상태값, 타입 같은 분류값은 대부분 Enum으로 관리하므로 꼭 숙지할 것!
728x90
'프로그래밍 > Spring' 카테고리의 다른 글
Hibernate실습, JPQL 활용2 - 4월 14일 (0) | 2025.04.14 |
---|---|
Hibernate실습, JPQL 활용 - 4월 14일 (0) | 2025.04.14 |
JPA- 4월 10일 (0) | 2025.04.10 |
Mock란? - 4월 10일 (1) | 2025.04.10 |
Mock이란? - 4월9일 (0) | 2025.04.10 |