์ํ์์ ๋ํด์ ๊ถ๊ธํ๋ค๋ฉด ์๋๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์!
โฐ OneTime ์๋น์ค ๋ฐ๋ก๊ฐ๊ธฐ
๐ OneTime ์๊ฐ๊ธ
๐ง๐ปโ๐ป GitHub
๐ธ Instagram
์ฃผ๋ณ์์ OneTime์ ์ฌ์ฉํ๋ ์น๊ตฌ๋ค์ด ์์ฆ ๋ค์ด ์ํ์์ด ๋๋ ค์ก๋ค
๋ ๋ง์ ๋ง์ด ํ๋ค ๐ฅฒ
์ฒ๋ฆฌ ์๋๋ฅผ ๋ณด๋ ์ด๋ ์๋ฒ์์์ ๋ฌธ์ ๋ผ๊ณ ์๊ฐ์ด ๋ค์๊ณ ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ DB ์ฑ๋ฅ ์ต์ ํ๋ฅผ ํด๋ณด๊ธฐ๋ก ๊ฒฐ์ ํ๋ค.
์ด๋ฒ ๊ธ์ ์ด์ ๋ํ ๋ด์ฉ์ผ๋ก ์ด์ด์ง๋ค.
์ด์ ์ ๋ก๊น ์ ๊ฐ์ ํ๋ฉด์ ๊ฐ API์ ๋ํ ์ฒ๋ฆฌ ์๋๋ฅผ ์ธก์ ํ ์ ์๊ฒ ๋์๋ค.
โ
[GET] {์๋ํฌ์ธํธ} request completed - 7786ms | status=200
์ํ์์์ ํน์ ์ด๋ฒคํธ์ ๋ค์ด๊ฐ๊ฒ ๋๋ฉด์, ์ฐธ์ฌํ ๋ชจ๋ ์ฐธ์ฌ์๋ค์ ๋ฑ๋ก ์ค์ผ์ค์ ๋ถ๋ฌ์ค๊ฒ ๋๋ค.
์๋ ์ฒด๊ฐ์ ๋ก๋ฉ์ด ๊ฐ์ฅ ์ค๋ ๊ฑธ๋ ธ๋ ์ด๋ฒคํธ๋ก, 7786ms
์ฆ 7์ด ์ด์์ ์ฒ๋ฆฌ ์๋๊ฐ ๊ฑธ๋ฆฐ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ด๋ ์ฌ์ฉ์ฑ์ ์์ด์ ์ฌ๊ฐํ ๋ฌธ์ ๊ฐ ๋๋ค. ์ ์ ๊ฐ ์ด๋ฒคํธ์ ์
์ฅํ ๋ 7์ด ์ด์์ ๊ธฐ๋ค๋ ค์ผํ๊ธฐ ๋๋ฌธ์ด๋ค.
์ฑ๋ฅ ์ธก์ ํด์
Grafana K6
๋ฅผ ์ฌ์ฉํ์๋ค.
๋ํ ์๋์ ์กฐ๊ฑด์ ๊ณ ์ ์ผ๋ก ๋์ด ์ธก์ ํ์๋ค.
1) 20๋ช ์ ๋์ ์ฌ์ฉ์๊ฐ ํธ์ถ :
vus: 20
2) 50๋ฒ ํธ์ถ ์ ์ข ๋ฃ :iterations: 50
์ ์ฒด ์ค์ผ์ค ์กฐํ๋ ์ฒซ ์ ์ฅ ์ 1๋ฒ๋ง ํธ์ถ ๋๊ธฐ์ 50๋ฒ๋ง ํธ์ถํด๋ณด๋ ๊ฒ์ผ๋ก ์ ํํ์๋ค.
์ด๊ธฐ์๋ ํ๊ท ์๋ต ์๊ฐ์ด 18.38s
๋ผ๋ ๋งค์ฐ ์ข์ง ์์ ์ฑ๋ฅ์ ๋ณด์๋ค.
๋๋ฌธ์ ํด๋น API๊ฐ ๊ฐ์ฅ ๋จผ์ ํด๊ฒฐํด์ผ ํ ๋ถ๋ถ์ด๋ผ๊ณ ์๊ฐ์ด ๋ค์๊ณ , ์ฐ์ ๋ฌธ์ ๋ฅผ ํ์ ํด๋ณด์๋ค.
2025-05-21T19:00:06.192+09:00 DEBUG 49903 --- [nio-8090-exec-1] org.hibernate.SQL :
select
s1_0.schedules_id,
s1_0.created_date,
s1_0.date,
s1_0.day,
s1_0.events_id,
s1_0.time,
s1_0.updated_date
from
schedules s1_0
where
s1_0.schedules_id=?
Hibernate:
select
s1_0.schedules_id,
s1_0.created_date,
s1_0.date,
s1_0.day,
s1_0.events_id,
s1_0.time,
s1_0.updated_date
from
schedules s1_0
where
s1_0.schedules_id=?
2025-05-21T19:00:06.192+09:00 TRACE 49903 --- [nio-8090-exec-1] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [387712]
2025-05-21T19:00:06.199+09:00 DEBUG 49903 --- [nio-8090-exec-1] org.hibernate.SQL :
select
s1_0.schedules_id,
s1_0.created_date,
s1_0.date,
s1_0.day,
s1_0.events_id,
s1_0.time,
s1_0.updated_date
from
schedules s1_0
where
s1_0.schedules_id=?
Hibernate:
select
s1_0.schedules_id,
s1_0.created_date,
s1_0.date,
s1_0.day,
s1_0.events_id,
s1_0.time,
s1_0.updated_date
from
schedules s1_0
where
s1_0.schedules_id=?
์ ๋ก๊ทธ๋ฅผ ๋ณด๋ฉด, schedules ํ
์ด๋ธ์์ ๋์ผํ ํจํด์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ๋ณต์ ์ผ๋ก ๋ฐ์ํ๊ณ ์์์ ์ ์ ์๋ค.
์ด๋ N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ์์ ๋ํ๋ธ๋ค.
Map<String, List<Selection>> groupedSelectionsByDate = member.getSelections().stream()
.filter(s -> s.getSchedule() != null && s.getSchedule().getDate() != null)
.collect(Collectors.groupingBy(
s -> s.getSchedule().getDate(),
LinkedHashMap::new,
Collectors.toList()
));
member.getSelections()
ํธ์ถ ์์๋ ์์ง ๋ฐ์ดํฐ๋ฒ ์ด์ค ์กฐํ๊ฐ ์ผ์ด๋์ง ์๋๋ค. ์ด ์์ ์์๋ JPA์ Lazy ํ๋ก์ ๊ฐ์ฒด๋ง ๋ฐํ๋๋ค.s.getSchedule()
์ ํธ์ถํ๊ฒ ๋๋ฉด, Hibernate๋ ๊ฐ Selection์ ๋ํด Schedule์ ๊ฐ๋ณ ์ฟผ๋ฆฌ๋ก ์กฐํํ๊ฒ ๋๋ค.N+1 ๋ฌธ์
๊ฐ ๋ฐ์ํ๋ค. @Query("""
SELECT s FROM Selection s
JOIN FETCH s.schedule sc
WHERE s.member = :member
""")
List<Selection> findAllByMemberWithSchedule(@Param("member") Member member);
@Query("""
SELECT s FROM Selection s
JOIN FETCH s.schedule sc
JOIN FETCH sc.event
WHERE s.user = :user
""")
List<Selection> findAllByUserWithScheduleAndEvent(@Param("user") User user);
๊ฐ๊ฐ ๋ฉค๋ฒ์ ์ ์ ์ ๋ํด์ selections
๊ณผ ์ฐ๊ด๋ schedules
ํ
์ด๋ธ์ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ํจ๊ป ๊ฐ์ ธ์ค๊ธฐ ์ํ fetch join ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ์๋ค.
์ ์ ์ ๊ฒฝ์ฐ์๋ events
ํ
์ด๋ธ๋ ํ์ํ๊ธฐ ๋๋ฌธ์ ์ด ๋ํ ํจ๊ป ๊ฐ์ ธ์ค๋๋ก ํ์๋ค.
select
s1_0.selections_id,
...
s2_0.schedules_id,
...
e1_0.events_id,
...
from
selections s1_0
join
schedules s2_0
on s2_0.schedules_id = s1_0.schedules_id
join
events e1_0
on e1_0.events_id = s2_0.events_id
where
s1_0.users_id = ?
์ฟผ๋ฆฌ ๋ด์ญ์ ๋ณด๋ฉด, ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก selections / schedules / events
ํ
์ด๋ธ์ ๋ชจ๋ ๊ฐ์ ธ์ค๊ณ ์๋ค.
๋๋๊ฒ๋ N+1๋ฌธ์ ๋ง ํด๊ฒฐํ์ ํ๊ท ์๋ต ์๊ฐ์ด 0.35s
๋ก ๊ฐ์ ๋์๋ค. ๋งค๋ฒ ์กฐ๊ธ์ฉ ์ฐจ์ด๊ฐ ์์ง๋ง ํ๊ท ์ ์ผ๋ก 0.3 ~ 0.5์ด
์ ๋๋ง ์์๋์๋ค.
์ฌ๊ธฐ์ ๊ทธ์น์ง ์๊ณ ๋์ฑ ๊ฐ์ ํ ์ ์๋๋ก ๋ค๋ฅธ ๋ฐฉ์๋ค๋ ์ ์ฉํด ๋ณด๊ธฐ๋ก ๊ฒฐ์ ํ๋ค.
DB ์ธ๋ฑ์ค์ ๋ํด์๋ ์ต๊ทผ์ ๊ณต๋ถ๋ฅผ ํ๊ธฐ ๋๋ฌธ์, ์ง๊ธ๊น์ง ์ ์ฉ์ ํด ๋ณธ ์ ์ ์์๋ค.
ํ์ง๋ง ๊ณต๋ถ๋ฅผ ํ๊ณ ๋๋, ์ ์ธ๋ฑ์ค๋ฅผ ๊ฑธ์ด์ผ ํ๊ณ & ํ์ฌ DB์์ ์ด๋ ๋ถ๋ถ์ ๊ฑธ๋ฉด ์ข์์ง ์ด๋์ ๋ ๋ ์ฌ๋ฆด ์๊ฐ ์์๋ค.
ํ์ฌ selections
ํ
์ด๋ธ์ ์ ์ ์ ๋ฉค๋ฒ์ ์ค์ผ์ค ์ ํ ๋ด์ญ์ ์ ์ฅํ๋ ์กฐํ ์ค์ฌ ํ
์ด๋ธ๋ก ์ฌ์ฉ๋๊ณ ์๋ค.
INSERT๋ ๋ฐ์ํ์ง๋ง, UPDATE๋ DELETE๋ ๊ฑฐ์ ์์ผ๋ฉฐ, API ์์ฒญ ํ๋ฆ์ ๋งค์ฐ ๋น๋ฒํ ์กฐํ(read ์ฐ์ฐ) ๊ฐ ๋ฐ์ํ๋ค.
๋ฐ๋ผ์, users_id
, members_id
, schedules_id
์ปฌ๋ผ์ ์ธ๋ฑ์ค๋ฅผ ๊ฑธ๋ฉด ์กฐํ ์ฑ๋ฅ์ ํ๊ธฐ์ ์ผ๋ก ํฅ์์ํฌ ์ ์๊ณ , ์ฐ๊ธฐ ๋ถํ๊ฐ ๊ฑฐ์ ์๊ธฐ ๋๋ฌธ์ ์ธ๋ฑ์ค ์ค๋ฒํค๋๋ ๋ฌด์ํ ์ ์๋ ์์ค์ด๋ผ๊ณ ํ๋จ๋์๋ค.
CREATE INDEX idx_selections_users_id ON selections(users_id);
CREATE INDEX idx_selections_members_id ON selections(members_id);
CREATE INDEX idx_selections_schedules_id ON selections(schedules_id);
์ DDL์ ํตํด์ ์ธ๋ฑ์ค๋ฅผ ์ง์ ๊ฑธ์ด์ค ์๊ฐ ์๋ค.
๊ฑธ๊ธฐ ์ ์ ์๋ ๋ช
๋ น์ด๋ฅผ ํตํด์ ํ์ฌ ์ธ๋ฑ์ค๋ฅผ ํ์ธํด ๋ณด์๋ค.
SHOW INDEX FROM selections;
๊ทธ๋ฐ๋ฐ PK ์ธ์ ๋ชจ๋ FK๋ค์๋ ์ด๋ฏธ ์ธ๋ฑ์ค๊ฐ ๊ฑธ๋ ค์๋ ๊ฒ์ ์ ์ ์์๋ค.
์ด์ ๋ํด ์ฐพ์๋ณด๋ MySQL์ FK์ ๋ํด์๋ ์๋์ผ๋ก ์ธ๋ฑ์ค๋ฅผ ์์ฑํด ์ค๋ค๊ณ ํ๋ค.
์ฐธ๊ณ ๋ธ๋ก๊ทธ
์๋ค์ํผ ์ธ๋ฑ์ค๋ฅผ ์ ์ ํ์ง ์๊ฒ ๊ฑธ๊ฒ ๋๋ฉด ์คํ๋ ค ์ฑ๋ฅ์ ์ ํ์ํฌ ์๊ฐ ์๋ค. FK๋ฅผ ๋ฌด๋ถ๋ณํ๊ฒ ์ฌ์ฉํจ์ผ๋ก์จ ์๋์น ์๊ฒ ์ด๋ฌํ ๊ฒฝ์ฐ๊ฐ ๋ฐ์ํ ์ ์์ ๊ฒ ๊ฐ๋ค.
๐ง๐ปโ๐ป FK๋ฅผ ์ฐ์ง ์์์ผ ํ๋ค๋ ์๊ฒฌ๋ ๋ง์ด ์กด์ฌํ๋ค. ์๋ง ์ด๋ฌํ ๋ถ๋ถ๋ค๊ณผ ๋๋ถ์ด ์ ์ฝ์กฐ๊ฑด์ผ๋ก ์ธํด์ ์ ์ดํ๊ธฐ ์ด๋ ค์ด ์ํฉ์ด ๋ฐ์ํ๋ ๊ฒ ๋๋ฌธ์ธ ๋ฏ ํ๋ค.
๋๋ ์์ง๊น์ง๋ FK๋ฅผ ๊ณ์ํด์ ์ฌ์ฉํ๊ณ ์๋๋ฐ, ์ถํ ๊ณต๋ถ๋ฅผ ํ ํ ๋ฐฉํฅ์ฑ์ ์ ํด๋ด์ผ๊ฒ ๋ค!
์ค์ผ์ค ํ
์ด๋ธ์ ๊ฒฝ์ฐ์๋ ์ด๋ฒคํธ๊ฐ ์์ฑ๋จ์ ๋ฐ๋ผ์, INSERT ์์
์ด ์ด๋ฃจ์ด์ง๋ค.
์ดํ ์ด๋ฒคํธ๋ฅผ ์์ ํจ์ ๋ฐ๋ผ์ INSERT or DELETE ์์
์ด ๋ฐ์ํ ์ฌ์ง๊ฐ ์์ง๋ง, SELECT์ ๋นํด ๋น๋๋ ๋งค์ฐ ์ ์ ํธ์ด๋ค.
๋๋ฌธ์ FK๋ฅผ ์ ์ธํ 1) date 2) day 3) time
์ ๋์์ผ๋ก ์ธ๋ฑ์ค๋ฅผ ๊ฑธ๊ธฐ๋ก ๊ฒฐ์ ํ๋ค.
CREATE INDEX idx_schedules_date ON schedules(date);
CREATE INDEX idx_schedules_day ON schedules(day);
CREATE INDEX idx_schedules_time ON schedules(time);
ํ์ง๋ง ์ฌ๊ธฐ์ ์ ์๋ฏธํ ๊ฒฐ๊ณผ๋ ์ป์ง ๋ชปํ์๋ค. ๋๋ ๊ทธ ์ด์ ๋ก ํด๋น ์ธ ์ปฌ๋ผ์ ์นด๋๋๋ฆฌํฐ๊ฐ ๋ฎ๊ธฐ ๋๋ฌธ์ด๋ผ๊ณ ํ๋จํ๋ค.
๊ฐ๊ฐ 178, 7, 49๋ก ๊ณ ์ ํ ๋ฐ์ดํฐ๋ค์ ๊ฐ์๊ฐ ์ ์ ํธ์ด๋ฉฐ, ๊ทธ๋ ๊ฒ ๋๋ค๋ฉด DB ์ตํฐ๋ง์ด์ ๊ฐ ์ธ๋ฑ์ค๋ฅผ ์ ํ๋ ๊ฒ์ด ๋ ๋น ๋ฅด๊ฒ ๋๋ฐ?
๋ผ๋ ํ๋จ์ ํ ๊ฐ๋ฅ์ฑ์ด ๋์์ง๋ค.
์ธ๋ฑ์ค๋ฅผ ์์ฑํ๋๋ฐ ์ฌ์ฉํ์ง ์๋๋ค๋ฉด ์ด๋ ์คํ๋ ค ๋ถํ์ํ ์ ์ฅ ๊ณต๊ฐ ์๋ชจ ๋ฐ ์ฐ๊ธฐ ์ฐ์ฐ ์ ์ฑ๋ฅ ์ ํ๋ฅผ ์ผ๊ธฐํ ์๋ ์๋ค.
๊ทธ๋ ๋ค๋ฉด ๋จ์ผ์ด ์๋๋ผ ๋ณตํฉ ์ธ๋ฑ์ค๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
์กฐํ๋ฅผ ํ ๋ date + time / day + time
์ ์กฐํฉ์ผ๋ก ์กฐํํ๋ ๊ฒฝ์ฐ๊ฐ ์๊ธฐ ๋๋ฌธ์ ๋ณตํฉ ์ธ๋ฑ์ค๋ฅผ ๊ฑธ์ด๋ณด์๋ค.
CREATE INDEX idx_schedules_date_time ON schedules(date, time);
CREATE INDEX idx_schedules_day_time ON schedules(day, time);
ํ์ง๋ง ์ฌ๊ธฐ์๋ ์ ์๋ฏธํ ์ฑ๋ฅ ๊ฐ์ ์ ํ์ง๋ ๋ชป ํ์๋ค.
EXPLAIN SELECT * FROM schedules
WHERE date = '2025.05.13' AND time = '09:00';
EXPLAIN SELECT * FROM schedules FORCE INDEX (idx_schedules_date_time)
WHERE date = '2025.05.13' AND time = '09:00';
์์ฒ๋ผ DB ์ตํฐ๋ง์ด์ ์ ํ๋จ vs ๊ฐ์ ์ธ๋ฑ์ค ์ฌ์ฉ ํ
์คํธ๋ ์งํํด๋ณด์๋๋ฐ, date + time
์ ๊ฒฝ์ฐ์๋ ์ธ๋ฑ์ค ์ฌ์ฉ์ด ์ฐ์ธํ ๊ฒฝ์ฐ๊ฐ ์์์ผ๋ ๋์ฒด๋ก ํฐ ์๋ฏธ๋ ์์๋ค.
๐ง๐ปโ๐ป ๋ฏธ๋ฏธํ ํจ๊ณผ ๋๋ฌธ์ ์ธ๋ฑ์ค๋ฅผ ๊ฑธ๊ธฐ์๋, ๊ทธ๋ก ์ธํ ์ฌ์ด๋ ์ดํํธ์ ๋ฆฌ์คํฌ๊ฐ ๋ ํด ๊ฒ์ด๋ผ๋ ์๊ฐ์ด ๋ค์ด schedules ํ ์ด๋ธ์์๋ ์ธ๋ฑ์ค๋ ๊ฑธ์ง ์๊ธฐ๋ก ๊ฒฐ์ ํ์๋ค.
์ง์์ ํ๊ณ๋ฅผ ๋๋ผ๊ณ , ์กฐ๊ธ ๋ ํ์ต์ ํ ํ์ ์ฑ๋ฅ ๊ฐ์ ์ ๋ ํด๋ณด์์ผ๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์ด ์ฌ๊ธฐ์ ๊ธ์ ๋ง๋ฌด๋ฆฌํ๋ ค๊ณ ํ๋ค.
18.32s -> 0.35
์ฆ 98%์ ์ฑ๋ฅ ๊ฐ์ ์ ํ ์ ์์๋ค. ํ์ง๋ง ์ด๋ N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํจ์ผ๋ก์จ ๋น๊ต์ ์ฝ๊ฒ ์ป์ด๋ธ ๊ฒฐ๊ณผ์ด๋ฏ๋ก, ์์ผ๋ก ๋ค๋ฅธ ๋ถ๋ถ์ ๋ ์ ์ฉํด ๋ณด๊ณ ์ถ๋ค๋ ์๊ฐ์ด ๋ ์ปค์ก๋ค.