여러 건을 DB에 삽입해야 할 때
나비 장터에는 이미지들을 업로드하는 상황이 있다.
교환글 하나당 최대 10장 까지 사진을 업로드할 수 있다.
이미지 등록은 다음과 같은 과정으로 이루어진다.
- 이미지를 bucket에 업로드한다.
- 생성된 bucket image 링크를 post 요청으로 서버에 보낸다.
- 서버는 자체 DB에 상세 이미지 링크들을 저장한다.
JPA의 saveAll은 어떻게 구현되어있을까?
한 게시글이 업로드될 때, 다수의 image url도 같이 DB에 저장되어야한다.
이 프로젝트에선 JPA를 사용하고 있기 때문에 다수의 엔티티를 저장할 때 saveAll 메서드를 호출하도록 구현했었다.
{
// ... 생략 ...
List<CardImage> images = cardCreateRequestDTO.images()
.stream()
.map(i -> i.toCardImageEntity(card))
.toList();
cardImageRepository.saveAll(images); // JPA saveAll
// ... 생략 ...
}
JPA의 saveAll 메서드는 객체 리스트를 주면 리스트에 있는 대상들을 DB에 저장해주는 역할을 한다.
그러다 문득 for-loop을 돌려가며 save를 호출할 때와 saveAll을 호출할 때 어떤 차이가 있는지 궁금해져서 saveAll이 어떻게 구현되어있는지 확인해보게 되었다.
// Jpa의 saveAll 구현체
@Transactional
@Override
public <S extends T> List<S> saveAll(Iterable<S> entities) {
Assert.notNull(entities, "Entities must not be null!");
List<S> result = new ArrayList<>();
for (S entity : entities) {
result.add(save(entity)); // loop를 통한 save 호출.
}
return result;
}
내부적으로는 loop를 돌려가며 save 메서드를 호출하는 것을 확인할 수 있었다.
다만, 여기서 주목해야할 포인트는 @Transactional 인데,
만약 비즈니스 로직에서 for-loop으로 save를 호출하게 되면 save가 호출될 때마다 Transaction을 열고 닫는 과정이 추가되겠지만,
saveAll 메서드는 처음 한 번만 Transaction을 열고 작업이 끝난 뒤 닫기 때문에 성능 상 훨씬 이점이 있다는 것을 확인할 수 있다.
saveAll은 bulk insert가 되는 친구인가?
상품을 등록하는 API는 프로젝트에서 자주 호출될 것으로 예상된다. 기왕이면 좀 더 빠를 수록 좋고, saveAll 이라는 이름만 보면 당연히 bulk insert 하는 방식으로 진행될 것 같다. 하지만 실제로는 n번의 insert 쿼리가 따로 날아가는 것을 확인할 수 있다.
결론부터 말하자면, JPA 환경이라면 bulk insert 하는 것은 불가능하다.
- Entity 객체는 ID가 매핑이 되어야한다.
- 하지만, bulk 형태의 insert 쿼리는 모든 Entity의 ID를 알 수가 없다.
- insert된 Entity의 ID를 알기 위해서는 row별로 insert쿼리가 실행될 수밖에 없다.
JPA의 Id 채번 방식
batch성 작업에 불리하다는 것을 설명하기 위해서는 JPA의 채번 방식을 살펴봐야한다.
- IDENTITY 전략
- DB에 id 할당을 위임하는 방식
- em.persist() 수행 시점에 즉시 insert 쿼리를 DB에 전달해서 삽입 후에 id를 얻어옴
- SEQUENCE 전략
- DB Sequence에 id 할당을 위임하는 방식
- em.persist() 수행 전에 DB Sequence 조회해서 id를 얻어옴
- TABLE 전략
- DB에 id를 관리하는 별도의 테이블을 만들어서 관리하는 방식
- SEQUENCE 전략과 동작 원리는 동일함
- id 값 증가를 위해 update 쿼리 수행이 추가됨
- AUTO
- DB 벤더에 따라 알맞은 채번 방식 선택
나비 장터에서는 mysql을 사용중인데, 디폴트 전략은 IDENTITY 전략이다.
- 엔티티 식별을 위해선 id가 필요한데, id를 얻기 위해서는 일단 insert를 진행해야되기 때문에 bulk insert 작업에서는 적합하지 않다.
- mysql에서는 SEQUENCE 방식을 지원하지 않는다.
- TABLE 방식은 테이블 관리에 별도의 리소스가 들어가고, id 증가에 별도의 update 쿼리가 수행된다는 단점이 있다.
위와 같은 이유 때문에 해당 부분에는 JPA를 고수하는 것이 적합하지 않겠다는 판단을 하게 되었다.
bulk insert 작업을 위해서 JDBC를 도입하게 됩니다.
결국 bulk insert 작업을 도입하기 위해 부분적으로 JDBC를 도입하게 되었다.
뭔가 원시적으로 돌아가는 느낌이 들지만, 훨씬 빠르기 때문에 이렇게 적용하게 되었다.
@Repository
@RequiredArgsConstructor
public class CardImageBatchRepository {
private final JdbcTemplate jdbcTemplate;
private static final String sql = "INSERT INTO card_images (created_date, modified_date, image_url, card_id) VALUES (?, ?, ?, ?)";
@Transactional
public void saveAll(List<CardImage> cardImages) {
jdbcTemplate.batchUpdate(
sql,
cardImages,
cardImages.size(),
(PreparedStatement preparedStatement, CardImage cardImage) -> {
preparedStatement.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now()));
preparedStatement.setTimestamp(2, Timestamp.valueOf(LocalDateTime.now()));
preparedStatement.setString(3, cardImage.getImageUrl());
preparedStatement.setLong(4, cardImage.getCard().getCardId());
}
);
}
}
적용 후에 10만 건의 게시글(각 게시글 당 10장의 이미지 링크) 업로드 기준으로 성능 테스트를 진행해보게 되었고 다음과 같은 성능 향상을 얻을 수 있었다.
약 55% 정도의 속도 개선을 만들어냈다..!
오늘의 결론
실버 불릿은 없다
JPA는 DB를 Java 객체로 추상화해서 바라볼 수 있다는 장점이 있지만 객체로 추상화하는 과정으로 인해 성능상의 trade off가 생긴다.
JDBC는 좋은 성능의 쿼리 사용이 가능하지만 그만큼 복잡도가 높아질 수 있다는 trade off가 있다.
Reference
[JPA] 기본 키 생성 전략(IDENTITY, SEQUENCE, TABLE)
자바 ORM 표준 JPA 프로그래밍JPA가 제공하는 DB 기본 키 할당 전략은 직접 할당 방식, 자동 생성 방식 두 가지이다.이 중 직접 할당 방식은 Application에서 기본 키를 직접 할당하는 방식이다.자동 생
velog.io
https://sabarada.tistory.com/195
[JPA] JPA의 AUTO_INCREMENT 테이블에서 다건 insert 시간 비교 - save vs saveAll
안녕하세요. 오늘은 JPA에서 auto_increment 테이블에 bulk insert를 지원하지 않는 이유에 대해서 알아보도록 하겠습니다. 그리고 JPA를 이용해서 다량의 데이터를 넣어보고 각 insert가 완료되기 까지의
sabarada.tistory.com
https://sabarada.tistory.com/220
[JPA] Spring JPA 환경에서 bulk insert를 효율적으로 해보자 - JPA의 한계와 JDBC 활용
안녕하세요. 오늘은 Spring JPA 환경에서 bulk insert를 효율적으로 하는 방법에 대해서 알아보는 시간을 가져보도록 하겠습니다. JPA를 사용하고 ID를 전략을 사용했을 때 JPA의 성능 문제는 이전에 [JPA
sabarada.tistory.com
https://helloworld.kurly.com/blog/bulk-performance-tuning/#%EC%A0%95%EB%A6%AC
BULK 처리 Write에 집중해서 개선해보기
애플리케이션, DB 모두 행복한 BULK 처리
helloworld.kurly.com
'Project > 나비 장터' 카테고리의 다른 글
[나비 장터] 같은 자원을 생성하려고 동시에 시도하는 경우엔 어떻게 해야 돼? (0) | 2024.03.09 |
---|---|
[나비 장터] 조회수 로직 개선기(cache와 write-back 전략) (1) | 2024.03.08 |
[나비 장터] 페이지네이션 어떻게 할래? (offset & cursor) (0) | 2024.03.05 |
[나비 장터] 프로젝트 소개 (0) | 2024.03.05 |