티스토리 뷰
JPA란
과거 자바 진행에서는 EJB 안에 엔티티 빈이라는 ORM 기술이 포함되어 있었으나, 너무 복잡하고 기술 성숙도도 떨어지고 J2EE 애플리케이션 서버에서만 동작하는 등의 어려움이 있었음. 이때 하이버네이트라는 오픈소스 ORM 프레임워크가 등장했는데 가볍고 실용적이고 기술 성숙도도 높았음. 결국 EJB 3.0에서 하이버네이트를 기반으로 새로운 자바 ORM 기술 표준이 만들어졌는데 이것이 JPA.
JPA가 제공하는 CRUD API
- 저장
정확히 이야기하면 아래 코드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다.
jpa.persist(member);
- 조회
String memberId = "helloId";
jpa.find(Member.class, memberId);
- 수정
JPA는 어떤 엔티티가 변경되었는지 추적하는 기능을 갖추고 있다. 따라서 엔티티의 값만 변경하면 UPDATE SQL을 생성해서 데이터베이스에 값을 변경한다.
Member member = jpa.find(Member.class, memberId);
member.setName("이름 변경');
- 연관된 객체 조회
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
- 하나 이상의 객체 조회
아래의 예제에서 from Member는 회원 엔티티 객체를 말하는 것이지 MEMBER 테이블이 아니다.
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
- 삭제
이렇게 삭제된 엔티티는 재사용하지 말고 자연스럽게 가비지 컬렉션의 대상이 되도록 두는 것이 좋다.
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA);
지연로딩
다른 객체와 연관된 객체를 조회할 때 당장 사용하지 않을 연관된 객체는 지연해서 조회하는 것. 즉, 사용할 때가 되어야 조회한다.
Member member = jpa.find(Member.class, memberId);
Order order = member.getOrder();
order.getOrderDate(); // 실제 order 객체를 사용할 때 조회!
객체의 비교
같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장. 동일성 비교(==)에 성공.
어노테이션
- @Entity: 이 클래스를 테이블과 매핑한다고 JPA에게 알려줌. 이 어노테이션이 사용된 클래스를 엔티티 클래스라 함.
- @Table: 엔티티 클래스에 매핑할 테이블 정보를 알려줌. 생략시 클래스 이름 = 테이블 이름.
- @Id: 기본 키
- @Column: 필드를 컬럼에 매핑. 이 어노테이션이 없으면 필드명으로 컬럼명 매핑.
- @Enumerated: 자바의 enum을 사용하려면 이 어노테이션으로 매핑해야 함.
- @Temporal: 자바의 날짜 타입은 이 어노테이션을 사용해서 매핑함.
- @Lob: CLOB, BLOB 타입을 매핑할 수 있음.
- @ManyToOne: 다대일
- @OneToMany: 일대다
- @JoinColumn: 외래 키를 매핑할 때 사용. 생략 가능
JPA 표준 속성
javax.persistence.jdbc.driver
javax.persistence.jdbc.user
javax.persistence.jdbc.password
javax.persistence.jdbc.url
- JPA를 사용하면 항상 트랜잭션 안에서 데이터를 변경해야 한다. 트랜잭션 없이 데이터를 변경하면 예외가 발생한다.
JPQL
- JPA를 사용하면 애플리케이션 개발자는 엔티티 객체를 중심으로 개발하고 데이터베이스에 대한 처리는 JPA에 맡겨야 한다. ... 애플리케이션이 필요한 데이터만 데이터베이스에서 불러오려면 결국 검색 조건이 포함된 SQL을 사용해야 한다. JPA는 JPQL이라는 쿼리 언어로 이런 문제를 해결한다.
- JPA는 SQL을 추상화한 JPQL이라는 객체지향 쿼리 언어를 제공한다. JPQL은 SQL과 문법이 거의 유사해서 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등을 사용할 수 있다. 둘의 가장 큰 차이점은 아래와 같다.
- JPQL은 엔티티 객체를 대상으로 쿼리한다. 쉽게 이야기해서 클래스와 필드를 대상으로 쿼리한다.
- SQL은 데이터베이스 테이블을 대상으로 쿼리한다.
영속성 관리
- 엔티티 매니저
- 이름 그대로 엔티티를 관리하는 관리자
- 엔티티를 저장하는 가상의 데이터베이스로 생각하면 된다.
- 엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드간에 공유하거나 재사용하면 안 된다.
- 엔티티 매니저는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다.
- 엔티티 매니저 팩토리
- 이름 그대로 엔티티 매니저를 만드는 공장
- 만드는 비용이 상당히 크기 때문에 한 개만 만들어서 애플리케이션 전체에서 공유하도록 설계되어 있다.
- 공장에서 엔티티 매니저를 생성하는 비용은 거의 들지 않는다.
- 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안 된다.
- 데이터베이스를 하나만 사용하는 애플리케이션은 일반적으로 EntityManagerFactory를 하나만 생성한다.
- 하이버네이트를 포함한 JPA 구현체들은 EntityManagerFactory를 생성할 때 커넥션풀도 만든다.
- 영속성 컨텍스트
- 엔티티를 영구 저장하는 환경
- 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다.
- 엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있고, 영속성 컨텍스트를 관리할 수 있다.
- 여러 엔티티 매니저가 같은 영속성 컨텍스트에 접근할 수도 있다.
- 특징
- 엔티티를 식별자 값(@Id)으로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다. 식별자 값이 없으면 예외가 발생한다.
- JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영하는데 이것을 플러시(flush)라 한다.
- 영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다.
- 1차 캐시
- 영속성 컨텍스트가 내부에 가지고 있는 캐시
- 영속 상태의 엔티티는 모두 1차 캐시(메모리)에 저장된다.
- em.find()를 호출하면 먼저 1차 캐시에서 엔티티를 찾고 만약 찾는 엔티티가 1차 캐시에 없으면 데이터베이스에서 조회한다. 엔티티 매니저는 데이터베이스를 조회해서 엔티티를 생성한다. 그리고 1차 캐시에 저장한 후에 영속 상태의 엔티티를 반환한다.
- 동일성 보장
- 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다. 따라서 둘은 같은 인스턴스고 동일성 비교 결과는 당연히 참이다.
- 트랜잭션을 지원하는 쓰기 지연
- 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소(쓰기 지연 SQL 저장소)에 INSERT SQL을 차곡차곡 모아둔다. 그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는 쓰기 지연이라 한다.
- 이 기능을 잘 활용하면 모아둔 등록 쿼리를 데이터베이스에 한 번에 전달해서 성능을 최적화할 수 있다.
- 변경 감지
- JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 된다.
- 이렇게 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경 감지라 한다.
- JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라 한다. 그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.
- 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.
- JPA의 기본 전략은 엔티티의 모든 필드를 업데이트한다. 이렇게 모든 필드를 사용하면 데이터베이스에 보내는 데이터 전송량이 증가하는 단점이 있지만, 다음과 같은 장점으로 인해 모든 필드를 업데이트한다.
- 모든 필드를 사용하면 수정 쿼리가 항상 같다. 따라서 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다.
- 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.
- 지연 로딩
- 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법
- 1차 캐시
- 엔티티의 생명주기
- 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
- 엔티티 객체를 생성했다. 지금은 순수한 객체 상태이며 아직 저장하지 않았다.
- 영속(managed): 영속성 컨텍스트에 저장된 상태
- 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장했다.
- 영속성 컨텍스트가 관리하는 엔티티
- 영속 상태의 엔티티는 모두 1차 캐시(메모리)에 저장된다.
- 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
- 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다.
- 특징
- 거의 비영속 상태에 가깝다.
- 식별자 값을 가지고 있다.
- 지연 로딩을 할 수 없다.
- 삭제(removed): 삭제된 상태
- 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.
- 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
- 플러시(flush())
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.(동기화)
- 플러시를 실행하면 구체적으로 다음과 같은 일이 일어난다.
- 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.(등록, 수정, 삭제 쿼리)
- 영속성 컨텍스트를 플러시하는 방법
- em.flush()를 직접 호출한다.
- 트랜잭션 커밋 시 플러시가 자동 호출된다.
- JPQL 쿼리 실행 시 플러시가 자동 호출된다.
- 엔티티 매니저에 플러시 모드를 직접 지정하려면 javax.persistence.FlushModeType을 사용하면 된다.
- FlushModeType.AUTO: 커밋이나 쿼리를 실행할 때 플러시(기본값)
- FlushModeType.COMMIT: 커밋할 때만 플러시
- 병합(merge())
- 병합은 파라미터로 넘어온 엔티티의 식별자 값으로 영속성 컨텍스트를 조회하고 찾는 엔티티가 없으면 데이터베이스에서 조회한다. 만약 데이터베이스에서도 발견하지 못하면 새로운 엔티티를 생성해서 병합한다.
- 병합은 준영속, 비영속을 신경 쓰지 않는다. 식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합하고 조회할 수 없으면 새로 생성해서 병합한다. 따라서 병합은 save or update 기능을 수행한다.
스키마 자동 생성
- 아래의 속성을 추가하면 애플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성한다.
<property name="hibernate.hbm2ddl.auto" value="create" />
- 스키마 자동 생성 기능이 만든 DDL은 운영 환경에서 사용할 만큼 완벽하지는 않으므로 개발 환경에서 사용하거나 매핑을 어떻게 해야 하는지 참고하는 정도로만 사용하는 것이 좋다.
- 기본 키 매핑
- 기본 키를 직접 할당하려면 @Id만 사용하면 되고, 자동 생성 전략을 사용하려면 @Id에 @GeneratedValue를 추가하고 원하는 키 생성 전략을 선택하면 된다.
연관관계의 주인
- 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다. 그런데 엔티티를 양방향으로 매핑하면 회원 -> 팀, 팀 -> 회원 두 곳에서 서로를 참조한다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳으로 늘어난다.
- 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까?
- 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관과계의 주인이라 한다.
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
- 어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
- 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
다양한 연관관계 매핑
- 먼저 연관관계가 있는 두 엔티티가 일대일 관계인지 일대다 관계인지 다중성을 고려해야 한다. 다음으로 두 엔티티 중 한쪽만 참조하는 단방향 관계인지 서로 참조하는 양방향 관계인지 고려해야 한다. 마지막으로 양방향 관계면 연관관계의 주인을 정해야 한다.
- 다대일: 단방향, 양방향
- 다대일 양방향 [N:1, 1:N]: 항상 서로 참조하게 하려면 연관관계 편의 메소드를 작성하는 것이 좋은데 편의 메소드는 한 곳에만 작성하거나 양쪽 다 작성할 수 있는데, 양쪽에 다 작성하면 무한루프에 빠지므로 주의해야 한다.
- 일대다: 단방향, 양방향
- 일대다 단방향 [1:N]: 일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다.
- 일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다. 본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 끝낼 수 있지만, 다른 테이블에 외래 키가 있으면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자
- 일대다 양방향 [1:N, N:1]: 일대다 양방향 매핑은 존재하지 않는다. 대신 다대일 양방향 매핑을 사용해야 한다.
- 일대다 단방향 [1:N]: 일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다.
- 일대일: 주 테이블 단방향, 양방향
- 일대일 관계를 구성할 때 객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다.
- 일대일: 대상 테이블 단방향, 양방향
- 다대다: 단방향, 양방향
- 복합 키를 사용하는 방법은 복잡하다. 단순히 컬럼 하나만 기본 키로 사용하는 거소가 비교해서 복합 키를 사용하면 ORM 매핑에서 처리할 일이 상당히 많아진다. 복합 키를 위한 식별자 클래스도 만들어야 하고 @IdClass 또는 @EmbeddedId도 사용해야 한다. 그리고 식별자 클래스에 equals, hashCode도 구현해야 한다.
- 추천하는 기본 키 생성 전략은 데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 것이다. 이것의 장점은 간편하고 거의 영구히 쓸 수 있으며 비즈니스에 의존하지 않는다. 그리고 ORM 매핑 시에 복합 키를 만들지 않아도 되므로 간단히 매핑을 완성할 수 있다.
고급 매핑
- 상속 관계 매핑
- ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.
- 슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택할 수 있다.
- 각각의 테이블로 변환
- 조인 전략
- 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다. 따라서 타입을 구분하는 컬럼을 추가해야 한다.
- 조인 전략
- 통합 테이블로 변환
- 서브타입 테이블로 변환
- 각각의 테이블로 변환
- @MappedSuperclass
- 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclass를 사용하면 된다.
- 복합 키와 식별 관계 매핑
- 조인 테이블
- 엔티티 하나에 여러 테이블 매핑하기
프록시와 연관관계 관리
- 프록시와 즉시로딩, 지연로딩
- 프록시
- 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 한다.
- 프록시 객체는 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다.
- 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
- 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다. 하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.
- 프록시
- 영속성 전이와 고아 객체
- 영속성 전이
- JPA에서 엔티티를 저장할 때 관련된 모든 엔티티는 영속 상태여야 한다. 따라서 부모 엔티티를 영속 상태로 만들고 자식 엔티티도 각각 영속 상태로 만든다. 이럴 때 영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한 번에 영속 상태로 만들 수 있다.
- 영속성 전이는 연관관계를 매핑하는 것과는 아무 관련이 없다. 단지 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다.
- 영속성 전이는 엔티티를 삭제할 때도 사용할 수 있다.
- 고아 객체
- JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 제거라 한다.
- 영속성 전이
값 타입
- 기본값 타입
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
...
}
- 위의 Member에서 String, int가 값 타입이다. Member 엔티티는 id라는 식별자 값도 가지고 생명주기도 있지만 값 타입인 name, age 속성은 식별자 값도 제거 된다. 그리고 값 타입은 공유하면 안 된다.
- 임베디드 타입(복합 값 타입)
- 새로운 값을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입이라 한다. 중요한 것은 직접 정의한 임베디드 타입도 int, String처럼 값 타입이라는 것이다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded
Period workPeriod;
@Embedded
Address homeAddress;
}
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
java.util.Date startDate;
@Temporal(TemporalType.DATE)
java.util.Date endDate;
public boolean isWork(Date date) {
...
}
}
@Embeddable
public class Address {
@Column(name="city")
private String city;
private String street;
private String zipcode;
}
- 임베디드 타입은 엔티티의 값일 뿐이다. 따라서 값이 속한 엔티티의 테이블에 매핑한다.
- 임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.
- @AttributeOverride: 속성 재정의
- 임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 된다.
- 임베디드 타입과 null
- 임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.
- 값 타입과 불변 객체
- 값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
- 값 타입 복사
- 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다. 대신에 값(인스턴스)을 복사해서 사용해야 한다.
- 값 타입 공유 참조
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
// 회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();
newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);
- 객체의 공유 참조는 피할 수 없다. 따라서 근본적인 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 된다.
- 불변객체
- 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다. 따라서 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.
- 한 번 만들면 절대 변경할 수 없는 객체를 불변 객체라 한다. 불변 객체의 값은 조회할 수 있지만 수정할 수 없다. 불변 객체도 결국은 객체다. 따라서 인스턴스의 참조 값 공유를 피할 수 없다. 하지만 참조 값을 공유해도 인스턴스의 값을 수정할 수 없으므로 부작용이 발생하지 않는다.
- 불변 객체를 구현하는 다양한 방법이 있지만 가장 간단한 방법은 생성자로만 값을 설정하고 수정자를 만들지 않으면 된다.
- 값 타입의 비교
- 값 타입을 비교할 때는 .equals()를 사용해서 동등성 비교를 해야 한다. 물론 equals() 메소드를 재정의해야 한다.
- 값 타입의 equals() 메소드를 재정의할 때는 보통 모든 필드의 값을 비교하도록 구현한다.
- 값 타입 컬렉션
- 값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션을 사용하면 된다.
- @CollectionTable을 생략하면 기본값을 사용해서 매핑한다. 기본값: {엔티티이름}_{컬렉션 속성 이름}, 예를 들어 Member 엔티티의 addressHistory는 Member_addressHistory 테이블과 매핑한다.
- 값 타입 컬렉션도 조회할 때 페치 전략을 선택할 수 있는데 LAZY가 기본이다.
- 값 타입은 식별자라는 개념이 없고 단순한 값들의 모음이므로 값을 변경해버리면 데이터베이스에 저장된 원본 데이터를 찾기는 어렵다.
- 특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면 된다. 문제는 값 타입 컬렉션이다. 값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관된다. 따라서 여기에 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다는 문제가 있다.
- 이런 문제로 인해 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.
- 따라서 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.
- 추가로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 따라서 데이터베이스 기본 키 제약 조건으로 인해 컬럼에 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없는 제약도 있다.
- 지금까지 설명한 문제를 해결하려면 값 타입 컬렉션을 사용하는 대신에 새로운 엔티티를 만들어서 일대다 관계로 설정하면 된다. 여기에 추가로 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.
정리
- 엔티티 타입의 특징
- 식별자(@id)가 있다. 엔티티 타입은 식별자가 있고 식별자로 구별할 수 있다.
- 생명 주기가 있다. 생성하고, 영속화하고, 소멸하는 생명 주기가 있다.
- em.persist(entity)로 영속화한다.
- em.remove(entity)로 제거한다.
- 공유할 수 있다. 참조 값을 공유할 수 있다. 이것을 공유 참조라 한다.
- 값 타입의 특징
- 식별자가 없다.
- 생명 주기를 엔티티에 의존한다. 스스로 생명주기를 가지지 않고 엔티티에 의존한다. 의존하는 엔티티를 제거하면 같이 제거된다.
- 공유하지 않는 것이 안전하다. 엔티티 타입과는 다르게 공유하지 않는 것이 안전하다. 대신에 값을 복사해서 사용해야 한다. 오직 하나의 주인만이 관리해야 한다. 불변 객체로 만드는 것이 안전하다.
객체지향 쿼리 언어
- 결국 데이터는 데이터베이스에 있으므로 SQL로 필요한 내용을 최대한 걸러서 조회해야 한다. 하지만 ORM을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다.
- JPQL은 이런 문제를 해결하기 위해 만들어졌는데 다음과 같은 특징이 있다.
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리다. JPQL을 사용하면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다. 그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.
- JPA는 JPQL뿐만 아니라 다양한 검색 방법을 제공한다. 다음은 JPA가 공식 지원하는 기능이다.
- JPQL
- Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
- 네이티브 SQL: JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.
- 다음은 JPA가 공식 지원하는 기능은 아니지만 알아둘 가치가 있다.
- QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크다.
- JDBC 직접 사용, MyBatis와 같은 SQL 매퍼 프레임워크 사용: 필요하면 JDBC를 직접 사용할 수 있다.
- JPQL
- JPQL은 엔티티 객체를 조회하는 객체지향 쿼리다. 문법은 SQL과 비슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다. 그리고 데이터베이스 방언만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다.
- JPQL은 SQL보다 간결하다. 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 코드가 간결하다.
- Criteria
- Criteria는 JPQL을 생성하는 빌더 클래스다. Criteria의 장점은 문자가 아닌 query.select(m).where(...)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다.
- 문자로 작성한 JPQL보다 코드로 작성한 Criteria의 장점은 다음과 같다.
- 컴파일 시점에 오류를 발견할 수 있다.
- IDE를 사용하면 코드 자동완성을 지원한다.
- 동적 쿼리를 작성하기 편하다.
- Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다. 따라서 사용하기 불편한 건 물론이고 Criteria로 작성한 코드도 한눈에 들어오지 않는다는 단점이 있다.
- QueryDSL
- QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다. QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다. 그리고 작성한 코드도 JPQL과 비슷해서 한눈에 들어온다. QueryDSL과 Criteria를 비교하면 Criteria는 너무 복잡하다.
- QueryDSL도 어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 만들어야 한다. QMember는 Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스다.
- 네이티브 SQL
- JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL이라 한다.
- JPQL을 사용해도 가끔은 특정 데이터베이스에 의존하는 기능을 사용해야 할 때가 있다. 이런 기능들은 전혀 표준화되어 있지 않으므로 JPQL에서 사용할 수 없다. 그리고 SQL은 지원하지만 JPQL이 지원하지 않는 기능도 있다. 이때는 네이티브 SQL을 사용하면 된다.
- 네이티브 SQL의 단점은 특정 데이터베이스에 의존하는 SQL을 작성해야 한다는 것이다. 따라서 데이터베이스를 변경하면 네이티브 SQL도 수정해야 한다.
- JDBC 직접 사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용
- 이럴 일은 드물겠지만, JDBC 커넥션에 직접 접근하고 싶으면 JPA는 JDBC 커넥션을 획득하는 API를 제공하지 않으므로 JPA 구현체가 제공하는 방법을 사용해야 한다.
- JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다. JDBC를 직접 사용하든 마이바티스 같은 SQL 매퍼와 사용하든 모두 JPA를 우회해서 데이터베이스에 접근한다. 문제는 JPA를 우회하는 SQL에 대해서는 JPA가 전혀 인식하지 못한다는 점이다. 최악의 시나리오는 영속성 컨텍스트와 데이터베이스를 불일치 상태로 만들어 데이터 무결성을 훼손할 수 있다.
- 이런 이슈를 해결하는 방법은 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.
- 참고로 스프링 프레임워크를 사용하면 JPA와 마이바티스를 손쉽게 통합할 수 있다. 또한 스프링 프레임워크의 AOP를 적절히 활용해서 JPA를 우회하여 데이터베이스에 접근하는 메소드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위에서 언급한 문제도 깔끔하게 해결할 수 있다.
- JPQL
- JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
- SELECT
- 엔티티와 속성은 대소문자를 구분한다. 반면에 SELECT, FROM, AS와 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
- 엔티티 명은 @Entity(name="XXX")로 지정할 수 있다. 엔티티 명을 지정하지 않으면 클래스명을 기본값으로 사용한다. 기본값인 클래스 명을 엔티티 명으로 사용하는 것을 추천한다.
- JPQL은 별칭을 필수로 사용해야 한다. AS는 생략할 수 있다.
- TypeQuery, Query
- 작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 TypeQuery와 Query가 있는데 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용하고, 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용하면 된다.
- 파라미터 바인딩
- JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩도 지원한다.
- 이름 기준 파라미터
- 파라미터를 이름으로 구분하는 방법
- 파라미터 앞에 :를 사용
- 위치 기준 파라미터
- ? 다음에 위치 값을 주면 된다. 위치 값은 1부터 시작한다.
- 위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.
- 파라미터 바인딩 방식을 사용하지 않고 직접 문자를 더해 만들어 넣으면 악의적인 사용자에 의해 SQL 인젝션 공격을 당할 수 있다. 또한 성능 이슈도 있는데 파라미터 바인딩 방식을 사용하면 파라미터의 값이 달라도 같은 쿼리로 인식해서 JPA는 JPQL을 SQL로 파싱한 결과를 재사용할 수 있다. 그리고 데이터베이스도 내부에서 실행한 SQL을 파싱해서 사용하는데 같은 쿼리는 파싱한 결과를 재사용할 수 있다. 결과적으로 애플리케이션과 데이터베이스 모두 해당 쿼리의 파싱 결과를 재사용할 수 있어서 전체 성능이 향상된다. 따라서 파라미터 바인딩 방식은 선택이 아닌 필수다.
- 프로젝션
- 엔티티 프로젝션
- SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 하고 [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다. 프로젝션 대상은 엔티티, 엠비디드 타입, 스칼라 타입이 있다. 스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 뜻한다.
- 엔티티 프로젝션
SELECT m FROM Member m
SELECT m.team FROM Member m
- 둘 다 엔티티를 프로젝션 대상으로 사용했다. 쉽게 생각하면 원하는 객체를 바로 조회한 것인데 컬럼을 하나하나 나열해서 조회해야 하는 SQL과는 차이가 있다. 참고로 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
- 임베디드 타입 프로젝션
- JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다. 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.
- 임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
- 스칼라 타입 프로젝션
- 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.
- 여러 값 조회
- 엔티티를 대상으로 조회하면 편리하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다.
- 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다.
- NEW 명령어
- SELECT 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다. 그리고 NEW 명령어를 사용한 클래스로 TypeQuery 사용할 수 있어서 지루한 객체 변환 작업을 줄일 수 있다.
- NEW 명령어를 사용할 때는 다음 2가지를 주의해야 한다.
- 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
- 페이징 API
- JPA는 페이징을 다음 두 API로 추상화했다.
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작한다)
- setMaxResults(int maxResult) : 조회할 데이터 수
- 데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다.
- JPA는 페이징을 다음 두 API로 추상화했다.
- 집합과 정렬
- 집합은 집합함수와 함께 통계 정보를 구할 때 사용한다.
- NULL 값은 무시하므로 통계에 잡히지 않는다.(DISTINCT가 정의되어 있어도 무시된다).
- 만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다. 단 COUNT는 0이 된다.
- DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
- DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
- GROUP BY, HAVING
- 이런 쿼리들을 보통 리포팅 쿼리나 통계 쿼리라 한다. 이러한 통계 쿼리를 잘 활용하면 애플리케이션으로 수십 라인을 작성할 코드도 단 몇 줄이면 처리할 수 있다. 하지만 통계 쿼리는 보통 전체 데이터를 기준으로 처리하므로 실시간으로 사용하기엔 부담이 많다. 결과가 아주 많다면 통계 결과만 저장하는 테이블을 별도로 만들어 두고 사용자가 적은 새벽에 통계 쿼리를 실행해서 그 결과를 보관하는 것이 좋다.
- 정렬(ORDER BY)
- JPQL 조인
- JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다.
- 혹시라도 JPQL 조인을 SQL 조인처럼 사용하면 문법 오류가 발생한다. JPQL은 JOIN 명령어 다음에 조인할 객체의 연관 필드를 사용한다.
- 컬렉션 조인
- 일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
- 세타 조인
- WHERE 절을 사용해서 세타 조인을 할 수 있다. 참고로 세타 조인은 내부 조인만 지원한다.
- JOIN ON 절
- JPA 2.1부터 조인할 때 ON 절을 지원한다.
- 페치 조인
- 페치 조인은 SQL에서 이야기하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이것은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어로 사용할 수 있다.
- 엔티티 페치 조인
- 페치 조인은 별칭을 사용할 수 없다.
- 컬렉션 페치 조인
- 일대다 조인은 결과가 증가할 수 있지만 일대일, 다대일 조인은 결과가 증가하지 않는다.
- 페치 조인과 DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령어다. JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다.
- JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
- 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.
- 페치 조인은 다음과 같은 한계가 있다.
- 페치 조인 대상에는 별칭을 줄 수 없다.
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 경로 표현식
- 상태 필드
- 단순히 값을 저장하기 위한 필드(필드 or 프로퍼티)
- 경로 탐색의 끝이다. 더는 탐색할 수 없다.
- 연관 필드: 연관관계를 위한 필드, 임베디드 타입 포함(필드 or 프로퍼티)
- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티
- 묵시적으로 내부 조인이 일어난다. 단일 값 연관 경로는 계속 탐색할 수 있다.
- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션
- 묵시적으로 내부 조인이 일어난다. 더는 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.
- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티
- 경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인이다.
- 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
- 경로 탐색은 주로 SELECT, WHERE 절(다른 곳에서도 사용됨)에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 준다.
- 상태 필드
- 서브 쿼리
- 서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다.
- 임베디드 타입 프로젝션
- Criteria
- Criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API다.
- QueryDSL
- 쿼리를 문자가 아닌 코드로 작성해도, 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트가 바로 QueryDSL이다. QueryDSL도 Criteria처럼 JPQL 빌더 역할을 하는데 JPA Criteria를 대체할 수 있다.
- 네이티브 SQL
- JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스에 종속적인 기능은 지원하지 않는다.
- 네이티브 SQL을 사용하면 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다.
- 객체지향 쿼리 심화
- 벌크 연산
- 벌크 연산은 executeUpdate() 메소드를 사용한다. 이 메소드는 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.
- 벌크 연산을 사용할 때는 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의해야 한다.
- 따라서 영속성 컨텍스트에 있는 상품A와 데이터베이스에 있는 상품A의 가격이 다를 수 있다. 따라서 벌크 연산은 주의해서 사용해야 한다.
- 벌크 연산을 수행한 직후에 정확한 상품A 엔티티를 사용해야 한다면 em.refresh()를 사용해서 데이터베이스에서 상품A를 다시 조회하면 된다.
- 벌크 연산 먼저 실행
- 가장 실용적인 해결책은 벌크 연산을 가장 먼저 실행하는 것이다.
- 벌크 연산 수행 후 영속성 컨텍스트 초기화
- 벌크 연산을 수행한 직후에 바로 영속성 컨텍스트를 초기화해서 영속성 컨텍스트에 남아 있는 엔티티를 제거하는 것도 좋은 방법이다.
- 벌크 연산
- 영속성 컨텍스트와 JPQL
- 쿼리 후 영속 상태인 것과 아닌 것
- 조회한 엔티티만 영속성 컨텍스트가 관리한다.
- 쿼리 후 영속 상태인 것과 아닌 것
- find() vs JPQL
- em.find() 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다. 따라서 해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로 성능상 이점이 있다(그래서 1차 캐시라 부른다).
- JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.
- JPQL과 플러시 모드
- 플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것이다. JPA는 플러시가 일어날 때 영속성 컨텍스트에 등록, 수정, 삭제한 엔티티를 찾아서 INSERT, UPDATE, DELETE SQL을 만들어 데이터베이스에 반영한다. 플러시를 호출하려면 em.flush() 메소드를 직접 사용해도 되지만 보통 플러시 모드에 따라 커밋하기 직전이나 쿼리 실행 직전에 자동으로 플러시가 호출된다.
- 플러시 모드는 FlushModeType.AUTO가 기본값이다. 따라서 JPA는 트랜잭션 커밋 직전이나 쿼리 실행 직전에 자동으로 플러시를 호출한다. 다른 옵션으로는 FlushModeType.COMMIT이 있는데 이 모드는 커밋 시에만 플러시를 호출하고 쿼리 실행 시에는 플러시를 호출하지 않는다. 이 옵션은 성능 최적화를 위해 꼭 필요할 때만 사용해야 한다.
300x250
'공부흔적 > JPA' 카테고리의 다른 글
TypeORM과 JPA에서의 Soft Delete (1) | 2024.11.21 |
---|---|
Gradle 프로젝트 JPA Oracle 연결하기 (0) | 2021.04.25 |
JPA가 제공하는 데이터베이스 기본 키 생성 전략 (0) | 2021.04.07 |
영속성 컨텍스트 초기화(clear)와 종료(close), 비영속과 준영속 (0) | 2021.04.06 |
SQL Mapper와 ORM(Object-Relational Mapper) (0) | 2021.04.02 |