1 ~ 2 : JPA 소개, JPA 시작

1장 : JPA 소개

과거

JDBC API 와 SQL를 사용했던 시절 비즈니스 로직에 집중하기 힘듬

SQL 매퍼(마이바티스, JdbcTemplate)를 사용하며 JDBC API 사용코드는 줄였지만, 여전히 반복 작업이 많음

ORM 프레임워크는 객체와 관계형 데이터베이스 간의 차이를 중간에서 해결해준다.

JPA는 자바 진영의 ORM 기술 표준이다.

JPA의 장점

  • CRUD SQL을 작성할 필요가 없다.

  • 조회된 결과를 객체로 매핑하는 작업을 자동으로 처리해준다.

  • JPA가 제공하는 네이티브 SQL 기능을 사용해서 직접 SQL을 작성하여 성능에 대한 대안을 마련한다.

  • ✨ 애플리케이션을 SQL이 아닌 객체 중심으로 개발하며 생산성과 유지보수가 좋아졌고 테스트를 작성하기도 편리해졌다.

  • 데이터베이스가 변경되는 정책 변경 대응에도 용이하다.

SQL을 직접 다룰 때 발생하는 문제점

반복, 반복 그리고 반복

회원 관리 기능을 개발해보자.

public class Member {

	private String memberId;
	private String name;
	...
}

✔️ 회원 조회

회원 조회용 SQL을 작성한다.

SELECT MEMBER_ID, NAME FROM MEMBER M WHERE MEMBER_ID = ? 

✔️ 회원 등록 기능

회원 등록용 SQL을 작성한다.

String sql = "InSESRT INTO MEMBER(MEMBER_ID, NAME) VALUES(?, ?)";
PreparedStatement

회원 객체를 데이터베이스가 아닌 자바 컬렉션에 보관한다면 다음과 같이 한 줄로 객체 저장이 가능하다.

list.add(member);

하지만 데이터베이스는 객체 구조와는 다른 데이터 중심의 구조를 가지므로 객체를 데이터베이스에 직접 저장하거나 조회할 수 없다. 따라서 개발자가 SQL과 JDBC API를 사용해서 변환 작업을 직접 해주어야 한다.

DAO(데이터 접근 계층)

SQL에 의존적인 개발

회원의 연락처도 함께 저장해달라는 요구사항이 추가되었다.

✔️ 등록 코드 변경

public class Member {

	private String memberId;
	private String name;
	private String tel; // 추가
	...
}

✔️ 조회 코드 변경

SELECT MEMBER_ID, NAME, TEL FROM MEMBER WHERE MEMBER_ID = ? 

✔️ 수정 코드 변경

UPDATE SQL와 MemeberDAO.update()의 일부 코드를 연락처가 수정될 수 있게 수정해야 된다.

✔️ 연관된 객체

회원은 어떤 한 팀에 필수 소속되어야 한다는 요구사항이 추가되었다

public class Member {

	private String memberId;
	private String name;
	private String tel; 
	...
}

// 추가
class Team {
	...
	private String teamName;
	...

}

Member 객체가 연관된 Team 객체 사용의 여부는 SQL에 달려있다. 이는 데이터 접근 계층을 사용해서 SQL을 숨겨도 어쩔 수 없이 DAO를 열어서 어떤 SQL이 실행되는지 확인해야 한다.

비즈니스 요구사항을 모델링한 객체를 엔티티라고 한다.

문제점

  • SQL에 모든 것을 의존하는 상황에서 개발자들이 엔티티를 신뢰하고 사용할 수 없다. DAO를 열어 어떤 SQL이 실행되고 어떤 객체들이 함께 조회되는지 일일이 확인해야 한다.

  • 물리적으로 SQL과 JDBC API를 데이터 접근 계층에 숨기는 데 성공했을지는 몰라도 논리적으로 엔티티와 강한 의존관계를 가지고 있다. 강한 의존 관계는 작은 요구사항 변경에도 DAO의 CRUD 코드와 SQL 대부분을 변경해야 한다.

