2장 JPA 시작

sua·2023년 4월 21일
0

해당 장에서는 JPA를 사용해서 테이블 하나를 등록/수정/삭제/조회하는 간단한 JPA 애플리케이션을 만들어보겠다.

2.1 이클립스 설치와 프로젝트 불러오기

해당 절은 필자는 IntelliJ를 사용하기 때문에 이클립스 설치 부분은 넘기겠다.
예제 프로젝트 불러오기 파트도 Gradle 기반 프로젝트로 진행하기 위해 프로젝트 생성하는 것으로 다뤄보겠다!

  1. Spring Initializr에서 다음과 같이 입력하여 프로젝트를 생성한다.

  2. 프로젝트를 열어서 빌드가 완료된 상태다.


2.2 H2 데이터베이스 설치

설치가 필요 없고 용량도 가벼운 H2 데이터베이스를 사용한다. H2 데이터베이스는 자바가 설치되어 있어야 동작한다.

H2 데이터베이스 설치 방법

  1. http://www.h2database.com 에 들어간다.

  2. Windows Installer을 내려받는다. (Version 2.1.214 )

  3. H2 Console을 실행시키면 http://localhost:8082에 접속되어 H2 데이터베이스에 접속할 수 있다.

  4. 언어를 한국어로 선택하고 설정 사항을 다음과 같이 입력한뒤 연결 버튼을 선택한다.
    - 드라이버 클래스 : org.h2.Driver
    - JDBC URL : jdbc:h2:tcp://localhost/~/test
    - 사용자명: sa
    - 비밀번호 : 입력 x

  5. test라는 이름의 데이터베이스에 서버 모드로 접근되어 다음과 같은 화면이 나와야 한다.... 그런데 이런 오류 문구가 떴다.

    해당 블로그를 참고해서 (https://bluayer.com/23) JDBC URL을 jdbc:h2:~/test로 변경해서 다시 시도해보았다.

    그랬더니 성공! 이제 다시 나와서 원래 하려던 것 처럼 jdbc:h2:tcp://localhost/~/test로 변경해서

    다시 연결해보면 성공한 것을 알 수 있다.

  6. 이제 아래 SQL을 입력해서 실행 버튼을 선택하면 MEMBER 테이블이 생성된다.

CREATE TABLE MEMBER (
	ID VARCHAR(255) NOT NULL, --아이디 (기본 키)
    NAME VARCHAR(255), -- 이름
    AGE INTEGER NOT NULL, --나이
    PRIMARY KEY (ID)
)



2.3 라이브러리와 프로젝트 구조

필요한 모든 라이브러리를 직접 내려받아서 관리하기는 어려우므로 Gradle 도구를 사용하겠다.
JPA 구현체로 하이버네이트를 사용하기 위한 핵심 라이브러리는 다음과 같다.

  • hibernate-core : 하이버네이트 라이브러리
  • hibernate-entitymanager : 하이버네이트가 JPA 구현체로 동작하도록 JPA 표준을 구현한 라이브러리
  • hibernate-jpa-2.1-api : JPA 2.1 표준 API를 모아둔 라이브러리

예제에서 사용할 프로젝트 구조는 다음과 같다.

2.3.1 그레이들과 사용 라이브러리 관리

그레이들은 라이브러리를 관리해주는 도구인데 build.gradle에 사용할 라이브러리를 적어주면 라이브러리르 자동으로 내려받아서 관리해준다.

build.gradle을 다음과 같이 작성해준다.

실행시켜 주면 내려받아서 라이브러리에 추가해준다.

JPA에 하이버네이트 구현체를 사용하려면 많은 라이브러리가 필요하지만 그 중에 핵심 라이브러리는 다음과 같다.

  • JPA, 하이버네이트 : JPA 표준과 하이버네이트를 포함하는 라이브러리로, hibernate-entitymanager를 라이브러리로 지정하면 다음 중요 라이브러리도 함께 내려받는다.
    • hibernate-core.jar
    • hibernate-jpa-2.1-api.jar
  • H2 데이터베이스 : H2 데이터베이스에 접속해야 하므로 h2 라이브러리도 지정했다.

2.4 객체 매핑 시작

  1. 애플리케이션에서 사용할 회원 클래스를 만들어보겠다.
package jpabook.start;

public class Member {
    private String id; // 아이디
    private String username; // 이름
    private Integer age; // 나이
    
    // Getter, Setter
    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;
    }
}

  1. resources 폴더 밑에 META-INF 폴더 밑에 persistence.xml 파일을 생성해서 다음과 같이 작성한다.

  2. JPA를 사용하기 위해 회원 클래스와 회원 테이블을 매핑 어노테이션으로 매핑한다.

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; // 나이

    // Getter, Setter
    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;
    }
}

