티스토리 뷰
JPA 소개
- 애플리케이션에서 SQL을 직접 다룰 때 발생하는 문제점을 요약하면 다음과 같다.
- 진정한 의미의 계층 분할이 어렵다.
- 엔티티를 신뢰할 수 없다.
- SQL에 의존적인 개발을 피하기 어렵다.
- 객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다.
- 반면에 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.
- SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다.
- 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연로딩이라 한다.
- JPA는 자바 ORM 기술에 대한 API 표준 명세다.
- JPA가 어려운 근본적인 이유는 ORM이 객체지향과 관계형 데이터베이스라는 두 기둥 위에 있기 때문이다.
JPA 시작
- 엔티티 매니저 팩토리는 애플리케이션 전체에서 딱 한 번만 생성하고 공유해서 사용해야 한다.
- 엔티티 매니저를 사용해서 엔티티를 데이터베이스에 등록/수정/삭제/조회할 수 있다.
- 참고로 엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드 간에 공유하거나
재사용하면 안 된다. - JPQL은 엔티티 객체를 대상으로 쿼리한다. 쉽게 이야기해서 클래스와 필드를 대상으로 쿼리한다.
- SQL은 데이터베이스 테이블을 대상으로 쿼리한다.
- JPQL은 데이터베이스 테이블을 전혀 알지 못한다.
영속성 관리
- 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에
공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로
스레드 간에 절대 공유하면 안 된다. - 영속성 컨테스트는 엔티티를 영구 저장하는 환경이라는 뜻
- 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다.
- 엔티티의 생명주기
- 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속(managed): 영속성 컨텍스트에 저장된 상태
- 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed): 삭제된 상태
- 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라 한다.
- 결국 영속 상태라는 것은 영속성 컨텍스트에 의해 관리된다는 뜻이다.
- 영속 상태는 식별자 값이 반드시 있어야 한다.
- 영속성 컨텍스트는 성능상 이점과 엔티티의 동일성을 보장한다.
- 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는
쓰기 지연이라 한다. - 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화한 후에 실제 데이터베이스 트랜잭션을
커밋한다. - SQL 수정 쿼리의 문제점은 수정 쿼리가 많아지는 것은 물론이고 비즈니스 로직을 분석하기 위해
SQL을 계속 확인해야 한다. 결국 직접적이든 간접적이든 비즈니스 로직이 SQL에 의존하게 된다. - 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경 감지라 한다.
- 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.
- JPA의 기본 전략은 엔티티의 모든 필드를 업데이트한다.
- 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
- 플러시라는 이름으로 인해 영속성 컨텍스트에 보관된 엔티티를 지운다고 생각하면 안 된다.
- 다시 한 번 강조하지만 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 것이 플러시
- 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
- 영속 상태였다가 더는 영속성 컨텍스트가 관리하지 않는 상태를 준영속 상태라 한다.
- 준영속 상태는 영속성 컨텍스트로부터 분리된 상태가.
- merge() 메소드는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환
엔티티 매핑
- hibernate.hbm2ddl.auto = create 일 경우 애플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성
- hbm2ddl 주의사항
- 운영 서버에서 create, create-drop, update 처럼 ddl을 수정하는 옵션은 절대 사용하면 안 됨
- 개발 환경에 따른 추천 전략
- 개발 초기 단계에는 create 나 update
- 초기화 상태로 자동화된 테스트를 진행하는 개발자 환경과 CI 서버는 create 또는 create-drop
- 테스트 서버는 update 또는 validate
- 스테이징과 운영 서버는 validate 또는 none
- @Column의 length와 nullable 속성을 포함해서 이런 기능들은 단지 DDL을 자동으로 생성할 때만 사용되고
JPA의 실행 로직에는 영향을 주지 않는다. - 기본 키 매핑
- 직접 할당: 기본 키를 애플리케이션에서 직접 할당한다.
- 자동 생성: 대리 키 사용 방식
- IDENTITY: 기본 키 생성을 데이터베이스에 위임한다.
- SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
- TABLE: 키 생성 테이블을 사용한다.
- IDENTITY 전략과 최적화
- 데이터를 데이터베이스에 INSERT한 후에 기본 키 값을 조회할 수 있으므로 엔티티에 식별자 값을 할당하려면
JPA는 추가로 데이터베이스를 조회해야 한다. - JDBC3에 추가된 Statement.getGeneratedKeys()를 사용하면 데이터를 저장하면서 동시에 생성된 기본 키 값도 얻어 올 수 있다.
- 데이터를 데이터베이스에 INSERT한 후에 기본 키 값을 조회할 수 있으므로 엔티티에 식별자 값을 할당하려면
- 권장하는 식별자 선택 전략
- 기본 키는 다음 3가지 조건을 모두 만족해야 함
- null 값은 허용하지 않는다.
- 유일해야 한다.
- 변해선 안 된다.
- 테이블 기본 키를 선택하는 전략
- 자연 키(natural key): 비즈니스에 의미가 있는 키
- 대리 키(surrogate key): 비즈니스와 관련 없는 임의로 만들어진 키, 대체 키로도 불린다.
- 자연 키보다는 대리 키를 권장한다.
- 비즈니스 환경은 언젠가 변하므로 자연 키를 사용하는 것은 권장되지 않는다.
- JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장한다.
- 기본 키는 다음 3가지 조건을 모두 만족해야 함
- @Access
- 필드 접근: AccessType.Field로 지정하면 필드에 직접 접근한다. 필드 접근 권한이 private이어도 접근할 수 있다.
- 프로퍼티 접근: AccessType.PROPERTY로 지정한다. 접근자(Getter)를 사용한다.
연관관계 매핑 기초
- 객체 연관관계와 테이블 연관관계의 가장 큰 차이는 객체 연관관계는 양방향 관계가 아니라 서로 다른 단방향 관계가 2개라는 점이다.
- 객체 연관관계 vs 테이블 연관관계 정리
- 객체는 참조(주소)로 연관관계를 맺는다.
- 테이블은 외래 키로 연관관계를 맺는다.
- 객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라고 한다.
- @JoinColumn을 생략하면 외래 키를 찾을 때 기본 전략을 사용한다.
- 기본 전략: 필드명 + _ + 참조하는 테이블의 컬럼명
- 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다.
- 연관관계의 주인
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
- 엔티티를 단방향으로 매핑하면 참조를 하나만 사용
- 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다.
따라서 둘 사이에 차이가 발생한다. - 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다.
- 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
- 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
- 연관관계의 주인은 외래 키가 있는 곳
- 연관관계의 주인만이 외래 키의 값을 변경할 수 있다.
- 순수한 객체까지 고려한 양방향 연관관계
- 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
- 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자.
- 한 번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다.
- 연관관계가 하나인 단방향 매핑은 언제나 연관관계의 주인이라는 점이다.
- 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐이다.
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
- 연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안 된다.
다양한 연관관계 매핑
- 다대일 양방향[N:1, 1:N]
- 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.
- 양방향 연관관계는 항상 서로를 참조해야 한다.
- 일대다 단방향 [1:N]
- 일대다 단방향 매핑의 단점: 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다.
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.
- 부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라 한다.
- 식별 관계: 받아온 식별자를 기본 키 + 외래 키로 사용한다.
- 비식별 관계: 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.
고급 매핑
- 조인 전략
- 각각의 테이블로 변환
- 장점
- 테이블이 정규화된다.
- 외래 키 참조 무결성 제약조건을 활용할 수 있다.
- 저장공간을 효율적으로 사용한다.
- 단점
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
- 조회 쿼리가 복잡하다.
- 데이터를 등록할 INSERT SQL을 두 번 실행한다.
- 특징
- JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼(@DiscriminatorColumn) 없이도 동작한다.
- 장점
- 통합 테이블 전략
- 장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.
- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
그러므로 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.
- 특징
- 구분 컬럼을 꼭 사용해야 한다. 따라서 @DiscriminatorColumn을 꼭 설정해야 한다.
- @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.
- 장점
- 서브타입 테이블 전략
- 장점
- 서브 타입을 구분해서 처리할 때 효과적이다.
- not null 제약조건을 사용할 수 있다.
- 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다. (SQL에 UNION을 사용해야 한다.)
- 자식 테이블을 통합해서 쿼리하기 어렵다.
- 특징
- 구분 컬럼을 사용하지 않는다.
- 장점
- 각각의 테이블로 변환
- 필수적 비식별 관계(Mandatory): 외래 키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
- 선택적 비식별 관계(Optional): 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.
- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
- 식별, 비식별 관계의 장단점
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다.
- 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
- 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다.
반면에 비식별 관계의 기본 키는 비즈니스와 전형 관계없는 대리 키를 주로 사용한다. - 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계보다 테이블 구조가 유연하지 못하다.
- 선택적 비식별 관계는 외래 키에 null을 허용하고 OUTER JOIN을 사용한다.
- @JoinTable의 속성 중 inverseJoinColumns는 반대방향 엔티티를 참조하는 외래 키
프록시와 연관관계 관리
- 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라 한다.
- 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데
이것을 프록시 객체라 한다. - 프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같다.
- 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다.
- 프록시 객체는 처음 사용할 때 한 번만 초기화한다.
- 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
- 엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
- 프록시 강제 초기화는 org.hibernate.HIbernate.initialize(order.getMember()); 메서드로 할 수 있다.
- 즉시 로딩: 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- 지연 로딩: 연관된 엔티티를 실제 사용할 때 조회한다.
- 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.
- 지연 로딩(LAZY): 연관된 엔티티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
- 즉시 로딩(EAGER): 연관된 엔티티를 즉시 조회한다. 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회한다.
- 하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라 한다.
- @fetch 속성의 기본 설정값은 다음과 같다.
- @ManyToOne, @OneToOne: 즉시 로딩(FetchType.EAGER)
- @OneToMany, @ManyToMany: 지연 로딩(FetchType.LAZY)
- 추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것이다.
- 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
- 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
- JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
- 영속성 전이는 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다.
- 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭데된다.
- 고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
- 엔티티를 영속 상태로 만들어서 데이터베이스에 저장할 때 연관된 엔티티도 모두 영속 상태여야 한다.
값 타입
- 값 타입은 값으로 사용하는 자바 기본 타입이나 객체를 말한다.
- 엔티티 타입은 식별자를 통해 지속해서 추적할 수 있지만, 값 타입은 식별자가 없고 숫자나 문자같은 속성만 있으므로 추적할 수 없다.
- 새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입이라 한다.
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
- 임베디드 타입은 기본 생성자가 필수다.
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
- 객체를 불변하게 만들면 값을 수정할 수 없으므로 객체가 공유 참조되는 부작용을 원천 차단할 수 있다.
- 따라서 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.
- 한 번 만들면 절대 변경할 수 없는 객체를 불변 객체라 한다.
- 동일성 비교: 인스턴스의 참조 값을 비교, == 사용
- 동등성 비교: 인스턴스의 값을 비교, equals() 사용
- 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 필수로 가진다고 볼 수 있다.
- 엔티티 타입의 특징
- 식별자가 있다.
- 엔티티 타입은 식별자가 있고 식별자로 구별할 수 있다.
- 생명 주기가 있다.
- 생성하고, 영속화하고, 소멸하는 생명 주기가 있다.
- 공유할 수 있다.
- 참조 값을 공유할 수 있다. 이것을 공유 참조라 한다.
- 식별자가 있다.
- 값 타입의 특징
- 식별자가 없다.
- 생명 주기를 엔티티에 의존한다.
- 공유하지 않는 것이 안전하다.
- 불변 객체로 만드는 것이 안전하다.
객체지향 쿼리 언어
- JPQL 특징
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPA가 공식 지원하는 기능
- Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
- 네이티브 SQL: JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.
- 공식은 아니지만 좋은 기능
- QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크
- JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용: 필요하면 JDBC를 직접 사용할 수 있다.
- JPQL은 엔티티 객체를 조회하는 객체지향 쿼리다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.
- JPQL은 SQL보다 간결하다.
- Criteria의 장점은 문자가 아닌 query.select(m).where(...)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다.
- Criteria의 장점
- 컴파일 시점에 오류를 발견할 수 있다.
- IDE를 사용하면 코드 자동완성을 지원한다.
- 동적 쿼리를 작성하기 편하다.
- Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다. 따라서 사용하기 불편한 건 물론이고 Criteria로 작성한 코드도 한눈에 들어오지 않는다는 단점이 있다.
- 네이티브 SQL의 단점은 특정 데이터베이스에 의존하는 SQL을 작성해야 한다는 것이다.
- 데이터베이스를 변경하면 네이티브 SQL도 수정해야 한다.
- JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다.
- 이런 이슈를 해결하는 방법은 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와
영속성 컨텍스트를 동기화하면 된다. - JPQL 의 파라미터 방식은 이름 기준 바인딩 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.
- JPQL의 파라미터 바인딩 방식은 선택이 아닌 필수다.
- SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 한다.
- JPQL로 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
- 임베디드 타입은 엔티티 타입이 아닌 값 타입이다. JPQL로 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
- 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.
- NEW 명령어를 사용할 때는 다음 2가지를 주의해야 한다.
- 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
- 세타 조인은 내부 조인만 지원한다.
- JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
- 최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 일어난다.
물론 일부는 빠를 수는 있지만 전체로 보면 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악영향을 미칠 수 있다.
따라서 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다. - 준영속 상태에서도 객체 그래프를 탐색할 수 있다.
- 페치 조인 대상에는 별칭을 줄 수 없다.
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
- 단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라 한다.
참고로 묵시적 조인은 모두 내부 조인이다. - 명시적 조인: JOIN을 직접 적어주는 것
- 묵시적 조인: 경로 표현식에 의해 묵시적으로 조인이 일어나는 것, 내부 조인만 할 수 있다.
- 경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인이다.
- 컬렉션은 경로 탐색의 끝이다.
- TREAT
- 자바의 타입 캐스팅과 비슷
- 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
- 네이티브 SQL이 특정 데이터베이스에 종속적인 기능을 지원하는 방법
- 특정 데이터베이스만 사용하는 함수
- 특정 데이터베이스만 지원하는 SQL 쿼리 힌트
- 인라인 뷰(From 절에서 사용하는 서브쿼리), UNION, INTERSECT
- 스토어 프로시저
- 특정 데이터베이스만 지원하는 문법
- 네이티브 SQL을 사용하면 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다.
- 여기서 가장 중요한 점은 네이티브 SQL로 SQL만 직접 사용할 뿐이지 나머지는 JPQL을 사용할 때와 같다. 조회한 엔티티도 영속성 컨텍스트에서 관리된다.
- 벌크 연산을 사용할 때는 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의해야 한다.
- 벌크 연산을 수행한 직후에 바로 영속성 컨텍스트를 초기화해서 영속성 컨텍스트에 남아 있는 엔티티를 제거하는 것도 좋은 방법이다.
- 조회한 엔티티만 영속성 컨텍스트가 관리한다.
- 영속성 컨텍스트는 영속 상태인 엔티티의 동일성을 보장한다.
- em.find()는 해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로 성능상이 이점이 있다.(그래서 1차 캐시라 부른다.)
- JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.
- JPQL은 항상 데이터베이스를 조회한다.
- JPQL로 조회한 엔티티는 영속 상태다.
- 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다.
- 플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것이다.
웹 애플리케이션 제작
- 의존성 전이란 의존관계가 있는 라이브러리도 함께 내려받아 라이브러리에 자동으로 추가하는 것
- Controller: 컨트롤러는 서비스 계층을 호출하고 결과를 뷰(JSP)에 전달한다.
- Service: 서비스 계층에는 비즈니스 로직이 있고 트랜잭션을 시작한다. 서비스 계층은 데이터 접근 계층인 리포지토리를 호출한다.
- Repository: JPA를 직접 사용하는 곳은 리포지토리 계층이다. 여기서 엔티티 매니저를 사용해서 엔티티를 저장하고 조회한다.
- Domain: 엔티티가 모여 있는 계층이다. 모든 게층에서 사용한다.
- @PersistenceContext
- 순수 자바 환경에서는 엔티티 매니저 팩토리에서 엔티티 매니저를 직접 생성해서 사용했지만, 스프링이나 J2EE 컨테이너를 사용하면 컨테이너가 엔티티 매니저를 관리하고 제공해준다. 따라서 엔티티 매니저 팩토리에서 엔티티 매니저를 직접 생성해서 사용하는 것이 아니라 컨테이너가 제공하는 엔티티 매니저를 사용해야 한다.
- @PersistenceContext는 컨테이너가 관리하는 엔티티 매니저를 주입하는 어노테이션이다.
- @Transactional은 테스트 코드에서 사용할 경우 테스트가 끝나면 트랜잭션을 강제로 롤백한다.
- 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만 병합을 사용하면 모든 속성이 변경된다.
스프링 데이터 JPA
- 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
웹 애플리케이션과 영속성 관리
- 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.
- 이 전략은 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻이다.
- 트랜잭션 범위의 영속성 컨텍스트 전략은 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다.
- 여러 스레드에서 동시에 요청이 와서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.
- 변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층까지만 동작하고 영속성 컨텍스트가 종료된 프리젠테이션 계층에서는 동작하지 않는다.
- 준영속 상태의 지연 로딩 문제를 해결하는 방법
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법
- OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법
- 글로벌 페치 전략 수정
- JPQL 페치 조인
- 강제로 초기화
- 엔티티에 있는 fetch 타입을 변경하면 애플리케이션 전체에서 이 엔티티를 로딩할 때마다 해당 전략을 사용하므로 글로벌 페치 전략이라 한다.
- 글로벌 페치 전략에 즉시 로딩 사용 시 단점
- 사용하지 않는 엔티티를 로딩한다.
- N + 1 문제가 발생한다.
- JPA를 사용하면서 성능상 가장 조심해야 하는 것이 바로 N + 1 문제다.
- JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용한다.
- 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것을 N + 1 문제라 한다.
- N + 1 문제는 JPQL 페치 조인으로 해결할 수 있다.
- FACADE 계층을 도입해서 서비스 계층과 프리젠테이션 계층 사이에 논리적인 의존성을 분리할 수 있다.
- FACADE 계층의 역할과 특징
- 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리해준다.
- 프리젠테이션 계층에서 필요한 프록시 객체를 초기화한다.
- 서비스 계층을 호출해서 비즈니스 로직을 실행한다.
- 리포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 찾는다.
- 모든 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한다.
- OSIV는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다
- 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이다. 이것을 요청 단 트랜잭션 방식의 OSIV라 한다.
- 프리젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법
- 엔티티를 읽기 전용 인터페이스로 제공
- 엔티티 래핑
- DTO만 반환
- 스프링 프레임워크가 제공하는 OSIV는 "비즈니스 계층에서 트랜잭션을 사용하는 OSIV" 다
- 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다.
- 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다. 이것을 트랜잭션 없이 읽기라 한다.
- 스프링이 제공하는 비즈니스 계층 트랜잭션 OSIV
- 영속성 컨텍스트를 프리젠테이션 계층까지 유지한다.
- 프리젠테이션 계층에는 트랜잭션이 없으므로 엔티티를 수정할 수 없다.
- 프리젠테이션 계층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연 로딩을 할 수 있다.
- 스프링 OSIV의 특징
- OSIV는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 같은 영속성 컨텍스트를 유지한다.
- 엔티티 수정은 트랜잭션이 있는 계층에서만 동작한다.
- 스프링 OSIV의 단점
- OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는 점을 주의해야 한다.
- 프리젠테이션 계층에서 지연 로딩에 의한 SQL이 실행된다.
컬렉션과 부가 기능
- Collection, List는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 된다. 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.
- Set은 엔티티를 추가할 때 엔티티가 있는지 비교해야 한다. 따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.
- List + @OrderColumn: 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다는 의미
- 순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리한다.
- 엔티티 그래프 기능은 엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능이다.
고급 주제와 성능 최적화
- JPA 표준 예외 정리
- 트랜잭션 롤백을 표시하는 예외
- 트랜잭션 롤백을 표시하지 않는 예외
- 트랜잭션 롤백 시 주의사항
- 트랜잭션을 롤백하는 것은 데이터베이스의 반영사항만 롤백하는 것이지 수정한 자바 객체까지 원상태로 복구해주지는 않는다.
- 따라서 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험하다. 새로운 영속성 컨텍스트를 생성해서 사용하거나
EntityManager.clear()를 호출해서 영속성 컨텍스트를 초기화한 다음에 사용해야 한다.
- 1차 캐시의 가장 큰 장점은 애플리케이션 수준의 반복 가능한 읽기이다.
- 영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다.
- 동일성: == 비교가 같다.
- 동등성: equals() 비교가 같다.
- 데이터베이스 동등성: @Id인 데이터베이스 식별자가 같다.
- 영속성 컨텍스트가 다를 때 엔티티 비교는 다음과 같다.
- 동일성: == 비교가 실패한다.
- 동등성: equals() 비교가 만족한다. 단 equals() 를 구현해야 한다. 보통 비즈니스 키로 구현한다.
- 데이터베이스 동등성: @Id인 데이터베이스 식별자가 같다.
- 엔티티를 비교할 때는 비즈니스 키를 활용한 동등성 비교를 권장한다.
- 프록시는 원본을 상속받은 자식 타입이므로 프록시의 타입을 비교할 때는 == 비교가 아닌 instanceof를 사용해야 한다.
- 프록시의 동등성 비교를 할 때 주의 사항
- 프록시의 타입 비교는 == 비교 대신에 instanceof를 사용해야 한다.
- 프록시의 멤버 변수에 직접 접근하면 안 되고 대신에 접근자 메서드를 사용해야 한다.
- 프록시를 부모 타입으로 조회하면 문제가 발생한다.
- 프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되는 문제가 있다.
- instanceof 연산을 사용할 수 없다.
- 하위 타입으로 다운캐스팅을 할 수 없다.
- 페치 조인은 SQL 조인을 사용해서 연관된 엔티티를 함께 조회하므로 N + 1 문제가 발생하지 않는다.
- 즉시 로딩의 가장 큰 문제는 성능 최적화가 어렵다는 점이다.
- 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다.
- 읽기 전용으로 엔티티를 조회하면 메모리 사용량을 최적화할 수 있다.
- 하이버네이트 전용 힌트인 org.hibernate.readOnly를 사용하면 엔티티를 읽기 전용으로 조회할 수 있다.
- 읽기 전용은 영속성 컨텍스트의 스냅샷을 보관하지 않으므로 엔티티를 수정해도 데이터베이스에 반영되지 않는다.
- 스프링 프레임워크를 사용하면 트랜잭션을 읽기 전용 모드로 설정할 수 있다. @Transactional(readOnly = true)
- 트랜잭션 밖에서 읽는다는 것은 트랜잭션 없이 엔티티를 조회한다는 뜻
- 트랜잭션을 사용하지 않으면 플러시가 일어나지 않으므로 조회 성능이 향상된다.
- 무상태 세션은 영속성 컨텍스트를 만들지 않고 심지어 2차 캐시도 사용하지 않는다. 무상태 세션은 영속성 컨텍스트가 없다.
- 트랜잭션을 지원하는 쓰기 지연은 데이터베이스 테이블 로우에 락이 걸리는 시간을 최소화한다.
- 이 기능은 트랜잭션을 커밋해서 영속성 컨텍스트를 플러시하기 전까지는 데이터베이스에 데이터를 등록, 수정, 삭제하지 않는다.
- 따라서 커밋 직전까지 데이터베이스 로우에 락을 걸지 않는다.
트랜잭션과 락, 2차 캐시
- 트랜잭션 ACID
- 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하든가 모두 실패해야 한다.
- 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
- 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.
- 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다.
- 격리 수준에 따른 문제점
- READ UNCOMMITTED
- 커밋되지 않은 데이터를 읽을 수 있다.
- 트랜잭션1이 데이터를 수정하고 있는데 커밋하지 않아도 트랜잭션 2가 수정 중인 데이터를 조회할 수 있는데,
이것을 DIRTY READ라고 한다.
- READ COMMITTED
- 커밋한 데이터만 읽을 수 있다.
- DIRTY READ 발생 안 함
- 반복해서 같은 데이터를 읽을 수 없는 상태인 NON-REPEATABLE READ가 발생할 수 있음
- REPEATABLE READ
- 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다.
- NON-REPEATABLE READ가 발생 안 함
- 반복 조회 시 결과 집합이 달라지는 PHANTOM READ가 발생할 수 있음
- SERIALIZABLE
- 가장 엄격한 트랜잭션 격리 수준
- PHANTOM READ가 발생하지 않음
- 동시성 처리 성능이 급격히 떨어질 수 있다.
- READ UNCOMMITTED
- 낙관적 락은 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법이다.
- 비관적 락은 트랜잭션이 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법이다.
- JPA 낙관적 락 중 OPTIMISTIC은 SELECT 쿼리로 조회해서 처음에 조회한 엔티티의 버전 정보와 비교한다.
- 엔티티를 수정하지 않고 단순히 조회만 해도 버전을 확인한다.
- JPA 비관전 락의 특징
- 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다.
- 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다.
- 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이것을 1차 캐시라 한다.
- 일반적인 웹 애플리케이션 환경은 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
- 하이버네이트를 포함한 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원하는데 이것을 공유 캐시 또는 2차 캐시라 한다.
- 1차 캐시는 JPA를 J2EE나 스프링 프레임워크 같은 컨테이너 위에서 실행하면 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션을 종료할 때 영속성 컨텍스트도 종료한다.
- 2차 캐시는 애플리케이션을 종료할 때까지 캐시가 유지된다.
- 2차 캐시는 동시성을 극대화하려고 캐시한 객체를 직접 반환하지 않고 복사본을 만들어서 반환한다.
- 2차 캐시의 특징
- 2차 캐시는 영속성 유닛 범위의 캐시다
- 2차 캐시는 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어서 반환한다.
- 2차 캐시는 데이터베이스 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성(a == b)을 보장하지 않는다.
- 하이버네이트와 EHCACHE 적용
- 엔티티 캐시: 엔티티 단위로 캐시한다. 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용한다.
- 컬렉션 캐시: 엔티티와 연관된 컬렉션을 캐시한다. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시한다.
- 쿼리 캐시: 쿼리와 파라미터 정보를 키로 사용해서 캐시한다. 결과가 엔티티면 식별자 값만 캐시한다.
- 쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시한다.
- 따라서 쿼리 캐시나 컬렉션 캐시를 사용하면 결과 대상 엔티티에는 꼭 엔티티 캐시를 적용해야 한다.
'책 소개' 카테고리의 다른 글
자기돌봄 (0) | 2024.08.23 |
---|---|
[요약] 도메인 주도 설계로 시작하는 마이크로서비스 개발 (0) | 2024.07.16 |
[요약] 자바 8 인 액션 (0) | 2024.06.30 |
[요약] 테스트 주도 개발 시작하기 (0) | 2024.06.25 |
[요약] 도메인 주도 개발 시작하기 (0) | 2024.06.22 |