새소식

Java

자바 ORM 표준 JPA 기본편 정리 [2]

  • -
728x90
자바 ORM 표준 JPA 기본편 정리 2

📌 프록시란?

JPA를 사용할 때의 많은 장점 중 하나로, 그래프를 통해 연관관계를 탐색할 수 있다는 것을 꼽을 수 있다. 하지만 엔티티들은 데이터베이스에 저장되어 있기 대문에 한 객체 조회 시 연관되어 있는 엔티티들을 모두 조회하는 것 보다는 필요한 연관관계만 조회해 오는 것이 효과적이다. 이런 상황을 위해 JPA 는 지연 로딩이라는 방식을 지원하는데 그 중에서도 우리가 일반적으로 가장 많이 사용하는 JPA 구현체인 하이버네이트 (Hibernate) 는 프록시 객체를 통해 지연 로딩을 구현하고 있다.

 

프록시는 '대신하다'라는 의미를 가지고 있다. 동작을 대신해주는 가짜 객체의 개념이라고 생각하면 편하다. 

즉, 프록시 클래스는 실제 클래스를 상속 받아서 만들어지며 겉 모양이 같다. 다만 실제 값이 필요할 때까지  DB 조회를 미룰 수 있어서, 한 Entity 와 연관된 다른 Entity 들을 모두 가져올 필요없을 때 프록시를 사용한다.

 

 

프록시 객체는 실제 객체의 참조를 보관해서 애플리케이션에서 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출하게 된다. 아래는 회원 과 팀 연관관계 상에서의 프록시 객체를 활용한 예시 코드이다.

Team team = new Team();
team.setName("teamA");

Member member = new Member();
member.setName("memberA");
member.setTeam(team);

em.persist(team);
em.persist(member);

em.flush();		// SQL 보냄
em.clear();		// 영속성 Context 1차 캐시 초기화

Member referMember = em.getReference(Member.class, member.getId());		// 프록시 객체 가져옴
System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());
// referMember.getId() 값은 메모리에 있던 값이기 때문에 DB 조회를 하지 않는다.
// referMember.getName() 값을 요청할 때 비로소 DB에 SELECT query를 보내서 값을 가져온다.

 

 

프록시 객체가 메서드를 호출하기 위해 실제 객체의 참조를 갖기 위해서 영속성  Context를 통해 DB 를 조회한다.

가져온 정보로 실제 Entity 를 생성하고, 프록시 객체가 해당 Entity를 가르키도록 설정한다. 프록시의 특징은 다음과 같다.

 

⭐ 프록시의 특징

1. 프록시 객체는 처음 사용할 때 한 번만 초기화된다.

2. 프록시 객체 초기화시, 프록시 객체가 실제 Entity 로 바뀌는 것이 아닌 참조를 통해 동작한다.

3. 프록시 객체는 원본 Entity 를 상속받는 상태이므로 타입 체크시 == 대신 instance of 를 사용하는 것이 좋다.

 

JPA는 하나의 트랜잭션 내에서는 == 이 알맞게 동작하도록 상황에 따라 다르게 작동한다.

1) 실제 Entity 를 조회 후 프록시 객체를 조회하는 경우

Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.getClass() = " + findMember.getClass());		// 실제 Entity

Member referMember = em.getReference(Member.class, member.getId());
System.out.println("referMember.getClass() = " + referMember.getClass());	// 실제 Entity

System.out.println("isEqualClass ? = " + (referMember.getClass() == findMember.getClass()));
// print "isEqualClass ? = true"

em.find() 로 1차 캐시에 실제 Entity 가 존재하기 때문에 em.getReference() 하더라도 실제 Entity 가 반환된다. 따라서 두 객체 클래스 타입은 동일하다.

 

2) 프록시 객체를 조회 후 실제 Entity 를 조회하는 경우

Member referMember = em.getReference(Member.class, member.getId());
System.out.println("referMember.getClass() = " + referMember.getClass());	// 프록시 객체

Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.getClass() = " + findMember.getClass());		// 프록시 객체

System.out.println("isEqualClass ? = " + (referMember.getClass() == findMember.getClass()));
// print "isEqualClass ? = true"