JPA와 문제 해결

  • JPA를 사용하면 객체를 데이터베이스에 저장하고 관리할 때, JPA가 개발자 대신에 적절한 SQL을 생성해서 데이터베이스에 전달한다.

jpa.persist(member);

패러다임의 불일치

애플리케이션을 개발하는 일은 끊임없이 증가하는 복잡성과의 싸움이다. 복잡성을 제어해야 유지보수가 쉬워지는데, 객체지향 프로그래밍은 추상화, 캡슙화, 정보은닉, 상속, 다형성 등과 같은 시스템의 복잡성을 제어할 수 있는 장치들이 존재한다.

객체와 관계형 데이터베이스의 패러다임 불일치 문제

  • 객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다.

  • 관계형 데이터베이스는 데이터 중심으로 구조화되어 있고, 집합적인 사고를 요구하고 객체지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다.

애플리케이션은 자바라는 객체지향 언어로 개발하고 데이터는 관계형 데이터베이스에 저장해야 한다면, 패러다임의 불일치 문제를 중간에서 해결해야 한다.

상속

JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해준다.

JDBC API를 사용해서 상속 관계에 있는 객체를 관계형 데이터베이스에 저장하는 것은 소모되는 비용이 크다.

✔️ JPA와 상속

생성

jpa.persist(album);

조회

String albumId = "id100";
Album album = jpa.find(Album.class, albumId);

연관관계

객체참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회하고, 테이블외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.

✔️ 객체를 테이블에 맞추어 모델링

  • 이 경우 객체를 테이블에 저장하거나 조회할 때는 편리하지만 외래 키의 값을 그대로 보관하므로, 객체를 참조를 통해서 조회할 수 없다. → 객체지향의 특징 잃어버림

  • 객체는 연관된 객체의 참조를 보관해야 참조를 통해 연관된 객체를 찾을 수 있다.

✔️ 객체지향 모델링

외래 키의 값을 그대로 보관하는 것이 아니라 연관된 참조를 보관하며 연관된 객체 조회가 바로 가능해진다.

  • 하지만 Member 객체는 team 필드로 연관관계를 맺고 MEMBER 테이블은 TEAM_ID 외래 키로 연관관계를 맺기 때문에 객체를 테이블에 저장하거나 조회하기가 쉽지 않다. 객체 모델은 외래 키가 필요 없고 단지 참조만 있으면 되는 반면 테이블은 참조가 필요 없고 외래 키만 있으면 되므로, 개발자가 중간에서 변환 역할을 해야 한다.

    • 저장 - 객체를 데이터베이스에 저장하려면 team 필드를 TEAM_ID 외래 키 값으로 변환해야 한다.

    • 조회 - 조회할 때는 TEAM_ID 외래 키 값을 Member 객체의 team 참조를 변환해서 객체에 보관해야 한다.

✔️ JPA와 연관관계

JPA는 연관관계와 관련된 패러다임의 불일치 문제를 해결해준다.

  • JPA는 참조를 외래 키로 변환해서 적절한 INSERT SQL을 데이터베이스에 전달하고, 조회할 때 외래 키를 참조로 변환하는 일도 처리해준다.

객체 그래프 탐색

참조를 사용해서 연관된 객체를 찾는 것을 객체 그래프 탐색이라 한다. 객체는 마음껏 객체 그래프를 탐색할 수 있어야 한다.

  • SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다.

  • 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수 없기 때문에, 결국 어디까지 객체 그래프 탐색이 가능한지 알아보려면 데이터 접근 계층인 DAO를 열어서 SQL을 직접 확인해야 한다. → 엔티티가 SQL에 논리적으로 종속

  • 연관된 모든 객체 그래프를 데이터베이스에서 조회해서 애플리케이션 메모리에 올려두는 것은 현실성이 없다. 결국 메소드를 상황에 따라 여러 만들어서 사용해야 한다.

