JPA는 스펙상 엔티티, 임베디드 값 타입에 리플렉션(Reflection), 프록시(Proxy) 기술을 적용하기 위해 public 또는 protected의 기본 생성자를 두는 것을 정의하고 있다.

 

 

리플렉션이란?

더보기

Java Reflection API

 

자바는 컴파일 시점에서 타입이 결정되는데 다음과 같이 구체적인 클래스로 생성하지 않을 경우 컴파일 시점에서 Object 타입으로 결정이 되기 때문에 Car 클래스의 메소드 등에 접근할 수가 없다.

 

Object car = new Car(); 

 

자바에서는 JVM이 실행되면 자바 코드가 컴파일러를 거쳐 바이트코드로 변환되어 static 영역에 저장되는데 리플렉션은 이 정보를 활용하여 구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해준다. 하지만 런타임 시점에 동적으로 타입을 분석하기 때문에 JVM을 최적화할 수 없는 등 여러 문제점이 있어서 일반적으로는 사용하지 않는 것이 좋다.

리플렉션은 애플리케이션 개발보다 프레임워크나 라이브러리에서 많이 사용하는데 프레임워크나 라이브러리는 사용자가 어떤 클래스를 만들지 예측할 수가 없기 때문에 리플렉션을 활용한다.

JPA도 동적으로 객체 생성시 리플렉션을 활용하는데 리플렉션으로 가져올 수 없는 정보 중 하나가 생성자의 인자 정보이다. 그래서 기본 생성자가 반드시 있어야 객체를 생성할 수 있다. 기본 생성자로 객체를 생성만 하면 필드 값 등을 리플렉션으로 넣어줄 수 있다.

 

 

Reflection API 간단히 알아보자.

Spring Framework를 학습하다 보면 Java Reflection API를 자주 접하게 된다. 하지만 Reflection API…

tecoble.techcourse.co.kr

 

 

하이버네이트 같은 구현체들은 기본 생성자가 없어도 라이브러리들을 통해 이러한 문제를 어느정도 회피하지만 완벽한 해결책이 아니기 때문에 기본 생성자를 필수로 두는 것이 좋다. 기본 생성자를 public으로 열어두면 의도치 않은 무분별한 생성이 될 수 있기 때문에  protected로 선언하여 다른 패키지에서 기본 생성자를 생성할 수 없도록 제한한다.

 

그리고 엔티티는 생성자나 Setter를 사용하는 것보다는 빌더(Builder) 패턴 또는 정적 팩토리 메서드(static factory method)를 사용하는 것이 좋은데 빌더 패턴은 필요한 데이터만 설정할 수 있고 유연성, 가독성이 뛰어나며 변경 가능성을 최소화 할 수 있다.

 

[Java] 빌더 패턴(Builder Pattern)을 사용해야 하는 이유

객체를 생성하기 위해서는 생성자 패턴, 정적 메소드 패턴, 수정자 패턴, 빌더 패턴 등을 사용할 수 있습니다. 개인적으로 객체를 생성할 때에는 반드시 빌더 패턴을 사용해야 한다고 생각하는

mangkyu.tistory.com

 

정적 팩토리 메서드(static factory method)는 객체 생성을 캡슐화하는 기법으로 객체를 생성하는 메소드를 만들고 static으로 선언하여 사용한다. 메서드명을 의미 있게 지을 수 있어 생성자에 비해 가독성이 좋아지고 호출할 때마다 새로운 객체를 생성할 필요가 없다. 그리고 하위 타입 객체를 반환할 수 있으며 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다. 

 

public static User createUser(String name, int age) {
    OrderItem orderItem = new OrderItem();
    this.name = name;
    this.age = age;
    
    return user;
}

 

파라미터가 많거나 변경 가능성이 많은 경우에는 빌더 패턴을, 그렇지 않은 경우에는 정적 팩토리 메소드를 사용하는 것이 좋다.

 

JPA는 SQL을 추상화한 객체 지향 쿼리 언어 JPQL을 제공, 객체를 대상으로 쿼리를 작성하며 특정 DB에 의존하지 않는다.

 

// Member m 별칭 사용 필수
String jpql = "select m From Member m where m.name like '%hello%'";

List<Member> result = em.createQuery(jpql, Member.class).getResultList();
//getResultList() : 항상 리스트 반환, 결과가 없으면 빈 리스트 반환
//getSingleResult() : 단일 객체 반환, 결과가 없거나 둘 이상이면 Exception 발생


//TypeQuery : 반환 타입이 명확할 때
TypedQeury<Member> query = em.createQuery("SELECT m From Member m", Member.class);

//Query : 반환 타입이 명확하지 않을 때
Query query = em.createQuery("SELECT m.username, m.age from Member m");
// 파라미터 바인딩
String jqpl = selct m from Member m where m.username=:username;
em.createQuery(jpql, Member.class).setParameter("username", username);

// DTO로 바로 조회 (패키지명 포함한 전체 클래스명)
// 순서와 타입이 일치하는 생성자 필요
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m

 