JPA는 한 트랜잭션 내에서 실제 Entity 객체와 프록시 객체의 비교 연산 동작의 완전성을 보장하기 위해, 프록시 객체 조회 후 실제 Entity 를 조회하는 경우라도, 두 객체가 모두 프록시 객체를 반환하도록 한다. 따라서 두 객체의 클래스 타입은 동일하다.

 

System.out.println("instanceof = " + (referMember instanceof Member));		// true
System.out.println("instanceof = " + (findMember instanceof Member));		// true

동일한 트랜잭션이 아닌 경우, == 을 사용한다면 상황에 따라 결과가 달라질 수 있기 때문에 instanceof 를 사용하는 것이 좋다.

 

4. 영속성 Context 에 찾고자 하는 Entity 가 이미 있다면 em.getReference() 하더라도 실제 Entity 가 반환된다.

5. 영속성 Context 의 도움을 받을 수 없는 준영속 상태인 경우, 프록시 객체를 초기화하려면 Exception 이 발생한다.

Member referMember = em.getReference(Member.class, member.getId());
em.detach(referMember);		// 영속성 Context에서 분리

System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());

 

위의 코드에서 처럼 영속성 Context 에서 더 이상 관리하지 않는 준영속 상태의 객체의 값을 가져오려 하는 경우, LazyInitializationException 예외가 발생한다.

 

Member referMember = em.getReference(Member.class, member.getId());
System.out.println("isLoaded ? = " + emf.getPersistenceUnitUtil().isLoaded(referMember));
// print "isLoaded ? = false"

Hibernate.initialize(referMember);		// 프록시 객체 강제 초기화

System.out.println("isLoaded ? = " + emf.getPersistenceUnitUtil().isLoaded(referMember));
// print "isLoaded ? = true"
System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());
// 강제 초기화를 이미 했기 때문에 getName()하더라도 DB에 query가 보내지지 않고 1차 캐시에서 값을 가져옴

 

 

⭐ 즉시 로딩과 지연 로딩

회원 예시로 설명을 진행한다. 비즈니스 로직 상에서 단순히 회원 정보만 필요하고 팀 정보는 필요없는 경우, 회원 을 조회할 때 을 함께 조회하는 것은 불필요하다. 이를 지연 로딩으로 설정하면 연관관계에 관한 값을 요청할 때 DB 에 Query를 보내는 방식으로 동작한다.

 

@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

Member member = em.find(Member.class, 1L);
// Member의 id, name Field만을 가져옴
System.out.println("member = " + member.getId() + ": " + member.getName());

Team team = member.getTeam();
// 연관관계에 관한 값을 요청할 경우, 그때서야 DB에 query를 보내서 team Field를 가져옴
System.out.println("team = " + team.getId() + ": " + team.getName());

만약 회원과 팀이 대부분 함께 사용되는 경우에는 즉시 로딩으로 설정해서 항상 같이 조회되도록 설정하면 된다.

 

@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

Member member = em.find(Member.class, 1L);
// Member의 모든 Field를 다 가져옴
System.out.println("member = " + member.getId() + ": " + member.getName());

Team team = member.getTeam();
// 단순히 member 객체의 Field에서 참조
System.out.println("team = " + team.getId() + ": " + team.getName());

 

💡 실무에서는 가급적 지연 로딩만 사용하는 게 권장된다. 즉시 로딩을 적용하면 예상치 못한 SQL이 발생하고, 특히 JPQL에서 N+1 문제를 일으킨다. 따라서 @ManyToOne@OneToOne의 경우 기본값이 즉시 로딩이므로 지연 로딩으로 설정해서 써야 합한다.

 

영속성 전이

특정 Entity 를 영속 상태로 만들 때, 연관된 Entity 도 함께 영속 상태로 만들고 싶을 때 사용하는 방법이다. 영속성 전이는 연관관계를 매핑하는 것과 아무 관련도 없고, 단지 Entity 영속화할 때 연관된 Entity도 함께 영속화하는 편리함을 제공할 뿐이다.

 

고아 객체

