Skip to content

[๐ŸŒฑ JPA ๋ฐฉํƒˆ์ถœ ์˜ˆ์•ฝ ๋Œ€๊ธฐ] ์ฝ”๋กœ๊ตฌ(๊ตฌ์ง„) ๋ฏธ์…˜ ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค.#587

Open
korogoo wants to merge 2 commits into
woowacourse:korogoofrom
korogoo:korogoo
Open

[๐ŸŒฑ JPA ๋ฐฉํƒˆ์ถœ ์˜ˆ์•ฝ ๋Œ€๊ธฐ] ์ฝ”๋กœ๊ตฌ(๊ตฌ์ง„) ๋ฏธ์…˜ ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค.#587
korogoo wants to merge 2 commits into
woowacourse:korogoofrom
korogoo:korogoo

Conversation

@korogoo

@korogoo korogoo commented Jun 18, 2026

Copy link
Copy Markdown

์ฒดํฌ ๋ฆฌ์ŠคํŠธ

  • ๋ฏธ์…˜์˜ ํ•„์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ์„ ๋ชจ๋‘ ๊ตฌํ˜„ํ–ˆ๋‚˜์š”?
  • Gradle test๋ฅผ ์‹คํ–‰ํ–ˆ์„ ๋•Œ, ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ํ†ต๊ณผํ–ˆ๋‚˜์š”?
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ •์ƒ์ ์œผ๋กœ ์‹คํ–‰๋˜๋‚˜์š”?

๋ฒ ์ด์Šค ์ฝ”๋“œ ์„ ํƒ ์ฒดํฌ

  • ์ด์ „ ๋ฏธ์…˜์˜ ๋‚ด ์ฝ”๋“œ์—์„œ ์‹œ์ž‘
  • ์ด์ „ ๋ฏธ์…˜์˜ ํŽ˜์–ด์˜ ์ฝ”๋“œ์—์„œ ์‹œ์ž‘

์–ด๋–ค ๋ถ€๋ถ„์— ์ง‘์ค‘ํ•˜์—ฌ ๋ฆฌ๋ทฐํ•ด์•ผ ํ• ๊นŒ์š”?

0๋‹จ๊ณ„ (JPA ์ธํ”„๋ผ ์„ธํŒ…)

NESTED โ†’ REQUIRES_NEW

ํ˜„์žฌ applicationService ๋‹จ์—์„œ Transactional ์„ ๋ถ™์ด๊ณ  ์žˆ๊ณ , ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ž‘ ์†Œํ†ตํ•˜๋Š” service ๋‹จ์—์„œ ํŠธ๋žœ์žญ์…˜์„ ์ „ํŒŒ๋ฐ›๋Š” ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค. WaitingPromotionService๋Š” ์˜ˆ์•ฝ ์ทจ์†Œ ํ›„ ๋Œ€๊ธฐ ์Šน๊ฒฉ์„ ๋‹ด๋‹นํ•˜๋Š”๋ฐ, ์Šน๊ฒฉ ์‹คํŒจ๊ฐ€ ์˜ˆ์•ฝ ์ทจ์†Œ ํŠธ๋žœ์žญ์…˜์„ ๋กค๋ฐฑ์‹œ์ผœ์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
์ด์ „์— NESTED๋ฅผ ์‚ฌ์šฉํ•œ ์ด์œ ๋„ ์ด๊ฑฐ ๋•Œ๋ฌธ์ธ๋ฐ, JpaTransactionManager๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ SavepointManager๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์•„์„œ NestedTransactionNotSupportedException์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

JPA ์—์„œ NESTED ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ์—๋Š” ์„ค์ •์ด ๊นŒ๋‹ค๋กญ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ applicationService ์—์„œ Transactional ์„ ์ œ๊ฑฐํ•˜๊ณ , promotion ํ˜ธ์ถœ ๋ถ€๋ฅผ REQUIRES_NEW๋กœ ๊ต์ฒดํ–ˆ์Šต๋‹ˆ๋‹ค.


1๋‹จ๊ณ„ JPA ์ „ํ™˜

JDBC vs JPA ์ฝ”๋“œ ๋น„๊ตํ•˜๊ธฐ

JDBC์—์„œ๋Š” ๋ฐ˜๋ณต๋˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

  • ๋ชจ๋“  ์ฝ๊ธฐ ๋ฉ”์„œ๋“œ์—์„œ JOIN reservation_time, JOIN theme์„ ์ž‘์„ฑ
  • ResultSet์—์„œ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” mapToDomain() ๋˜๋Š” RowMapper๋ฅผ ํด๋ž˜์Šค๋งˆ๋‹ค ๊ตฌํ˜„
  • Date.valueOf(localDate), rs.getDate(...).toLocalDate() ๊ฐ™์€ ํƒ€์ž… ๋ณ€ํ™˜ ์ฝ”๋“œ

