[JPA] JPA 시작



JPA 시작하기

개요

사용 라이브러리

  • hibernate-entitymanager
  • h2

h2 DB를 설치하여, 학습용 DB로 사용한다. 또한, JPA를 사용하기 위해 hibernate 라이브러리를 사용한다.



객체 매핑

  • 기초적인 자바 프로젝트를 maven과 함께 생성하여, 객체 매핑에 대해 알아본다.

    참고로, spring이나 spring boot를 사용하지 않는다. 단순히 main 메서드로 동작하는 프로젝트이다.

  • 예시 프로젝트의 링크는 상단에 작성해두었으니 참고하시기 바란다.


회원 테이블 생성

  • H2 콘솔을 통해 아래 SQL을 입력하여, MEMBER 테이블을 생성한다.
CREATE TABLE MEMBER (
	ID VARCHAR(255) NOT NULL,
	NAME VARCHAR(255),
	AGE INTEGER,
	PRIMARY KEY (ID)
)


회원 클래스

  • JPA 애플리케이션에서 사용될 회원 클래스를 작성한다.
  • 해당 클래스는 MEMBER 테이블과 매핑될 클래스이다.
public class Member {
  private String id;

  private String username;

  private Integer age;

	// Getter, Setter 생략

}


매핑 애너테이션 설정

  • JPA를 사용하려면 가장 먼저 회원 클래스와 회원 테이블을 매핑해야 한다.
  • 애너테이션을 통해 매핑할 수 있다.
@Entity
@Table(name="MEMBER")
public class Member {

  @Id
  @Column(name = "ID")
  private String id;

  @Column(name = "NAME")
  private String username;

	//매핑정보가 없다.
  private Integer age;

	//Getter, Setter 생략
}
  • 사용된 애너테이션은 다음과 같다.
    • @Entity
    • @Table
    • @Id
    • @Column
  • 애너테이션 설명은 아래에서 계속한다.


매핑 애너테이션 설명

  • @Entity
    • 클래스 레벨에 적용한다.
    • 해당 클래스를 테이블과 매핑한다고 JPA에게 알려주는 역할을 한다.
    • 해당 애너테이션이 적용된 클래스를 엔티티 클래스라고 한다.
  • @Table
    • 클래스 레벨에 적용한다.
    • 엔티티 클래스에 매핑할 테이블 정보를 JPA에게 알려주는 역할을 한다.
    • 위 예시 코드에서는 name 속성으로 테이블의 이름을 지정하여, ‘MEMBER 테이블’과 ‘Member 엔티티’를 매핑하였다.
      • @Table(name = "테이블이름")
    • 해당 애너테이션을 생략하면 클래스 이름을 테이블 이름으로 매핑한다.
  • @Id
    • 필드 레벨에 적용한다.
    • 엔티티 클래스의 필드를 테이블의 기본 키 (Primary Key)에 매핑한다.
    • 해당 애너테이션이 적용된 필드를 식별자 필드라고 한다.
  • @Column
    • 필드 레벨에 적용한다.
    • 해당 필드를 컬럼에 매핑한다.
    • 위 예시 코드에서는 name 속성으로 매핑할 컬럼의 이름을 지정하여, ‘MEMBER 테이블의 NAME 컬럼’과 ‘name 필드’를 매핑하였다.
      • @Column(name = "매핑할 컬럼명")
  • 매핑 정보가 없는 필드
    • 매핑 애너테이션을 생략하면 필드명을 사용해서 컬럼명으로 매핑한다.
    • 이름이 age인 필드에 매핑 정보를 생략하면, age컬럼으로 자동 매핑한다.
    • 만약, 대소문자를 구분하는 DB를 사용하면, @Column 을 사용하여 명시적으로 매핑해야 한다.

javax.persistence : JPA 애너테이션의 패키지



persistence.xml 설정

persistence.xml 이란?

  • JPA는 persistence.xml 를 통해 필요한 설정 정보를 관리한다.
  • 해당 xml 파일은 /src/main/resource/META-INF 경로에 위치한다.
  • 해당 경로에 위치했을 때, 별도의 설정없이 해당 persistence.xml 파일을 JPA가 인식할 수 있다.


persistence.xml 작성

<?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" />

    </properties>

  </persistence-unit>