orphanRemval, 부모 Entity 와 연관관계가 끊어진 자식 Entity 를 의미한다. 이 경우 고아 객체를 제거하게끔 설정할 수 있다. 아래 코드는 영속성 전이와 고아 객체에 대한 예시다.

 

@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
    List<Member> members = new ArrayList<>();
    
    public void addMember(Member member) {
        this.members.add(member);
        member.setTeam(this);
    }
}
Member member1 = new Member();
Member member2 = new Member();

Team team = new Team();
team.setName("teamA");

team.addMember(member1);
team.addMember(member2);

em.persist(team);
// Team의 members Field는 전이 설정이 되어있기 때문에, team을 영속화하면 list에 속한 member도 영속화된다.

team.getMembers().remove(0);
// 부모 Entity에서 첫번째 자식 Entity와의 연관관계를 끊었으므로 member1은 고아 객체가 된다.
// orphanRemval = true 설정이 되어있기 때문에 고아 객체는 자동으로 삭제된다.

 

영속성 전이와 고아 객체의 생명주기

- 두 개념은 특정 Entity ( Team )만이 해당 Entity ( Member ) 를 소유하는 경우에만 사용해야 한다. 그렇지 않은 경우, 다른 Entity 에서 예상치 못하게 추가되거나 삭제될 수 있기 때문이다.

- 두 개념을 모두 사용하면 부모 Entity 를 통해서 자식의 생명주기를 관리할 수 있게 되어 도메인 주도 설계의 Aggregate Root 개념을 구현할 때 유용한다.


📚 값 타입

JPA 의 데이터 타입은 크게 2가지가 있다.

 

1. Entity 타입

- @Entity 로 정의하는 객채

- 데이터가 변해도 식별자를 통해 지속해서 추적 가능

 

2. 값 타입

- 단순히 값으로 사용하는 자바 기본 타입/객체

- 식별자가 없고 값만 있으므로 변경시 추적 불가, 변경하면 안된다.

- 값 타입을 소유한 Entity 에 생명주기를 의존

- 기본 값 타입, Embedded 타입, Collection 값 타입 등으로 분류

 

기본 값 타입

String name;
int age; 		// 자바 기본 타입 (primitive type)
Integer count;  // Wrapper 클래스

자바 기본 타입, Wrapper 클래스, String 등이 있고, 기본 값 타입의 생명주기는 Entity에 의존적이다.  예를 들어 회원 Entity를 삭제하면 해당 Entity의 기본 값 타입의 Field도 함께 삭제된다. 따라서 값 타입은 외부에 공유하면 안된다. 기본적으로 자바의 기본 타입은 항상 값을 복사하도록 동작하고, Wrapper 클래스나 String과 같은 특수한 클래스는 공유는 가능하더라도 불변 객체로 동작하여 한 번 만들어진 객체는 데이터 수정이 불가하다.

 

Embedded 타입

주로 기본 값 타임을 모아서 새로운 값 타입을 직접 정의하는 것을 의미한다. 용도에 맞게 값 타입을 구성할 수 있으므로 재사용이 가능하고 응집도가 높다.

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    public Address() {
    
    }
}

@Entity
public class Member {
    ...
    @Embedded
    private Address homeAddress;
}

값 타입을 정의하는 곳에 @Embeddable, 값 타입을 사용하는 곳에 @Embedded로 표현할 수 있고 더불어 Embedded 타입으로 사용할 클래스에는 기본 생성자가 필수로 존재해야 한다. Embedded 타입은 Entity의 값일 뿐이므로, 이 타입을 사용하더라도 매핑하는 테이블은 변함이 없어야 한다.

 

불변 객체

값 타입을 여러 Entity에서 공유하면 예상치 못한 부작용(side effect)가 발생할 수 있다. 자바 기본 타입에 값을 대입하면 항상 복사하지만, Embedded 타입과 같이 직접 정의한 값 타입은 객체 타입이기 때문에 값을 대입하면 참조 값이 공유된다. 이 자체를 막을 수는 없지만, 공유되더라도 값을 바꿀 수 없도록 불변 객체로 설정함으로써 부작용을 막을 수는 있다.

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    public Address() {
    }
    
    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
    
    private void setCity(String city) {
        this.city = city;
    }

    private void setStreet(String street) {
        this.street = street;
    }

    private void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }
}

 

