7. [JPA] fetch type - 로딩 기법
JPA에는 두 가지 로딩 기법이 존재한다.
즉시로딩과 지연로딩이라고 한다.
이 두 개의 로딩 전략은 엔티티를 조회할 때 적용된다. update, delete, insert에는 로딩 전략 대상이 아니다.
즉시로딩은 뭔가?
엔티티 매니저를 통해 엔티티를 조회하면 연관관계에 매핑되어 있는 엔티티도 함께 조회
지연로딩은 뭔가?
엔티티 매니저를 통해 엔티티를 조회하면 연관관계에 매핑되어 있는 엔티티를 실제 사용할 때 조회
즉시로딩, 지연로딩에 대한 설명을 글로만 보면 이해하기 쉽지 않다.
예를 들어 설명해보자.
Member 엔티티 클래스, Phone 엔티티 클래스가 있다고 하자.
이 둘은 서로 객체 연관관계 설정이 되어 있다.
아래와 같이 Member 테이블에 Phone 엔티티가 즉시로딩 전략으로 설정되어 있다고 하자.
이와같이 설정이 되어 있을 때 Member 엔티티를 조회하게 되면 Member에 속한 Phone 엔티티들도 함께 조회된다.
조회가 되면 Member와 Phone 엔티티는 영속성 컨텍스트내에 영속 상태로 존재한다.
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, mappedBy = "member") private Collection<Phone> phones = new ArrayList<>(); | cs |
이번에는 지연 로딩 설정으로 되어 있다고 하자.
다음과 같이 설정이 되어 있을 때 Member 엔티티를 조회하게 되면 Phone 엔티티는 DB에서 조회하지 않는다.
그렇다고 phones 인스턴스가 null로 되어 있지는 않다. proxy 형태로 생성되어 있을 뿐이다.
실제 Phone 엔티티가 초기화 되는 시점은 phones.get(0).getId() 와 같이 엔티티를 사용했을 때이다.
1 2 | @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST, mappedBy = "member") private Collection<Phone> phones = new ArrayList<>(); | cs |
자~ 이제 테스트를 해보자.
[즉시로딩]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | @Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class EegerLoadingTest { @PersistenceContext private EntityManager em; @Test @Transactional public void 즉시로딩() { PersistenceUnitUtil unitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil(); EagerMember member = new EagerMember(); member.setId(1); EagerPhone phone = new EagerPhone(); phone.setId(1); member.addPhone(phone); EagerPhone phone1 = new EagerPhone(); phone1.setId(2); member.addPhone(phone1); em.persist(member); em.flush(); em.clear(); member = em.find(EagerMember.class, 1); // phones 초기화 되어 있음 boolean loaded = unitUtil.isLoaded(member.getPhones()); assertThat(true, is(loaded)); } } @Data @ToString(exclude = "phones") @Entity class EagerMember { @Id @Column(name = "MEMBER_ID") private int id; @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, mappedBy = "member") private Collection<EagerPhone> phones = new ArrayList<>(); public void addPhone(EagerPhone phone) { this.phones.add(phone); phone.setMember(this); } } @Data @Entity class EagerPhone { @Id @Column(name = "PHONE_ID") private int id; @ManyToOne private EagerMember member; } | cs |
테스트를 수행하면 Phones 엔티티가 초기화 되어 있는 것을 확인할 수 있다.
참고로 영속성 컨텍스트에 엔티티가 초기화 되어 있는지를 확인하기 위해서는 PersistenceUnitUtil을 이용하면 된다.
mappedBy 란?
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다. (엔티티 주인 반대쪽에 mappedBy 속성 추가해야 함)
외래키를 가지고 있는 엔티티가 주인이다.
테이블 관점에서 회원 테이블에는 팀 테이블의 외래키를 가지고 있다. 즉, 회원 테이블이 주인이 된다.
[지연로딩]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | @Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class LazyLoadingTest { @PersistenceContext private EntityManager em; private PersistenceUnitUtil unitUtil; @Before public void before() { unitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil(); } @Test @Transactional public void lazyManyToOne() { LMember member = new LMember(); member.setId(1); LPhone lPhone = new LPhone(1, "010-1111-1111"); lPhone.setLMember(member); LPhone lPhone1 = new LPhone(2, "010-2222-2222"); lPhone1.setLMember(member); em.persist(lPhone); em.persist(lPhone1); em.flush(); em.clear(); LPhone entityPhone = em.find(LPhone.class, 1); LMember entityMember = entityPhone.getLMember(); boolean loaded = unitUtil.isLoaded(entityMember); assertThat(false, is(loaded)); String className = entityMember.getClass().getName(); assertThat(className, StringContains.containsString("jvst")); entityMember.getId(); loaded = unitUtil.isLoaded(entityMember); assertThat(true, is(loaded)); } @Test @Transactional public void lazyOneToMany() { LMember member = new LMember(); member.setId(1); LPhone lPhone = new LPhone(1, "010-1111-1111"); member.addPhone(lPhone); LPhone lPhone1 = new LPhone(2, "010-2222-2222"); member.addPhone(lPhone1); // 영속 상태 전 phoneList 객체는 ArrayList 타입이다. assertThat(member.getPhoneList().getClass(), sameInstance(ArrayList.class)); // 영속 상태로 저장 em.persist(member); em.flush(); em.clear(); LMember entityMember = em.find(LMember.class, 1); assertThat(1, is(entityMember.getId())); System.out.println("----------------------------------------------------"); List<LPhone> phoneList = entityMember.getPhoneList(); // 엔티티 초기화 안 되어 있음 boolean loaded = unitUtil.isLoaded(phoneList); assertThat(false, is(loaded)); // 원본 컬렉션인 phoneList는 PersistentBag 래퍼 컬렉션으로 감싸져 있다. assertThat(phoneList.getClass(), sameInstance(PersistentBag.class)); // 엔티티 초기화 System.out.println("init entity"); phoneList.get(0).getId(); // 엔티티 초기화 되어 있음 loaded = unitUtil.isLoaded(phoneList); assertThat(true, is(loaded)); System.out.println("----------------------------------------------------"); } } @Data @ToString(exclude = "phoneList") @Entity class LMember { @Id @Column(name = "MEMBER_ID") private int id; @OneToMany(fetch = FetchType.LAZY, mappedBy = "lMember", cascade = CascadeType.PERSIST) private List<LPhone> phoneList = new ArrayList<>(); public void addPhone(LPhone phone) { this.phoneList.add(phone); phone.setLMember(this); } } @Getter @Setter @AllArgsConstructor @NoArgsConstructor @Entity class LPhone { @Id @Column(name = "PHONE_ID") private int id; @Column(name = "PHONE_NAME") private String number; @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) private LMember lMember; public LPhone(int id, String number) { this.number = number; this.id = id; } } | cs |
지연로딩은 즉시로딩보다는 추가 설명이 필요할 듯 하여 두 개의 테스트 코드로 분리했다.
ManyToOne과 OneToMany 테스트이다.
lazyManyToOne() 테스트 코드를 확인해 보면 일단 테스트 데이터를 만들기 위해 LMember, LPhone 엔티티 클래스를 생성하고 영속화 시켰다. 정상 테스트를 위해서는 엔티티매니저를 초기화 시켜야 하기 때문에 em.clear() 코드가 추가됐다.
만약 해당 코드가 존재하지 않으면 LMember, LPhone 엔티티가 영속성 컨텍스트에 존재하기 때문에 지연 로딩 테스트가 실패하게 될 것이다.
일단 LPhone 엔티티를 조회한다. 해당 엔티티는 영속성 컨텍스트에 존재하지 않기 때문에 DB 조회 후 엔티티를 초기화 할 것이다.
1 | LPhone entityPhone = em.find(LPhone.class, 1); | cs |
LPhone 엔티티 클래스에 연관관계로 설정되어 있는 LMember는 fetch = FetchType.LAZY로 설정되어 있다.
다음의 entityMember 에는 어떤 객체를 레퍼런스 하고 있을까?
1 | LMember entityMember = entityPhone.getLMember(); | cs |
엔티티 초기화 여부를 확인해 보면 false로 되어 있다. (LAZY 로딩 기법을 사용했기 때문에 영속성 컨텍스트에 LMember 엔티티가 존재하면 안 된다.)
1 2 | boolean loaded = unitUtil.isLoaded(entityMember); assertThat(false, is(loaded)); | cs |
다음 테스트 코드를 통해 LMember 엔티티 클래스는 proxy 객체를 참조하고 있다는 것을 알 수 있다.
proxy 객체 이름을 확인해 보면 com.kyu.boot.jpa.fetchtype.LMember_$$_jvst45c_b 와 같이 되어 있음을 알 수 있다.
참고로 프록시 이름에 항상 jvst 문자가 포함된다.
1 2 | String className = entityMember.getClass().getName(); assertThat(className, StringContains.containsString("jvst")); | cs |
lazyOneToMany() 테스트 코드를 확인해 보면 위의 설명과 거의 비슷하지만 LPhone 엔티티 클래스가 List 객체에 담겨져 있다.
그럼 다음의 phoneList 에는 어떤 객체가 레퍼런스 되어 있을까?
1 | List<LPhone> phoneList = entityMember.getPhoneList(); | cs |
다음의 코드를 통해 phoneList 는 초기화 되어 있지 않음을 알 수 있다.
1 2 | boolean loaded = unitUtil.isLoaded(phoneList); assertThat(false, is(loaded)); | cs |
JPA는 컬렉션을 PersistentBag 래퍼 클래스로 감싼다.
위의 전체 테스트 코드를 보면 phoneList가 영속 상태 전인 상태에서는 ArrayLIst 타입이고 영속 상태가 되었을 때에는 PersistentBag 타입으로 감싸진다.
1 | assertThat(phoneList.getClass(), sameInstance(PersistentBag.class)); | cs |
실제 phoneList에 속한 LPhone 엔티티를 사용함으로써 엔티티가 초기화 된다.
1 | phoneList.get(0).getId(); | cs |
엔티티 초기화 검증
1 2 | loaded = unitUtil.isLoaded(phoneList); assertThat(true, is(loaded)); | cs |
여기까지 fetchType의 두 가지 로딩 기법에 대해서 알아봤다.
'프로그래밍' 카테고리의 다른 글
spring data elasticsearch 사용해 보기 (6) | 2017.08.31 |
---|---|
10. [JPA] JPQL (0) | 2017.08.28 |
9. [JPA] @Enumerated (0) | 2017.08.23 |
8. [JPA] Attribute Converter (0) | 2017.08.23 |
6. [JPA] 영속성 전이 - Cascade (0) | 2017.08.17 |
5. [JPA] 엔티티 매니저 (0) | 2017.08.16 |
4. [JPA] 엔티티 매니저 팩토리 (0) | 2017.08.10 |
3. [JPA] 영속성 컨텍스트란? (0) | 2017.08.01 |