JPA는 매핑 어노테이션을 분석해서 어떤 객체가 어떤 테이블과 관계가 있는지 알아낸다.

  1. @Entity : 이 클래스를 테이블과 매핑한다고 JPA에게 알려준다. 이러한 클래스를 엔티티 클래스라고 한다.

  2. @Table : 엔티티 클래스에 매핑할 테이블 정보를 알려준다. 여기서는 name 속성을 사용해서 Member 엔티티를 MEMBER 테이블에 매핑했다. 이 어노테이션을 생략하면 클래스 이름을 테이블 이름으로 매핑한다.

  3. @Id : 엔티티 클래스의 필드를 테이블의 기본 키에 매핑한다. 여기서는 엔티티의 id 필드를 테이블의 ID 기본 키 컬럼에 매핑했다. 이렇게 @Id가 사용된 필드를 식별자 필드라고 한다.

  4. @Column : 필드를 컬럼에 매핑한다. 여기서는 name 속성을 사용해서 Member 엔티티의 username 필드를 MEMBER 테이블의 NAME 컬럼에 매핑했다.

  5. 매핑 정보가 없는 필드 : age 필드에는 매핑 어노테이션이 없다. 이렇게 매핑 어노테이션을 생략하면 필드명을 사용해서 컬럼명으로 매핑한다. 여기서는 필드명이 age이므로 age 컬럼으로 매핑했다. 만약 대소문자를 구분하는 데이터베이스를 사용하면 @Column(name="AGE")처럼 명시적으로 매핑해야 한다.


2.5 persistence.xml 설정

JPA를 실행하기 위한 기본 설정 파일인 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" />

            <!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
        </properties>
    </persistence-unit>

</persistence>
  • 설정 파일은 persistence로 시작한다. 이곳에 XML 네임스페이스와 사용할 버전을 지정한다. JPA 2.1을 사용하려면 이 xmlns와 version을 명시하면 된다.
  • persistence-unit(영속성 유닛) : JPA 설정은 여기서부터 시작하는데 일반적으로 연결할 데이터베이스당 하나의 영속성 유닛을 등록한다. 그리고 영속성 유닛에는 고유한 이름을 부여해야 하는데 여기서는 jpabook이라는 이름을 사용했다.
  • JPA 표준 속성
    • javax.persistence.jdbc.driver : JDBC 드라이버
    • javax.persistence.jdbc.user : 데이터베이스 접속 아이디
    • javax.persistence.jdbc.password : 데이터베이스 접속 비밀번호
    • javax.persistence.jdbc.url : 데이터베이스 접속 URL
  • 하이버네이트 속성
    • hibernate.dialect: 데이터베이스 방언 설정

javax.persistence로 시작하는 속성은 JPA 표준 속성으로 특정 구현체에 종속되지 x
hibernate로 시작하는 속성은 하이버네이트 전용 속성이므로 하이버네이트에서만 사용할 수 있음


2.5.1 데이터베이스 방언

JPA는 특정 데이터베이스에 종속되지 x -> 다른 데이터베이스로 손쉽게 교체할 수 있음.
하지만 데이터베이스마다 다음과 같이 SQL문법과 함수가 조금씩 다르다.

  • 데이터 타입 : 가변 문자 타입으로 MySQL은 VARCHAR, 오라클은 VARCHAR2를 사용
  • 다른 함수명 : 문자열을 자르는 함수로 SQL 표준은 SUBSTRING()을 사용하지만 오라클은 SUBSTR()을 사용
  • 페이징 처리 : MySQL은 LIMIT을 사용하지만 오라클은 ROWNUM을 사용

이렇게 SQL 표준을 지키지 않거나 특정 데이터베이스만의 고유한 기능을 JPA에서는 방언이라고 한다.
하이버네이트를 포함한 대부분의 JPA 구현체들은 이런 문제를 해결하기 위해 다양한데이터베이스 방언 클래스를 제공한다.

그림을 보면 개발자는 JPA가 제공하는 표준 문법에 맞추어 JPA를 사용하면 되고 특정 데이터베이스에 의존적인 SQL은 데이터베이스 방언이 처리해준다.
-> 데이터베이스가 변경되어도 애플리케이션 코드를 변경할 필요 없이 데이터베이스 방언만 교체하면 된다.

하이버네이트는 다양한 데이터베이스 방언을 제공하는데 대표적으로는

  • H2 : org.hibernate.dialect.H2Dialect
  • 오라클 10g: org.hibernate.dialect.Oracle10gDialect
  • MySQL: org.hibenate.dialect.MySQL5InnoDBDialect

여기서는 h2 데이터베이스를 사용하므로 hibernate.dialect 속성을 org.hibernate.dialect.H2Dialect로 설정했다.


2.6 애플리케이션 개발

객체 매핑과 persistence.xml로 JPA 설정도 완료했기 때문에 JPA 애플리케이션을 개발할 수 있다. 시작해보도록 하겠다.