불변 객체는 생성 시점 이후 절대 값을 변경할 수 없는 객체라는 의미로, 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않거나 private으로 접 구현 가능하다.

 

Collection 값 타입

값 타입을 하나 이상 저장할 때, List나 Set과 같은 Collection을 사용한다. 하지만 DB에는 Coleecton을 하나의 테이블에 저장할 수 없기 때문에 Collection을 저장하기 위한 별도의 테이블이 필요하다.

 

@Entity
public class Member {
    ...
    @Embedded
    private Address homeAddress;
    
    @ElementCollection
    @CollectionTable(
            name = "ADDRESS",
            joinColumns = @JoinColumn(name = "MEMBER_ID")
    )
    private List<Address> addressHistory = new ArrayList<>();
}

예시 코드처럼 annotation으로 테이블 설정을 할 수 있다. Collection을 위한 테이블은 원래 Entity의 PK를 기준으로 JOIN한다. 하지만 Collection 값 타입은 다음과 같은 제약 사항 때문에 사용하는 걸 권하지 않는다.

 

- 값 타입은 Entity와는 달리 식별자 개념이 없다.

- 값은 변경하면 추적이 어렵다.

- 값 타입 Collection에 변경 사항이 발생하면, 주인 Entity와 연관된 모든 데이터를 삭제하고, 값 타입 Collection에 있는 현재 값을 모두 다시 저장한다.

- 값 타입 Collection을 매핑하는 테이블은 null 값을 허용하면 안되고, 중복 저장 방지를 위해 모든 Column을 묶어서 PK를 구성해야 한다.

 

💡 따라서 실무에서는 이 대신 1:N 연관관계 설정을 고려하는 게 좋다.

 

@Entity
public class AddressEntity {
    @Id
    @GeneratedValue
    private Long id;

    private Address address;
}

@Entity
public class Member {
    ...
    @Embedded
    private Address homeAddress;
    
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();
}

 

1:N 관계를 위한 Entity를 만들고, 해당 Entity에서 값 타입을 사용하도록 구현하며, 영속성 전이와 고아 객체 제거 옵션을 사용해서 값 타입 Collection처럼 사용한다.


📚 JPQL (Java Persistence Query Language)

JPA를 사용하면 Entity 객체를 중심으로 개발할 수 있지만, 문제는 DB를 검색할 때의 query다. 검색할 때도 테이블이 아닌 Entity 객체를 대상으로 검색하도록 JPQL를 사용한다. JPA는 SQL을 추상화한 객체 지향 쿼리 언어인 JPQL을 제공한다. JPQL은 특정 DB SQL에 의존하지 않는다는 점이 특징이며, 추상화되었더라도 결국에는 보내질 때는 SQL로 변환된다.

 

⭐ 기본 문법

- Entity와 속성은 대소문자를 구분한다. (ex. Member, username)

- JPQL 키워드는 대소문자를 구분하지 않는다. (ex. SELECT, From)

- 테이블 이름이 아닌 Entity 이름을 사용한다.

- 별칭은 필수로 사용해야 한다. (as는 생략 가능)

- 반환 타입이 명확할 때는 TypedQuery를, 그렇지 않을 때는 Query를 사용한다.

TypedQuery<Member> query = em.createQuery("select m from Member as m", Member.class);
Query query = em.createQuery("select m.name, m.age from Member m");
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);
Query query3 = em.createQuery("select m.username, m.age from Member m", Member.class);

query1 과 같이 엔티티를 반환받을 때는 TypedQuery에 엔티티를 선언하면 되고 query2 에서 처럼 username 을 받을 땐 TypedQuery에 String 을 선언해도 된다.

query3 과 같이 username 과 age 즉, 반환 타입이 명확하지 않을 땐 Query를 사용한다.

 

- getResultList()

결과가 하나 이상일 때, 리스트 반환

결과가 없으면 빈 리스트 반환

 

- getSingleResult()