✔️ JPA와 객체 그래프 탐색

JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다.

  • JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 마음껏 조회할 수 있다. 이 기능은 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라 한다.

  • 이런 기능을 사용하려면 객체에 JPA와 관련된 어떤 코드들을 심어야 하는 것은 아닐까 ? JPA는 지연 로딩을 투명하게 처리한다. 메소드의 구현 부분에 JPA와 관련된 어떤 코드도 직접 사용하지 않는다.

  • JPA는 연관된 객체를 즉시 함께 조회할지 아니면 실제 사용되는 시점에 지연해서 조회할지를 간단한 설정으로 정의할 수 있다.

비교

데이터베이스는 기본 키의 값으로 각 로우(row)를 구분하는 반면에 객체는 동일성(identity) 비교동등성(equality) 비교라는 두 가지 비교 방법이 있다.

  • 동일성 비교는 == 비교로, 객체 인스턴스의 주소 값을 비교한다.

  • 동등성 비교는 equals() 메소드를 사용해서 객체 내부의 값을 비교한다.

✔️ JPA와 비교

JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.

String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Memeber.class, memberId);

member1 == member2; // 같다.

정리

객체 모델과 관계형 데이터베이스 모델은 지향하는 패러다임이 서로 다르다. 이 틈을 메우기 위해 개발자가 소모해야 하는 비용이 점점 더 많아지게되고 결국 데이터 중심의 모델로 변해간다. 자바 진영에서는 오랜 기간 이 문제에 대한 숙제를 안고 있었고, 그 결과물이 JPA다.

JPA란 무엇일까 ?

ORM(Object-Relational Mapping)은 객체와 관계형 데이터베이스를 매핑한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑해서 패러다임의 불일치 문제를 해결해준다.

하이버네이트 프레임워크는 자바 진영에 가장 보편적인 ORM 프레임워크다.

example

ORM 프레임워크를 사용하면 객체를 데이터베이스에 저장할 때 INSERT SQL을 직접 작성하는 것이 아니라 객체를 마치 자바 컬렉션에 저장하듯이 ORM 프레임워크에 저장하면 된다. 그러면 ORM 프레임워크가 적절한 INSERT SQL을 생성해서 데이터베이스에 객체를 저장해준다.

JPA 소개

JPA는 자바 ORM 기술에 대한 API 표준 명세다. 쉽게 이야기해서 인터페이스를 모아둔 것이다. JPA라는 표준 덕분에 특정 구현 기술에 의존도를 줄일 수 있다.

왜 JPA를 사용해야 하는가 ?

  • 생산성

  • 유지보수

  • 패러다임의 불일치 해결

  • 성능

  • 데이터 접근 추상화와 벤더 독립성

2장 : JPA 시작

애플리케이션 개발

import javax.persistence.*;
import java.util.List;

public class JpaMain {

	public static void main(String[] args) {
	
		// [엔티티 매니저 팩토리] - 생성
		EntityManagerFactory emf = Persistence.createEntityMangerFactory("jpabook");
		
		// [엔티티 매니저] - 생성
		EntityManager em = emf.createEntityManagr();

		// [트랜잭션] - 획득
		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) { ... }

}

엔티티 매니저 설정

엔티티 매니저 팩토리 생성

  • JPA를 시작하려면 persistence.xml의 설정 정보를 사용해서 엔티티 매니저 팩토리를 생성한다.

  • 엔티티 매니저 팩토리는 애플리케이션 전체에서 딱 한 번만 생성하고 공유해서 사용해야 한다.

    • 설정 정보를 읽어서 JPA를 동작시키기 위한 기반 객체를 만들고 JPA 구현체에 따라서는 데이터베이스 커넥션 풀도 생성하므로 엔티티 매니저 팩토리 생성 비용이 크다.

