블로그 이름 뭐하지
[Spring] JPA 본문
JDBC가 아닌 JPA를 사용하는 이유
JDBC는 SQL 의존적이라 변경에 취약하기 때문이다.
public class Memo {
private Long id;
private String username;
private String contents;
}
예를 들어 해당 객체 데이터를 DB에 저장하고 싶다면 JDBC에서는 아래와 같은 처리 과정을 거친다.
//1. DB 테이블 생성
create table memo (
id bigint not null auto_increment,
contents varchar(500) not null,
username varchar(255) not null,
primary key (id)
);
//2. application에서 SQL 작성
String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
String sql = "SELECT * FROM memo";
//3. SQL을 JDBC로 실행
jdbcTemplate.update(sql, "Robbie", "오늘 하루도 화이팅!");
jdbcTemplate.query(sql, ...);
//4. SQL 결과를 객체로 직접 생성
@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String username = rs.getString("username");
String contents = rs.getString("contents");
return new MemoResponseDto(id, username, contents);
}
이 상태에서 DB에 password를 추가한다면, 테이블도 새로 생성하고, SQL과 결과값도 수정해야하는 번거로움이 생긴다.
JPA를 사용하면 DB와 Java의 매핑을 통해 SQL문을 자동으로 생성하고,
객체를 통해 간접적으로 DB 데이터를 다룰 수 있어, 데이터 변경에 유리하다.
ORM(Object-Relational-Mapping)
자바의 객체와 DB를 연결하는 프로그래밍 기법
객체와 DB를 연결하여 간단한 SQL을 자바언어로만 다룰 수 있게 한다.
데이터 베이스 시스템이 추상화 되어 있어 데이터 베이스 시스템에 대한 종속성이 줄어든다.
JPA
Java의 ORM 기술 표준 명세.
Java에서 DB를 사용하는 방식을 정의한 인터페이스의 모음이다.
대표적으로 ORM 프레임 워크인 Hibernate를 사용하여 JPA 인터페이스를 구현한다.
JPA는 application과 JDBC Api 사이에서 동작하며,
JPA를 사용할 시 JPA 내부에서 JDBC Api를 사용하여 SQL을 생성하고 DB와 통신하는 방식을 취한다.
JPA 필요한 설정
1) 프로젝트 생성 시
- Language: Java
- Build system: Gradle
- JDK: 17
- Gradle DSL: Groovy
2) src > main > resources > META-INF > persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="memo">
<class>com.sparta.entity.Memo</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.user" value="{지정해둔 sql ID, 보통은 root}"/>
<property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/{만들어둔 db명}"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
</properties>
</persistence-unit>
</persistence>
3) build.gradle
// JPA 구현체인 hibernate
implementation 'org.hibernate:hibernate-core:6.1.7.Final'
// MySQL
implementation 'mysql:mysql-connector-java:8.0.28'
// mysql의 버전이 8.1 이상인 경우
// implementation 'com.mysql:mysql-connector-j:8.2.0'
영속성 컨텍스트(Persistence Context)
Entity 객체를 보다 효율적으로 쉽게 관리하기 위해 만들어진 공간
JPA를 사용하면서 개발자들은 직접 SQL을 작성하지 않아도 DB에 데이터를 CRUD하는 것이 가능해졌다.
위에서 설명한 과정을 처리하기 위해서 JPA는 영속성 컨텍스트에 Entity 객체를 저장하여 DB와 소통한다.
Entity
DB의 테이블과 매핑되어 JPA에 의해 관리되는 객체
일반적인 자바 객체와 크게 차이는 없고, 여러 어노테이션을 통해 DB와 연결된다.
package com.sparta.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
@Id
private Long id;
// nullable: null 허용 여부
// unique: 중복 허용 여부 (false 일때 중복 허용)
@Column(name = "username", nullable = false, unique = true)
private String username;
// length: 컬럼 길이 지정
@Column(name = "contents", nullable = false, length = 500)
private String contents;
}
Annotation | 설명 | |
@Entity(name = "클래스명") | JPA가 관리할 수 있는 Entity 클래스로 지정한다. name 으로 클래스 이름을 지정할 수 있다. 지정하지 않으면 default 값은 해당 클래스 명이 된다. |
|
@Table(neme = "테이블명") | 매핑할 테이블의 이름을 지정한다. name으로 테이블의 이름을 지정할 수 있다. 지정하지 않으면 defalut 값은 해당 Entity의 이름이 된다. |
|
@Column | (name= "컬럼명") | 필드와 매핑할 테이블의 컬럼을 지정 (default = 객체의 필드명) |
(nullable= "true") | 데이터의 null값 허용 여부 지정 (default = true) | |
(unique= "true") | 데이터의 중복값 여부 지정 (default = false) | |
(length= 50) | 데이터의 값(문자)의 길이 지정 (default = 225) | |
@Id | 테이블의 기본 키를 지정한다. Entity를 구분하고 관리할 때 사용되는 식별자 역할이다. |
|
@GenerateValue (strategy = GenerationType.IDENTITY) | 해당 옵션을 기본 키 값에 추가 시 기본 키 생성을 DB에 위임한다. (strategy = GenerationType.IDENTITY) 는 auto_increment 설정이다. |
EntityManager
Entity들을 관리하는 역할을 하며, Entity를 저장, 조회, 수정, 삭제(CRUD)한다.
영속성 컨텍스트 안에 Entity를 저장한다.
EntityManagerFactory를 통해 생성하여 사용할 수 있다.
EntityManagerFactory
일반적으로 DB 하나에 하나만 생성되며, 애플리케이션이 동작하는 동안 사용된다.
트랜잭션(Transaction)
SQL의 Select, Insert, Delete, Update를 이용할 때, DB의 상태가 변화된다.
트랜잭션은 DB의 상태를 변화시키기 위해 수행하는 하나의 작업 단위이다.
작업의 단위는 해당 SQL의 명령문 하나가 아닌 여러 개를 뜻한다.
예를 들어 상품을 구매할 때,
해당 상품의 재고가 남아있는지 확인하고, (Select)
나의 잔고를 확인하고, (Select)
해당 상품의 재고를 하나 삭제하고 (Delete)
상품을 결제하여 내가 결제한 상품란에 넣는다. (Insert)
이런 일련의 과정을 모두 하나의 작업단위, 트랜잭션이라고 한다.
트랜잭션에서는 명령문 모두가 성공적으로 수행된 경우에만 commit(저장)하고,
아닌 경우에는 모든 변경을 rollback(이전상태로 되돌림)한다.
부분적으로 갱신되어 벌어지는 문제들을 막는 것이다.
예를 들어 어떤 문제로 상품 결제가 취소되었다면, 재고의 삭제와 내 통장의 인출 또한 취소 되어야 한다.
하지만 이미 삭제와 인출이 시행된 경우에는 되돌리기가 번거로워진다.
그래서 트랜잭션은 모든 작업이 성공적으로 수행된 경우에만 삭제와 인출을 진행하는 것이다.
JPA에서는 해당 트랜잭션의 특징을 반영하여, 영속성 컨텍스트로 관리하고 있는 Entity의 변경이 발생하면
해당 정보를 쓰기 지연 저장소에 전부 가지고 있다가 마지막에 SQL을 모두 DB로 보내 변경을 요청한다.
@Test
@DisplayName("EntityTransaction 성공 테스트")
void test1() {
EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.
et.begin(); // 트랜잭션을 시작합니다.
try { // DB 작업을 수행합니다.
Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
memo.setId(1L); // 식별자 값을 넣어줍니다.
memo.setUsername("Robbie");
memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");
em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.
et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
// commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
} finally {
em.close(); // 사용한 EntityManager 를 종료합니다.
}
emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}
영속성 컨텍스트의 기능
1차 캐시
영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있다.
Entity 객체들은 1차 캐시, 즉 캐시 저장소에 저장된다.
1차 캐시는 Map 자료구조 형태로 key에는 @Id 로 매핑한 식별자 값을, value에는 해당 클래스의 객체를 저장한다.
1) Entity 저장 시
새로운 객체가 영속상태가 될 때(persist) 해당 객체를 1차 캐시에 저장한다.
@Test
@DisplayName("1차 캐시 : Entity 저장")
void test1() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo();
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("1차 캐시 Entity 저장");
em.persist(memo);
et.commit();
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
2) Entity 조회 시
a. 해당 ID가 1차 캐시에 없을 때
1차 캐시에 조회(find) 했을 때, 해당 Id가 없다면 DB Select로 조회 후 캐시 저장소에 저장하고 값을 반환한다.
DB에서 데이터 조회만 하는 경우에는 데이터의 변경이 발생하지 않으므로 트랜잭션이 없어도 조회가 가능하다.
@Test
@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하지 않은 경우")
void test2() {
try {
Memo memo = em.find(Memo.class, 1);
System.out.println("memo.getId() = " + memo.getId());
System.out.println("memo.getUsername() = " + memo.getUsername());
System.out.println("memo.getContents() = " + memo.getContents());
} catch (Exception ex) {
ex.printStackTrace();
} finally {
em.close();
}
emf.close();
}
b. 해당 ID가 1차 캐시에 있을 때
1차 캐시에 조회(find) 했을 때, 해당 Id가 있다면 1차 캐시에서 바로 가져와 반환한,다.
@Test
@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하는 경우")
void test3() {
try {
Memo memo1 = em.find(Memo.class, 1);
System.out.println("memo1 조회 후 캐시 저장소에 저장\n");
Memo memo2 = em.find(Memo.class, 1);
System.out.println("memo2.getId() = " + memo2.getId());
System.out.println("memo2.getUsername() = " + memo2.getUsername());
System.out.println("memo2.getContents() = " + memo2.getContents());
} catch (Exception ex) {
ex.printStackTrace();
} finally {
em.close();
}
emf.close();
}
1차 캐시 사용의 장점
1) DB 조회 횟수를 줄인다
2) DB row 1개 당 객체 1개가 사용되도록 해, 객체의 동일성을 보장한다
@Test
@DisplayName("객체 동일성 보장")
void test4() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo3 = new Memo();
memo3.setId(2L);
memo3.setUsername("Robbert");
memo3.setContents("객체 동일성 보장");
em.persist(memo3);
Memo memo1 = em.find(Memo.class, 1);
Memo memo2 = em.find(Memo.class, 1);
Memo memo = em.find(Memo.class, 2);
System.out.println(memo1 == memo2); //Id: 1로 조회해서 동일한 객체 > true
System.out.println(memo1 == memo); //다른 Id로 조회해서 다른 객체 > false
et.commit();
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
3) Entity 삭제 시
삭제할 Entity를 1차 캐시에서 먼저 조회(find) 후, 없으면 DB에서 가져와 저장한다.
이후 삭제할 Entity를 Delete 상태로(remove) 만들고, 트랜잭션 commit 후 요청한 SQL이 DB로 전송된다.
@Test
@DisplayName("Entity 삭제")
void test5() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = em.find(Memo.class, 2);
em.remove(memo);
et.commit();
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
쓰기 지연 저장소
JPA는 트랜잭션처럼 SQL을 모아서 commit 후, 한 번에 DB에 반영한다.
이를 구현하기 위해 SQL을 모아두는 장소가 쓰기지연 저장소이다.
@Test
@DisplayName("쓰기 지연 저장소 확인")
void test6() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo();
memo.setId(2L);
memo.setUsername("Robbert");
memo.setContents("쓰기 지연 저장소");
em.persist(memo);
Memo memo2 = new Memo();
memo2.setId(3L);
memo2.setUsername("Bob");
memo2.setContents("과연 저장을 잘 하고 있을까?");
em.persist(memo2);
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
트랜잭션 Commit 호출 전까지는 SQL 요청을 하지 않다가 Commit 이후 Insert SQL 2개가 순서대로 요청되었다.
commit 전까지는 쓰기 지연 저장소에 SQL을 모아 두었다가 한 번에 요청을 보낸 것이다.
flush()
트랜잭션 commit 이후 추가적인 동작으로, 영속성 컨텍스트의 변경 내용을 DB에 반영한다.
즉, 쓰기 지연 저장소의 SQL 들을 DB에 요청하는 역할을 수행한다.
@Test
@DisplayName("flush() 메서드 확인")
void test7() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo();
memo.setId(4L);
memo.setUsername("Flush");
memo.setContents("Flush() 메서드 호출");
em.persist(memo);
System.out.println("flush() 전");
em.flush(); // flush() 직접 호출
System.out.println("flush() 후\n");
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
flush() 메서드가 호출되자 DB에 쓰기지연 저장소의 SQL이 요청되었다.
이미 쓰기 지연 저장소의 SQL이 요청되어서 트랜잭션이 commit 된 후에도 SQL 기록이 보이지 않는다.
트랜잭션을 설정하지 않고, flush 메서드를 호출하면 TransactonRequiredException 오류가 발생하므로
데이터가 변경하는 Insert, Update, Delete 의 경우에는 트랜잭션이 필요하다.
변경감지(Dirty Checking)
영속성 컨텍스트에 저장된 Entity가 변경될 때마다 Update SQL을 쓰기 지연 저장소에 저장하는 것은 비효율적이다.
JPA에서는 영속성 컨텍스트에 Entity를 저장할 때 최초의 상태(LocatedState)를 저장한다.
트랜잭션이 commit되고 flush가 호출되면 Entity의 현재 상태와 최초의 상태를 비교한다.
이에 변경 내용이 있으면 Update SQL을 생성하여 쓰기 지연 저장소에 저장 후,
모든 쓰기 지연 저장소의 SQL을 DB에 요청한다.
마지막으로 DB의 트랜잭션이 commit되며 DB에 반영된다.
즉, 변경하고 싶은 데이터를 먼저 조회 후 해당 Entity 객체의 데이터를 변경하면
자동으로 SQL이 생성되고 DB에 반영되는 형태이다.
@Test
@DisplayName("변경 감지 확인")
void test8() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
System.out.println("변경할 데이터를 조회합니다.");
Memo memo = em.find(Memo.class, 4);
System.out.println("memo.getId() = " + memo.getId());
System.out.println("memo.getUsername() = " + memo.getUsername());
System.out.println("memo.getContents() = " + memo.getContents());
System.out.println("\n수정을 진행합니다.");
memo.setUsername("Update");
memo.setContents("변경 감지 확인");
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
Entity의 상태
1) 비영속(Transient)
new 연산자를 통해 인스턴스화 된 Entity 객체.
아직 영속성 컨텍스트에 저장되지 않아 JPA의 관리를 받지 않는다.
2) 영속(Managed)
persist(entity) : 비영속 상태의 Entity를 Entity Mananger를 통해 영속성 컨텍스트에 저장하여 관리되는 상태로 만든다.
@Test
@DisplayName("비영속과 영속 상태")
void test1() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo(); // 비영속 상태
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("비영속과 영속 상태");
em.persist(memo);// 영속 상태
et.commit();
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
3) 준영속(Detached)
영속 상태였다가 분리된 상태를 말한다.
▶영속 상태 > 준영속 상태
detach(entity) : 특정 Entity만 준영속 상태로 전환
@Test
@DisplayName("준영속 상태 : detach()")
void test2() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = em.find(Memo.class, 1);
// em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
System.out.println("em.contains(memo) = " + em.contains(memo)); // true (영속상태)
System.out.println("detach() 호출");
em.detach(memo); // 영속 상태 > 준영속 상태
System.out.println("em.contains(memo) = " + em.contains(memo)); // false (준영속상태)
System.out.println("memo Entity 객체 수정 시도");
memo.setUsername("Update");
memo.setContents("memo Entity Update");
System.out.println("트랜잭션 commit 전");
et.commit(); // 준영속상태의 entity는 JPA의 변경감지 기능을 사용할 수 없어 SQL 수행x
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
clear() : 영속성 컨텍스트의 모든 Entity를 준영속 상태로 전환,
영속성 컨텍스트 틀은 유지하나 내용은 초기화 한 것이라 생각하면 좋다.
@Test
@DisplayName("준영속 상태 : clear()")
void test3() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo1 = em.find(Memo.class, 1);
Memo memo2 = em.find(Memo.class, 2);
System.out.println("em.contains(memo1) = " + em.contains(memo1)); //true
System.out.println("em.contains(memo2) = " + em.contains(memo2)); //true
System.out.println("clear() 호출");
em.clear(); // 모든 entity를 준영속상태로 만들고 영속성 컨텍스트 초기화
System.out.println("em.contains(memo1) = " + em.contains(memo1)); //false
System.out.println("em.contains(memo2) = " + em.contains(memo2)); //false
System.out.println("memo#1 Entity 다시 조회");
Memo memo = em.find(Memo.class, 1);
// find시 DB에 저장된 ID를 바탕으로 다시 1차 캐시에 해당 Entity를 저장하게 되므로
// 영속상태가 된다.(Detach도 마찬가지)
System.out.println("em.contains(memo) = " + em.contains(memo)); // true
System.out.println("\n memo Entity 수정 시도");
memo.setUsername("Update");
memo.setContents("memo Entity Update");
System.out.println("트랜잭션 commit 전");
et.commit(); // 다시 영속상태가 되어 updateSQL도 실행
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
close(): 영속성 컨텍스트의 모든 Entity를 준영속 상태로 전환,
영속성 컨텍스트를 종료하여 영속성 컨텍스트를 더이상 사용할 수 없다.
@Test
@DisplayName("준영속 상태 : close()")
void test4() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo1 = em.find(Memo.class, 1);
Memo memo2 = em.find(Memo.class, 2);
System.out.println("em.contains(memo1) = " + em.contains(memo1)); // true
System.out.println("em.contains(memo2) = " + em.contains(memo2)); // true
System.out.println("close() 호출");
em.close(); // 모든 Entity를 준영속 상태로 바꾸고, 영속성 컨텍스트 종료
Memo memo = em.find(Memo.class, 2);
// Session/EntityManager is closed 메시지와 함께 오류 발생
// (영속성 컨텍스트가 종료되어 사용할 수 없기 때문임)
System.out.println("memo.getId() = " + memo.getId());
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
▶준영속 상태 > 영속 상태
merge(entity) : 전달받은 Entity를 사용하여 새로운 상태의 Entity를 반환한다.
동작방식은 다음과 같다.
파라미터로 전달된 Entity의 Id 값으로 영속성 컨텍스트를 조회한다.
해당 Entity가 영속성 컨텍스트에 없으면 DB에서 조회 후,
조회한 Entity를 영속성 컨텍스트에 저장하고 병합하여 update SQL을 수행한다.(수정)
해당 Entity가 DB에도 없다면 새롭게 Entity를 생성하고 영속성 컨텍스트에 저장 후 Insert SQL을 수행한다(저장)
이처럼 merge는 비영속, 준영속 모두 파라미터로 받을 수 있으며 상황에 따라 수정과 저장을 할 수 있다.
merge의 주의할 점은 비영속, 준영속 상태일 때의 객체와 merge 이후의 객체가 다르다는 점이다.
앞서 배운 detact(), close() 이후 find()로 영속상태로 전환되는 경우,
JVM의 힙 메모리에 남아있던 준영속 상태의 객체를 다시 연결하여 영속 상태로 만들어 주므로,
준영속 상태가 되기 이전과 find 이후의 객체가 동일하다.
하지만 merge의 경우 준영속, 비영속 상태의 객체 내용을 새로 생성될 객체에 복사 한 후
영속성 컨텍스트에 새로운 객체를 반환하는 형식으로, 두 객체는 완전히 다른 객체로서 존재하게 된다.
@Test
@DisplayName("merge() : 저장")
void test5() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo(); // 비영속 상태
memo.setId(3L);
memo.setUsername("merge()");
memo.setContents("merge() 저장");
System.out.println("merge() 호출");
Memo mergedMemo = em.merge(memo); // merge로 영속상태로 변환 > Insert SQL
System.out.println("em.contains(memo) = " + em.contains(memo));
// false >> memo는 여전히 비영속 객체인 상태이다.
System.out.println("em.contains(mergedMemo) = " + em.contains(mergedMemo));
// true >> memo 객체의 내용을 복사하여 새로운 객체를 영속성 컨텍스트에 연결했으므로
// memo와 mergedMemo는 다른 객체이며, mergedMemo는 영속 상태로 존재한다.
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
@Test
@DisplayName("merge() : 수정")
void test6() {
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = em.find(Memo.class, 3);
System.out.println("em.contains(memo) = " + em.contains(memo)); //true
System.out.println("detach() 호출");
em.detach(memo); // 준영속 상태로 전환
System.out.println("em.contains(memo) = " + em.contains(memo)); //false
System.out.println("준영속 memo 값 수정");
memo.setContents("merge() 수정");
System.out.println("\n merge() 호출");
Memo mergedMemo = em.merge(memo); //영속상태로 전환
System.out.println("mergedMemo.getContents() = " + mergedMemo.getContents());
// merge()수정 >> memo가 준영속상태일때 수정한 것이 반영되는 이유
// memo는 준영속상태의 객체로서 영속성 컨텍스트와 무관한 객체로 존재한다.
// 해당 수정 사항은 memo 객체에만 반영된 것이다.
// merge는 memo 객체의 모든 필드 값을 새로운 객체로 복사하므로,
// 수정된 memo 객체를 반영하게 되고 트랜잭션 commit 시점에 update SQL이 날아가게 된다.
System.out.println("em.contains(memo) = " + em.contains(memo));//false
System.out.println("em.contains(mergedMemo) = " + em.contains(mergedMemo));//true
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
4) 삭제(Removed)
remove(entity) : 삭제하기 위해 조회해 온 영속상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환한다.
SpringBoot JPA
필요한 설정
1) build.gradle : spring-boot-starter-data-jpa 추가
// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2) application.properties : Hibernate 설정
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
- show_sql, format_sql, use_sql_comments 설정
:Hibernate가 DB에 요청하는 모든 SQL을 보기 좋게 출력함 - ddl-auto
create : 기존 테이블을 삭제 후 다시 생성(Drop + Create)
create-drop : create와 같으나 종료 시점에 테이블을 Drop
update : 변경된 부분만 반영
validate : Entity와 테이블이 정상 매핑되었는지만 확인
none : 아무것도 하지 않음
SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성해준다.
(application.properties에 DB 정보를 입력해주면 이를 토대로 EntityMangerFactory 생성)
@PersistenceContext 애너테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용할 수 있다.
@PersistenceContext
EntityManager em;
Spring 프레임워크에서는 DB의 트랜잭션 개념을 적용할 수 있도록 트랜잭션 애너테이션을 제공한다.
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
...
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
...
}
@Transactional 애너테이션을 클래스나 메서드에 추가시 트랜잭션 개념을 적용할 수 있다.
readOnly = true 옵션은 트랜잭션에서 데이터를 읽을 때 사용되며, 데이터를 수정하려하면 예외가 발생한다.
save 메서드는 @Transactional이 추가되어 있고, readOnly = true 옵션인 @Transactional을 덮어쓰게 되어
readOnly = false 옵션으로 적용된다.
@Test
@Transactional
@Rollback(value = false)
// 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
@DisplayName("메모 생성 성공")
void test1() {
Memo memo = new Memo();
memo.setUsername("Robbert");
memo.setContents("@Transactional 테스트 중!");
em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
@Test
@DisplayName("메모 생성 실패")
void test2() {
Memo memo = new Memo();
memo.setUsername("Robbie");
memo.setContents("@Transactional 테스트 중!");
em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
전자는 트랜잭션 적용 성공 테스트고 후자는 실패 테스트이다.
@Transaction을 사용하지 않아 실패한 것인데, 이처럼 데이터를 저장, 수정, 삭제할 때는 트랜잭션 적용이 필요하다.
조회의 경우에도 트랜잭션 환경이 필요할 때가 있는데, 이때는 readOnly = true 옵션을 설정하면 좋다.
영속성 컨텍스트와 트랜잭션의 생명주기
스프링 컨테이너 환경에서는 영속성 컨텍스트와 트랜잭션의 생명주기가 일치한다.
트랜잭션이 유지되는 동안은 영속성 컨텍스트의 기능을 사용할 수 있다.
트랜잭션이 Service부터 Repository까지 유지될 수 있는 이유는 트랜잭션 전파 기능 때문이다.
@Transaction은 트랜잭션 전파 옵션을 지정할 수 있는데, 디폴트 값은 Propagation.REQUIRED이며
부모 메서드에 트랜잭션이 존재하면 자식 메서드의 트랜잭션이 부모의 트랜잭션에 합류한다.
Spring Data JPA
JPA를 쉽게 사용할 수 있게 만들어 둔 하나의 모듈이다.
JPA를 추상화시킨 Repository 인터페이스를 제공하며,
Repository 인터페이스는 Hibernate와 같은 JPA 구현체를 사용한다.
Spring Data JPA에서는 JpaRepository 인터페이스를 구현하는 클래스를
자동으로 생성한다.
Spring 서버가 뜰 때, JpaRepository를 상속받은 인터페이스가 스캔이 되면
해당 인터페이스 정보를 토대로 SimpleJpaRepository 클래스를 생성하고,
해당 클래스를 Bean으로 등록한다.
이에 구현 클래스를 따로 작성하지 않아도 JpaRepository 인터페이스를 통해 JPA의 기능을 사용할 수 있다.
Spring Data JPA 사용방법
JpaRepository<"@Entity 클래스", "@Id의 데이터 타입">을 상속받는 Interface로 선언한다.
public interface MemoRepository extends JpaRepository<Memo, Long> {}
Spring Data Jpa 에 의해 자동으로 Bean 등록이 되고,
Entity 클래스 위치에 Memo Entity를 추가하였으므로,
MemoRepository는 DB의 memo 테이블과 연결되어 CRUD를 처리한다.
1) save
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
Memo saveMemo = memoRepository.save(memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);
return memoResponseDto;
}
memoRepository의 save 메서드를 보면 영속성 컨텍스트에 Entity를 저장하는 코드가 작성되어 있고,
해당 메서드에 @Transactional이 적용되어있다.
save의 파라미터로는 저장하려는 entity 객체를 넣으면 된다.
save를 사용할 경우 persist()와 merge()의 기능을 전부 사용할 수 있다.
2) findAll
public List<MemoResponseDto> getMemos() {
// DB 조회
return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
}
조회이니 @Transactional은 적용되어 있지 않다.
해당 테이블의 전체 데이터를 조회할 수 있다.
3) findById
private Memo findMemo(Long id) {
return memoRepository.findById(id).orElseThrow(() ->
new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
);
}
반환타입이 Optional이며, 파라미터는 Entity의 Id 값이다.
Optional<Entity>를 반환타입으로 받고 null을 체크하거나, orElseThrow로 null일 경우 예외를 던지도록 처리한다.
4) update
@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findMemo(id);
// memo 내용 수정 >> repository가 아닌 entity 클래스 내에서 처리한다.
memo.update(requestDto);
return id;
}
SimpleJpaRepository에 update라는 메서드는 존재하지 않는다.
따라서 영속성 컨텍스트의 변경 감지를 통해 update를 진행하고, 변경감지를 위해 @Transactional을 추가한다.
5) delete
public Long deleteMemo(Long id) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findMemo(id);
// memo 삭제
memoRepository.delete(memo);
return id;
}
delete 메서드를 이용해 해당 Entity를 테이블에서 삭제할 수 있다.
파라미터로는 Entity 객체를 넣어준다.
save()와 같이 @Transactional 이 적용되어 있다.
JPA Auditing
데이터의 생성 수정 시간을 활용하기 위해 사용하는 기능이다.
개발자가 매번 일일히 작성할 수는 없으므로, Spring Data Jpa에서는 시간에 대해 자동으로 값을 넣어준다.
해당 기능을 사용하기 위해서는 @SpringBootApplication이 설정된 클래스에 @EnableJpaAuditing을 추가해야한다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate
@Column(updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createdAt;
@LastModifiedDate
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt;
}
- @MappedSuperClass
: Jpa Entity 클래스들이 해당 추상 클래스를 상속(extends)할 경우 createdAt, modifiedAt를 컬럼으로 인식한다. - @EntityListeners(AuditingEntityListener.class)
: 해당 클래스에 Auditig 기능을 포함시켜 준다 - @CreatedDate
: Entity 객체가 생성되어 저장될 때, 시간이 자동으로 저장된다.
최초 생성시간을 저장하고, 그 이후에는 수정이 불가하므로 updatable = false 옵션을 추가한다. - @LastModifiedDate
: 조회한 Entity 객체의 값을 변경할 때, 변경된 시간이 자동으로 저장된다.
처음 생성 시간이 저장된 이후 변경이 일어날 때마다 해당 변경시간으로 업데이트한다. - @Temporal
: 날짜 타입을 매핑할 때 사용한다
: 날짜타입(Date(2024-11-13), Time(20:12:23), Timestamp(2024-11-13 20:13:14.771100))
Query Methods
Spring Data JPA에서는 메서드의 이름으로 SQL을 생성할 수 있다.
//servive
public List<MemoResponseDto> getMemos() {
return memoRepository.findAllByOrderByModifiedAtDesc().stream().map(MemoResponseDto::new).toList();
}
//repository
package com.sparta.memo.repository;
import com.sparta.memo.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface MemoRepository extends JpaRepository<Memo, Long> {
List<Memo> findAllByOrderByModifiedAtDesc();
}
정의된 규칙에 맞게 메서드를 선언하면 해당 메서드의 이름을 분석하여 SimpleJpaRepository에서 구현한다.
예시로 제시된 findAllByOrderByModifiedAtDesc의 경우 Memo 테이블에서 수정시간을 기준으로 전체 데이터를 내림차순으로 가져오는 SQL을 실행하라는 뜻이다.
만약 List<Memo> findAllByUsername(String username) << 이렇게 Query Method를 선언했을 경우,
Username에 값을 전달해주기 위해 파라미터에 해당 값과 변수 타입을 지정해준다.
참고한 링크
스프링 데이터 JPA, 5분 만에 알아보기 | 요즘IT
스프링 데이터 JPA를 알려면 먼저 SQL을 몰라도 데이터베이스를 조작할 수 있게 해주는 편리한 도구인 ORM 개념을 알아야 합니다. 그러고 나서 JPA를 알아야 비로소 스프링 데이터 JPA를 알 수 있습
yozm.wishket.com
[JPA] commit, flush, Entity Manager의 clear()와 close()에서 궁금한 부분들 탐구 + 데이터 삭제 및 수정 시 1
🙋🏻♀️JPA에 관해 공부를 하다가 문득 궁금한게 생겼다. 우선 flush와 commit에 대해 어떤 내용을 공부했는지 간단히 살펴보자 ! 영속 컨텍스트는 엔티티를 식별자 값으로 구분한다. 따라서 영
velog.io
'Spring' 카테고리의 다른 글
[Spring] 쿠키와 세션, JWT (0) | 2024.11.13 |
---|---|
[Spring] Bean (0) | 2024.11.13 |
[Spring] IOC(제어의 역전)와 DI(의존성 주입) (0) | 2024.10.10 |
[Spring] 3 Layer Architecture (0) | 2024.10.10 |
[Spring] JDBC (0) | 2024.09.30 |