JPA는 where, having절에서만 서브 쿼리가 가능하고 from절에서는 서브 쿼리가 불가능하기 때문에 이 경우에는 join으로 풀어서 해결하거나 네이티브 SQL를 사용해서 해결한다. 어려운 경우 쿼리를 분해해서 2번 날리거나 서브 쿼리로 필터가 어느정도 된 결과를 애플리케이션에서 추가로 조작하는 등의 방법도 있다.

 

join을 직접 사용하지 않아도 연관 필드의 객체 그래프를 탐색하면 (m.team) 묵시적 내부 조인이 발생하는데 FROM절에 영향을 줄 수 있고 상황 파악이 어렵기 때문에 명시적으로 조인을 선언해주는 것이 좋다.

 

 


 

페치 조인 (join fetch),  DISTINCT

 

 

JPQL에서 제공하는 페치 조인을 사용하면 조인 시 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회할 수 있다. 글로벌 패치 전략을 지연 로딩으로 사용하는 경우 연관된 엔티티나 컬렉션은 사용 시점에 조회를 하는데 이때 연관 필드를 for문으로 탐색하게 되면 N+1 쿼리 문제가 발생하게 된다. 하지만 페치 조인을 사용하면 지연 로딩을 적용하지 않고 즉시 조회로 연관 엔티티의 값까지 전부 가져올 수 있다.

(일반 조인: SELECT T.* / 페치 조인: SELECT T.*, M.*)

 

String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teams) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
        //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
        //그냥 조인의 경우 member.getUsername()으로 접근 시 멤버 조회 쿼리 발생 (N+1)
        System.out.println(“-> username = " + member.getUsername()+ ", member = " + member); }
}

 

페치 조인 대상에는 별칭을 가급적 사용하지 않는 것이 좋고, 둘 이상의 컬렉션은 페치 조인 할 수 없다.

일대일, 다대일 같은 단일 값 연관 필드가 아닌 대다 컬렉션을 페치 조인하고 페이징 API를 사용하면 하이버네이트는 경고 로그를 남기고 메모리에서 페이징을 하기 때문에 매우 위험하다.

 

 객체 그래프의 사상은 연관된 데이터를 전부 불러오는 것인데 일대다 조인은 데이터가 늘어나기 때문에 페이징을 쓰면 중간에 짤리게 된다. 그래서 일대다 조인의 경우에는 hibernate.default_batch_fetch_size 글로벌 설정으로 BatchSize를 설정하면 (애노테이션도 있지만 글로벌 설정 권장) 설정 size만큼(default 50) select * from x where in (?, ?, ? ...) 이런식으로 조회

 

그래도 페치 조인은 성능 최적화에 매우 중요하기 때문에 글로벌 페치 전략은 지연 로딩으로, 최적화가 필요한 곳은 페치 조인을 적용하는 것이 좋다. (만약 여러 테이블을 join 해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면 페치 조인 대신 기본 조인을 하고 필요한 데이터만 조회해서 DTO로 반환하는 것이 좋다.)

 

추가로 JPQL에서는 join을 할 때 DISTINCT를 사용하면 특별한 기능이 추가 되는데 특정 팀에 해당하는 멤버를 join 할 때, 테이블 관점에서 보면 특정 팀에 해당하는 멤버 수만큼 결과 row가 생기는 것이 맞지만 JPA의 객체 관점에서 보면 팀 객체가 중복되어서 리스트로 반환되는 것이다. 그래서 JPQL은 DISTINCT 사용 시 중복 엔티티를 제거해준다. 

 

 


 

기타

 

JPQL 다형성 쿼리

//JPQL type()
select i from Item i where type(i) IN (Book, Movie)

//SQL
select i from Item i where i.DTYPE in (‘B’, ‘M’)

//JPQL treat()
select i from Item i where treat(i as Book).auther = ‘kim’

//SQL
select i.* from Item i where i.DTYPE = ‘B’ and i.auther = ‘kim’

 

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값(연관 엔티티는 외래 값)을 사용한다.

 

Named 쿼리를 사용하면 JPQL을 애노테이션, XML에 정의해두고 애플리케이션 로딩 시점에 쿼리를 검증해서 정적 쿼리로 사용할 수 있다.

@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {

}

List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "회원1")
    .getResultList();

 

벌크 연산

 

한번에 너무 많은 데이터를 변경하는 경우 변경 감지 기능은 너무 많은 SQL을 생성하는데 벌크 연산을 사용하면 변경 감지 대신 쿼리 한번으로 많은 데이터를 변경할 수 있다. 벌크 연산은 먼저 flush()를 한 다음 영속성 컨텍스트가 아닌 데이터베이스에 직접 쿼리를 전송하는데, 이때 영속성 컨텍스트에 있는 엔티티들은 벌크 연산의 처리 결과가 적용이 안 되어있는 상태다. 그래서 데이터 정합성에 문제 위험이 있기 때문에 1)벌크 연산 후에는 em.clear()로 영속성 컨텍스트 초기화를 해주거나 2)벌크 연산을 먼저 수행하고 조회를 하는 식으로 사용하는 것이 좋다.

 

