1. JPA란
가. JPA란
1) 자바 진영 ORM 기술 표준
•
ORM(Object-relational Mapping): 객체 관계 매핑
→ 핵심: OOP와 RDB의 패러다임의 불일치를 해결
→ 개발자는 객체지향 패러다임에 따라 객체 설계
→ 관계형 데이터베이스의 원리에 따라 DB 설계
→ ORM 프레임워크는 중간에서 객체와 관계형 데이터베이스를 맵핑
2) JDBC와 어플리케이션 사이에서 SQL 명령 생성
•
JPA는 JDBC API를 활용해서 SQL 명령을 RDB에 보낸다
•
JPA는 전달 받은 Entity를 분석해서 적절한 SQL 명령을 생생한다
3) Hibernate를 포함한 구현체의 인터페이스
•
세 가지 구현체 중에서 대부분 하이버네이트를 사용함
4) JPA 동작 원리
•
Persistence 클래스에서 persistence.xml 파일의 설정 정보를 조회함
•
설정 정보를 참고하여 EntityManagerFactory 생성
•
EntityManagerFactory에서 EntityManager 생성
•
EntityManager를 통해서 Transaction이 발생함
→ 일반적으로 하나의 Transaction이 종료되면 해당 EntityManager는 삭제됨
•
JPA의 모든 데이터 변경은 Transaction 안에서 발생
5) JPQL vs SQL
•
JPQL: 객체를 대상으로 검색하는 객체 지향 쿼리
•
비교
◦
JPQL: 엔티티 객체 대상으로 쿼리 작성
→ RDBMS마다 SQL이 다르지만 JPQL을 사용하면 설정 정보(dialect)만 바꿔주면 JPQL은 변경하지 않고 사용 가능(생산성 향상)
◦
SQL: 데이터베이스 테이블을 대상으로 쿼리 작성
나. JPA가 필요한 이유
•
SQL 중심의 개발에는 다음과 같은 문제점이 존재함
→ 대부분의 웹 개발의 DB는 관계형 데이터베이스를 사용하므로 SQL 중심의 개발이 이루어짐
→ 관계형 데이터베이스의 성능, 트랜잭션 등의 특징을 대체할 만한 DB가 없음
1) 객체 CRUD와 SQL 중복 작업 제거
•
Member라는 객체에 String phoneNumber 필드를 추가하면 관련 있는 모든 SQL 명령(Insert, Select, Update)에 대해 일일이 해당 필드를 추가하는 작업을 해야 함
•
JPA 사용에 따라 Member 객체에 phoneNumber 필드만 추가하고 다른 SQL 명령을 수정할 필요가 없음. JPA가 알아서 해당 필드를 반영한 SQL 명령을 작성함
2) 패러다임의 불일치 해소
•
객체지향 프로그래밍과 관계형 데이터베이스의 패러다임에는 큰 차이가 존재
→ 상속, 연관관계, 데이터 타입, 데이터 식별 방법 등의 차이
•
객체를 관계형 데이터베이스에 맵핑하는 것을 개발자가 일일이 작업해야 함
•
JPA를 사용하면 프레임워크가 객체와 RDB에 대해 직접 맵핑함에 따라 패러다임의 불일치로부터 고민해야하는 여러 문제를 신경쓰지 않아도 됨
•
참고) 학습 시 OOP vs RDB 어떤 것에 더 우선순위를 두어야 할까?
→ 영한님의 의견) RDB가 더 중요, 프로그래밍 트렌드가 DB 트렌드 보다 더 빠르고 쉽게 변화므로 보다 본질적인 RDB에 대한 이해에 더 우선순위를 두고 학습해야 함
다. JPA 기본 설정
•
JPA는 SQL 표준을 따르지 않는 각 RDBMS의 방언(dialect)을 배제하고 있음
•
특정 RDBMS의 방언을 JPA에 적용하기 위해 해당 RDBMS의 dialect를 아래와 같이 지정해야 함
# h2 문법을 mysql로 변경
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
spring.datasource.hikari.jdbc-url=jdbc:h2:mem://localhost/~/testdb;MODE=MYSQL
JSON
복사
2. 영속성 관리 - 내부동작 방식
가. 영속성 컨텍스트
1) 영속성 컨텍스트란
•
"엔티티를 영구적으로 저장하는 환경"
•
EntityManager.persist(entity);
→ EntityManager에 해당 entitiy가 저장됨
→ 저장되는 곳은 DB가 아니라 영속성 컨텍스트에 저장됨
2) 영속성 컨텍스트의 특징
•
엔티티는 엔티티 매지너를 통해서 영속성 컨텍스트에 접근
•
영속성 컨텍스트는 물리적 저장공간이 아니라 논리적인 개념
•
엔티티 매니저:영속성 컨텍스트 = N:1로 맵핑됨
나. 엔티티의 생명주기
1) 비영속(new/transient)
•
영속성 컨텍스트와 관계 없는 상태, 새로운 상태
•
JPA 관련 작업을 실행하지 않고 객체만 생성한 상태
2) 영속(managed)
•
영속성 컨텍스트에 의해 관리되는 상태
•
persist 호출에 따라 엔티티가 영속상태가 됨
•
persist 호출이 없더라도 엔티티 조회에 따라 1차 캐시에 특정 엔티티가 할당되는 경우에도 영속상태가 됨
// 이하 실행 후의 상태
EntityManager.persist(entity);
Java
복사
3) 준영속(detached)
•
영속성 컨텍스트에서 분리되었지만 객체는 DB 내 존재하는 상태
// 이하 실행 후의 상태
EntityManager.persist(entity);
EntityManager.detach(entity)
Java
복사
4) 삭제(removed)
•
객체 자체가 삭제되어 DB 내에도 존재하지 않는 상태
// 이하 실행 후의 상태
EntityManager.remove(entity);
Java
복사
다. 영속성 컨테스트의 이점
1) 1차 캐시
•
엔티티가 영속 상태가 됨에 따라 1차 캐시에 저장
→ 1차 캐시: 하나의 트랜잭션 안에서의 캐쉬로 동작
→ 특정 엔티티메니저는 트랜잭션이 종료되면 삭제되기 때문에 캐시도 함께 삭제됨
•
1차 캐시에 저장된 엔티티에 대해 조회할 때, DB를 거치지 않고 캐시에서 해당 객체를 꺼낼 수 있음
•
조회 시, 캐시에 특정 객체가 없다면 해당 객체를 DB에서 조회하여 1차 캐시로 저장
•
1차 캐시라는 특징 때문에 성능 상 큰 이점은 없으나 객체지향적으로 설계할 때, 본 이점을 살릴 수 있음
→ 단, 하나의 트랜잭션이 복잡한 비지니스 로직을 다룬다면 성능 상 이점이 존재
2) 동일성(Identity) 보장
•
동일한 아이디("member1")로 조회해서 반환 받은 엔티티의 동일성이 보장됨
→ 단, 동일한 트랜잭션 안에서만 동일성이 보장됨. 같은 캐시에서 가져온 값에만 해당
EntityManager.persist(Memeber);
Member m1 = EntityManger.find(Member.class, "member1");
// 두번째 매개변수("member1")는 primary key 가리킴
Member m2 = EntityManger.find(Member.class, "member1");
System.out.println(m1 == m2); // true
Java
복사
3) 트랜잭션을 지원하는 쓰기 지연
•
엔티티 등록 상의 이점
→ 트랜잭션 상에서 발생하는 SQL 쿼리를 저장하고 있다가 트랜잭션 종료 시점에 한꺼번에 실행함
→ transactional write-behind
•
영속화 단계에서 쿼리명령을 실행하지 않고, 쓰기 지연 SQL 저장소에 SQL을 저장하고 있다가 commit 되는 순간 DB로 저장한 SQL 명령을 전달함
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction()
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//커밋하는 순간 데이터베이스에 'INSERT SQL' 2개를 보낸다.
transaction.commit();
Java
복사
•
각 엔티티가 영속화됨에 따라 해당 엔티티는 1차 캐시에도 저장되고 관련 SQL 명령은 쓰기 지연 SQL 저장소에 저장됨
4) 변경 감지(Dirty Checking)
•
엔티티 수정 상의 이점
→ 특정 엔터티를 조회한 결과에 대해 수정 작업을 한 후, 수정된 엔티티를 추가적으로 저장하지 않아도 됨
→ 자동으로 변경된 부분을 감지하여 Update SQL 명령을 생성함
transaction.begin(); // [트랜잭션] 시작
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);
//em.update(member) 이런 코드가 불필요, 커밋하는 시점에 자동 반영됨
transaction.commit(); // [트랜잭션] 커밋
Java
복사
•
변경 감지 원리
→ 1. transaction 시작 후
→ 2. 1차 캐시에 저장된 Entity와 스냅샷으로 저장된 Entity를 비교함
→ 스냅샷은 최초로 DB에서 1차 캐시로 가져왔을 때의 Entity
→ 3. 1차 캐시와 스냅샷의 Entity가 다르다면 Update SQL 명령을 생성해서 '쓰기 지연 SQL 저장소'에 일시 저장함
→ 4. '쓰기 지연 SQL 저장소'에 저장된 SQL 명령을 DB에 전달
→ 5. transaction commit
•
JPA를 사용할 때, 변경된 엔티티를 다시 저장하는 코드 작성을 지양할 것
→ 마치 자바의 컬렉션 처럼 List의 요소를 변경한 후 다시 List에 변경한 요소를 저장하지 않는 것처럼 JPA는 동작함
→ 다시 말해, 저장하지 않아도 변경사항이 자동으로 DB에 반영됨
라. 플러시
1) 플러시(flush)란
•
영속성 컨텍스트의 변경내용을 DB에 반영하는 것
2) 플러시 호출에 따른 동작
•
Entity 변경 감지
•
Entity가 변경되었다면, 수정된 엔티티에 대한 Update SQL 쿼리를 '쓰기 지연 SQL 저장소'에 등록
•
'쓰기 지연 SQL 저장소'에 저장된 SQL 명령을 DB에 전송
3) 플러시 호출 방법
•
직접 호출: EntityManager.flush()
→ 주로 테스트할 때 사용함
•
자동 호출 1: transaction commit 발생
•
자동 호출 2: JPQL 쿼리 실행 시
→ JPQL에서 주로 Entity 조회를 하는데 쓰기 지연 SQL 저장소에 Insert 명령이 남아 있고 DB로 전달되지 않는다면 특정 Entity를 조회할 수 없음
→ 위와 같은 이유로 JPQL 쿼리 실행 시 자동으로 flush 호출됨
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// memberA, B, C 대상으로 조회
// 이하 조회 쿼리가 동작하려면 위의 엔티티가 DB에 저장되어야 함
query = em.createQuery("select m from Member m", Member.class);List<Member> members= query.getResultList();
Java
복사
4) flush 주의할 점
•
flush 호출에 따라 영속성 컨텍스트나 1차 캐쉬를 비우는 것을 의미하지 않음
→ '쓰기 지연 SQL 저장소'를 비운다고 이해
•
'트랜잭션' 작업 단위에 따라 영속성 컨텍스트의 변경사항을 DB와 동기화
마. 준영속 상태
1) 준영속 상태
•
영속성 컨텍스트에서 특정 엔티티가 빠지는 것
•
준영속 상태가 되면 해당 엔티티에 대해 영속성 컨텍스트에서 제공하는 다양한 기능 활용이 불가함
→ 트랜잭션 커밋을 해도 해당 엔티티에는 별다른 일이 발생하지 않음
2) 준영속 상태를 만드는 방법
•
em.detach(entity): 특정 엔티티만 준영속 상태로 전환
•
em.clear(): 영속성 컨텍스트 초기화
•
em.close(): 영속성 컨텍스트 종료
3. 엔티티 매핑
가. 객체와 테이블 매핑
1) @Entity
•
@Entity가 붙으면 해당 클래스는 JPA가 관리하고 이를 엔티티라 명함
→ 해당 어노테이션이 붙지 않으면 JPA 관리 대상에서 빠짐
•
엔티티 객체는 반드시 기본 생성자를 정의해야 함
→ JPA 프레임워크에서 동적으로 엔티티 객체를 사용하기 위함. 표준
•
엔티티 객체는 final, enum, interface, inner class의 형태로 사용될 수 없음
→ abstract class or class로 사용됨
•
엔티티 객체의 필드에 final 사용할 수 없음
→ JPA가 동적으로 조작할 수 없게 됨
나. 데이터베이스 스키마 자동 생성
다. 필드와 컬럼 매핑
1) 맵핑 어노테이션 정리
•
@Column
•
@Temporal: 날짜 타입 매핑
•
@Enumerated: enum 타입 매핑
•
@Lob
•
@Transient: 특정 필드를 컬럼에 매핑 X
@Entity
public class Member {
@Id
private Long id;
// RDB의 속성명을 객체명과 달리하고 싶을 때 아래와 같이 사용
// RDB의 속성명이 "name"이 됨
@Column(name = "name")
private String username;
// 어노테이션을 달지 않았지만 자동으로 관련 타입으로 매핑됨
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
// RDB 테이블에 관련 속성 생성 X
// 특정 필드를 메모리에서만 관리하고 싶을 때 사용
@Transient
private String temp
}
Java
복사
2) @Column
라. 기본 키 매핑
4. 연관관계 매핑 기초
가. 연관관계 매핑이 필요한 이유는?
1) RDBMS 설계 → 객체지향적 설계로 변경 가능
2) 객체지향적 설계로 변경되면 이점은?
•
패러다임 일치에 따라 코드 가독성 향상
•
객체 그래프 탐색 가능
5. 값 타입
가. Entity와 VO의 사용법의 차이는?
1) 의도치 않은 부수 효과 발생 가능
2) VO의 Primitive Type과 Wrapper Class는 다르게 취급됨
•
Primitive Type은 공유와 변경 모두 불가
•
공유 가능하나 변경 불가
3) VO를 Entity와 연결 방법
•
VO에 @Embeddable으로 정의
•
VO가 사용되는 Entity에 @Embedded으로 활용
•
한 곳에 관련 어노테이션이 붙으면 다른 곳에서는 생략이 가능하나, 명시성을 위해 양쪽에 관련 어노테이션을 붙이는 것을 권장
4) Entity와 VO의 연속관계
•
Entity → VO → VO
•
Entity → VO → Entity
나. VO를 불변객체로 만들어야 하는 이유는?
1) 의도치 않은 부작용 방지
•
참조 공유에 따라 의도치 않게 발생할 수 있는 부작용을 막기 위함
2)VO를 불변객체로 만드는 방법
•
오직 생성자로 속성값을 주입
•
단, 빈 생성자를 만들어야 하기 때문에 속성값에 final은 붙일 수 없다
•
JPA에서 빈 생성자를 토대로 객체를 생성하기 때문에 진정한 의미의 불변객체로 작업은 불가
다. JPA에서 VO Collection은 어떻게 사용할 수 있는가?
1) VO Collection → RDBMS에서 1:N 관계
•
1:N 구조로 관리됨
→ Member 객체 내 List<Address> history 필드가 존재한다면, Memeber 테이블 이하에 자식 테이블로 1:N 맵핑되는 Address라는 테이블이 필요함
→ RDBMS에서 필드는 단일한 Record이므로 컬렉션에 매칭되는 표현이 존재하지 않음
•
Address는 VO를 영속화한 테이블이므로 테이블 내 자체 인공키를 만들면 안된다.
→ 자체 인공키가 있으면 Entity지 VO가 아님
→ 부모 테이블(Member)에 대한 FK 및 Address 내 주요 필드(동등성 비교에 사용되는)에 대해 모두 PK를 적용한다
2) VO Collection 정의
•
@ElementCollection, @ColletionTable, @JoinColumn 적용
3) VO Collection 활용
•
저장
•
조회
•
수정
•
삭제
Reference
•
김영한, Java ORM 표준 JPA 프로그래밍