결과가 없으면 javax.persistence.NoResultException

결과가 둘 이상이면 javax.persistence.NoUniqueResultException

 

TypedQuery<Member> query = em.createQuery("select m from Member as m", Member.class);

Member singleResult = query.getSingleResult();
List<Member> resultList = query.getResultList();

 

- 파라미터는 이름 기준으로 =: 를 사용해서 설정할 수 있다.

Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);

// 쿼리, 엔티티
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
query.setParameter("username", "member1");

Member singleResult = query.getSingleResult();
System.out.println("singleResult = " + singleResult);

Hibernate: 
/* select
    m 
from
    Member m 
where
    m.username = :username */ select
        member0_.id as id1_0_,
        member0_.age as age2_0_,
        member0_.TEAM_ID as team_id4_0_,
        member0_.username as username3_0_ 
    from
        Member member0_ 
    where
        member0_.username=?

 

Projection

SELECT 절에 조회할 대상을 지정하는 것을 의미한다. 그 대상으로는 Entity, Embedded 타입, 스칼라 타입 등이 있다.

DISTINCT 로 중복을 제거할 수 있다. 더불어, 여러 종류의 대상을 조회할 수도 있다.

 

1. Object & Object[] 타입으로 조회

[1]
List resultList = em.createQuery("select m.username, m.age from Member m")
                    .getResultList();

Object o =resultList.get(0);
Object[] result = (Object[]) o;

[2]
List<Object[]> resultList = em.createQuery("select m.name, m.id from Member m")
    .getResultList();

for (Object[] o: resultList) {
	System.out.println("o = " + o[0] + ", " + o[1]);
}

Hibernate: 
/* select
    m.username,
    m.age 
from
    Member m */ select
        member0_.username as col_0_0_,
        member0_.age as col_1_0_ 
    from
        Member member0_

 

2. DTO로 바로 조회

public class MemberDTO {
    private String name;
    private int age;