String qlString = "update Product p " + 
                  "set p.price = p.price * 1.1 " +
                  "where p.stockAmount < :stockAmount";
                  
// executeUpdate() 결과는 변경 된 엔티티 수 반환
// UPDATE, DELETE 지원
int resultCount = em.createQuery(qlString)
                    .setParameter("stockAmount", 10)
                    .executeUpdate();

 

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

 

프록시

em.find() : 데이터 베이스를 통해 실제 엔티티 객체 조회

em.getReference() : 데이터베이스 조회를 미루는 프록시(가짜) 엔티티 객체 조회

 

프록시 객체는 실제 엔티티 클래스를 상속 받아서 생성되며 실제 엔티티의 참조를 보관하고 있다가 프록시 객체가 호출이 되면 그때 영속성 컨텍스트에 초기화 요청을 해서 실제 엔티티를 생성하고 참조를 통해 실제 엔티티를 호출한다.

 

이때 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라 프록시 객체를 통해 실제 엔티티에 접근하는 것이고 영속성 컨텍스트에 조회 요청 시 실제 엔티티가 이미 있으면 em.geReference() 호출해도 실제 엔티티가 반환된다.

 


 

연관관계 매핑 (fetch)

 

즉시 로딩 (fetch = FetchType.EAGER)

 

즉시 로딩 사용 시, 예상치 못한 SQL이 발생할 수 있고 N+1 문제를 일으킨다. 객체 하나를 조회하기 위한 쿼리를 날렸는데 연관된 객체들을 전부 조회하는 것이다. @ManyToOne, @OneToOne은 기본이 EAGER로 되어있다.

 

 

지연 로딩 (fetch = FetchType.LAZY)

 

참조 객체를 프록시 객체로 가져오고 사용 시점에 실제 객체를 조회하게 된다. 대부분 지연 로딩을 사용하는 것이 좋으며 @OneToMany, @ManyToMany는 기본이 LAZY로 되어있다.

 


 

영속성 전이 (CASCADE)

 

영속성 전이를 사용하면 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 저장하는데 엔티티를 함께 영속화하는 편리함을 제공할 뿐, 연관관계 매핑과는 관련이 없다.

CascadeType.(ALL, PERSIST, REMOVE, MERGE, REFRESH, DETACH)에 따라 범위를 지정할 수 있다.

 

 

고아 객체 (orphanRemoval)

 

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 옵션으로, 참조가 제거된 엔티티를 고아 객체로 보고 삭제하며 CascadeType.REMOVE처럼 동작한다.

참조하는 곳이 하나일 때 사용해야하며, 특정 엔티티가 개인 소유할 때 주로 사용한다. (@OneToOne, @OneToMany만 가능)

 

(CascadeType.ALL + orphanRemoval=true 로 설정 시, 부모 엔티티가 자식 엔티티의 생명 주기를 관리할 수 있다.)

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

 

관계형 데이터베이스는 상속 관계가 없지만 JPA는 객체 상속과 유사한 슈퍼타입, 서브타입의 논리 모델으로 상속관계 매핑을 구현할 수 있다.

 

@Inheritance(strategy=InheritanceType.XXX ) : 상속 선언, 전략 설정

@DiscriminatorColumn(name="DTYPE") : 부모 클래스에서 자식 테이블을 구분할 수 있는 타입 Column

@DiscriminatoerValue("XXX") : 자식 클래스에 입력하는 DTYPE 이름으로 생략 시, 엔티티명으로 생성

 

 

@InheritanceType

 

1. JOINED

각각 테이블로 생성, 외래 키 참조 무결성 제약조건을 따르면서 저장 공간을 효율적으로 나눌 수 있지만 조회 시 조인을 많이 사용하며 쿼리가 복잡하고 INSERT를 2번 호출해야하는 단점이 있다.

 

2. SINGLE_TABLE

DTYPE으로 자식 테이블을 구분할 수 있기 때문에 하나의 테이블에 전부 집어넣는 단일 테이블 방식으로 조회 쿼리가 단순하고 빠르지만 테이블이 너무 커질 수 있고 자식 Column에 null 값을 허용해야 하는 단점이 있다.

 

3. TABLE_PER_CLASS 

조회 시 UNION SQL이 필요하고 성능이 느리고 자식 테이블을 통합해서 쿼리하기 어려운 문제 등으로 권장 X

 

 

@MappedSuperclass

 

공통 속성이 필요한 경우 이를 상속으로 뺄 수 있는데 이는 실제 테이블에 영향을 주는 것은 아니고 단순하게 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할만 한다. 직접 생성하거나 사용할 일이 없으므로 추상 클래스로 사용하는 것이 좋고 엔티티가 생기는 것이 아니기 때문에 당연히 조회, 검색 등은 할 수 없다.

 

 

 

 

 

+ Recent posts