엔티티 매니저 생성

  • 엔티티 매니저 팩토리에서 엔티티 매니저를 생성한다.

  • JPA의 기능 대부분은 이 엔티티 매니저가 제공한다.

  • 대표적으로 엔티티 매니저를 사용해서 엔티티를 데이터베이스에 등록/수정/삭제/조회할 수 있다. 엔티티 매니저는 내부에 데이터소스(데이터베이스 커넥션)를 유지하면서 데이터베이스와 통신한다. 따라서 애플리케이션 개발자는 엔티티 매니저를 가상의 데이터베이스로 생각할 수 있다.

  • 참고로 엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드 간에 공유하거나 재사용하면 안 된다.

종료

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

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

트랜잭션 관리

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

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

EntityTracsaction tx = em.getTransaction();
try {

	tx.begin();
	logic(em);
	tx.commit();

} catch (Exception e) {
		tx.rollback();
}

비즈니스 로직

회원 CRUD

public static void logic(EntityManager em) {

	String id = "id1";
	Member member = new Member();
	member.setId(id);
	member.setUsername("soobin");
	member.setAge(20);

	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.createAuerty("select m from Member m", Member.class)
			.getResultList()
	System.out.println("members.size = " + members.size());

	em.remove(members);
}

등록

  • 엔티티를 저장하려면 엔티티 매니저의 persist() 메소드에 저장할 엔티티를 넘겨주면 된다. JPA는 회원 엔티티의 매핑 정보(어노테이션)를 분석해서 SQL을 만들어 데이터베이스에 전달한다.

수정

  • JPA는 어떤 엔티티가 변경되었는지 추적하는 기능을 갖추고 있다. 따라서 엔티티의 값만 변경하면 UPDATE SQL을 생성해서 데이터베이스에 값을 변경한다.

삭제

  • 멘티티를 삭제하려면 엔티티 매니저의 remove() 메소드에 삭제하려는 엔티티를 넘겨준다.

한 건 조회

  • find() 메소드는 조회할 엔티티 타입과 @Id 로 데이터베이스 테이블의 기본 키와 매핑한 식별자 값으로 엔티티 하나를 조회하는 가장 단순한 조회 메소드다.

JPQL

JPQL은 JPA가 제공하는 SQL을 추상화한 객체지향 쿼리 언어이다.

JPA를 사용하면 애플리케이션 개발자는 엔티티 객체를 중심으로 개발하고 데이터베이스에 대한 처리는 JPA에 맡겨야 한다. 문제는 검색 쿼리다. JPA는 엔티티 객체를 중심으로 개발하므로 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 한다.

테이블이 아닌 엔티티 객체를 대상으로 검색하려면 데이터베이스의 모든 데이터를 애플리케이션으로 불러와서 엔티티 객체로 변경한 다음 검색해야 하는데, 이는 사실상 불가능하다. 애플리케이션이 필요한 데이터만 데이터베이스에서 불러오려면 결국 검색 조건이 포함된 SQL을 사용해야 하는데, JPA는 JPQL이라는 쿼리 언어로 이런 문제를 해결한다.

JPQL과 SQL 차이점
  • JPQL은 엔티티 객체를 대상으로 쿼리한다. 클래스와 필드를 대상으로 쿼리한다.

  • SQL은 데이터베이스 테이블을 대상으로 쿼리한다.

  • JPQL을 사용하려면 먼저 em.createQuery 메소드를 실행해서 쿼리 객체를 생성한 후 쿼리 객체의 getResultList() 메소드를 호출하면 된다.

  • JPA는 JPQL을 분석해서 적절한 SQL을 만들어 데이터베이스에서 데이터를 조회한다.

  • JPQL은 데이터베이스 테이블을 전혀 알지 못한다.

SQL 표준을 지키지 않거나 특정 데이터베이스만의 고유한 기능을 JPA에서는 방언(Dialect)이라 한다.

photo source angelaziegeler

Last updated