    public MemberDTO(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
String query = "select new hellojpa.MemberDTO(m.name, m.age) from Member m"
List<MemberDTO> resultList = em.createQuery(query, MemberDTO.class)
    .getResultList();

for (MemberDTO m: resultList) {
	System.out.println("m = " + m.getName() + ", " + m.getAge());
}

이 경우에는 패키지 명을 포함한 전체 클래스 명을 입력해야 하고, 순서와 타입이 일치하는 생성자가 존재해야 한다.

 

Paging

List<Member> resultList = em.createQuery("select m from Member m order by m.age desc", Member.class)
	.setFirstResult(0)
	.setMaxResults(10)
	.getResultList();
    
Hibernate: 
/* select
    m 
from
    Member m 
order by
    m.age desc */ select
        member0_.id as id1_0_,
        member0_.age as age2_0_,
        member0_.TEAM_ID as team_id4_0_,
        member0_.username as username3_0_ 
    from
        Member member0_ 
    order by
        member0_.age desc limit ? 
        // setFirstResult 가 0 이여서 ?
        // 0 이 아니면 + offset

setFirstResult() - 조회 시작 위치, 0부터 시작
setMaxResults() - 조회할 데이터 수

 

필요한 데이터만 나눠서 가져오는 것을 의미한다. 조회 시작 위치와 조회할 데이터 수를 지정해주면 간단히 가능하다.

 

Join

List<Member> resultList= em.createQuery("select m from Member m inner join m.team t", Member.class)
	.getResultList();	// 'inner'는 생략 가능
List<Member> resultList = em.createQuery("select m from Member m left outer join m.team t", Member.class)
	.getResultList();	// 'outer'는 생략 가능
List<Member> resultList = em.createQuery("select m from Member m, Team t where m.name = t.name", Member.class)
	.getResultList();

Inner Join, Outer Join 과 ON 절을 활용한 Join 도 가능하다.

 

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);

em.persist(member);

em.flush();
em.clear();

String query = "select m from Member m inner join m.team";
List<Member> result = em.createQuery(query, Member.class)
                .getResultList();
                
Hibernate: 
/* select
    m 
from
    Member m 
inner join
    m.team */ select
        member0_.id as id1_0_,
        member0_.age as age2_0_,
        member0_.TEAM_ID as team_id4_0_,
        member0_.username as username3_0_ 
    from
        Member member0_ 
    inner join
        Team team1_ 
            on member0_.TEAM_ID=team1_.id

Sub Query

[NOT] EXISTS, ALL, ANY, SOME, [NOT] IN 등의 함수를 이용하여 Sub Query 를 작성할 수 있다. 표준 JPA에서는 WHERE, HAVING 절에서만 사용 가능하지만, 하이버네이트에서는 SELECT 절도 가능하다. FROM 절의 Sub Query는 현재 JPQL에서 불가능하다.

String query = "select m from Member m where m.team = any (select t from Team t)";
List<Member> resultList = em.createQuery(query, Member.class)
    .getResultList();
    
/*
ALL - 모두 만족하면 참
ANY, SOME = 조건을 하나라도 만족하면 참, 같은 의미
*/

Inner Join, Outer Join 과 ON 절을 활용한 Join 도 가능하다.

 

JQPL 함수

- 기본함수

CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE, ABS, MOD, SIZE, INDEX 등의 함수를 기본으로 제공한다.

String query = "select concat('a', 'b') from Member m";
String query = "select upper(m.name) from Member m";
String query = "select size(t.members) from Team t";

 

-사용자 정의 함수

하이버네이트의 경우 사용자 정의 함수를 미리 방언에 추가한 후 사용할 수 있다.

public class MyH2Dialect extends H2Dialect {
    public MyH2Dialect() {
        registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}

..
..

<properties>
	<property name="hibernate.dialect" value="hellojpa.MyH2Dialect"/>
</properties>

..
.. 

String query = "select function('group_concat', m.name) from Member m";

 

경로 표현식

. 을 찍어서 객체 그래프를 탐색하는 것을 의미한다.

- 상태 Field

String query = "select m.name from Member m";
List<String> resultList = em.createQuery(query, String.class)
    .getResultList();

Entity의 Field 중에서 단순히 값을 저장하기 위한 Field 를 의미한다. 경로 탐색의 끝으로 추가적인 탐색을 할 수 없다. (member.name)

 

- 단일 값 연관 Field

String query = "select m.team from Member m";
// "select t from Member m join m.team t" 처럼 명시적 Join으로 표현 가능
// "select m.team.name from Member m" 처럼 추가 탐색 가능
List<Team> resultList = em.createQuery(query, Team.class)
    .getResultList();
    
..
..

select m.*
from Member m
inner join Team t on m.team_id = t.team_id

@ManyToOne, @OneToOne 연관관계인 경우로 탐색 대상이 Entity인 Field를 의미한다. 묵시적으로 Inner Join이 발생하며 추가적인 탐색을 할 수 있다. (member.team)

 

- Collection 값 연관 Field

String query = "select m.orders from Member m";
// "select m.orders.address from Member m" 와 같은 추가 탐색 불가능
// "select o.address" from Member m join m.orders o" 처럼 명시적 Join으로 추가 탐색 가능
List<Order> resultList = em.createQuery(query, Order.class)
    .getResultList();

@OneToMany@ManyToMany 연관관계인 경우로 탐색 대상이 Collection인 Field를 의미한다. (ex. member.orders) 묵시적으로 Inner Join이 발생하며, 추가적인 탐색을 할 수 없다. 다만, FROM절에서 명시적 Join을 통해 별칭을 얻으면 그를 통해 탐색이 가능하다.

 

💡 실무에서는 가급적 묵시적 JOIN 대신에 명시적 JOIN을 사용하는 편이 좋다. JOIN은 SQL 튜닝에 중요한 표인트인데, 묵시적 JOIN은 한눈에 파악하기 어려운 부분이 있기 때문에 혼란을 낳을 수 있다.

 

⭐ Fetch Join

💡 성능 최적화 관점에서, 실무에서 정말 중요한 부분이다.

연관된 Entity 혹은 Collection을 SQL 한 번으로 함께 조회하는 기능이다.

String query1 = "select m from Member m join m.team";	// 일반 join문
List<Member> resultList = em.createQuery(query1, Member.class)
    .getResultList();
for (Member m: resultList) {
    System.out.println("m = " + m.getName());
    // FetchType.LAZY이므로, 영속성 Context에는 아직 team을 위한 정보가 없는 상태
    System.out.println("t = " + m.getTeam().getName());
    // team 관련된 정보 요청이 들어오면 그때서야 SELECT query를 보내 정보를 가져옴
}

String query2 = "select m from Member m join fetch m.team";		// fetch join문
List<Member> resultList = em.createQuery(query2, Member.class)
    .getResultList();
for (Member m: resultList) {
    System.out.println("m = " + m.getName());
    // join fetch했므로, 영속성 Context에는 member 그리고 연관된 team 정보가 모두 있는 상태
    System.out.println("t = " + m.getTeam().getName());
    // 1차 캐시에서 정보를 가져옴
}

💡 만약 꽤 규모가 큰 Application에서 일반 JOIN문으로 한 Entity와 연관된 Entity 정보를 가져온다면, N+1 문제가 발생할 수 있다. Fetch Join을 사용하면 연관된 Entity 정보들을 한 번에 가져오므로 N+1 문제를 방지할 수 있다. 즉. Fetch Join은 글로벌 로딩 전략보다 우선적으로, 즉시 로딩 속성으로 Entity를 조회하는 것이다. 따라서 객체 그래프를 SQL 한 번으로 조회할 때 주로 사용한다.

 

다형성 Query

- Type

조회 대상을 특정 자식으로 한정할 때 사용한다. 아래는 Item 중에 Book 과 Movie를 조회하는 예시 JPQL이다.

[JPQL]
String query = "select i from Item i where type(i) IN (Book, Movie)";

[SQL]
String query = "select i from i where i.Dtype IN ('B', 'M')";

 

- Treat

자바의 TypeCasting 과 유사한 개념으로, 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. 아래는 부모인 Item과 자식인 Book 에 대한 예시 JPQL이다.

[JPQL]
String query = "select i from Item i where treat(i as Book).author = 'joon'";

[SQL]
String query = "select i.* from Item i where i.Dtype ='B' and i.author = 'Kim';

 

Named Query

미리 정의해서 이름을 부여해두고 사용하는 JPQL로, 정적인 Query이다. Application 로딩 시점에 초기화 후 캐시해서 재사용되어 성능상 이점이 있고, 더불어 로딩 시점에 Query를 검증해준다.

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

...
...

List<Member> resultList = em.createNamedQuery("Member.findByName", Member.class)
    .setParameter("name", "memberA")
    .getResultList();

 

벌크 연산

Query 한 번으로 테이블의 여러 Entity를 변경할 때 사용한다. 대량의 Field 값 갱신이 필요한 경우에 일반 UPDATE 문으로 한다면 엄청 많은 UPDATE SQL이 실행된다. 벌크 연산으로 한 번의 Query 로 가능하다.

String query = "update Member m " +
	"set m.age = age * 2 " +
	"where m.age > 0";
int resultCount = em.createQuery(query)
	.executeUpdate();

UPDATEDELETE를 지원하며, 실행 결과는 영향받은 Entity 수를 반환한다. 벌크 연산은 영속성 Context를 무시하고 DB에 직접 Query를 보내는 점을 유의해서 사용해야 한다.

 

정리하면 벌크연산이 영속성 컨텍스트를 무시하고 DB를 업데이트하기 때문에 값을 조회하면 이전의 값이 남아있다.

Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);

em.createQuery("update Member m set m.age = 20")
				.executeUpdate();

Member findMember = em.find(Member.class, member.getId());

System.out.println("findMember = " + findMember.getAge());
// findMember = 10

 그렇기 때문에 벌크연산 후에 영속성 컨텍스트를 초기화 해주어야 한다!

 

Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);

em.createQuery("update Member m set m.age = 20")
				.executeUpdate();

// 영속성 컨텍스트 초기화       
em.clear(); 

Member findMember = em.find(Member.class, member.getId());

System.out.println("findMember = " + findMember.getAge());
// findMember = 20

 

728x90
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.