일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 406 NOT_ACCEPTABLE
- 빈 스코프
- testresttemplate
- SpringBootTest
- 정적 컨텐츠
- entity
- 컴포넌트 스캔
- Not Acceptable
- JPA
- 테스트 코드
- 랜덤 포트
- 가을야구
- restTemplate
- 통합 테스트
- 기본 생성자
- 좋은 객체지향 설계 원칙
- jdbc template
- RunWith
- 스프링 데이터 JPA
- Controller
- jdbc
- 스프링 컨테이너
- 의존성 주입
- 키움
- Java Reflection API
- 의존관계 자동 주입
- 스프링 IoC 컨테이너
- ResponseEntity
- 405 METHOD_NOT_ALLOWED
- 스프링
- Today
- Total
코드네임 JY
[프로젝트] 즉시 로딩과 지연 로딩 본문
🎾 프록시
즉시 로딩과 지연 로딩의 매커니즘을 이해하려면, '프록시' 라는 개념을 먼저 알아야 한다!
'프록시' 라는 단어의 의미는 '무언가를 대신한다' 라는 느낌이 강하다! 예를 들어 '프록시 서버' 라는 것이 있지 않나!
Hibernate는 '프록시 클래스' 라는 가짜 클래스를 만들 수 있다. 이는 실제 클래스를 상속 받아서 만들어진다.
그래서 우리가 직접 코드를 짜거나.. 등의 행위로 프록시를 직접 만들어서 사용할 일은 없다!
프록시 클래스는 실제 클래스를 상속 받아서 만들어지기 때문에, 실제 클래스와 겉 모양은 같다.
그리고 내부에는 실제 객체의 참조(target) 를 보관한다! 따라서 프록시 객체는 특정 시점에 실제 객체를 호출 할 수 있다!
프록시 클래스는 em.getReference( ) 를 통해서 생성할 수 있다. 다음 코드의 동작 과정을 보자.
Student student = em.getReference(Student.class, 1L);
student.getName(); // name 필드를 가져올 때
em.getReference( ) 를 통해 프록시 클래스를 만들면, 실제 엔티티를 가져오는 것이 아닌 프록시 클래스가 만들어진다.
첫째 줄의 코드만 수행했을 때는 기존 클래스를 상속한 프록시 클래스만 하나 만들어지고 추가적인 동작은 하지 않는다.
하지만 둘째 줄의 코드를 수행했을 때, 프록시 클래스는 참조(target)를 통해 실제 엔티티에 접근 할 수 있게 된다.
일단 영속성 컨텍스트에게 초기화 요청으로 DB에서 엔티티를 조회한 후, 실제 엔티티를 생성해놓는다.
(여기서 '초기화' 는 '데이터베이스를 통해서 실제 엔티티를 만들어내는 과정' 을 말함)
그리고 만들어진 실제 엔티티에 참조(target)를 통해 접근할 수 있는 것이다. 실제 엔티티로 바뀌는 것은 절대 아니다!
위에서 본 프록시의 특징을 데이터베이스에 쿼리를 날린다고 생각해보면 다음과 같이 정리할 수 있다.
✅ 프록시 객체를 생성한 시점(getReference)에는 데이터베이스에 쿼리를 날리지 않음
✅ 엔티티의 값이 실제로 사용되는 시점(getName)에 데이터베이스에 쿼리를 날려 조회 (SELECT name FROM Student..)
자바에서 서로 다른 두 클래스가 특정 연관관계를 가지고 있을 때도 프록시 매커니즘을 사용한다.
연관관계로 이어진 엔티티를 하나가 로딩될 때 나머지도 즉시 가져오냐, 아니면 실제 필요한 시점에서만 지연 해서 가져오냐!
🏐 즉시 로딩 (EAGER)
플레이리스트 프로젝트에서 사용하는 Music 엔티티와 Artist 엔티티는 다대일 관계를 가지고 있다.
그래서 Music 엔티티에 Artist 엔티티를 가지도록 설계하였다! 그림으로 표현하면 다음과 같다.
아티스트를 등록하면, 음악을 등록할 때 아티스트를 선택한 후 나머지 정보들을 입력하고 음악을 등록할 수 있다.
등록한 음악을 조회하는 화면을 간단하게 보여주면 다음과 같다.
위 화면을 보면, 음악을 조회할 때 아티스트도 같이 조회 해야 한다. 음악이 어떤 아티스트의 노래인지 알아야하기 때문이다!
음악을 조회할 때 JOIN 명령어를 통해서 아티스트까지 한 번에 조회 하는 방법을 '즉시(EAGER) 로딩' 이라고 한다.
코드는 @ManyToOne 어노테이션에서 EAGER 옵션을 설정해주면 된다. (참고로 @____ToOne의 default 옵션은 EAGER)
음악 조회 시, 즉시 로딩 방법을 사용할 때 실제로 Hibernate가 DB에 쿼리를 보낸 로그를 살펴보자.
-- MusicController.findMusic (음악 조회 시 발생하는 쿼리)
Hibernate:
select
music0_.music_id as music_id1_2_0_,
music0_.artist_id as artist_i6_2_0_,
music0_.cover_img as cover_im2_2_0_,
music0_.link as link3_2_0_,
music0_.lyrics as lyrics4_2_0_,
music0_.title as title5_2_0_,
artist1_.artist_id as artist_i1_0_1_,
artist1_.genre as genre2_0_1_,
artist1_.name as name3_0_1_,
artist1_.profile_img as profile_4_0_1_,
artist1_.type as type5_0_1_
from
music music0_
inner join
artist artist1_
on music0_.artist_id=artist1_.artist_id
where
music0_.music_id=?
-- MusicController.getCoverImg (음악 앨범 커버 이미지 조회 시 발생하는 쿼리)
Hibernate:
select
music0_.music_id as music_id1_2_0_,
music0_.artist_id as artist_i6_2_0_,
music0_.cover_img as cover_im2_2_0_,
music0_.link as link3_2_0_,
music0_.lyrics as lyrics4_2_0_,
music0_.title as title5_2_0_,
artist1_.artist_id as artist_i1_0_1_,
artist1_.genre as genre2_0_1_,
artist1_.name as name3_0_1_,
artist1_.profile_img as profile_4_0_1_,
artist1_.type as type5_0_1_
from
music music0_
inner join
artist artist1_
on music0_.artist_id=artist1_.artist_id
where
music0_.music_id=?
앗 참고로 음악 등록할 때 앨범 커버 이미지도 올리는데, 조회할 때 음악의 id를 조회한 후 GET 방식으로 파일 이미지를 가져온다.
핵심은! 음악 정보를 가져올 때도 음악의 id를 조회하지만, 앨범 커버 이미지를 불러올 때도 음악의 id를 통해 조회한다는 것이다!
음악을 조회할 때 MUSIC 테이블과 ARTIST 테이블이 JOIN 명령어로 묶여졌다. 왜 그렇게 동작할까?
즉시 로딩은 JOIN 명령어를 사용해서 쿼리 하나로 여러 테이블을 한꺼번에 조회 하기 때문이다. 그래서 둘이 묶인 것이다!
그리고 앨범 커버 이미지를 조회할 때 발생한 쿼리를 보자. 음..? 여기에도 JOIN 명령어를 사용해서 쿼리가 날아갔다.
약간 띠용(?)한 느낌이 든다.. 왜냐면 음악의 앨범 커버를 조회할 때는 아티스트 정보는 필요없기 때문 이다..! 🧐🧐
(그것도 그렇고 사실 JOIN이 포함된 쿼리가 간단하게 짧은 쿼리에 비해 이해하기 조금 더 어렵지 않나 ㅎㅎ..)
🏉 지연 로딩 (LAZY)
그렇다면 지연 로딩으로 엔티티를 연결하면 어떻게 될까?
일단 코드는 @ManyToOne 어노테이션에서 LAZY 옵션을 설정해주면 된다.
음악 조회 시, 이번에는 지연 로딩 방법을 사용할 때 Hibernate가 DB에 쿼리를 보낸 로그를 살펴보자.
-- MusicController.findMusic (음악 조회 시 발생하는 쿼리)
Hibernate:
select
music0_.music_id as music_id1_2_0_,
music0_.artist_id as artist_i6_2_0_,
music0_.cover_img as cover_im2_2_0_,
music0_.link as link3_2_0_,
music0_.lyrics as lyrics4_2_0_,
music0_.title as title5_2_0_
from
music music0_
where
music0_.music_id=?
Hibernate:
select
artist0_.artist_id as artist_i1_0_0_,
artist0_.genre as genre2_0_0_,
artist0_.name as name3_0_0_,
artist0_.profile_img as profile_4_0_0_,
artist0_.type as type5_0_0_
from
artist artist0_
where
artist0_.artist_id=?
-- MusicController.getCoverImg (음악 앨범 커버 이미지 조회 시 발생하는 쿼리)
Hibernate:
select
music0_.music_id as music_id1_2_0_,
music0_.artist_id as artist_i6_2_0_,
music0_.cover_img as cover_im2_2_0_,
music0_.link as link3_2_0_,
music0_.lyrics as lyrics4_2_0_,
music0_.title as title5_2_0_
from
music music0_
where
music0_.music_id=?
오! 뭔가 좀 다르다! 공부할 맛이 나겠는데요? 🤪🤪 어떤 부분이 다른지 천천히 살펴보자.
일단 음악 조회 시 발생하는 쿼리에서 즉시 로딩에서는 하나의 쿼리로 JOIN 명령어를 통해 테이블이 합쳐졌던 것과 다르게,
지연 로딩에서는 MUSIC 테이블에 대한 쿼리와 ARTIST 테이블에 대한 쿼리가 둘 다 나갔다.
@Controller
public class MusicController {
@GetMapping("/music/{id}")
public String findMusic(@PathVariable Long id, Model model) {
MusicResponseDto music = musicService.findOneMusicById(id);
model.addAttribute("music", music);
return "music/selectMusic";
}
}
@Service
public class MusicService {
public MusicResponseDto findOneMusicById(Long id) {
Music findMusic = musicRepository.findOneById(id); // MUSIC 테이블에 쿼리가 나가는 시점
byte[] fileImage = ImageUtils.decompressImage(findMusic.getCoverImg());
return new MusicResponseDto(findMusic.getId(), findMusic.getTitle(),
findMusic.getArtist(), findMusic.getLyrics(), findMusic.getLink(), fileImage);
// ARTIST 테이블에 쿼리가 나가는 시점 (getArtist)
}
}
@Repository
public class MusicRepository {
public Music findOneById(Long id) {
return em.find(Music.class, id);
}
}
음악 조회 로직을 간단하게 보면 위 코드와 같은데, MUSIC 테이블에 쿼리가 나가는 시점은 id를 통해 엔티티를 조회할 때 이다.
ARTIST 테이블에 쿼리가 나가는 시점은 Artist 엔티티의 데이터가 실제로 필요한 시점(getArtist) 에 발생한다!
그래서 지연 로딩을 사용하면 ARTIST 테이블 입장에서, 실제로 ARTIST 테이블 내 데이터가 사용될 때 쿼리가 나가는 것 이다!
위에서 설명한 프록시의 매커니즘과 비슷하지 않나? 그렇다! 이는 프록시 매커니즘을 사용해서 필요한 시점에 쿼리가 나간다!
앨범 커버 이미지 조회 시 발생하는 쿼리도 보면, 오직 MUSIC 테이블에만 쿼리가 나간 것 을 볼 수 있다.
두 연관 관계가 즉시 로딩 으로 묶여 있을 때는 어쩔 수 없이 JOIN을 통해 테이블을 묶어 한 번에 조회 해야 했지만,
지연 로딩 에서 ARTIST 테이블은 필요할 때만 찾으면 되고, 이미지는 MUSIC 테이블에만 있으니 여기에만 쿼리가 나간 것 이다!
🥏 정리
그렇다면 이번 프로젝트에서 MUSIC 테이블과 ARTIST 테이블은 즉시 로딩과 지연 로딩 중 어떤 것으로 연결되어야 할까?
음악 조회할 때는 JOIN으로 묶어서 조회하던, 따로 쿼리를 2개 써서 조회하던 큰 상관은 없다고 생각한다! 이건 내 맘이지~ 🤗🤗
하지만 커버 이미지를 불러올 때 즉시 로딩은 애꿎은(?) ARTIST 테이블까지 불렀다. 지연 로딩은 MUSIC 테이블만 부른 반면에!
따라서 MUSIC 테이블과 ARTIST 테이블 간의 관계는 지연(LAZY) 로딩 으로 설정하는 것이 좋다고 생각한다!
즉시 로딩과 지연 로딩
✅ 즉시 로딩 : JOIN 명령어를 통해서 하나의 쿼리로 여러 테이블을 동시에 조회
✅ 지연 로딩 : 데이터가 필요한 시점에 쿼리를 날려 조회
음악 조회 시 발생하는 쿼리
✅ 즉시 로딩 : MUSIC 테이블과 ARTIST 테이블이 JOIN 되어 한꺼번에 조회
✅ 지연 로딩 : 일단 MUSIC 테이블에 쿼리를 날려 조회하고, 아티스트가 필요한 시점에 ARTIST 테이블에 쿼리를 날려 조회
➡️ "두 테이블이 모두 필요한 것은 맞아..! 그래서 JOIN으로 묶여서 들어오던 따로 두 개의 쿼리를 날려 들어오던 노상!"
음악 앨범 커버 이미지 조회 시 발생하는 쿼리
✅ 즉시 로딩 : MUSIC 테이블과 ARTIST 테이블이 JOIN 되어 한꺼번에 조회 (사실 ARTIST는 불필요)
✅ 지연 로딩 : 음악에 이미지 데이터가 존재하므로, MUSIC 테이블에만 쿼리를 날려 조회
➡️ "ARTIST 테이블까지 불러올 필요는 없으므로, 지연 로딩으로 설정하는 것이 좋겠네!"
즉시 로딩을 사용하면 예상하지 못한 쿼리가 나가게 되고,
N+1 문제(EAGER 관계로 묶여있는 것을 가져오기 위해 추가적인 쿼리가 계속 나가야 함)를 야기한다!
가급적이면 지연 로딩만 사용하는 것을 권장한다!
'백엔드 공부' 카테고리의 다른 글
[UMC] 4th UMC 해커톤 후기 (0) | 2023.07.13 |
---|---|
[프로젝트] 넌 못 지나간다 (0) | 2023.02.20 |
[프로젝트] 테이블 설계 & 엔티티 세팅 (0) | 2023.02.14 |
[프로젝트] 요구사항 분석 및 구현 단계 (0) | 2023.02.14 |
[스프링 JPA] 연관관계 매핑 (2) (0) | 2023.02.13 |