</persistence>
  • 설정 분석
    • <persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1">
      • 설정 파일은 persistence 태그로 시작한다.
    • <persistence-unit name="jpabook">
      • JPA 설정은 영속성 유닛(persistence-unit) 이라는 것부터 시작하게 된다.
      • 해당 태그를 통해, 연결할 데이터베이스당 하나의 영속성 유닛을 등록한다.
      • 영속성 유닛에는 고유한 이름으로 부여해야 한다.
        • 위 코드에선 ‘jpabook’이라는 이름을 부여했다.


  • 속성 분석
    • JPA 표준 속성
      • javax.persistence.jdbc.driver : JDBC 드라이버
      • javax.persistence.jdbc.user : DB 접속 아이디
      • javax.persistence.jdbc.password : DB 접속 비밀번호
      • javax.persistence.jdbc.url : DB 접속 URL
    • 하이버네이트 속성
      • hibernate.dialect : DB 방언 설정
      • hibernate.show_sql : 하이버네이트가 실행한 SQL을 출력한다.
      • hibernate.format_sql : 하이버네이트가 실행한 SQL을 출력할 때 보기 쉽게 정렬한다.
      • hibernate.use_sql_comments : 쿼리를 출력할 때 주석도 함께 출력한다.
      • hibernate.id.new_generator_mappings : JPA 표준에 맞춘 새로운 키 생성 전략을 사용한다.

        해당 사항은 나중에 자세히 다룬다.

    • 설명
      • 이름이 javax.persistence 로 시작하는 속성은 JPA 표준 속성이다. 따라서 특정 구현체 (Hibernate 등)에 종속되지 않는다.
      • hibernate 로 시작하는 속성은 하이버네이트 전용 속성이므로 하이버네이트에서만 사용할 수 있다.

      DB 방언에 대해서는 아래에서 자세히 다룬다.


데이터베이스 방언

  • JPA는 특정 DB에 종속적이지 않은 기술이다.
  • 따라서, 다른 DB로 손쉽게 교체할 수 있다.
  • 하지만 각 DB마다 SQL 문법과 함수가 조금씩 다르다는 문제가 있다.
  • SQL 표준을 지키지 않거나 특정 DB만의 고유한 기능을 JPA에서는 방언이라고 한다.
  • 이러한 문제를 해결하고자, JPA 구현체(Hibernate 등)에서는 다양한 DB 방언 클래스를 제공한다.
    • 특정 DB에 의존적인 SQL을 DB 방언이 처리해준다.
    • 따라서, DB가 변경되어도 애플리케이션 코드를 변경할 필요 없이, DB 방언만 교체하면 된다.



애플리케이션 개발

JPA 애플리케이션 실행 클래스 (main)

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);

  }
}
  • 위 코드는 크게 아래처럼 구분할 수 있다.
    • 엔티티 매니저 설정
    • 트랜잭션 관리
    • 비즈니스 로직
  • 각 항목에 대해 하나씩 알아보자.


엔티티 매니저 설정

  1. 엔티티 매니저 팩토리 생성
    • EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
      • 위 코드를 작성시, META-INF/persistence.xml 파일에서 이름이 jpabook인 영속성 유닛을 찾아서 엔티티 매니저 팩토리를 생성한다.
        • 즉 매개변수로 전달받은, 해당 이름을 갖는 영속성 유닛을 persistence.xml 에서 찾아내어, 해당 정보로 엔티티 매니저 팩토리를 생성한다.
      • 엔티티 매니저 팩토리는 애플리케이션 전체에서 딱 한 번만 생성하고 공유해서 사용해야 한다.
        • 왜냐하면, 엔티티 매니저 팩토리를 만드는 과정에서 소요되는 비용이 매우 크다.
        • 소요 비용
          • persistence.xml 설정 정보 읽은 뒤, JPA를 동작시키기 위한 기반 객체 생성
          • JPA 구현체에 따라서는 DB 커넥션 풀까지 생성
    • JPA 시작 전에 먼저, **persistence.xml 의 설정 정보를 사용하여 엔티티 매니저 팩토리를 생성해야 한다.**
    • 이때 Persistence 클래스가 사용된다.
      • 해당 클래스는 엔티티 매니저 팩토리를 생성해서 JPA를 사용할 수 있게 준비한다.


  1. 엔티티 매니저 생성
    • EntityManager em = emf.createEntityManager();
      • 엔티티 매니저 팩토리에서 엔티티 매니저를 생성한다.
    • 엔티티 매니저
      • JPA의 기능 대부분은 엔티티 매니저가 제공한다.
        • 대표적인 기능으로, 엔티티 매니저를 통한 엔티티 CRUD 작업이 존재한다.
      • 엔티티 매니저는 내부에 데이터소스(DB 커넥션)을 유지하면서 DB와 통신한다.
        • 따라서, 개발자는 엔티티 매니저를 가상의 DB로 생각할 수 있다.
      • 엔티티 매니저는 DB 커넥션과 밀접한 관계가 있으므로, 쓰레드 간의 공유나 재사용을 하면 안된다.


  1. 종료
    • em.close();
      • 마지막으로 사용이 끝난 엔티티 매니저는 위와 같이 반드시 종료해야 한다.
    • emf.close();
      • 애플리케이션을 종료할 때, 엔티티 매니저 팩토리도 위와 같이 반드시 종료해야 한다.


트랜잭션 관리

  • JPA를 사용하면, 항상 트랜잭션 안에서 데이터를 변경해야 한다.
  • 트랜잭션 없이 데이터를 변경하면 예외가 발생한다.
  • 트랜잭션을 시작하려면, 엔티티 매니저에서 트랜잭션 API를 받아와야 한다.
EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득

try {
  tx.begin(); //트랜잭션 시작
  logic(em);  //비즈니스 로직
  tx.commit();//트랜잭션 커밋

} catch (Exception e) {
  e.printStackTrace();
  tx.rollback(); //트랜잭션 롤백

}
  • EntityTransaction tx = em.getTransaction();
    • 엔티티 매니저로부터 트랜잭션을 받아온다.
  • tx.commit();
    • 비즈니스 로직이 정상 동작하면, 트랜잭션을 커밋한다.
  • tx.rollback();
    • 예외가 발생하면 트랜잭션을 롤백한다.


비즈니스 로직

  • 위 예시에서의 비즈니스 로직은 다음과 같다.
    • 회원 엔티티를 하나 생성한 다음 엔티티 매니저를 통해 DB에 CRUD 작업을 한다.
      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);
        
      }
    
    • 등록, 수정, 삭제, 조회 작업이 엔티티 매니저를 통해서 수행된다.


  • 등록
    • em.persist(member);
    • 엔티티를 저장하려면, 엔티티 매니저의 persist() 메서드에 저장할 엔티티를 넘겨주면 된다.
    • JPA가 매핑 정보(애너테이션)를 분석해서 만들어낸 SQL
      • INSERT INTO MEMBER (ID, NAME, AGE) VALUES ('id1', '지한', 2)


  • 수정
    • member.setAge(20);
    • 엔티티를 수정한 후에 수정 내용을 반영하려면, 그저 엔티티의 프로퍼티를 수정하는 것이 전부이다.
    • JPA는 어떤 엔티티가 변경되었는지 추적하는 기능을 가지고 있기 때문에, 엔티티의 값만 변경하면 알아서 UPDATE SQL을 생성하여 DB에 값을 변경한다.
    • JPA가 매핑 정보(애너테이션)를 분석해서 만들어낸 SQL
      • UPDATE MEMBER SET AGE=20, NAME='지한' WHERE ID='id1'


  • 삭제
    • em.remove(member);
    • 엔티티를 삭제하려면, 엔티티 매니저의 remove() 메소드에 삭제하려는 엔티티를 넘겨준다.
    • JPA가 매핑 정보(애너테이션)를 분석해서 만들어낸 SQL
      • DELETE FROM MEMBER WHERE ID='id1'


  • 한 건 조회
    • Member findMember = em.find(Member.class, id);
    • ‘조회할 엔티티 타입’과 ‘@ID’을 통해, DB 테이블의 기본 키와 매핑한 식별자 값으로 엔티티 하나를 조회한다.
    • JPA가 매핑 정보(애너테이션)를 분석해서 만들어낸 SQL
      • SELECT * FROM MEMBER WHERE ID='id1'


  • 여러 건 조회
    • 아래에서 자세히 설명한다.


JPQL

  • 하나 이상의 회원 목록을 조회하는 코드를 자세히 살펴보자.
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
  • 원래 JPA는 엔티티 객체를 중심으로 개발하므로, 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 한다. 이에 대한 과정을 나타내면 아래와 같다.
    • DB의 모든 데이터를 APP으로 불러온다.
    • 불러온 데이터를 모두 엔티티 객체로 변경한다.
    • 검색을 진행한다.
  • 하지만 이러한 과정은 사실상 불가능하다.
  • 따라서, 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL을 사용해야 한다.
    • JPA는 이러한 문제를 JPQL이라는 쿼리 언어로 해결한다.


  • JPQL
    • 쿼리 언어의 한 종류이다.
    • SQL과 문법이 거의 유사하여 SELECT, FROM, WHERE, GROUP BY, BAVING, JOIN 등을 사용할 수 있다.
    • SQL과의 차이점
      • JPQL: 엔티티 객체를 대상으로 쿼리한다.
      • SQL: DB 테이블을 대상으로 쿼리한다.


  • JPQL 예시
    • select m from Member
    • 여기서의 from Member 는 회원 엔티티 객체를 뜻한다. (MEMBER 테이블을 뜻하는 것이 아니다.)
    • JPQL은 DB테이블을 전혀 알지 못한다.


  • JPQL 사용
    • JPQL을 사용하려면, 먼저 em.createQuery(JPQL, 반환 타입) 메서드를 실행해서 쿼리 객체를 생성한 후, 쿼리 객체의 getResultList() 메서드를 호출하면 된다.
    • JPA가 매핑 정보(애너테이션)를 분석해서 만들어낸 SQL
      • SELECT M.ID, M.NAME, M.AGE FROM MEMBER M
  • 보다 자세한 내용은 추후에 다루겠다.





  • 김영한, 『자바 ORM 표준 JPA 프로그래밍』, 에이콘
본 게시글은 위 교재를 기반으로 정리한 글입니다.