프로젝트 와사비는 WiSoft 연구실의 기술 블로그를 제작하는 프로젝트이다.
요구사항에는 게시글에는 여러 사진이 포함될 수 있다
가 존재하며, 아래와 같이 코드를 작성 할 수 있다.
BoardService
@Transactional
public WriteBoardResponse writeBoard(final WriteBoardRequest request) {
...
final Board board = ... //게시글 생성 코드
boardRepository.save(board); //게시글 저장
final String[] images = request.imageUrls();
if (images != null) {
Arrays.stream(images)
.forEach(image -> {
final BoardImage boardImage
= BoardImage.createBoardImage(image, board);
boardImageRepository.save(boardImage); //게시글 이미지 저장
});
}
return boardMapper.entityToWriteBoardResponse(board);
}
현재 게시글 저장
& 게시글 이미지 저장
로직이 각각 호출되는 상황이다.
→ 이를 하나로 합칠 수는 없을까? ex) 게시글이 저장되면.. 게시글 이미지가 저장되도록..
Board.java : cascade 설정 변경
@Entity
public class Board {
...
@OneToMany(mappedBy = "board", **cascade = CascadeType.PERSIST**)
private Set<BoardImage> boardImages = new HashSet<>();
}
수정된 BoardService
@Transactional
public WriteBoardResponse writeBoard(final WriteBoardRequest request) {
final Board board = ... // 게시글 생성 코드
boardRepository.save(board); //게시글 저장
saveImages(board, request); //이미지 저장
return boardMapper.entityToWriteBoardResponse(board);
}
private void saveImages(final Board board, final WriteBoardRequest request) {
final String[] images = request.imageUrls();
if (images != null) {
Arrays.stream(images) //매핑 후 저장 로직을 호출하지 않음.
.map(image -> BoardImage.createBoardImage(image, board))
.toList();
}
}
CascadeType.PERSIST
로 설정함으로써 게시글이 저장될 때, 연관된 BoardImage도 같이 저장이 된다.
코드는 줄였으나.. 실제 로직을 호출할 때 실행되는 쿼리는 어떨까?
Request Body에는 게시글에 2개의 이미지를 첨부. (”hihi”, “hoho”)
{
"memberId": 1,
"title": "title",
"content": "content",
"tags": [
"hihi"
],
"imageUrls": [
"hihi",
"hoho"
]
}
실제 호출된 쿼리
2023-07-18T18:16:42.744+09:00 DEBUG 13435 --- [io-8080-exec-10] org.hibernate.SQL :
insert
into
board
(content,created_at,member_id,title,updated_at,views,id)
values
(?,?,?,?,?,?,default)
2023-07-18T18:16:42.745+09:00 DEBUG 13435 --- [io-8080-exec-10] org.hibernate.SQL :
insert
into
board_image
(board_id,url,id)
values
(?,?,default)
2023-07-18T18:16:42.746+09:00 DEBUG 13435 --- [io-8080-exec-10] org.hibernate.SQL :
insert
into
board_image
(board_id,url,id)
values
(?,?,default)
쿼리 수를 보면 총 3개로, 게시글 저장 (1개) & 이미지 저장 (2개)
가 날라가고 있다.
JPA Batch Insert를 이용해 쿼리를 하나로 날릴 수는 없을까?
Insert문을 작성할 때, 아래와 같이 여러 값을 한번에 입력하는 방식으로, 하나의 트랜잭션으로 본다.
INSERT INTO payment_back (amount, order_id)
VALUES
(1, 2),
(1, 2),
(1, 2),
(1, 2)
JPA에서 제공하는 Batch Insert는 어떤 방식으로 동작할까?
- JPA의 특징인
쓰기 지연
을 사용해, 저장소에 쿼리를 모아두었다가 한번에 실행!- 쓰기 지연 동작 그림
application.yml
spring:
jpa:
database: mysql
properties:
hibernate.jdbc.batch_size: 50
hibernate.order_inserts: true
hibernate.order_updates: true
hibernate.dialect: org.hibernate.dialect.~~~
hibernate.show_sql: true
datasource:
url: jdbc:~~://localhost:{port}/~~?&rewriteBatchedStatements=true
driver-class-name: com.~~.jdbc.Driver
설정할 값들은 아래와 같다.
batch_size
: 한 번에 트랜잭션에서 다룰 size를 지정order_inserts
: 데이터 일괄 입력시 사용 (default는 false)order_updates
: 데이터 일괄 수정시 사용 (default는 false)rewriteBatchedStatements
: batch true💡 다만 중요한 부분이 있는데, 바로 엔티티의 id 생성 방식
@GeneratedValue
이다.눈치가 빠르다면 알아챘겠지만.. JPA Batch Insert는 쓰기 지연을 사용해 동작한다.
→ 이는 데이터베이스에 저장된 뒤 id가 할당되는IDENTITY
전략을 사용하면 사용할 수 없다는 의미!!
쎄한 느낌은 틀리지 않았다. 현재 와사비에서는 아래와 같이 id 생성 전략을 IDENTITY
로 지정해놓았다.
@Entity
public class Board {
@Id
**@GeneratedValue(strategy = IDENTITY)**
private Long id;
...
}
왜
IDENTITY
전략을 택했는가?
해당 전략의 특징은 주로 PostgreSQL에서 사용되는 전략으로, 데이터베이스에 위임할 수 있는데,
시퀀스보다 간편하다는 느낌에 선택했던 것 같다. (깊이 생각해보지 않은 것 같다.)
그렇다고 성능 향상을 위한 Batch Insert를 위해 id 생성 전략을 바꾸는 게 맞을까?
배보다 배꼽이 큰 경우인지는 않은지.. 팀원들과 논의를 해보아야겠다.
참고자료