0. 이 글에서 정리하고자 하는 내용
- 영속성 컨텍스트란 무엇인지
- EntityManager가 무엇인지
1. 개발 환경 준비
1-1. H2 설치 및 연동
http://h2database.com/html/main.html
각 환경에 맞는 h2를 설치한다.
설치된 h2에서 bin디렉토리에 있는 h2.sh로 실행한다.
초기 db파일을 생성하기 위해서는 tcp모드가 아닌 일반 모드로 실행을 하고 이후 tcp모드로 전환한다.
1-2. Spring 준비
start.spring.io(https://start.spring.io/)에서 아래 라이브러리를 추가한 프로젝트를 다운받는다.
최종적으로 내 gradle은 이렇게 나온다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.1'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.cvs'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml은 다음과 같이 설정했다.
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/Desktop/toy/cvs/store
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
properties:
hibernate:
show_sql: true
format_sql: true
# default_batch_fetch_size: 100
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: trace
2. 영속성 컨텍스트
이 말을 이해하기 위한 배경으로는 jpa가 무엇인지를 생각해봐야 한다.
2-1. JPA와 ORM
JPA : Java Persistence API
이 Persistence(영속성)이라는게 처음 접하면 상당히 생소한 개념이다.
나는 이걸 같은 내용이지만 다른 문장으로 이해를 했다.
"저장하는 성질"
즉 데이터를 저장하는 API다.
그러면 도대체 JPA를 사용하는 것이 왜 좋은가?
이 모든 것을 자동으로 해주기 때문이다. 변경 감지(dirty check)부터 저장까지 원래대로면 사용자가 직접 쿼리를 날려서 db에 저장을 해야 했지만 이 프로세스를 편하게 구축해 둔 API다.
그러면 이게 코드 상에서는 어떻게 흘러가는가? 우리는 이걸 ORM이라고 부른다.
ORM : Object Relationship Mapping
즉 객체와 관계를 맺어 각 데이터를 객체 단위로 다루는 것이다.
다시 정리를 하면 JPA는 java에서 사용하는 ORM 기술이다.
2-2. 코드와 함께
나는 앞으로 편의점을 운영하는 백엔드 서비스를 구축하려고 한다.
아래와 같이 직원에 대한 엔티티가 존재한다고 생각하자.
@Entity
@Getter
public class Staff {
@Id
@GeneratedValue
private Long id;
@Column(name = "name")
private String name;
@Column(name = "role")
@Enumerated(value = EnumType.STRING)
private StaffRole role;
}
직원에 대해 식별 id, 이름, 역할로 구분한다고 생각하자.(시급까지 들어가면 좋겠지만 최대한 간단하게 가져가려고 한다.)
그러면 이게 db에 저장이 되어야 할 텐데 저장을 할 수 있는 코드를 작성해 보자.
public interface StaffRepository {
Long save(Staff staff);
}
@Repository
@RequiredArgsConstructor
public class StaffRepositoryImpl implements StaffRepository{
private final EntityManager em;
@Override
public Long save(Staff staff) {
em.persist(staff);
return find(staff.getId()).getId();
}
@Override
public Staff find(Long id) {
return em.find(Staff.class, id);
}
}
여기서 save를 위해 EntityManager에서 persist를 호출하고 있다.
EntityManager는 말 그대로 Entity들을 관리하는 매니저 클래스이고 persist는 일반 Entity객체를 영속성 컨텍스트 객체로 저장하기 위한 함수이다. 그렇다면 이 저장이 정상적으로 이루어지는지 테스트를 해보자.
@SpringBootTest
@Transactional
class StaffRepositoryImplTest {
@Autowired
private StaffRepository staffRepository;
@Test
public void 직원_저장_테스트() throws Exception{
// given
Staff staff1 = new Staff("직원1", StaffRole.NORMAL);
Staff staff2 = new Staff("매니저1", StaffRole.MANAGER);
// when
Long staff1Id = staffRepository.save(staff1);
Long staff2Id = staffRepository.save(staff2);
// then
Assertions.assertEquals(staff1.getId(), staff1Id);
Assertions.assertEquals(staff2.getId(), staff2Id);
}
}
여기서 쿼리에 집중해 보자.
Hibernate에서 insert문이 호출되고 있다. EntityManager를 사용하면 실제 db까지 자동으로 반영을 해주고 있다. 해당 쿼리는 persist가 호출되며 발생한 쿼리이다. 즉 우리가 만든 java객체가 실제 db까지 반영이 된다는 것이다.
그렇다면 이 객체는 동일 객체일까?
@Test
public void 직원_영속성_객체_테스트() throws Exception{
// given
Staff staff1 = new Staff("직원1", StaffRole.NORMAL);
Staff staff2 = new Staff("매니저1", StaffRole.MANAGER);
// when
Long staff1Id = staffRepository.save(staff1);
Long staff2Id = staffRepository.save(staff2);
Staff findStaff1 = staffRepository.find(staff1Id);
Staff findStaff2 = staffRepository.find(staff2Id);
System.out.println("staff1 :: " + staff1);
System.out.println("findStaff1 :: " + findStaff1);
System.out.println("staff2 :: " + staff2);
System.out.println("findStaff2 :: " + findStaff2);
// then
Assertions.assertEquals(staff1, findStaff1, "staff1의 해쉬코드가 일치해야합니다.");
Assertions.assertEquals(staff2, findStaff2, "staff2의 해쉬코드가 일치해야합니다.");
}
동일하다. 이 객체를 가지고 EntityManager에서 영속성 객체로 관리를 하고 있기 때문이다.
그렇다면 이 객체의 라이프 사이클은 어떻게 될까?
오라클 공식 문서를 살펴보았다.
비영속 상태(New/Transient)
우선 최초에 우리가 만든 객체는 비영속 상태로 존재한다.
영속 상태(Managed)
이 비영속 상태의 객체에서 EntityManager에 persist를 통해 객체를 영속성 컨텍스트 객체로 등록한다. 해당 상태가 Managed상태이다.
준영속 상태(Detached)
객체가 사라지지는 않았지만 EntityManager의 관리에서 벗어나 영속성 컨텍스트 객체가 아니게 된 상태이다.
삭제 상태(Removed)
해당 객체가 정말로 삭제가 된 상태이다. 이는 DB에서 Data까지 삭제됨을 말한다.
3. 요약 및 마무리
EntityManager는 Entity객체에 대해 영속성 컨텍스트로 관리를 하기 위한 매니저 클래스이다.
영속성 컨텍스트가 된 객체는 EntityManager에서 컨텍스트 생명주기를 관리함에 따라 각 변동이 발생할 시 반영까지 해주며 persist가 된 객체와 find로 찾아낸 객체는 동일 객체이다.
글을 작성하면서 코드는 현재 보시는 글 보다 템포가 더 빠르게 같이 만들어지고 있습니다.
https://github.com/0113bernoyoun/jpa-cvs