이 책의 특징
이 책은 JPA를 사용해서 엔터프라이즈 애플리케이션을 개발하려는 모든 자바 개발자를 대상으로 한다. 이 책의 내용을 이해하려면 자바 언어와 JDBC를 사용한 데이터베이스 프로그래밍, 그리고 객체지향 프로그래밍과 관계형 데이터베이스에 대해 어느 정도 알고 있어야 한다. 추가로 웹 개발과 스프링 프레임워크에 대한 기초 지식이 필요하며 JUnit을 다룰 수 있어야 한다.
정리
JPA란 무엇인가?
- JPA(Java Persistence API)는 자바 진영의 ORM 기술 표준이다. 그렇다면 ORM이란 무엇일까? ORM(Object-Relational Mapping)은 이름 그대로 객체와 관계형 데이터베이스를 매핑한다는 뜻이다.
- ORM 프레임워크는 객체와 테이블으 매핑해서 패러다임의 불일치 문제를 개발자 대신 해결해준다. 예를 들어 ORM 프레임워크를 사용하면 객체를 데이터베이스에 저장할 때 INSERT SQL을 직접 작성하는 것이 아니라 객체를 마치 자바 컬렉션에 저장하듯이 ORM 프레임워크에 저장하면 된다.
객체 매핑 시작
- @Entity
- 이 클래스를 테이블과 매핑한다고 JPA에게 알려준다. 이렇게 @Entity가 사용된 클래스를 엔티티 클래스라 한다.
- @Table
- 엔티티 클래스에 매핑할 테이블 정보를 알려준다. 여기서는 name 속성을 사용해서 Member 엔티티를 MEMBER 테이블에 매핑했다. 이 어노테이션을 생략하면 클래스 이름을 테이블 이름으로 매피이한다.
- @Id
- 엔티티 클래스의 필드를 테이블의 기본 키에 매핑한다. @Id가 사용된 필드를 식별자 필드라 한다.
- @Column
- 필드를 컬럼에 매핑한다.
- @매핑 정보가 없는 필드
- 매핑 어노테이션을 생략하면 필드명을 사용해서 컬럼명으로 매핑한다.
persistence.xml 설정
- JPA는 persistence.xml을 사용해서 필요한 설정 정보를 관리한다.이 설정 파일이 META-INF/persistence.xml 클래스 패스 경로에 있으면 별도의 설정 없이 JPA가 인식할 수 있다.
애플리케이션 개발
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(); //엔티티 매니저 팩토리 종료
}
코드는 크게 3부분으로 나뉘어 있다. (엔티티 매니저 설정, 트랜잭션 관리, 비지니스 로직)
- 엔티티 매니저 설정
- JPA를 시작하려면 우선 persistence.xml의 설정 정보를 사용해서 엔티티 매니저 팩토리를 생성해야 한다. 엔티티 매니저 팩토리는 애플리케이션 전체에서 딱 한 번만 생성하고 공유해서 사용해야 한다.
- 엔티티 매니저 팩토리에서 엔티티 매니저를 생성한다. JPA의 기능 대부분은 이 엔티티 매니저가 제공한다. 대표적으로 엔티티 매니저를 사용해서 엔티티를 데이터베이스에 등록/수정/삭제/조회할 수 있다.
- 엔티티 매니저는 내부에 데이터소스(데이터베이스 커넥션)를 유지하면서 데이터베이스와 통신한다.
- 따라서 애플리케이션 개발자는 엔티티 매니저를 가상의 데이터베이스로 생각할 수 있다.
- 참고로 엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드간에 공유하거나 재사용하면 안 된다.
- 트랜잭션 관리
- JPA를 사용하면 항상 트랜잭션 안에서 데이터를 변경해야 한다. 트랜잭션 없이 데이터를 변경하면 예외가 발생한다. 트랜잭션을 시작하려면 엔티티 매니저에서 트랜잭션 API를 받아와야 한다.
- 비지니스 로직
- 비지니스 로직은 단순하다. 회원 엔티티를 하나 생성한 다음 엔티티 매니저를 통해 데이터베이스에 등록, 수정, 삭제, 조회한다.
-
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); }
- 등록, 수정, 삭제, 한 건 조회는 SQL을 전혀 사용하지 않는다. 문제는 검색 쿼리다. JPA는 엔티티 객체를 중심으로 개발하므로 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 한다.
- JPA는 SQL을 추상화한 JPQL이라는 객체지향 쿼리 언어를 제공한다.
- JPQL과 SQL의 가장 큰 차이점은 다음과 같다.
- JPQL은 엔티티 객체를 대상으로 쿼리한다. 쉽게 이야기해서 클래스와 필드를 대상으로 쿼리한다.
- SQL은 데이터베이스 테이블을 대상으로 쿼리한다.
영속성 관리
- 엔티티 매니저 팩토리와 엔티티 매니저
- 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안 된다.
- 영속성 컨텍스트란?
- 영속성 컨텍스트와 식별자 값
- 영속성 컨텍스트와 데이터베이스 저장
- 영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다.
- 1차 캐시
- 영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라 한다. 영속 상태의 엔티티는 모두 이곳에 저장된다. 조회를 할 때 먼저 1차 캐시에서 엔티티를 찾고 만약 찾는 엔티티가 1차 캐시에 없으면 데이터베이스에서 조회한다.
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라 한다. 그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.
- 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.
- 지연 로딩
- 지연 로딩은 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법이다.
- JPA를 이해하는 데 가장 중요한 용어는 영속성 컨텍스트다. 우리말로 번역하기가 어렵지만 해석하자면 '엔티티를 영구 저장하는 환경'이라는 뜻이다.
- 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- 이것은 논리적인 개념에 가깝고 눈에 보이지 않는다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다.
- 영속성 컨텍스트의 특징
- 영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다. 식별자 값이 없으면 예외가 발생한다.
- JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영하는데 이것을 플러시라 한다.
연관관계 매핑 기초
- 엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 예를 들어 주문 엔티티는 어떤 상품을 주문했는지 알기 위해 상품 엔티티와 연관관계가 있고 상품 엔티티는 카테고리, 재고 등 또 다른 엔티티와 관계가 있다. 그런데 객체는 참조를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다. 이 둘은 완전히 다른 특징을 가진다. 객체 관계 매핑에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일이다. 연관관계 매핑을 이해하기 위한 핵심 키워드는 다음과 같다.
- 방향 : 단방향, 양방향이 있다. 예를 들어 회원과 팀이 관계가 있을 때 회원 -> 팀 또는 팀 -> 회원 둘 중 한 쪽만 참조하는 것을 단방향 관계라 하고, 회원 -> 팀, 팀 -> 회원 양쪽 모두 서로 참조하는 것을 양방향 관계라 한다. 방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향이다.
- 다중성 : 다대일, 일대다, 일대일, 다대다 다중성이 있다. 예를 들어 회원과 팀이 관계가 있을 때 여러 회원은 한 팀에 속하므로 회원과 팀을 다대일 관계다. 반대로 한 팀에 여러 회원이 소속될 수 있으므로 팀과 회원은 일대다 관계다.
- 연관관계의 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.
단방향 연관관계
- 연관관계 중에서 다대일 단방향 관계를 가장 먼저 이해해야 한다.
- 객체 연관관계와 테이블 연관관계의 가장 큰 차이
- 참조를 통한 연관관계는 언제나 단방향이다. 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 결국 연관관계 하나 더 만들어야 한다. 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라 한다. 하지만 정확히 이야기하면 이것은 양방향 관계가 아니라 서로 다른 당방향 관계 2개다. 반면에 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.
- 객체 관계 매핑
- @ManyToOne : 이름 그대로 다대일 관계라는 매핑 정보다. 회원과 팀은 다대일 관계다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
- @JoinColumn(name = "TEAM_ID") : 조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다. 이 어노테이션은 생략할 수 있다.
양방향 연관관계 사용
- 팀과 회원은 일대다 관계다. 따라서 팀 엔티티에 컬렉션인 List<Member> members를 추가하고, 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용한다. mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름 값으로 주면 된다.
연관관계의 주인
- @OneToMany는 직관적으로 이해가 될 것이다. 문제는 mappedBy 속성이다. 엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다. 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다. 그런데 엔티티를 양방향으로 매핑하면 회원 -> 팀, 팀 -> 회원 두 곳에서 서로를 참조한다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳으로 늘어난다.
- 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까?
- 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.
양방향 매핑의 규칙 : 연관관계의 주인
- 양방향 연관관계 매핑 시 지켜야 할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
- 어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
- 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
- 연관관계의 주인을 테이블에 외래 키가 있는 곳으로 정해야 한다. 여기서는 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다.
QueryDSL
- JPA Criteria는 문자가 아닌 코드로 JPQL을 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있고 IDE 자동완성 기능의 도움을 받을 수 있는 등 여러 가지 장점이 있다. 하지만 Criteria의 가장 큰 단점은 너무 복잡하고 어렵다는 것이다. 작성된 코드를 보면 그 복잡성으로 인해 어떤 JPQL이 생성될지 파악하기 쉽지 않다.
- QueryDSL을 사용하려면 우선 com.mysema.query.jpa.impl.JPAQuery 객체를 생성해야 하는데 이때 엔티티 매니저를 생성자에 넘겨준다.
- 쿼리 작성이 끝나고 결과 조회 메소드를 호출하면 실제 데이터베이스를 조회한다. 보통 uniqueResult() 나 list()를 사용하고 파라미터로 프로젝션 대상을 넘겨준다. 대표적인 결과 조회 메소드는 다음과 같다.
- uniqueResult() : 조회 결과가 한 건일 때 사용한다. 조회 결과가 없으면 null을 반환하고 결과가 하나 이상이면 com.mysema.query.NonUniqueResultException 예외가 발생한다.
- singleResult() : uniqueResult() 와 같지만 결과가 하나 이상이면 처음 데이터를 반환한다.
- list() : 결과가 하나 이상일 때 사용한다. 결과가 없으면 빈 컬렉션을 반환한다.
- 페이징과 정렬
- 정렬은 orderBy를 사용하는데 쿼리 타입(Q)이 제공하는 asc(), desc()를 사용한다. 페이징은 offset과 limit을 적절히 조합해서 사용하면 된다.
네이티브 SQL
- JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스에 종속적인 기능은 지원하지 않는다. 예를 들어 다음과 같은 것들이다.
- 특정 데이터베이스만 지원하는 함수, 문법, SQL 쿼리 힌트
- 인라인 뷰(From 절에서 사용하는 서브쿼리), UNION, INTERSECT
- 스토어드 프로시저
스프링 데이터 JPA
- 스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다. 이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결한다. 우선 CRUD를 처리하기 위한 공통 인터페이스를 제공한다.
- 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
- 쿼리 메소드 기능
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
- @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의
- 쿼리 메소드 기능은 스프링 데이터 JPA가 제공하는 마법 같은 기능이다. 대표적으로 메소드 이름만으로 쿼리를 생성하는 기능이 있는데 인터페이스에 메소드만 선언하면 해당 메소드의 이름으로 적절한 JPQL 쿼리를 생성해서 실행한다. 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.
고급 주제와 성능 최적화
- 예외 처리
- JPA를 사용할 때 발생하는 다양한 예외와 예외에 따른 주의점이 있다.
- JPA 표준 예외는 크게 2가지로 나눌 수 있다.
- 트랜잭션 롤백을 표시하는 예외
- 트랜잭션 롤백을 표시하지 않는 예외
- 엔티티 비교
- 엔티티를 비교할 때 주의점이 있다.
- 프록시 심화 주제
- 프록시로 인해 발생하는 다양한 문제점과 해결 방법이 있다.
- 성능 최적화
- N+1 문제 : N+1 문제가 발생하면 한 번에 상당히 많은 SQL이 실행된다. N+1 문제가 발생하는 상황과 해결 방법이 있다.
- 읽기 전용 쿼리의 성능 최적화 : 엔티티를 단순히 조회만 하면 영속성 컨텍스트에 스냅샷을 유지할 필요도 없고 영속성 컨텍스트를 플러시할 필요도 없다. 엔티티를 읽기 전용으로 쿼리할 때 성능 최적화 방안이 있다.
- 배치 처리 : 수백만 건의 데이터를 처리해야 하는 배치 처리 상황에 JPA를 다루는 방안이 있다.
- SQL 쿼리 힌트 사용 : 하이버네이트를 통해 SQL 쿼리 힌트를 사용하는 방법이 있다.
- 트랜잭션을 지원하는 쓰기 지연과 성능 최적화 : 트랜잭션을 지원하는 쓰기 지연을 통해 성능을 최적화하는 방법이 있다.
트랜잭션과 락
- 트랜잭션과 격리 수준
- 트랜잭션은 ACID라 하는 원자성, 일관성, 격리성, 지속성을 보장해야 한다.
- 격리성을 완벽히 보장하려면 트랜잭션을 거의 차례대로 실행해야 한다. 이렇게 하면 동시성 처리 성능이 매우 나빠진다. 이런 문제로 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.
- READ UNCOMMITED(커밋되지 않은 읽기)
- READ COMMITTED(커밋된 읽기)
- REPEATABLE READ(반복 가능한 읽기)
- SERIALIZABLE(직렬화 가능)
- 낙관적 락과 비관적 락 기초
- 낙관적 락은 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다는 특징이 있다.
- 데이터베이스가 제공하는 락 기능을 사용한다.
- 낙관적 락은 이름 그대로 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법이다.
- 비관적 락은 이름 그대로 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법이다.
'Book' 카테고리의 다른 글
Do it! Vue.js 입문 (0) | 2021.10.03 |
---|
댓글