Hello, JPA
들어가며
이 포스팅의 내용은 <<자바 ORM 표준 JPA 프로그래밍>>의 2장 부분을 따라한 내용이다. 테스트 환경은 H2 데이터베이스에 IDE는 이클립스를 사용했다. 예제 프로젝트는 <github.com/holyeye/jpabook> 에서 다운받을 수 있다.
테이블 생성
H2 데이터베이스를 실행시키면 간단하게 사용할 수 있는 화면이 나온다. 거기에서 CREATE TABLE 문을 실행시킨다.
CREATE TABLE MEMBER (
ID VARCHAR(255) NOT NULL,
NAME VARCHAR(255),
AGE INTEGER,
PRIMARY KEY (ID)
)실습을 진행할 테이블을 만든 후에는, 애플리케이션에서 사용할 회원 클래스를 만들자.
회원 클래스 생성
JPA를 사용하려면 가장 먼저 회원 클래스와 회원 테이블을 매핑해야 한다. 즉, 회원 테이블에 있는 ID, NAME, AGE 를 각각 JAVA 클래스의 인스턴스 변수로 매핑해야 한다는 의미이다.
package jpabook.start;
import javax.persistence.*;
@Entity
@Table(name = "MEMBER")
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME")
private String username;
private Integer age;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}어노테이션의 의미는 다음과 같다.
@Entity
이 클래스를 테이블과 매핑한다고 JPA에게 선언한다. 엔티티 애노테이션이 사용된 클래스를 엔티티 클래스라고 한다.
@Table
엔티티 클래스에 매핑할 테이블 정보를 알려준다. 여기서는 name 속성을 이용하여 이 클래스를 MEMBER 테이블과 매핑했다. 이 애노테이션을 생략하면 클래스 이름을 테이블 이름으로 매핑한다.
@id
엔티티 클래스의 필드를 테이블의 Primary key로 매핑한다. 여기서는 엔티티 클래스의 id를 Primary key로 매핑했다.
@Column
필드를 컬럼에 매핑했다. 만약 매핑 정보를 주지 않는다면(위의 예제에서는 age), 필드명을 이용하여 컬럼과 매핑한다.
persistence.xml 설정
JPA는 persistence.xml을 이용하여 필요한 설정 정보를 관리한다. 이 설정 파일이 META-INF/persistence.xml에 있으면 별도의 설정 없이 JPA가 인식할 수 있다.
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1">
<persistence-unit name="jpabook">
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.use_sql_comments" value="true" />
<property name="hibernate.id.new_generator_mappings" value="true" />
<!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
</properties>
</persistence-unit>
</persistence>persistence-unit
JPA 설정은 영속성 유닛(persistence-unit)으로 시작한다. 영속성 유닛, 즉 persistence-unit은 연결된 하나의 데이터베이스당 하나의 영속성 유닛을 등록한다.
JPA 표준속성
- property name=“javax.persistence.jdbc.driver” value=“org.h2.Driver”
- property name=“javax.persistence.jdbc.user” value=“sa”
- property name=“javax.persistence.jdbc.password” value=""
- property name=“javax.persistence.jdbc.url” value=“jdbc:h2:tcp://localhost/~/test”
JPA 표준 속성은 javax로 시작한다. 위의 네 속성은 각각 드라이버, 유저명, 패스워드, db url이다. JPA 표준 속성은 구현체와 관계없이 어디서든 사용할 수 있다.
하이버네이트 속성
- property name=“hibernate.dialect” value=“org.hibernate.dialect.H2Dialect”
hibernate로 시작하는 속성은 하이버네이트 전용 속성이다. 이 속성은 하이버네이트에서만 사용할 수 있다.
dialect는 JPA가 많은 데이터베이스를 지원해야 하는 문제 때문에 생긴 속성이다. JPA는 DB 플렛폼에 종속되지 않는 기술이다. 그러나 현실적으로 DB들이 제공하는 고유한 기능, 혹은 DB마다 다른 기능들이 있다. 예를 들면
- 데이터 타입 : MySQL은 VARCHAR, 오라클은 VARCHAR2를 사용
이런 DB마다 다른 점을 커버하기 위해 방언 속성이 추가되었다. JPA 표준 문법에 맞추어 JPA를 사용하면 특정 데이터베이스에 의존적인 SQL은 방언 클래스에서 알아서 수정해서 뿌려주게 된다.
애플리케이션 개발
코드부터 보자. 코드는 다음과 같다.
package jpabook.start;
import javax.persistence.*;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성
EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득
try {
tx.begin(); //트랜잭션 시작
logic(em); //비즈니스 로직
tx.commit();//트랜잭션 커밋
} catch (Exception e) {
e.printStackTrace();
tx.rollback(); //트랜잭션 롤백
} finally {
em.close(); //엔티티 매니저 종료
}
emf.close(); //엔티티 매니저 팩토리 종료
}
public static void logic(EntityManager em) {
String id = "id1";
Member member = new Member();
member.setId(id);
member.setUsername("지한");
member.setAge(2);
//등록
em.persist(member);
//수정
member.setAge(20);
//한 건 조회
Member findMember = em.find(Member.class, id);
System.out.println("findMember=" + findMember.getUsername() + ", age=" + findMember.getAge());
//목록 조회
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
System.out.println("members.size=" + members.size());
//삭제
em.remove(member);
}
}로직 구현 흐름은 다음과 같다.
엔티티 매니저 팩토리 생성 > 엔티티 매니저 생성 > 트렌잭션 획득 >
트렌잭션 시작 > 비즈니스 로직 시작 > 트렌잭션 커밋
하나씩 처음부터 훑어보자.
엔티티 매니저 팩토리 생성
JPA를 시작하려면 우선 persistence.xml의 설정 정보를 보고 엔티티 매니저 팩토리를 생성해야 한다. 이 때 Persistence 클래스를 사용하는데 이 클래스는 엔티티 매니저 팩토리를 생성하여 JPA를 사용할 수 있게 해 준다.
즉 persistence.xml에서 설정한
<persistence-unit name="jpabook">것을 보고,
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");엔티티 매니저 팩토리를 끌어오는 거 같다. 엔티티 매니저 팩토리를 생성하면서 설정 정보를 읽어서 JPA를 동작시킬 기반 객체를 만들고, 데이터베이스 커넥션 풀도 생성하므로 생성 비용이 아주 크다. 따라서 엔티티 매니저 팩토리는 어플리케이션 전체에서 딱 한 번만 생성하고 공유하여 사용해야 한다.
엔티티 매니저 생성
엔티티 매니저 펙토리에서 엔티티 매니저를 생성할 수 있다. JPA 기능의 거의 대부분을 엔티티 매니저가 제공한다. 엔티티를 데이터베이스에 CRUD 하는 것 또한 엔티티 매니저를 통해 할 수 있다. 엔티티 매니저는 내부에 DB 커넥션을 유지하면서 데이터베이스와 통신한다. 따라서 애플리케이션 개발자는 엔티티 매니저를 가상의 데이터베이스라고 생각할 수 있다.
엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로 쓰레드끼리 공유, 재사용하면 안 된다.
종료
사용이 끝난 엔티티 매니저는 반드시 종료해야 한다.
em.close();애플리케이션이 종료될 때 엔티티 매니저 팩토리도 종료해야 한다.
emf.close();트렌잭션 관리
JPA에서는 트렌잭션 없이 데이터를 변경할 수 없다. (변경을 시도하면 에러가 난다.) 트렌잭션을 시작하려면 엔티티 매니저에서 트랜잭션 객체를 받아와 실행시키면 된다.
비즈니스 로직
비즈니스 로직을 보면, 트렌잭션 매니저를 가지고 CRUD를 하는 것을 알 수 있다. 엔티티 매니저에 객체를 등록하면, JPA는 객체의 매핑 정보를 분석하여 SQL을 생성한다.
위와 같은 코드에서는 다음과 같은 SQL을 만들 것이다.
INSERT INTO MEMBER (ID, NAME, AGE) VALUES (‘id1’, ‘지한’, 2);
엔티티를 수정할때는 따로 뭘 할 필요 없이 영속객체의 값만 바꾸면 된다. JPA는 어떤 엔티티가 변경되었는 추적하는 기능이 있어서, 값만 변경하면 업데이트 쿼리를 생성하여 DB로 쏴준다. 굉장히 편하지만 어떻게 보면 위험한데, 영속객체를 for문 같은걸로 쭉 바꿔버리면 DB 전체가 업데이트가 쳐지는 놀라운 광경을 보게 될 것이다. 조심해야 한다.
삭제는 엔티티 매니저의 remove 메소드를 사용하면 되고, 한 건 조회는 엔티티 매니저의 find 메소드를 사용하면 된다.
애매한 것이 하나 이상의 회원 목록을 조회하는 코드인데, 위의 예제에서는 JPQL을 사용하여 문제를 해결했다. JPQL은 SQL을 추상화한 쿼리 언어로, SQL 문법과 거의 유사하다. 다만 현재 프로젝트에서는 사용하지 않고 있기 때문에 자세히 다루지는 않을 것이다.
- spring data jpa를 사용하면 이런 간단한 list 조회 쿼리는 더 간단하게 짤 수 있다.
- 결정적으로 JPQL은 쿼리를 날려봐야 잘못된 것을 알 수 있다. QueryDsl이나 Specification을 사용하면 좀 더 문제를 아름답게 해결할 수 있다.
다만 JPQL이라는 언어가 있는 것 정도는 알아둬야 할 거 같다. JPQL은 엔티티 객체, 즉 클래스와 필드를 대상으로 쿼리를 날리기 때문에 데이터베이스를 어떤 거를 사용하는지 관계 없이 쿼리를 날릴 수 있다. 추상화된 엔티티 객체를 대상으로 쿼리를 날리기 때문이다.
이 점이 SQL과 비교하여 가장 큰 차이점인 거 같다. JPQL은 데이터베이스 테이블을 전혀 알지 못한다.