배경
개인 SNS 프로젝트에서 글쓰기 기능과 함께 사진을 여러장 올릴 수 있는 상황입니다.
이미지 첨부X
먼저 이미지 없이 작성했을 때 걸리는 시간과 발생하는 쿼리에 대해 보겠습니다.
이미지 첨부O
사진을 약 8장을 첨부해서 같이 올린다고 가정하겠습니다.
프로젝트 규모 자체가 작고, 서버에 이미지를 업로드 하는 프로젝트지만
이런 사소한 부분 하나하나 고쳐나가고 효율적으로 짜고 싶어서 작성했습니다.
환경
Java 17
Spring 6.0.11 (Spring Boot 3.1.2)
Gradle 8.2.1
Junit 5.9.3, Mockito 5.3.1
JWT
현재 로직 분석
먼저 글쓰기에 대한 로직을 간단하게 보겠습니다.
@Transactional
public PostCreateResponseDto createPost(final PostCreateRequestDto postCreateDto, final Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new MutsaSnsAppException(NOT_FOUND_USER, NOT_FOUND_USER.getMessage()));
// 1. 이미지가 없으면 기본 이미지 설정을 위해 draft 값 true 게시글 저장
if (postCreateDto.getImages() == null || postCreateDto.getImages().isEmpty()) {
Post post = postRepository.save(postCreateDto.toEntity(user, true));
String imageUrl = String.format("/static/%d/%s", 0, "base.png");
imageRepository.save(PostImage.builder()
.post(post)
.image(imageUrl)
.build());
return new PostCreateResponseDto(post);
}
// 2. 이미지가 존재하면 기본 이미지는 false, 게시글 저장
Post post = postRepository.save(postCreateDto.toEntity(user, false));
// 저장된 게시글 ID의 값을 사용해서 images 저장
List<MultipartFile> images = postCreateDto.getImages();
String postDir = String.format("post/%d/", post.getId());
try {
Files.createDirectories(Path.of(postDir));
} catch (IOException e) {
log.error("IOException = {}", e);
throw new MutsaSnsAppException(SERVER_ERROR, SERVER_ERROR.getMessage());
}
for (MultipartFile image : images) {
String postFilename = generatePostFilename(image);
String postPath = postDir + postFilename;
Path path = Path.of(postPath);
try {
image.transferTo(path);
} catch (IOException e) {
log.error("IOException = {}", e);
throw new MutsaSnsAppException(SERVER_ERROR, SERVER_ERROR.getMessage());
}
String imageUrl = String.format("/static/%d/%s", post.getId(), postFilename);
imageRepository.save(PostImage.builder()
.post(post)
.image(imageUrl)
.build());
}
return new PostCreateResponseDto(post);
}
코드가 상당히 지저분하고 보기 좋지 않음을 알 수 있는데... 차차 고쳐나가겠습니다.
이미지를 어떻게 가공하고 처리하는지에 대한 내용은 생략하겠습니다.
image 저장 로직을 먼저 보겠습니다.
1. Dto 안에 담겨있는 List<MultipartFile>로 저장 후,
2. 만든 List를 for문을 돌려 이름과 경로를 뽑아 save를 진행하는 과정입니다.
이러한 과정으로 총 8번 save 로직이 발생하고 데이터의 크기가 커질 수록 더 많은 시간이 걸릴 것을 알 수 있습니다.
그럼 어떻게 하면 이 로직을 줄이고 성능을 올릴 수 있을까요?
대안
JDBC를 직접 사용하여 bulk insert
JPA는 saveAll을 호출하면 안 되기 때문에 JDBC에서 bulk insert를 하는 방법에 대해 알아보겠습니다.
JPA는 왜 saveAll을 사용할 수 없는지 마지막에 링크 남기겠습니다.
PostImage Entity
JdbcRepository ( batch )
PostImageService
정말 쉽고 간단하게 사용할 수 있습니다.
성능을 비교해보겠습니다.
성능 비교
데이터가 8건, 조금이나마 성능 차이가 나는 것을 확인할 수 있습니다.
로직을 보겠습니다.
post와 함께 한 번에 처리되는 것을 볼 수 있습니다.
더 많은 데이터로 비교해보기
이번에는 1000건이 넘는 데이터로 비교해보겠습니다.
bulk insert
save()
bulk insert를 사용했을 때와 사용하지 않았을 때의 성능 차이가 나는 것을 확인할 수 있습니다.
10,000건의 데이터 bulk insert
10,000건의 데이터 save()
확실히 데이터가 많으면 성능 차이를 더 느낄 수 있습니다.
마치며
엄청 큰 성능 개선은 아니였지만 이런 작은 학습과 시도 하나 하나 배워가는게 너무 재미있습니다.
다음에는 이거보다 더 줄일 수 있는 방법을 학습해서 포스팅하겠습니다.
JpaRepository에 saveAll을 사용할 수 없는 이유에 대해서는 링크로 남기겠습니다.
https://cheese10yun.github.io/jpa-batch-insert/
'JPA' 카테고리의 다른 글
JPA: 알람 기능과 댓글 기능, 도메인 로직과 서비스 로직의 트랜잭션 분리 작업 (0) | 2024.06.11 |
---|---|
JPA: 페이징이 필요한 이유와 페이징 성능 개선하기 - No Offset 사용 (0) | 2024.06.05 |
Querydsl: OneToMany 관계에서 Projection DTO 값 List를 어떻게 갖고올까? (0) | 2023.04.01 |
JPA: 게시글 전체 조회 N + 1를 어떻게 처리할까? (0) | 2023.03.06 |