먼저, 애플리케이션을 시작하는 코드를 보자. 엔티티 매니저 팩토리로 엔티티 매니저를 생성하는 것은 팩토리 메소드 패턴을 이용하는 것 같다ㅎㅎ

import javax.persistence.*;

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) {
			tx.rollback(); // [트랜잭션] - 롤백
		} finally {
			em.close(); // [엔티티 매니저] - 종료
		}
		emf.close();  // [엔티티 매니저 팩토리] - 종료
	}

	// 비즈니스 로직
	private static void logic(EntityManager em) {

	}
}

코드는 크게 3부분으로 나뉘어 있다.

  • 엔티티 매니저 설정
  • 트랜잭션 관리
  • 비즈니스 로직

이른 차근차근 살펴보도록 하겠다.

2.6.1 엔티티 매니저 설정

다음 그림은 엔티티 매니저의 생성 과정을 나타낸 것이다. 이를 보면서 분석해보자.

엔티티 매니저 팩토리 생성

JPA를 시작하려면 우선 persistence.xml 설정 정보를 사용해서 엔티티 매니저 팩토리를 생성해야 한다. 이때 Persistence 클래스를 사용하여 엔티티 매니저 팩토리를 생성해서 JPA를 사용할 수 있게 준비한다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");

이렇게 코드를 짜면 META-INF/persistence.xml에서 이름이 jpabook인 영속성 유닛을 찾아서 엔티티 매니저 팩토리를 생성한다. 이때 persistence.xml의 설정 정보를 읽어서 JPA를 동작시키기 위한 기반 객체를 만들고 + JPA 구현체에 따라서는 데이터베이스 커넥션 풀도 생성하므로 -> 엔티티 매니저 팩토리를 생성하는 비용은 아주 크다. => 엔티티 매니저 팩토리는 애플리케이션 전체에서 딱 한 번만 생성하고 공유해서 사용해야 한다. (이 부분은 뭔가 싱글톤 패턴이 떠오른다.)

엔티티 매니저 생성

EntityManager em = emf.createEntityManager();

엔티티 매니저 팩토리에서 엔티티 매니저를 생성한다. JPA의 기능 대부분은 이 엔티티 매니저가 제공한다. 대표적으로 엔티티 매니저를 사용해서 엔티티를 데이터베이스에 등록/수정/삭제/조회할 수 있다.

엔티티매니저는 내부에 데이터소스(데이터베이스 커넥션)를 유지하면서 데이터베이스와 통신한다. -> 애플리케이션 개발자는 엔티티 매니저를 가상의 데이터베이스로 생각할 수 있다.
엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드 간에 공유하거나 재사용하면 안 된다.

종료

사용이 끝난 엔티티 매니저는 다음처럼 반드시 종료해야 한다.

em.close(); // 엔티티 매니저 종료

애플리케이션을 종료할 때 엔티티 매니저 팩토리도 종료해야 한다.

emf.close(); // 엔티티 매니저 팩토리 종료

2.6.2 트랜잭션 관리

JPA를 사용하면 항상 트랜잭션 안에서 데이터를 변경해야 한다.
트랜잭션 없이 데이터를 변경하면 예외가 발생한다.
트랜잭션을 시작하려면 엔티티 매니저에서 트랜잭션 API를 받아와야 한다.

EntityTransaction tx = em.getTransaction();

try {
	tx.begin(); // 트랜잭션 시작
	logic(em); // 비즈니스 로직 실행
	tx.commit(); // 트랜잭션 커밋
} catch (Exception e) {
	tx.rollback(); // 예외 발생 시 트랜잭션 롤백
}

트랜잭션 API를 사용해서 비즈니스 로직이 정상 동작하면 트랜잭션을 커밋하고, 예외가 발생하면 트랜잭션을 롤백한다.


2.6.3 비즈니스 로직

회원 엔티티를 하나 생성한 다음 엔티티 매니저를 통해 데이터베이스에 등록, 수정, 삭제, 조회하는 비즈니스 로직을 작성해보겠다.

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

해당 코드의 출력 결과는 다음과 같다.

비즈니스 로직을 보면 등록, 수정, 삭제, 조회 작업이 엔티티 매니저를 통해서 수행되는 것을 알 수 있다. 엔티티 매니저는 객체를 저장하는 가상의 데이터베이스처럼 보인다.

등록

String id = "id1";
Member member = new Member();
member.setId(id);
member.setUsername("지한");
member.setAge(2);

// 등록
em.persist(member);

엔티티를 저장하려면 엔티티 매니저의 persist() 메소드에 저장할 엔티티를 넘겨주면 된다. 예제에서는 회원 엔티티를 생성하고 em.persist(member)를 실행해서 엔티티를 저장했다. JPA는 회원 엔티티의 매핑 정보를 어노테이션으로 분석해서

