์ํ์์ ๋ํด์ ๊ถ๊ธํ๋ค๋ฉด ์๋๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์!
โฐ OneTime ์๋น์ค ๋ฐ๋ก๊ฐ๊ธฐ
๐ OneTime ์๊ฐ๊ธ
๐ง๐ปโ๐ป GitHub
๐ธ Instagram
์ง๋ ์ฑ๋ฅ ๊ฐ์ ๊ธฐ1์์๋ ์กฐํ ์ฑ๋ฅ์ ๊ฐ์ ํ๊ธฐ ์ํด์, N+1 ๋ฌธ์ ํด๊ฒฐ๊ณผ ์ธ๋ฑ์ค๋ฅผ ์ ์ฉํด ๋ณด์๋ค.
์ด๋ฒ์๋ ์ด๋ฒคํธ ์์ฑ API์ ๋ํ ๊ฐ์ ์ ์ฌ๋ฌ ๋ฐฉ๋ฉด์ผ๋ก ๋์ ํด ๋ณผ ๊ณํ์ด๋ค. ํด๋น ๊ธ์ ์ด์ ๋ํ ๋ด์ฉ์ผ๋ก ์ด์ด์ง๋ค.
๋ณดํต์ ๊ฒฝ์ฐ์๋ ํฌ๊ฒ ๋ณ๋ชฉ์ด ์๊ธฐ๋ ์ผ์ ์์ผ๋, ์์ ๊ฐ์ด ํ ๋ฌ์ ํต์งธ๋ก ๋ฒ์๋ก ์ง์ ํ๋ ๊ฒฝ์ฐ์๋ ์์ฑํ๋ ๋ฐ ๊ฝค ์ค๋ ์๊ฐ์ด ๊ฑธ๋ฆด ์ ์๋ค. ํนํ ์๊ฐ๋ 00:00 ~ 24:00
์ผ๋ก ํ๋ค๋ฉด ๋์ฑ ๊ทธ๋ ๋ค.
์ค์ ๋ก ์์ ๊ฐ์ด ์ด๋ฒคํธ๋ฅผ ๋ง๋ค์ด ์ฌ์ฉํ์๋ ์ ์ ๋ถ๋ค๋ ์๊ธฐ ๋๋ฌธ์, ์ด๋ฒคํธ ์์ฑ ์ฒ๋ฆฌ ์๋ ๊ฐ์ ์ด ํ์ํ๋ค๊ณ ํ๋จ๋์๋ค.
์ฑ๋ฅ ์ธก์ ํด์
Grafana K6
๋ฅผ ์ฌ์ฉํ์๋ค.
๋ํ ์๋์ ์กฐ๊ฑด์ ๊ณ ์ ์ผ๋ก ๋์ด ์ธก์ ํ์๋ค.
1) 5๋ช ์ ๋์ ์ฌ์ฉ์๊ฐ ํธ์ถ :
vus: 5
2) 20๋ฒ ํธ์ถ ์ ์ข ๋ฃ :iterations: 20
3)6์ 1์ผ ~ 6์ 30์ผ / 00:00 ~ 24:00
๋ก ๋ฒ์ ์ง์
์กฐํ์ ๋นํด์ ๋น๋ฒํ๊ฒ ์ด๋ฃจ์ด์ง๋ ๊ฒฝ์ฐ๊ฐ ์๋๊ธฐ๋ ํ๊ณ , ํธ์ถ์ ๋๋ฌด ๋ง์ด ์ก์ผ๋ฉด ํ ์คํธ ์๊ฐ์ด ๊ณผ๋ํ๊ฒ ์์๋์ด์ ์์ ๊ฐ์ด ์ง์ ํ์๋ค.
์ด๊ธฐ์๋ ์์ ๊ฐ์ด ํ๊ท ์๋ต ์๊ฐ์ด 16.56s
๋ก ์ธก์ ๋์๋ค.
StopWatch
Spring์ StopWatch
๋ ์ฌ๋ฌ ์์
์ ์คํ ์๊ฐ์ ์ธก์ ํ์ฌ, ์ด๋ค ์์
์ด ์ฑ๋ฅ ๋ณ๋ชฉ์ ์ ๋ฐํ๋์ง ๋ถ์ํ๋ ๋ฐ ์ ์ฉํ ๋๊ตฌ์ด๋ค.
ํ์ฌ ์ธ์ฆ ์ฌ์ฉ์์ ์ด๋ฒคํธ๋ฅผ ์์ฑํด ์ฃผ๋ createEventForAuthenticatedUser
๋ฉ์๋๋ ์ฌ๋ฌ ์์
(save, QR ์์ฑ, ์ค์ผ์ค ์ ์ฅ)์ ํฌํจํ๊ณ ์๋ค.
์ด๋ฅผ ์ ๋ฐํ๊ฒ ๋ถ์ํ๊ธฐ ์ํด, StopWatch
๋ก ๊ฐ ๋จ๊ณ์ ์์ ์๊ฐ์ ์ธก์ ํด ๋ณด์๋ค.
@Transactional
public CreateEventResponse createEventForAuthenticatedUser(CreateEventRequest createEventRequest, String authorizationHeader) {
StopWatch stopWatch = new StopWatch("CreateEvent");
stopWatch.start("saveEvent");
Event savedEvent = eventRepository.save(createEventRequest.toEntity());
stopWatch.stop();
stopWatch.start("createQrCode");
createAndAddQrCode(savedEvent);
stopWatch.stop();
stopWatch.start("saveParticipation");
User user = jwtUtil.getUserFromHeader(authorizationHeader);
EventParticipation eventParticipation = EventParticipation.builder()
.user(user)
.event(savedEvent)
.eventStatus(EventStatus.CREATOR)
.build();
eventParticipationRepository.save(eventParticipation);
stopWatch.stop();
stopWatch.start("saveSchedules");
validateAndSaveSchedules(savedEvent, createEventRequest);
stopWatch.stop();
log.info("\nโ
[CreateEvent ํ๋กํ์ผ๋ง ๊ฒฐ๊ณผ]\n{}", stopWatch.prettyPrint());
return CreateEventResponse.of(savedEvent);
}
โ
[CreateEvent ํ๋กํ์ผ๋ง ๊ฒฐ๊ณผ]
StopWatch 'CreateEvent': 17.522903625 seconds
---------------------------------------------
Seconds % Task name
---------------------------------------------
00.01018171 00% saveEvent
00.13092308 01% createQrCode
00.03012458 00% saveParticipation
17.35167425 99% saveSchedules
๋ถ์ ๊ฒฐ๊ณผ, ์ด ์์ ์๊ฐ์ ์ฝ 17.5์ด์์ผ๋ฉฐ, ์ด ์ค 99% ์ด์์ด saveSchedules
๊ณผ์ ์์ ๋ฐ์ํ์๋ค.
์ด๋ ์ด๋ฒคํธ ์์ฑ ์, 30๋ถ ๋จ์ ์ค์ผ์ค์ ์์ฑํ๊ณ ์ ์ฅํ๋ ๊ณผ์ ์์ ์ฑ๋ฅ ๋ณ๋ชฉ์ด ๋ฐ์ํ๋ค๋ ์๋ฏธ์ด๊ธฐ์, ์ด๋ฅผ ์ฐ์ ์ ์ผ๋ก ํด๊ฒฐํ๊ณ ์ ํ์๋ค.
48 (30๋ถ ๋จ์ ์ค์ผ์ค) ร 30 (์ผ) ร 20 (์์ฒญ ์)
= 28,800๊ฐ์ Schedule insert
์์์ ๊ฐ์ ํ ๋๋ก๋ผ๋ฉด, 20๋ฒ ํธ์ถ์ ํ์ ๋ ๋ฐ์ํ๋ INSERT ์ฐ์ฐ์ ์ด 28,800๋ฒ์ด ๋๋ค.
saveAll
์ด save
์ ๋นํด ์ฑ๋ฅ์ด ์ข์ง๋ง, ์ด ๋ํ ๊ฒฐ๊ตญ์๋ save ๋ฌธ์ ํ ๋ฒ์ฉ ํธ์ถํ๋ ๊ตฌ์กฐ๊ฐ ๋๋ค.
์ด๋ฅผ ๊ฐ์ ํ๊ธฐ ์ํด์, Bulk Insert
๋ฐฉ์์ ํ์ฉํด๋ณด๊ธฐ๋ก ํ๋ค.
Bulk Insert
๋ ๋ค์์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ๋ฌถ์ด ๋๋์ผ๋ก ์ฝ์
ํ๋ ๋ฐฉ์์ ์๋ฏธํ๋ค.
์๋ฅผ ๋ค์ด, ์ผ๋ฐ์ ์ธ insert๋ ๋ค์๊ณผ ๊ฐ์ด ํ๋์ ๋ฐ์ดํฐ์ ๋ํด ํ๋์ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ค:
insert into schedules (date, time) values ('2025.06.01', '09:00');
insert into schedules (date, time) values ('2025.06.01', '09:30');
insert into schedules (date, time) values ('2025.06.01', '10:00');
...
ํ์ง๋ง Bulk Insert
๋ ๋ค์๊ณผ ๊ฐ์ด ๋ค์์ ๋ฐ์ดํฐ๋ฅผ ํ ์ฟผ๋ฆฌ๋ก ๋ฌถ์ด ์ ์กํจ์ผ๋ก์จ, DB์์ ํต์ ํ์๋ฅผ ์ค์ด๊ณ ์ฑ๋ฅ์ ํฌ๊ฒ ํฅ์์ํจ๋ค:
insert into schedules (date, time) values
('2025.06.01', '09:00'),
('2025.06.01', '09:30'),
('2025.06.01', '10:00');
์ด ๋ฐฉ์์ ํนํ ์์ฒ, ์๋ง ๊ฑด์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํด์ผ ํ๋ ์ํฉ์์ ํจ๊ณผ๊ฐ ํฌ๋ฉฐ, ๋๋ ์ ์ฅ ์์ ์ ์ฑ๋ฅ ๋ณ๋ชฉ์ ํด๊ฒฐํ๋ ์ฃผ์ ์ ๋ต ์ค ํ๋๋ก ํ์ฉ๋๊ณค ํ๋ค.
์ฌ๊ธฐ์ ํ ๊ฐ์ง ์ ์ฝ์ด ์กด์ฌํ๋ค.
๋๋ ๋ณดํต id์ ๋ํด์๋ ์๋์ ๊ฐ์ด IDENTITY ์ ๋ต์ ํ์ฉํด ์ฝ๋๋ฅผ ์์ฑํ๋ค.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
ํ์ง๋ง ํด๋น ์ ๋ต์ ์ฌ์ฉํ๋ค๋ฉด, ์ผ๋ฐ์ ์ผ๋ก๋ Hibernate batch insert
๋ฅผ ์ง์ํ์ง ์๋๋ฐ ๊ทธ ์ด์ ๋ ์๋์ ๊ฐ๋ค.
- IDENTITY๋ DB์์ insert ์์ ์ ID๋ฅผ ์์ฑํจ
- ์ฆ, ๊ฐ row๋ง๋ค insert ํ์ DB์์ auto_increment๋ PK๋ฅผ ๋ฐ์์์ผ ํจ
- Hibernate batch insert๋ฅผ ํ์ฉํ๊ธฐ ์ํด์๋ ๋ชจ๋ ID๋ฅผ ๋ฏธ๋ฆฌ ์์์ผ ํ๋๋ก ๋ฌถ์ ์ ์์
- ํ์ง๋ง IDENTITY๋ insert ํ์์ผ ID๋ฅผ ์ ์ ์์ผ๋ฏ๋ก, ํ๋์ฉ insertํ ์๋ฐ์ ์์
Hibernate์ ์ ์ฝ ์ฌํญ์ ์ฐํํ๊ธฐ ์ํด, Spring์ JdbcTemplate์ ํ์ฉํ ์ง์ Bulk Insert ๋ฐฉ์์ ํ์ฉํด ๋ณด๋ ค๊ณ ํ๋ค.
์ด๋ฅผ ํตํด @GeneratedValue(strategy = IDENTITY)
์ ๋ต์ ๊ทธ๋๋ก ์ ์งํ๋ฉด์๋, insert ์ฟผ๋ฆฌ๋ฅผ ํ๋๋ก ๋ฌถ์ด ๋๋ ์ฝ์
์ ์ํํ ์ ์๋ค.
ScheduleBatchRepository
@Repository
@RequiredArgsConstructor
public class ScheduleBatchRepository {
private final JdbcTemplate jdbcTemplate;
public void insertAll(List<Schedule> schedules) {
String sql = "INSERT INTO schedules (events_id, date, day, time, created_date, updated_date) VALUES (?, ?, ?, ?, ?, ?)";
Timestamp now = Timestamp.valueOf(LocalDateTime.now());
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Schedule schedule = schedules.get(i);
ps.setLong(1, schedule.getEvent().getId());
ps.setString(2, schedule.getDate());
ps.setString(3, schedule.getDay());
ps.setString(4, schedule.getTime());
ps.setTimestamp(5, now);
ps.setTimestamp(6, now);
}
@Override
public int getBatchSize() {
return schedules.size();
}
});
}
}
JDBC ํ
ํ๋ฆฟ์ ํ์ฉํ๋ ScheduleBatchRepository
๋ฅผ ๋ง๋ค์ด ์ค ํ์, ํด๋น ๋ฉ์๋๋ฅผ ์๋น์ค ๋จ์์ saveAll()
๋์ ํธ์ถํ๋๋ก ํ์๋ค.
scheduleRepository.saveAll(schedules); // ๊ธฐ์กด
scheduleBatchRepository.insertAll(schedules); // ๋ณ๊ฒฝ ํ
rewriteBatchedStatements=true
Bulk Insert
๋ฅผ ์ ์ฉํ์์๋ ์ฑ๋ฅ ๊ฐ์ ํจ๊ณผ๊ฐ ๋ํ๋์ง ์์๊ณ , SQL ๋ก๊ทธ๋ ์ถ๋ ฅ๋์ง ์์ ๋ฌธ์ ์ ์์ธ์ ํ์
ํ๊ธฐ ์ด๋ ค์ ๋ค.
์์นญ์ ํด ๋ณธ ๊ฒฐ๊ณผ, MySQL์ ๊ฒฝ์ฐ rewriteBatchedStatements=true
์ต์
์ ๋ช
์์ ์ผ๋ก ์ค์ ํ์ง ์์ผ๋ฉด JDBC ๋๋ผ์ด๋ฒ๊ฐ ์ค์ ๋ก ๋ฐฐ์น ์ฟผ๋ฆฌ๋ฅผ ํ๋์ ์ฟผ๋ฆฌ๋ก ํฉ์น์ง ์๋๋ค๋ ์ฌ์ค์ ์๊ฒ ๋์๋ค.
๋๋ฌธ์ ์๋์ ๊ฐ์ด DB URL์ ์์ ํ์ฌ ์ต์ ์ ์ถ๊ฐํด์ฃผ์๋ค.
jdbc:mysql:// ... &rewriteBatchedStatements=true
Bulk Insert
๋ฅผ ์ ๋๋ก ์ ์ฉํ ๊ฒฐ๊ณผ, ์ฒ๋ฆฌ ์๋๊ฐ 16.56s -> 0.41s
๋ก ํฌ๊ฒ ๊ฐ์ ๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค!
โ
[CreateEvent ํ๋กํ์ผ๋ง ๊ฒฐ๊ณผ]
StopWatch 'CreateEvent': 0.215752875 seconds
--------------------------------------------
Seconds % Task name
--------------------------------------------
0.034693542 16% saveEvent
0.0626085 29% createQrCode
0.027162833 13% saveParticipation
0.091288 42% saveSchedules
๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์๋, saveSchedules
๊ฐ ์ฐจ์งํ๋ ๋น์จ์ด ๋ง์ด ๋ฎ์์ง ๊ฒ์ ๋ณผ ์ ์๋ค.
๐ง๐ปโ๐ป ๋ค์์ผ๋ก ๋์
createQrCode
๋จ๊ณ์ ๋ํ ๊ฐ์ ์ ์งํํด๋ณผ๊น ํ์์ง๋ง, ํด๋น ๋ฉ์๋๋ฅผ ์ฃผ์์ฒ๋ฆฌ ํ ํ ์ธก์ ํ์์ ๋์๋ ์ฒ๋ฆฌ ์๋์ ์ฐจ์ด๊ฐ ๋ฏธ๋ฏธํ์๋ค.
๋๋ฌธ์ ๊ฐ์ฅ ํฐ ๋ณ๋ชฉ ์ง์ ์ด์๋saveSchedules
๋จ๊ณ๋ฅผ ๊ฐ์ ํ ๊ฒ์ ๋ง์กฑํ๋ฉฐ ์ฌ๊ธฐ์ ๋ง๋ฌด๋ฆฌ ํ๊ณ ์ ํ๋ค.
16.56s -> 0.41s (97.5%)
๊ฐ์ ๋์๋ค. Bulk Insert
๋ฅผ ๊ณ ๋ คํด ๋ณด์์ผ๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.Bulk Insert
๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋, rewriteBatchedStatements=true
์ต์
์ ๊ผญ ์ง์ ํด์ฃผ์ด์ผ ํ๋ค.StopWatch
๋ผ๋ ์ฑ๋ฅ ์ธก์ ํด์ ์๊ฒ ๋์๋ค. ์์ผ๋ก๋ ์ธ์ธํ๊ฒ ๋ณ๋ชฉ ์ง์ ์ ์ฐพ์๋ผ ๋ ํ์ฉํด๋ณด์์ผ๊ฒ ๋ค.