[Section3] Spring MVC - JPA 기반 액세스 계층

2023. 6. 21. 16:31

🧑🏻‍💻 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 위치

➡️ 데이터 저장, 조회 등의 작업은 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 설정

  1. JPA에서 사용하는 엔티티 클래스를 정의하고 애플리케이션 실행 시,  이 엔티티와 매핑되는 테이블을 데이터베이스에 자동으로 생성
    → 이전에 배운 Spring Data JDBC에서는 직접 테이블을 생성하기 위한 스키마를 직접 지정해야했지만 JPA에서는 그럴 필요 없음 
  2. 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

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() 호출하여 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영함

 

BELATED ARTICLES

more