JPA์—์„œ๋Š” @ManyToOne์œผ๋กœ ๊ด€๊ณ„๋ฅผ ํ•œ ๋ฒˆ ์„ ์–ธํ•˜๋ฉด ์ดํ›„ ์กฐํšŒ์—์„œ Hibernate๊ฐ€ JOIN ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฝ๋‹ˆ๋‹ค.
๋˜ํ•œ, ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๋กœ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ถ”์ƒํ™” ๋˜์–ด ์žˆ์–ด ํƒ€์ž… ๋ณ€ํ™˜๋„ ์—†์Šต๋‹ˆ๋‹ค.
๋Œ€์‹  FetchType์— ๋”ฐ๋ผ ๋กœ๋”ฉ ์ „๋žต์„ ์„ ํƒํ•ด์•ผ ํ•˜๊ณ , LAZY๋ฅผ ์“ฐ๋ฉด N+1 ๋ฌธ์ œ๋กœ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด์„œ @EntityGraph๋‚˜ fetch join ๊ฐ™์€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ๊ณ ๋ฏผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


INSERT ๋กœ๊ทธ ๋น„๊ต

๊ฐ™์€ ์˜ˆ์•ฝ ์ƒ์„ฑ ์š”์ฒญ์—์„œ ๋ฐœํ–‰๋˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค.

JDBC:

SELECT id, start_at FROM reservation_time WHERE id = ?
SELECT id, name, description, thumbnail_url FROM theme WHERE id = ?
SELECT COUNT(1) FROM waiting WHERE reservation_date = ? AND time_id = ? AND theme_id = ?
INSERT INTO reservation (name, date, time_id, theme_id) VALUES (?, ?, ?, ?)

JPA:
select rte1_0.id, rte1_0.start_at from reservation_time rte1_0 where rte1_0.id=?
select te1_0.id, te1_0.description, te1_0.name, te1_0.thumbnail_url from theme te1_0 where
te1_0.id=?
select we1_0.id from waiting we1_0
    where we1_0.reservation_date=? and we1_0.time_id=? and we1_0.theme_id=?
    fetch first ? rows only
insert into reservation (name, date, theme_id, time_id, id) values (?, ?, ?, ?, default)

id ์ฒ˜๋ฆฌ ๋ฐฉ์‹

JDBC๋Š” id ์ปฌ๋Ÿผ์„ ์ง์ ‘ insert ํ•˜์ง€ ์•Š๊ณ  AUTO_INCREMENT์— ์œ„์ž„ํ•˜๊ณ , JPA๋Š” id ์ปฌ๋Ÿผ์— default๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

์ค‘๋ณต ์ฒดํฌ ์ฟผ๋ฆฌ ์ฐจ์ด

JDBC์˜ SELECT COUNT(1)์€ ์ „์ฒด๋ฅผ ์„ธ์ง€๋งŒ, JPA existsBy~ ๋ฉ”์„œ๋“œ๋Š” FETCH FIRST 1 ROWS ONLY๋กœ ๋ณ€ํ™˜๋˜์–ด ํ•˜๋‚˜๋ฅผ ์ฐพ์œผ๋ฉด ์ฆ‰์‹œ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค.


๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ์™€ JPA ์—”ํ‹ฐํ‹ฐ ๋ถ„๋ฆฌ

๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ์— JPA ๊ด€๋ จ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•ด JPA ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ณ„๋„๋กœ ๋‘์—ˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๊ฐ์ฒด๊ฐ„ ๋ณ€ํ™˜์„ ์œ„ํ•ด Entity ๋‚ด๋ถ€์— toDomain() / from() ์ •์  ๋ณ€ํ™˜ ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.



2๋‹จ๊ณ„ ๋‚ด ์˜ˆ์•ฝ ๋ชฉ๋ก ์กฐํšŒ

๋ฉ”์„œ๋“œ ์ด๋ฆ„ ์ฟผ๋ฆฌ๋ฅผ JPQL๋กœ ์ „ํ™˜

์ฒ˜์Œ์—๋Š” ๋ฉ”์„œ๋“œ ์ด๋ฆ„ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค ํ–ˆ๊ณ , ํ˜„์žฌ ์‹œ๊ฐ„ ์ดํ›„์˜ ์˜ˆ์•ฝ๋งŒ ํ•„ํ„ฐ๋ง ์ด๋ผ๋Š” ์กฐ๊ฑด์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด date์™€ time.startAt ๋‘ ์ปฌ๋Ÿผ์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฉ”์„œ๋“œ ์ด๋ฆ„์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋”๋‹ˆ ์•„๋ž˜์™€ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค.

findByCustomerNameAndDateAfterOrCustomerNameAndDateAndTime_StartAtAfter(...)

๋ฉ”์„œ๋“œ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ์–ด์„œ ๊ฐ€๋…์„ฑ์ด ๋„ˆ๋ฌด ๋–จ์–ด์ง„๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๊ณ , ํ˜„์žฌ๋Š” JPQL๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

@Query("""
    SELECT r FROM ReservationEntity r
    WHERE r.customerName = :customerName
      AND (r.date > :date OR (r.date = :date AND r.time.startAt > :time))
    """)

