Reference. 한 번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 Online
이전 글
더보기
1. N+1 이슈
FetchType.EAGER
와 FetchType.LAZY
- User 엔티티에 UserHistory는 무조건 필요로 하는 데이터X
- User 엔티티에서 getUserHistories를 호출할 때 데이터를 가져오는 것을
LAZY Fetch
- 단,
LAZY
는 항상 가능한 것이 아님, 세션(영속성 컨텍스트가 관리할 때)일 때 동작
- JPA에선
@Transactional
을 통해 트랜잭션이 열려있을 때 동작
ToString
,JSON Serialize
를 쓸 때Exclude
하지 않고getter
를 쓰면LAZY
효과를 잃음
- 단,
- User 객체를 호출하면 바로 릴레이션을 모두 쿼리를 통해 조회하는 것을
EAGER Fetch
- 연관관계의
FetchType
default 값- EAGGER
- @ManyToOne
- @OneToOne
- LAZY
- @OneToMany
- @ManyToMany
- EAGGER
User.java
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_Id", insertable = false, updatable = false)
@ToString.Exclude
private List<UserHistory> userHistories = new ArrayList<>();
EAGER, LAZY 사용의 문제 예시
data.sql 추가
insert into review(`id`, `title`, `content`, `score`, `user_id`, `book_id`) values(1, '내 인생을 바꾼 책', '좋았어요', 5.0, 1, 1);
insert into review(`id`, `title`, `content`, `score`, `user_id`, `book_id`) values(2, '너무 진도가 빨라요', '조금 별로였어요', 3.0, 2, 2);
insert into comment(`id`, `comment`, `review_id`) values(1, '좋았어요', 1);
insert into comment(`id`, `comment`, `review_id`) values(2, '별로 였습니다.', 1);
insert into comment(`id`, `comment`, `review_id`) values(3, '그냥 그랬습니다.', 2);
Comment.java 생성
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Comment extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
private String comment;
@ManyToOne
@ToString.Exclude
private Review review;
}
Review.java
@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Review extends BaseEntity{
...
@ManyToOne
@ToString.Exclude
private User user;
@ManyToOne
@ToString.Exclude
private Book book;
@OneToMany
@JoinColumn(name = "review_id")
private List<Comment> comments;
}
ReviewRepositoryTest.java
@SpringBootTest
public class ReviewRepositoryTest {
@Autowired
private ReviewRepository reviewRepository;
@Test
@Transactional //JPA는 트랜잭션 단위로 실행 되므로 필요 (findAll은 JPQL 사용)
void reviewTest() {
List<Review> reviews = reviewRepository.findAll();
System.out.println(reviews);
}
}
//실행 결과
//불필요한 조회가 실행 (Review에 @ManyToOne은 EAGGER, @OneToMany LAZY로 인해 발생)
Hibernate:
select
book0_.id as id1_1_0_,
book0_.created_at as created_2_1_0_,
book0_.updated_at as updated_3_1_0_,
book0_.category as category4_1_0_,
book0_.deleted as deleted5_1_0_,
book0_.name as name6_1_0_,
book0_.publisher_id as publishe8_1_0_,
book0_.status as status7_1_0_,
publisher1_.id as id1_5_1_,
publisher1_.created_at as created_2_5_1_,
publisher1_.updated_at as updated_3_5_1_,
publisher1_.name as name4_5_1_,
bookreview2_.id as id1_3_2_,
bookreview2_.created_at as created_2_3_2_,
bookreview2_.updated_at as updated_3_3_2_,
bookreview2_.average_review_score as average_4_3_2_,
bookreview2_.book_id as book_id6_3_2_,
bookreview2_.review_count as review_c5_3_2_
from
book book0_
left outer join
publisher publisher1_
on book0_.publisher_id=publisher1_.id
left outer join
book_review_info bookreview2_
on book0_.id=bookreview2_.book_id
where
book0_.id=?
and (
book0_.deleted = 0
)
...
해결방안
Review.java 수정
...
public class Review extends BaseEntity{
//@ManyToOne의 FetchType default은 EAGGER
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Book book;
//@OneToMany의 FetchType default은 LAZY
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "review_id")
private List<Comment> comments;
}
ReviewRepositoryTest.java 실행 결과
- 불필요한 조회 없이 필요한 쿼리만 조회하여 출력
Hibernate:
select
review0_.id as id1_6_,
review0_.created_at as created_2_6_,
review0_.updated_at as updated_3_6_,
review0_.book_id as book_id7_6_,
review0_.content as content4_6_,
review0_.score as score5_6_,
review0_.title as title6_6_,
review0_.user_id as user_id8_6_
from
review review0_
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
[Review(super=BaseEntity(createdAt=2021-08-16T22:53:40.119677, updatedAt=2021-08-16T22:53:40.119677), id=1, title=내 인생을 바꾼 책, content=좋았어요, score=5.0, comments=[]), Review(super=BaseEntity(createdAt=2021-08-16T22:53:40.124253, updatedAt=2021-08-16T22:53:40.124253), id=2, title=너무 진도가 빨라요, content=조금 별로였어요, score=3.0, comments=[])]
EAGER, LAZY 사용의 차이 예시
EAGGER
- 조회 쿼리가 실행되면 영속성 컨텍스트 캐쉬를 통해 데이터 조회 (쿼리 추가 실행 X)
Review.java
...
public class Review extends BaseEntity{
...
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "review_id")
private List<Comment> comments;
}
테스트 코드 및 실행 결과
@Test
@Transactional
void reviewTest() {
List<Review> reviews = reviewRepository.findAll();
// System.out.println(reviews);
System.out.println("전체를 가져왔습니다.");
System.out.println(reviews.get(0).getComments());
System.out.println("첫번째 리뷰의 코멘트들을 가져왔습니다.");
System.out.println(reviews.get(1).getComments());
System.out.println("두번째 리뷰의 코멘트들을 가져왔습니다.");
}
//실행 결과
Hibernate:
select
review0_.id as id1_6_,
review0_.created_at as created_2_6_,
review0_.updated_at as updated_3_6_,
review0_.book_id as book_id7_6_,
review0_.content as content4_6_,
review0_.score as score5_6_,
review0_.title as title6_6_,
review0_.user_id as user_id8_6_
from
review review0_
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
전체를 가져왔습니다.
[]
첫번째 리뷰의 코멘트들을 가져왔습니다.
[]
두번째 리뷰의 코멘트들을 가져왔습니다.
LAZY
- 전체 조회쿼리 실행 후 필요로 하는 객체에 getter를 호출할 때 쿼리가 실행 (쿼리실행 이점)
Review.java
...
public class Review extends BaseEntity{
...
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id")
private List<Comment> comments;
}
실행 결과
Hibernate:
select
review0_.id as id1_6_,
review0_.created_at as created_2_6_,
review0_.updated_at as updated_3_6_,
review0_.book_id as book_id7_6_,
review0_.content as content4_6_,
review0_.score as score5_6_,
review0_.title as title6_6_,
review0_.user_id as user_id8_6_
from
review review0_
전체를 가져왔습니다.
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
[]
첫번째 리뷰의 코멘트들을 가져왔습니다.
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
[]
두번째 리뷰의 코멘트들을 가져왔습니다.
EAGER, LAZY는 쿼리 실행시점의 문제, N+1의 실행 문제는 여전히 존재
N+1 문제 해결하는 대표적인 2가지 방법
1. @Query를 통해 Fetch 쿼리를 Custom
ReviewRepository.java
distinct
를 통해 중복되는 데이터를 제거,join fetch
를 통해Review
와Comment
조인
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
@Query(value = "select distinct r from Review r join fetch r.comments")
List<Review> findAllByFetchJoin();
}
테스트 코드 작성 및 결과
//테스트 코드
@Test
@Transactional
void reviewTest() {
List<Review> reviews = reviewRepository.findAllByFetchJoin();
reviews.forEach(System.out::println);
}
//실행 결과
Hibernate:
select
distinct review0_.id as id1_6_0_,
comments1_.id as id1_4_1_,
review0_.created_at as created_2_6_0_,
review0_.updated_at as updated_3_6_0_,
review0_.book_id as book_id7_6_0_,
review0_.content as content4_6_0_,
review0_.score as score5_6_0_,
review0_.title as title6_6_0_,
review0_.user_id as user_id8_6_0_,
comments1_.created_at as created_2_4_1_,
comments1_.updated_at as updated_3_4_1_,
comments1_.comment as comment4_4_1_,
comments1_.review_id as review_i5_4_1_,
comments1_.review_id as review_i5_4_0__,
comments1_.id as id1_4_0__
from
review review0_
inner join
comment comments1_
on review0_.id=comments1_.review_id
Review(super=BaseEntity(createdAt=2021-08-16T23:20:30.947191, updatedAt=2021-08-16T23:20:30.947191), id=1, title=내 인생을 바꾼 책, content=좋았어요, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2021-08-16T23:20:30.962623, updatedAt=2021-08-16T23:20:30.962623), Id=1, comment=좋았어요), Comment(super=BaseEntity(createdAt=2021-08-16T23:20:30.970733, updatedAt=2021-08-16T23:20:30.970733), Id=2, comment=별로 였습니다.)])
Review(super=BaseEntity(createdAt=2021-08-16T23:20:30.954070, updatedAt=2021-08-16T23:20:30.954070), id=2, title=너무 진도가 빨라요, content=조금 별로였어요, score=3.0, comments=[Comment(super=BaseEntity(createdAt=2021-08-16T23:20:30.981602, updatedAt=2021-08-16T23:20:30.981602), Id=3, comment=그냥 그랬습니다.)])
2. @EntityGraph 사용
- Spring JPA 2.1버전 이후 부터 사용 가능
ReviewRepository.java
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
...
//findAllByEntityGraph(), findAll() 모두 동일한 결과
@EntityGraph(attributePaths = "comments")
@Query("select r from Review r")
List<Review> findAllByEntityGraph();
or
@EntityGraph(attributePaths = "comments")
List<Review> findAll();
}
테스트 코드 작성 및 결과
//테스트 코드
@Test
@Transactional
void reviewTest() {
//findAllByEntityGraph(), findAll() 모두 동일한 결과
List<Review> reviews = reviewRepository.findAllByEntityGraph();
or
List<Review> reviews = reviewRepository.findAll();
reviews.forEach(System.out::println);
}
//실행결과
Hibernate:
select
review0_.id as id1_6_0_,
comments1_.id as id1_4_1_,
review0_.created_at as created_2_6_0_,
review0_.updated_at as updated_3_6_0_,
review0_.book_id as book_id7_6_0_,
review0_.content as content4_6_0_,
review0_.score as score5_6_0_,
review0_.title as title6_6_0_,
review0_.user_id as user_id8_6_0_,
comments1_.created_at as created_2_4_1_,
comments1_.updated_at as updated_3_4_1_,
comments1_.comment as comment4_4_1_,
comments1_.review_id as review_i5_4_1_,
comments1_.review_id as review_i5_4_0__,
comments1_.id as id1_4_0__
from
review review0_
left outer join
comment comments1_
on review0_.id=comments1_.review_id
Review(super=BaseEntity(createdAt=2021-08-16T23:25:42.237550, updatedAt=2021-08-16T23:25:42.237550), id=1, title=내 인생을 바꾼 책, content=좋았어요, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2021-08-16T23:25:42.248465, updatedAt=2021-08-16T23:25:42.248465), Id=1, comment=좋았어요), Comment(super=BaseEntity(createdAt=2021-08-16T23:25:42.255866, updatedAt=2021-08-16T23:25:42.255866), Id=2, comment=별로 였습니다.)])
Review(super=BaseEntity(createdAt=2021-08-16T23:25:42.243117, updatedAt=2021-08-16T23:25:42.243117), id=2, title=너무 진도가 빨라요, content=조금 별로였어요, score=3.0, comments=[Comment(super=BaseEntity(createdAt=2021-08-16T23:25:42.260489, updatedAt=2021-08-16T23:25:42.260489), Id=3, comment=그냥 그랬습니다.)])
2. 영속성 컨텍스트 발생 이슈
- JPA에서 save()를 하면 데이터베이스에서 저장하기 전 영속성 컨텍스트 캐쉬에 저장
UserRepositoryTest.java
@Test
void embedTest(){
...
User user1 = new User();
user1.setName("joshua");
user1.setHomeAddress(null);
user1.setCompanyAddress(null);
userRepository.save(user1);
User user2 = new User();
user2.setName("jordan");
user2.setHomeAddress(new Address());
user2.setCompanyAddress(new Address());
userRepository.save(user2);
entityManager.clear(); //영속성 컨텍스트 초기화 (준영속)
userRepository.findAll().forEach(System.out::println);
userRepository.findAllRowRecords().forEach(a -> System.out.println(a.values()));
}
- 영속성 컨텍스트 캐쉬에는
Address
가null인 user1
과빈 객체를 가진 user2
모두 존재entityManager.clear()
를 통해서 캐쉬를 지우고 새로 엔티티를 로딩해서 데이터 확인
영속성 컨텍스트 불일치 예시1 (객체 주소 참조)
테스트 코드 성공
@Test
void embedTest(){
...
//entityManager.clear();
assertAll(
() -> assertNull(userRepository.findById(7L).get().getHomeAddress()),
() -> assertEquals(userRepository.findById(8L).get().getHomeAddress().getClass(), Address.class)
);
}
테스트 코드 실패
@Test
void embedTest(){
...
entityManager.clear();
assertAll(
() -> assertNull(userRepository.findById(7L).get().getHomeAddress()),
() -> assertEquals(userRepository.findById(8L).get().getHomeAddress().getClass(), Address.class)
);
}
//실행결과
Multiple Failures (1 failure)
java.lang.NullPointerException: <no message>
...
영속성 컨텍스트 불일치 예시2 (컬럼 type)
- 시간이 불일치하는 현상 발생
- 영속성 컨텍스트 캐쉬는 ms까지 표시
데이터베이스는 초까지 표시 (
@Column(columnDefinition = "datetime"
)
EntityManager.clear()
를 하면 데이터베이스에 값을 조회함으로 초까지만 표시
- 영속성 컨텍스트 캐쉬는 ms까지 표시
데이터베이스는 초까지 표시 (
Comment.java
-commentedAt
추가
...
public class Comment extends BaseEntity{
...
@Column(columnDefinition = "datetime")
private LocalDateTime commentedAt;
}
CommentRepository.java 생성
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
}
CommentRepositoryTest.java
@Test
@Transactional
void commentTest() {
Comment comment = commentRepository.findById(3L).get();
comment.setCommentedAt(LocalDateTime.now());
commentRepository.saveAndFlush(comment);
entityManager.clear(); //clear 여부로 결과가 다름 (주석여부로 테스트)
System.out.println(commentRepository.findById(3L).get());
}
//실행 결과
//entityManager.clear()가 있는 경우 (데이터베이스 조회)
Comment(super=BaseEntity(createdAt=2021-08-18T21:45:31.205062, updatedAt=2021-08-18T21:45:31.907460), Id=3, comment=그냥 그랬습니다., commentedAt=2021-08-18T21:45:32)
//entityManager.clear()가 없는 경우 (영속성 컨텍스트 캐쉬 조회)
Comment(super=BaseEntity(createdAt=2021-08-18T21:49:13.881998, updatedAt=2021-08-18T21:49:14.463898700), Id=3, comment=그냥 그랬습니다., commentedAt=2021-08-18T21:49:14.402901700)
영속성 컨텍스트 불일치 예시3 (default 값)
- JPA에선 기본적으로 모든 컬럼을
insert
/update
처리- 등록 / 수정할 컬럼만 저장하기 위해
@DynamicInsert
/@DynamicUpdate
사용변경 전
//CommentRepositoryTest.java @Test @Transactional void commentTest() { Comment comment = new Comment(); comment.setComment("별로에요"); commentRepository.saveAndFlush(comment); } //실행 결과 Hibernate: insert into comment (created_at, updated_at, comment, commented_at, review_id) values (?, ?, ?, ?, ?) ...
변경 후
//Comment.java @DynamicInsert public class Comment extends BaseEntity{ ... } //CommentRepositoryTest.java @Test @Transactional void commentTest() { Comment comment = new Comment(); comment.setComment("별로에요"); commentRepository.saveAndFlush(comment); } //실행 결과 Hibernate: insert into comment (created_at, updated_at, comment) values (?, ?, ?) ...
- 등록 / 수정할 컬럼만 저장하기 위해
- 엔티티 속성에
@Column.columnDefinition
로default
설정 (값이 없으면default
저장)@DynamicInsert public class Comment extends BaseEntity{ ... @Column(columnDefinition = "datetime(6) default now(6)") private LocalDateTime commentedAt; }
- 객체와 데이터베이스 사이에 차이가 발생(객체는 null, 데이터베이스는 default값)
- 예) Entity는
commentedAt=null
, 데이터베이스는commentedAt=2021-08-18T22:16:47.998390
테스트 및 실행결과
@Test @Transactional void commentTest() { Comment comment = new Comment(); comment.setComment("별로에요"); commentRepository.saveAndFlush(comment); entityManager.clear(); System.out.println(comment); commentRepository.findAll().forEach(System.out::println); } //실행 결과 Hibernate: insert into comment (created_at, updated_at, comment) values (?, ?, ?) Comment(super=BaseEntity(createdAt=2021-08-18T22:16:47.921105500, updatedAt=2021-08-18T22:16:47.921105500), Id=4, comment=별로에요, commentedAt=null) ... Comment(super=BaseEntity(createdAt=2021-08-18T22:16:47.494533, updatedAt=2021-08-18T22:16:47.494533), Id=1, comment=좋았어요, commentedAt=2021-08-18T22:16:47.494533) Comment(super=BaseEntity(createdAt=2021-08-18T22:16:47.501291, updatedAt=2021-08-18T22:16:47.501291), Id=2, comment=별로 였습니다., commentedAt=2021-08-18T22:16:47.501291) Comment(super=BaseEntity(createdAt=2021-08-18T22:16:47.505398, updatedAt=2021-08-18T22:16:47.505398), Id=3, comment=그냥 그랬습니다., commentedAt=2021-08-18T22:16:47.505398) Comment(super=BaseEntity(createdAt=2021-08-18T22:16:47.921106, updatedAt=2021-08-18T22:16:47.921106), Id=4, comment=별로에요, commentedAt=2021-08-18T22:16:47.998390)
- 예) Entity는
3. DirtyCheck 성능 이슈
- 영속석 컨텍스트는
@Transactional
안에 있는 로직을 관리- 영속화 된 엔티티에서 수정이 발생하면
save()
가 없어도 수정문 실행
@Transactional
을 제거하면 그 안에 영속성 컨텍스트를 관리하지 않으므로 수정문 실행X
- 영속화 된 엔티티에서 수정이 발생하면
//CommentService.java
@Transactional
public void updateSomething() {
List<Comment> comments = commentRepository.findAll();
for (Comment comment : comments){
comment.setComment("별로에요");
// commentRepository.save(comment);
}
}
//CommentServiceTest.java
@Test
void commentTest() {
commentService.init();
commentService.updateSomething();
}
//실행결과
//조회 후 수정쿼리 실행
...
Hibernate:
update
comment
set
updated_at=?,
comment=?
where
id=?
Dirty Check
- 영속성 컨텍스트의 변경 감지를 통해 예상치 못한 수정이 발생 (
save()
없는데 수정문 실행)
- 성능적인 이슈 발생
- 트랜잭션 내에서 조회를 한 엔티티에 대해선 Dirty Check를 발생 (대용량 데이터 경우 위험)
- 성능적인 이슈 해결 방법
@Transactional(readOnly = true)
를 사용,Flush Mode
가MANUAL
로 설정 (default Auto
)
Flush
가 Auto로 발생하지 않으므로Dirty Check
가 생략
//CommentService.java @Transactional(readOnly = true) public void updateSomething() { List<Comment> comments = commentRepository.findAll(); for (Comment comment : comments){ comment.setComment("별로에요"); // commentRepository.save(comment); } } //CommentServiceTest.java @Test void commentTest() { commentService.init(); commentService.updateSomething(); } //실행결과 //조회 쿼리만 실행 select comment0_.id as id1_4_, comment0_.created_at as created_2_4_, comment0_.updated_at as updated_3_4_, comment0_.comment as comment4_4_, comment0_.commented_at as commente5_4_, comment0_.review_id as review_i6_4_ from comment comment0_
'백엔드 > JPA' 카테고리의 다른 글
[ JPA ] 9. 임베디드 타입 활용 (0) | 2021.08.18 |
---|---|
[ JPA ] 8. 커스텀 쿼리 사용 (0) | 2021.08.18 |
[ JPA ] 7. 영속성 전이 (Cascade) (0) | 2021.08.12 |
[ JPA ] 6-4. 트랜잭션 매니저 (TransactionManager) (0) | 2021.08.08 |
[ JPA ] 6-3. Entity 생명주기 (1) | 2021.08.08 |