버전
springboot 2.4.9
mysql 8.0.35
ngrinder 3.5.8
배경
공지와 투표는 별개의 도메인이지만
한 화면에서 최근 날짜 순으로 공지와 투표를 같이 보여줘야 하는 요구사항이 있었다.
이때 공지와 투표를 시간순으로 정렬해야 하고
데이터 형식을 하나로 맞춰서 보여줘야 한다는 점에서 쿼리에서 union을 사용했다.
하지만 데이터가 늘어나는 상황에서 2개의 테이블을 합치는 작업은 시간이 많이 걸리고
이는 사용자의 불편함으로 이어지게 된다.
성능 개선 방법 1 : Union에서 Union All로 변경
union은 테이블을 합치면서 중복제거 작업을 하는데 이를 위해 임시테이블을 만든다.
union all을 사용하면 이런 작업을 스킵할 수 있다.
아래는 쿼리를 추출하고
explain 명령어를 통해 optimizer가 실행 계획을 어떻게 수립했는지를 분석한 내용입니다.
# 쿼리문
select
nv.type,
nv.id,
u.profile_img as profileImg,
nv.content,
u.users_id as userId,
nv.created_time as createdAt
from
((select
vote_id as id,
users_id,
title as content,
created_time,
'V' as type
from
Vote v
where
v.room_id=?
and v.delete_status=?)
union
(select
notice_id as id,
users_id,
notice_item as content,
created_time,
'N' as type
from
Notice n
where
n.room_id=?
and n.delete_status=?)) nv
inner join
Users u
on u.users_id = nv.users_id
order by
nv.created_time desc limit ?
아래 2개의 테이블에서 첫 번째는 union을 사용했을 때의 실행계획이고
두 번째는 union all을 사용했을 때의 실행계획이다.
두 번째를 보면 중복제거를 위해 임시 테이블을 만드는 작업을 스킵하는 것을 확인할 수 있다.
하지만 여전히 문제점을 확인할 수 있는 부분이 있다.
table full scan을 사용하고 있고 정렬 작업을 위해 임시 테이블을 사용하는 부분이다.
| table | type | key | extra |
| <derived2> | ALL | null | Using temporary; Using filesort |
| u | eq_ref | PRIMARY | null |
| v | ALL | null | Using where |
| n | ALL | null | Using where |
| <union2,3> | ALL | null | Using temporary |
| table | type | key | extra |
| <derived2> | ALL | null | Using temporary; Using filesort |
| u | eq_ref | PRIMARY | null |
| v | ALL | null | Using where |
| n | ALL | null | Using where |
성능 개선 방법 2 : 조회 테이블 사용
조회용 테이블을 만들어서 사용하면 union을 사용하지 않을 수 있다.
notice 테이블과 vote 테이블의 정보를 합쳐놓은 notice_vote 테이블을 생성하였다.
# 쿼리문
select
noticevote0_.type as col_0_0_,
noticevote0_.notice_vote_id as col_1_0_,
user1_.profile_img as col_2_0_,
noticevote0_.content as col_3_0_,
noticevote0_.users_id as col_4_0_,
noticevote0_.created_time as col_5_0_
from
notice_vote noticevote0_
inner join
users user1_
on (
noticevote0_.users_id=user1_.users_id
)
where
noticevote0_.room_id=?
and noticevote0_.delete_status=? limit ?
조회용 테이블을 사용할 경우 기존 투표, 공지 테이블과 데이터를 동기화하는 작업이 필요하다.
투표, 공지 테이블에서 삽입/수정/삭제 작업이 발생할 경우 동기화가 필요하고
이때 이벤트를 사용해서 비동기로 조회용 테이블의 데이터를 갱신하였다.
// 공지를 생성하는 메서드
@Transactional
public void createNotice(String noticeItem, Long userId, Long roomId) {
roomStore.getRoomEnteredByUser(userId, roomId);
var notice = Notice.builder()
.noticeItem(noticeItem)
.roomId(roomId)
.userId(userId)
.build();
var savedNotice = noticeStore.saveNotice(notice);
publisher.publishEvent(
SaveNoticeVoteEvent.builder()
.id(savedNotice.getId())
.type(NOTICE_TYPE)
.content(noticeItem)
.createdTime(savedNotice.getCreatedTime())
.roomId(roomId)
.userId(userId)
.build()
);
}
// 투표를 생성하는 메서드
@Transactional
public void createVote(String title, List<String> choiceArr, Long userId, Long roomId) {
roomStore.getRoomEnteredByUser(userId, roomId);
var vote = Vote.builder()
.title(title)
.roomId(roomId)
.userId(userId)
.build();
var voteItemList = choiceArr.stream()
.map(VoteItem::new)
.collect(Collectors.toList());
var savedVote = voteStore.saveVote(vote, voteItemList);
publisher.publishEvent(
SaveNoticeVoteEvent.builder()
.content(title)
.createdTime(savedVote.getCreatedTime())
.userId(userId)
.roomId(roomId)
.type(VOTE_TYPE)
.id(savedVote.getId())
.build()
);
}
// 조회용 테이블을 생성하는 메서드
@Async
@TransactionalEventListener
public void saveNoticeVote(SaveNoticeVoteEvent event){
noticeVoteStore.saveNoticeVote(
NoticeVote.builder()
.content(event.getContent())
.noticeVoteId(event.getId())
.createdTime(event.getCreatedTime())
.roomId(event.getRoomId())
.userId(event.getUserId())
.type(event.getType())
.build()
);
}
테스트
300명의 가상유저를 가정하고 15분 동안 투표 생성 API , 공지 생성 API , 투표와 공지 조회 API를 테스트해 봤다.
시간이 흐르면서 데이터가 쌓일수록 union을 사용했을 때보다
조회용 테이블을 사용했을 때 TPS가 더 일정하게 나오는 것을 확인할 수 있다.


'프로젝트 > 집안일 관리 시스템' 카테고리의 다른 글
| [보안 강화] Refresh Token을 사용해보자 (2) | 2025.08.10 |
|---|---|
| [기능 구현] 메모 기능에서 WebSocket을 사용해보자 (0) | 2025.08.05 |
| [성능 개선] Batch 작업을 최적화해보자 (1) | 2024.12.04 |
| [성능 개선] 데이터베이스로 날라가는 쿼리의 수를 줄여보자 (0) | 2024.01.07 |
| [성능 개선] 인덱스를 사용해보자 (0) | 2024.01.06 |