๋‹จ, JPQL์€ ๋ฌธ์ž์—ด์ด๋ผ ์ปดํŒŒ์ผ ํƒ€์ž„์— ์˜ค๋ฅ˜๋ฅผ ์žก์„ ์ˆ˜ ์—†๊ณ , ํ•„๋“œ๋ช…์ด ๋ฐ”๋€Œ๋ฉด ๋Ÿฐํƒ€์ž„์—์„œ ํ„ฐ์ง‘๋‹ˆ๋‹ค.
๋˜ํ•œ r.time.startAt์ฒ˜๋Ÿผ ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ์˜ ํ•„๋“œ๋ฅผ WHERE ์กฐ๊ฑด์—์„œ ์ฐธ์กฐํ•˜๋ฉด Hibernate๊ฐ€ ๋ฌต์‹œ์ ์œผ๋กœ INNER JOIN์„ ์ถ”๊ฐ€ํ•˜๋Š”๋ฐ, ์ด JOIN์€ ์ฝ”๋“œ์— ๋ช…์‹œ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์‹ค์ œ ๋™์ž‘ ๋ฐฉ์‹์„ ์•Œ๊ณ ์žˆ์ง€ ์•Š์œผ๋ฉด ์˜๋„์น˜ ์•Š์€ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


@EntityGraph๋กœ N+1 ๋ฐฉ์ง€

findById ์ฟผ๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

select re1_0.id, re1_0.name, re1_0.date,
       t1_0.id, t1_0.description, t1_0.name, t1_0.thumbnail_url, re1_0.theme_id,
       t2_0.id, t2_0.start_at, re1_0.time_id
from reservation re1_0
left join theme t1_0 on t1_0.id=re1_0.theme_id
left join reservation_time t2_0 on t2_0.id=re1_0.time_id
where re1_0.id=?

@EntityGraph(attributePaths = {"time", "theme"})๋ฅผ ์„ ์–ธํ•˜๋ฉด ์ด๋ ‡๊ฒŒ LEFT JOIN์œผ๋กœ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
JDBC์—์„œ findAll์— ๋งค๋ฒˆ JOIN์„ ์ง์ ‘ ์ž‘์„ฑํ•˜๋˜ ๊ฒƒ๊ณผ ๋น„๊ตํ•˜๋ฉด ํ›จ์”ฌ ๊ฐ„๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.



3๋‹จ๊ณ„ ์˜ˆ์•ฝ ๋Œ€๊ธฐ (๋Œ€๊ธฐ ์ˆœ๋ฒˆ)

ROW_NUMBER() OVER (PARTITION BY ...) ์œˆ๋„์šฐ ํ•จ์ˆ˜๋Š” JPQL์ด ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋Œ€๊ธฐ ์ˆœ๋ฒˆ ๊ณ„์‚ฐ์€ native query ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
์ฒ˜์Œ์—” ์„œ๋น„์Šค ๋ ˆ์ด์–ด์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ๋ฒจ๋กœ ๊ณ„์‚ฐํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ๊ณ ๋ฏผํ–ˆ์ง€๋งŒ, ๋Œ€๊ธฐ ์ˆ˜๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์ „์ฒด๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ ค ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ๋ถ€๋‹ด์Šค๋Ÿฝ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.



4๋‹จ๊ณ„ ์˜ˆ์•ฝ ์ทจ์†Œ ์‹œ ์ž๋™ ์Šน๊ฒฉ

์˜ˆ์•ฝ ์ทจ์†Œ(cancelReservationByIdAndPromoteWaiting)์—์„œ WaitingPromotionService.promoteBySlot()์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
์Šน๊ฒฉ ์‹คํŒจ๊ฐ€ ์˜ˆ์•ฝ ์ทจ์†Œ๋ฅผ ๋กค๋ฐฑ์‹œํ‚ค์ง€ ์•Š๋„๋ก REQUIRES_NEW๋กœ ๋ถ„๋ฆฌํ–ˆ๊ณ , ์Šน๊ฒฉ ์ค‘ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋Š” catch๋กœ ์žก์•„์„œ log.error ๋กœ ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋งˆ๋ฌด๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
๋งŒ์•ฝ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๋ฌธ์ œ๋ฅผ ๋” ๊ฒฌ๊ณ ํ•˜๊ฒŒ ์žก๊ณ ์‹ถ๋‹ค๋ฉด, ๋Œ€๊ธฐ ์Šน๊ฒฉ์„ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ์•„์˜ˆ ๋ถ„๋ฆฌํ•˜๊ณ , ๋ช‡์ฐจ๋ก€์˜ ์žฌ์‹œ๋„ ๋กœ์ง์„ ๋„ฃ๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, ์ด ๋ถ€๋ถ„์€ ๋ฏธ์…˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์—ฌ ์•„์ง์€ ๊ตฌํ˜„ํ•˜์ง€ ์•Š๊ณ  ๊ตฌ์ƒ๋งŒ ํ•ด๋‘์—ˆ์Šต๋‹ˆ๋‹ค.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant