public List<AllPostsResponseDto> getPostService(String search) {
List<Post> findPosts = (search == null) ? postDataRepository.findAll() : postDataRepository.findAllByTitleContaining(search);
List<AllPostsResponseDto> postList = new ArrayList<>();
for (Post e : findPosts) {
AllPostsResponseDto allPostsResponseDto = AllPostsResponseDto.builder()
.postId(e.getId())
.title(e.getTitle())
.writer(e.getWriter().getUid())
.createDate(e.getCreateDate())
.imagePreview(e.getImagePreview()).build();
postList.add(allPostsResponseDto);
}
Collections.sort(postList, Comparator.comparing(AllPostsResponseDto::getCreateDate));
return postList;
}
현재 위 코드의 문제점은 writer 부분을 가져올 때 지연로딩으로 가져오기 때문에 N+1 문제가 발생한다. 만약 페치조인을 사용하면 얼마나 성능이 좋아질 지 궁금해서 실험을 해보려고 한다. 일단 엔티티 정보는 아래와 같다.
Post Entity
@Entity
@Getter
public class Post {
@Id
@GeneratedValue
@Column(name = "post_id")
private Long id;
private String title;
@Lob
private String content;
private LocalDateTime createDate;
private LocalDateTime updateDate;
private String imagePreview;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member writer;
}
Member Entity
@Entity
@Getter
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String uid;
private String pwd;
private String name;
private String authority;
@OneToMany(mappedBy = "writer")
private List<Post> posts = new ArrayList<>();
}
페치조인 적용
PostDataRepository
@Repository
public interface PostDataRepository extends JpaRepository<Post, Long> {
List<Post> findAllByTitleContaining(String search);
@Query("SELECT p FROM Post p JOIN FETCH p.writer WHERE :search IS NULL OR p.title LIKE %:search%")
List<Post> findAllWithFetchJoin(String search);
}
public List<AllPostsResponseDto> getPostService(String search) {
Date startDate = new Date();
List<Post> findPosts = (search == null) ? postDataRepository.findAll() : postDataRepository.findAllByTitleContaining(search);
// List<Post> findPosts = postDataRepository.findAllWithFetchJoin(search);
List<AllPostsResponseDto> postList = new ArrayList<>();
for (Post e : findPosts) {
AllPostsResponseDto allPostsResponseDto = AllPostsResponseDto.builder()
.postId(e.getId())
.title(e.getTitle())
.writer(e.getWriter().getUid())
.createDate(e.getCreateDate())
.imagePreview(e.getImagePreview()).build();
postList.add(allPostsResponseDto);
}
Collections.sort(postList, Comparator.comparing(AllPostsResponseDto::getCreateDate));
Date endDate = new Date();
long timeDifference = startDate.getTime() - endDate.getTime();
System.out.println("시간 차이 (밀리초): " + timeDifference + "ms");
return postList;
}
레포지토리에 페치조인을 추가하고 우선 주석을 달고 페치 조인 하기 전 성능을 시간 측정을 통해 테스트 한다. 과연 테스트 결과는??
쿼리 로그
Hibernate:
/* select
generatedAlias0
from
Post as generatedAlias0 */ select
post0_.post_id as post_id1_1_,
post0_.content as content2_1_,
post0_.create_date as create_d3_1_,
post0_.image_preview as image_pr4_1_,
post0_.title as title5_1_,
post0_.update_date as update_d6_1_,
post0_.member_id as member_i7_1_
from
post post0_
Hibernate:
select
member0_.member_id as member_i1_0_0_,
member0_.authority as authorit2_0_0_,
member0_.name as name3_0_0_,
member0_.pwd as pwd4_0_0_,
member0_.uid as uid5_0_0_
from
member member0_
where
member0_.member_id=?
Hibernate:
select
member0_.member_id as member_i1_0_0_,
member0_.authority as authorit2_0_0_,
member0_.name as name3_0_0_,
member0_.pwd as pwd4_0_0_,
member0_.uid as uid5_0_0_
from
member member0_
where
member0_.member_id=?
2023-09-06 22:05:06.511 INFO 8476 --- [nio-8080-exec-3] com.lemonSoju.blog.service.PostService : 시간 차이 (밀리초): 5810ms
6000ms 정도니깐 6초 정도 걸린다.. 생각보다 오래 걸린다. 사용하면서 딱히 불편하다는 생각은 안해봤지만 그래도 성격이 급한 사람들에게는 느리게 보일 것 같다는 생각이 든다. 그리고 지금은 사용자 데이터가 2개 밖에 없어서 총 3개의 쿼리가 날아갔지만 사용자가 1000인경우 전체 글을 불러오는데 사용자에 대한 추가적인 쿼리가 1000개 더 나가서 총 1001개의 쿼리가 날라갈 것이다. 이전에 말했듯이 N+1 문제가 발생한다.
쿼리 로그
Hibernate:
/* SELECT
p
FROM
Post p
JOIN
FETCH p.writer
WHERE
:search IS NULL
OR p.title LIKE :search */ select
post0_.post_id as post_id1_1_0_,
member1_.member_id as member_i1_0_1_,
post0_.content as content2_1_0_,
post0_.create_date as create_d3_1_0_,
post0_.image_preview as image_pr4_1_0_,
post0_.title as title5_1_0_,
post0_.update_date as update_d6_1_0_,
post0_.member_id as member_i7_1_0_,
member1_.authority as authorit2_0_1_,
member1_.name as name3_0_1_,
member1_.pwd as pwd4_0_1_,
member1_.uid as uid5_0_1_
from
post post0_
inner join
member member1_
on post0_.member_id=member1_.member_id
where
? is null
or post0_.title like ?
5600ms 정도니깐 0.4초가 줄어들었다. 시간 차이가 기대만큼은 안 줄었지만 그래도 확실히 줄긴 했다. 그리고 쿼리고 1개 밖에 안나가는 것을 확인할 수 있다.
인덱스 추가
단어를 검색하면 해당 단어를 포함하는 제목을 가진 글의 목록을 출력하는데 title에 인덱싱 기능을 넣으면 속도가 빨리지지 않을까 생각해서 인덱스 기능을 추가하기로 했다.
Post.Entity
@Table(indexes = {@Index(name = "idx_title", columnList = "title")})
public class Post {
...
}
하지만 오히려 성능이 떨어졌다. 찾아보니 ddl-auto 기능을 none으로 설정해놔서 DB에서 직접 인덱싱을 설정해야 한다고 한다.
jpa:
hibernate:
ddl-auto: none
CREATE INDEX idx_title ON Post (title);
위 명령으로 DB에 직접 인덱싱을 설정했지만 검색어에 대한 데이터를 가져오는데 걸린 시간은 100ms 정도여서 시간 측정이 거의 의미가 없었다. 전체 글을 불러오는 시간과 달리 검색어에 대한 데이터를 가져오는 것은 이미 충분히 빨라서 인덱싱을 안해도 될 듯하다.
캐싱
아무래도 문제는 전체 글을 가져오는데 6초 가량에서 현재 5.5초까지 0.5초 정도 줄였지만 아직 너무 느린 것 같아서 스프링에서 제공하는 캐싱 기능을 써보았다.
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("postCacheStore");
}
@Cacheable(value = "postCacheStore", condition = "#search == null")
@CacheEvict(value = "postCacheStore", allEntries = true)
전체 글을 가져오는 메소드 위에 첫번째 어노테이션을 추가하였고 새로운 글을 생성하거나 글을 삭제하거나 수정하는 경우에는 두번째 어노테이션을 추가해서 모든 캐시를 날리는 방식으로 진행했다. 결과는 첫번째 시도에는 이전과 같이 5.5초 정도가 걸렸지만 두번째 시도부터는 애초에 메소드가 실행되지 않으므로 거의 즉각적으로 전체글이 출력된다.
결론은
페치 조인으로 6초 -> 5.5초 정도로 0.5초를 줄였고
인덱싱은 아직까지 큰 의미가 없어서 적용하지 않았고
마지막으로 캐싱기능으로 5.5 -> 0초 정도로 프론트 서버와 네트워크 시간 측정을 제외한 백엔드 로직에서는 즉각적인 response를 넘길 수 있었다.
다음 도전 과제는 아래와 같다.
1. 5.5초 정도 걸리는 첫 로딩 시간 줄이기.
2. 글을 생성하거나 삭제할 때 캐시 데이터 동기화가 가능한지 알아보기.
'웹 개발 > 블로그 만들기 프로젝트' 카테고리의 다른 글
스프링 데이터 JPA - @EntityGraph 사용 및 Page로 받아오기 (0) | 2023.11.19 |
---|---|
이팩티브 자바 따라하기 (0) | 2023.08.11 |
git action을 이용한 자동 배포 (0) | 2023.07.23 |
Jwt 적용하기 (0) | 2023.07.05 |
aws 비용 줄이기 (0) | 2023.07.04 |
댓글