[Section3] Spring MVC - JPA 기반 액세스 계층
🧑🏻💻 TIL(Today I Learned)
✔️ JPA?, 엔티티 매핑
💡 JPA(Java Persistence API)란?
➡️ Java에서 사용하는 ORM(Object Relational Mapping) 기술의 표준 사양(명세, Specfication)
➡️ 표준 사양은 Java의 인터페이스로 사양이 정의되어 있기 때문에 JPA라는 표준 사양을 구현한 구현체는 따로 있다는 것 의미
표준 사양
: 특정 분야에서 사용되는 기술, 프로토콜, 언어 또는 인터페이스 등의 표준화된 명세
: 해당 분야의 다양한 기술 및 구현체들이 상호 운용성을 가질 수 있도록 일관된 규칙과 규격 제공하며 호환성을 확보하고 개발자들 간의 협업을 용이하게 함
🔎 Hibernate ORM
➡️ JPA에서 정의해 둔 인터페이스를 구현한 구현체
➡️ JPA에서 지원하는 기능 이외에 Hibernate 자체적으로 사용할 수 있는 API 또한 지원함
🔎 데이터 액세스 계층에서의 JPA
➡️ 데이터 저장, 조회 등의 작업은 JPA를 거쳐 구현체인 Hibernate ORM을 통해 이루어짐
➡️ Hibernate ORM은 내부적으로 JDBC API 이용해서 데이터베이스에 접근함
🔎 영속성 컨텍스트(Persistence Context)
Persistence
: 영속성, 지속성이라는 의미
: 무언가를 금방 사라지지 않고 오래 지속되게 한다!
➡️ JPA에서는 테이블과 매핑되는 엔티티 객체 정보를 영속성 컨텍스트에 보관하여 애플리케이션 내에 오래 지속되도록 함
➡️ 보관된 엔티티 정보는 데이터베이스 테이블에 데이터를 저장, 수정, 조회, 삭제 하는 데 사용됨
➡️ JPA API 중에서 엔티티 정보를 영속성 컨텍스트에 저장하는 API를 사용하면 영속성 컨텍스트의 1차 캐시에 엔티티 정보 저장됨
💡 JPA 실습
✔️ build.gradle 의존성 추가
➡️ 위와 같이 spring-boot-starter-data-jpa 를 추가하면 Spring Data JPA를 포함해서 JPA API를 사용할 수 있음
(Spring Data JPA가 아닌 JPA API만 사용하고 싶다면 별도의 라이브러리 추가해야 함 )
✔️ application.yml에 JPA 설정
- JPA에서 사용하는 엔티티 클래스를 정의하고 애플리케이션 실행 시, 이 엔티티와 매핑되는 테이블을 데이터베이스에 자동으로 생성
→ 이전에 배운 Spring Data JDBC에서는 직접 테이블을 생성하기 위한 스키마를 직접 지정해야했지만 JPA에서는 그럴 필요 없음 - JPA 동작과정을 이해하기 위해 JPA API 통해서 실행되는 SQL 쿼리 로그 출력해줌
✔️ Configuration 작성
@Configuration
// Spring에서 Bean 검색 대상인 Configuration 클래스로 간주
// (2)와 같이 @Bean 애너테이션이 추가된 메서드를 검색한 후 해당 메서드에서 리턴하는 객체 Spring Bean으로 추가해줌
public class JpaBasicConfig { // (1)
private EntityManager em;
private EntityTransaction tx;
@Bean // (2)
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
// CommandLineRunner 객체를 람다 표현식으로 정의해 주면 애플리케이션 부트스트랩이 완료된 후에 람다 표현식에 정의한 코드 실행
return args -> {
// TODO 이 곳에 학습할 코드를 타이핑하세요!
};
}
📍 CommandLineRunner
: 서버 구동 시점에 초기화 작업으로 무엇인가 넣고 싶다면 사용할 수 있는 방법 중 하나
: 이 인터페이스를 통해 빈은 애플리케이션 시작시 실행되거나 특정 작업 수행 가능
👉🏻 Callback used to run the bean
: 콜백은 특정한 상황이나 이벤트가 발생했을 때 실행되는 함수 또는 메서드 의미
: 즉 빈이 실행할 때 사용되는 콜백이라는 것, 콜백을 통해 빈이 실행되는 시점에 원하는 동작 수행 가능
✔️ 영속성 컨텍스트에 Member 엔티티 저장하기
package com.codestates.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Getter
@Setter
@NoArgsConstructor
@Entity // JPA에서 해당 클래스를 엔티티 클래스로 인식함
public class Member {
@Id // JPA에서 해당 클래스를 엔티티 클래스로 인식함
@GeneratedValue // 기본키가 되는 식별자를 자동으로 설정해줌
private Long memberId;
private String email;
public Member(String email) {
this.email = email;
}
}
package com.codestates.basic;
import com.codestates.entity.Member;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
@Configuration
// Spring에서 Bean 검색 대상인 Configuration 클래스로 간주
// (2)와 같이 @Bean 애너테이션이 추가된 메서드를 검색한 후 해당 메서드에서 리턴하는 객체 Spring Bean으로 추가해줌
public class JpaBasicConfig { // (1)
// JPA의 영속성 컨텍스트는 EntityManager 클래스에 의해서 관리됨
private EntityManager em;
@Bean // (2)
// EntityManagerFactory 객체를 Spring으로부터 DI 받을 수도 있음
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
// EntityManagerFactory의 craeateEntityManager() 메서드 이용해서 EntityManager 객체 얻기
// JPA의 API 사용가능
this.em = emFactory.createEntityManager();
// CommandLineRunner 객체를 람다 표현식으로 정의해 주면 애플리케이션 부트스트랩이 완료된 후에 람다 표현식에 정의한 코드 실행
return args -> {
example01();
};
}
private void example01() {
Member member = new Member("hgd@gmail.com");
// 영속성 컨텍스트에 member 객체 정보 저장 --> 1차 캐시에 엔티티 정보 저장
// member 객체는 쓰기 지연 SQL 저장소에 INSERT 쿼리 형태로 등록
// 하지만 em.persist(member) 호출할 경우 영속성 컨텍스트에 저장하지만 실제 테이블에 회원 정보를 저장하지는 않음
// 아래 로그를 확인하면 INSERT 쿼리 보이지 x
em.persist(member);
// member 객체가 잘 저장되었는지 find(Member.class, 1L) 메서드로 조회
// find(조회할 엔티티 클래스의 타입, 조회할 엔티티 클래스의 식별자 값)
Member resultMember = em.find(Member.class, 1L);
System.out.println("Id: " + resultMember.getMemberId() + ", email: " + resultMember.getEmail());
}
}
🔻 실행 결과
Hibernate: drop table if exists member CASCADE Hibernate: drop sequence if exists hibernate_sequence Hibernate: create sequence hibernate_sequence start with 1 increment by 1 Hibernate: create table member (member_id bigint not null, email varchar(255), primary key (member_id))
...
Hibernate: call next value for hibernate_sequence Id: 1, email: hgd@gmail.com
➡️ ID 가 1인 Member의 email 주소를 영속성 컨텍스트에서 조회하고 있는 것을 알 수 있음
➡️ memeber 객체 정보를 출력하는 로그에서 JPA가 내부적으로 테이블을 자동 생서하고 테이블의 기본키를 할당해주는 것을 확인할 수 있음
✔️ member 정보 실제 테이블에 저장하기
package com.codestates.basic;
import com.codestates.entity.Member;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
@Configuration
// Spring에서 Bean 검색 대상인 Configuration 클래스로 간주
// (2)와 같이 @Bean 애너테이션이 추가된 메서드를 검색한 후 해당 메서드에서 리턴하는 객체 Spring Bean으로 추가해줌
public class JpaBasicConfig { // (1)
// JPA의 영속성 컨텍스트는 EntityManager 클래스에 의해서 관리됨
private EntityManager em;
private EntityTransaction tx;
@Bean // (2)
// EntityManagerFactory 객체를 Spring으로부터 DI 받을 수도 있음
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
// EntityManagerFactory의 craeateEntityManager() 메서드 이용해서 EntityManager 객체 얻기
// JPA의 API 사용가능
this.em = emFactory.createEntityManager();
// EntityManager 통해서 Transaction 객체를 얻음
// JPA에서는 Transaction 객체를 기준으로 데이터베이스의 테이블에 데이터 저장함
this.tx = em.getTransaction();
// CommandLineRunner 객체를 람다 표현식으로 정의해 주면 애플리케이션 부트스트랩이 완료된 후에 람다 표현식에 정의한 코드 실행
return args -> {
example02();
};
}
private void example02() {
// Transaction을 시작하기 위한 begin() 메서드 호출
tx.begin();
Member member = new Member("hgd@gmail.com");
// member 객체 영속성 컨텍스트에 저장
em.persist(member);
// commit() 메서드 호출하는 시점에 영속성 컨텍스트에 저장되어 member 객체를 데이터베이스의 테이블에 저장
tx.commit();
// 아래 find() 메서드를 호출하면 영속성 컨텍스트에 저장한 member 객체 1차 캐시에서 조회
// 이미 1차 캐시에 member 객체 정보가 있기 때문에 테이블에 SELECT 쿼리 전송 x
Member resultMember1 = em.find(Member.class, 1L);
System.out.println("Id: " + resultMember1.getMemberId() + ", email: " + resultMember1.getEmail());
// 식별자 값이 2L인 member 객체 조회
// 하지만 존재하지 않기 때문에 다음 코드의 결과는 true
// 영속성 컨텍스트에 존재하지 않기 때문에 테이블에 직접 SELECT 쿼리 전송함
Member resultMember2 = em.find(Member.class, 2L);
System.out.println(resultMember2 == null);
}
}
➡️ tx.commit() 했기 때문에 member에 대한 INSERT 쿼리는 실행되어 쓰기 지연 SQL 저장소에서 사라짐
🔻 실행 결과
Hibernate: drop table if exists member CASCADE
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table member (member_id bigint not null, email varchar(255), primary key (member_id))
Hibernate: call next value for hibernate_sequence
Hibernate: insert into member (email, member_id) values (?, ?)
Id: 1, email: hgd@gmail.com
Hibernate: select member0_.member_id as member_i1_0_0_, member0_.email as email2_0_0_ from member member0_ where member0_.member_id=? true
➡️ 위 실행 결과를 보면 SELECT 쿼리가 실행된 것을 알 수 있음
→ 2L에 해당하는 memeber2 객체가 영속성 컨텍스트 안에 없기 때문에 추가적으로 테이블에서 한번 더 조회하는 것!
🏁 핵심
- em.persist()를 호출하면 영속성 컨텍스트의 1차 캐시에 엔티티 클래스의 객체가 저장되고 쓰기 지연 SQL 저장소에 INSERT 쿼리가 등록됨
- tx.commit()을 하는 순간 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리가 실행되고 실행된 INSERT 쿼리는 쓰기 지연 SQL 저장소에서 제거됨
- em.find() 호출하면 먼저 1차 캐시에서 해당 객체가 있는지 조회하고 없으면 테이블에 SELECT 쿼리 전송해서 조회
✔️ 쓰기 지연을 통한 영속성 컨텍스트와 테이블에 엔티티 일괄 저장
private void example03() {
tx.begin();
Member member1 = new Member("hgd1@gmail.com");
Member member2 = new Member("hgd2@gmail.com");
// member1, member2 객체 영속성 컨텍스트에 저장
em.persist(member1);
em.persist(member2);
// 아래 메서드가 호출됨과 동시에 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리 모두 실행되고 실행된 쿼리 제거
tx.commit();
}
✔️ 테이블에 저장된 데이터 JPA 이용해서 업데이트하기
private void example04() {
tx.begin();
// member 객체 영속성 컨텍스트 1차 캐시에 저장
em.persist(new Member("hgd@gmail.com"));
//영속성 컨텍스트 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리 실행
// --> 저장
tx.commit();
tx.begin();
// 저장된 member 객체를 1차 캐시에서 조회 --> 테이블에서 조회 x
Member member1 = em.find(Member.class, 1L);
// setter 메서드로 정보 업데이트
member1.setEmail("hgd@yahoo.co.kr");
// 쓰기 지연 SQL 저장소에 등록된 UPDATE 쿼리 실행
tx.commit();
}
UPDATE 쿼리 실행 과정
-> 영속성 컨텍스트에 엔티티가 저장될 경우에 저장되는 시점의 상태 그대로 가지고 있는 스냅샷 생성함
-> 후에 해당 엔티티의 값을 setter 메서드로 변경한 후 tx.commit() 을 하면 변경된 엔티티와 전에 떠놓은 스냅샷을 비교한 후 변경된 값이 있다면 쓰기 지연 SQL 저장소에 UPDATE 쿼리를 등록하고 UPDATE 쿼리 실행함
✔️ 영속성 컨텍스트와 테이블의 엔티티 삭제
private void example05() {
tx.begin();
em.persist(new Member("hgd@gmail.com"));
tx.commit();
tx.begin();
Member member = em.find(Member.class, 1L);
// 아래 메서드를 통해 영속성 컨텍스트의 1차 캐시에 있는 엔티티 제거 요청
// 쓰기 지연 SQL 저장소에 DELETE 쿼리 등록
em.remove(member);
// 영속성 컨텍스트의 1차 캐시에 있는 엔티티를 제거
// 쓰기 지연 SQL 저장소에 등록된 DELETE 쿼리 실행
tx.commit();
}
EntityManager의 flush() API
: tx.commit() 메서드가 호출되면 JPA 내부적으로 em.flush() 호출하여 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영함
'SEB_BE_45 > 공부 정리' 카테고리의 다른 글
[Section3] Spring MVC - 테스팅(Testing) (0) | 2023.06.28 |
---|---|
[Section3] Spring MVC - 트랜잭션(Transaction) (0) | 2023.06.26 |
[Section3] Spring MVC - JDBC 기반 데이터 액세스 실습 (0) | 2023.06.21 |
[Section3] Spring MVC - 비즈니스 로직에 대한 예외처리 (0) | 2023.06.15 |
[Sectoion3] Spring MVC - 예외 처리 (0) | 2023.06.14 |