INSERT INTO MEMBER (ID, NAME, AGE) VALUES ('id1', '지한', 2)

다음과 같은 SQL을 만들어서 데이터베이스에 전달한다.

수정

// 수정
member.setAge(20);

엔티티를 수정한 후에 수정 내용을 반영하려면 em.update() 같은 메소드를 호출해야 할 것 같은데 단순히 엔티티의 값만 변경했다. JPA는 어떤 엔티티가 변경되었는지 추적하는 기능을 갖추고 있다 -> 엔티티의 값만 변경하면 UPDATE SQL을 생성해서 데이터베이스에 값을 변경한다. 그리고 em.update() 같은 메소드도 없다!

UPDATE MEMBER SET AGE=20, NAME='지한'
WHERE ID='id1'

프로젝트 진행할 때 해당 사항을 알지 못하고 update하는 로직일 경우 createQuery 메소드를 통해서 update시키는 SQL문을 생성해서 변경을 했었는데... 그럴 필요가 없었다니😂 앞으로는 주의해서 개발해야겠다..

삭제

em.remove(member);

엔티티를 삭제하려면 엔티티 매니저의 remove() 메소드에 삭제하려는 엔티티를 넘겨준다. JPA는 DELETE SQL을 생성해서 실행한다.

DELETE FROM MEMBER WHERE ID = 'id1'

삭제에서도.. remove 메소드를 사용하지 않고 createQuery 메소드를 통해서 delete 시키는 SQL문을 생성해서 삭제시켰었다.. 이 부분도 기억해둬야겠다..!

한 건 조회

// 한 건 조회
Member findMember = em.find(Member.class, id);

find() 메소드는 조회할 엔티티 타입과 @Id로 데이터베이스 테이블의 기본 키와 매핑한 식별자 값으로 엔티티 하나를 조회하는 가장 단순한 조회 메소드다. 이 메소드를 호출하면 다음 SELECT SQL을 생성해서 데이터베이스에 결과를 조회한다. 그리고 조회한 결과 값으로 엔티티를 생성해서 반환한다.

SELECT * FROM MEMBER WHERE ID='id1'

2.6.4 JPQL

하나 이상의 회원 목록을 조회하는 다음 코드를 자세히 살펴보자.

TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();

JPA를 사용하면 애플리케이션 개발자는 엔티티 객체를 중심으로 개발하고 데이터베이스에 대한 처리는 JPA에 맡겨야 한다. but, 문제는 검색 쿼리다.
테이블이 아닌 엔티티 객체를 대상으로 검색하려면 데이터베이스의 모든 데이터를 애플리케이션으로 불러와서 엔티티 객체로 변경한 다음 검색해야 하는데 이는 사실상 불가능하다. 애플리케이션이 필요한 데이터만 데이터베이스에 불러오려면 결국 검색 조건이 포함된 SQL을 사용해야 한다. JPA는 JPQL이라는 쿼리 언어로 이런 문제를 해결한다.
JPA는 SQL을 추상화한 JPQL이라는 객체지향 쿼리 언어를 제공한다. JPQL은 SQL과 문법이 거의 유사해서 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등을 사용할 수 있다.

  • JPQL은 엔티티 객체를 대상으로 쿼리한다. 즉, 클래스와 필드를 대상으로 쿼리한다.
  • SQL은 데이터베이스 테이블을 대상으로 쿼리한다.

위의 코드에서 find Member는 회원 엔티티 객체를 말하는 것이지 MEMBER 테이블이 아니다. JPQL은 데이터베이스 테이블을 전혀 알지 못한다.
JPQL을 사용하려면 em.createQuery(JPQL, 반환 타입) 메소드를 실행해서 쿼리 객체를 생성한 후 쿼리 객체의 getResultList() 메소드를 호출하면 된다.
JPA는 JPQL을 분석해서 다음과 같은 적절한 SQL을 만들어 데이터베이스에서 데이터를 조회한다.

SELCT M.ID, M.NAME, M.AGE FROM MEMBER M

2.7 정리

JPA를 사용하기 위한 개발 환경을 설정하고, JPA를 사용해서 객체 하나를 테이블에 등록/수정/삭제/조회하는 간단한 애플리케이션을 만들어보았다. 반복적인 JDBC API와 결과 값 매핑을 처리해주는 JPA 덕분에 코드량이 상당히 많이 줄어들었고 SQL도 작성할 필요가 없었다!!
이 책을 보기 전에는 JPQL이라는 것이 정확히 무엇인지 이해가 되지 않았는데, JPA가 제공하는 메소드에서는 검색하는 것에 한계가 있고 이를 해결하기 위해 객체지향 쿼리 언어인 JPQL을 사용한다는 것을 알게 되어서 좋았다.ㅎㅎ

profile
가보자고

0개의 댓글