Skip to content

[๐ŸŒฑ JPA ๋ฐฉํƒˆ์ถœ ์˜ˆ์•ฝ ๋Œ€๊ธฐ] ๊ธ€๋ Œ ๋ฏธ์…˜ ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค.#589

Open
nn98 wants to merge 33 commits into
woowacourse:mainfrom
nn98:jpa
Open

[๐ŸŒฑ JPA ๋ฐฉํƒˆ์ถœ ์˜ˆ์•ฝ ๋Œ€๊ธฐ] ๊ธ€๋ Œ ๋ฏธ์…˜ ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค.#589
nn98 wants to merge 33 commits into
woowacourse:mainfrom
nn98:jpa

Conversation

@nn98

@nn98 nn98 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

๋‹จ๊ณ„๋ณ„ ๋„๋‹ฌ ์ง€์ 

0๋‹จ๊ณ„

  • ์ •์ƒ ๋™์ž‘ / ํ…Œ์ŠคํŠธ ํ†ต๊ณผ / ํŠน์ดํ•œ ๊ตฌ์กฐ์ง€๋งŒ ์„ค๋ช… ๊ฐ€๋Šฅ
  • ์ด๋ฏธ ๊ฐ์ฒด ๊ธฐ๋ฐ˜ ์ฐธ์กฐ๋ผ ์œ ์ง€ / ์ฐธ์กฐ ๊ด€๊ณ„ ์—ญ์ „ (์˜ˆ์•ฝ/๋Œ€๊ธฐ์™€ ์„ธ์…˜์˜ ์ฐธ์กฐ ๊ด€๊ณ„) ์‹œ๋„

1๋‹จ๊ณ„

  • ์ „์ฒด Repository ์ „ํ™˜
  • ์˜ˆ์•ฝ ์ƒ์„ฑ์„ ์œ„ํ•ด ํ•„์š”ํ•œ ํ…Œ๋งˆ, ์‹œ๊ฐ„๋Œ€๋ฅผ id ๊ธฐ๋ฐ˜์œผ๋กœ ์ž˜ ์กฐํšŒํ•ด์„œ ๋„ฃ์–ด์คŒ
  • findById... ์‹คํ–‰์‹œ select 2๋ฒˆ ๋ฐœ์ƒ
  • ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ๊ด€์ฐฐ AI ๋กœ ํ…Œ์ŠคํŠธ์ฝ”๋“œ ์งœ์„œ ๋Œ๋ ธ๋Š”๋ฐ ๋ญ”๊ฐ€ ๋ณต์žกํ•˜๊ฒŒ ๋‚˜์˜ด

2๋‹จ๊ณ„

  • ๋ชฉ๋ก ์กฐํšŒ ๋„ค์ž„์ฟผ๋ฆฌ vs JPQL

ํ˜„์žฌ ์ธ์ฆ/์ธ๊ฐ€๊ฐ€ ๊ตฌํ˜„๋˜์ง€ ์•Š์€ ์ฝ”๋“œ์ด๋ฏ€๋กœ MemberId ๊ฐ€ ์•„๋‹Œ name ์œผ๋กœ ์‹๋ณ„ ๊ฐ€๋Šฅ.

๋‹ต๋ณ€

๋ฉ”์„œ๋“œ ์ด๋ฆ„ ์ฟผ๋ฆฌ๊ฐ€ ๋– ์˜ค๋ฆ„. ๊ธฐ๋ณธ์ ์œผ๋กœ ํ•ด๋‹น ์—”ํ‹ฐํ‹ฐ์˜ ํ•„๋“œ๋กœ ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๊ฐ’์— ๋Œ€ํ•œ ์กฐํšŒ๋Š”
๋ฉ”์„œ๋“œ ์ด๋ฆ„ ์ฟผ๋ฆฌ ์ž‘์„ฑ๋งŒ์œผ๋กœ ์ž๋™ ๊ตฌํ˜„๋˜๊ธฐ ๋•Œ๋ฌธ

  • ์„ ํƒ

๋ณธ์ธ(์˜ ์ด๋ฆ„)์˜ ์˜ˆ์•ฝ ๋ชฉ๋ก ์กฐํšŒ

๋„ค์ž„์ฟผ๋ฆฌ.

๋‹ค๋งŒ ์ด๋ฒˆ ๋‹จ๊ณ„์—์„œ ์‹คํ—˜ํ•˜๊ณ ์ž ํ–ˆ๋˜ ๋‚ด์šฉ์„ ์ถฉ์กฑํ•˜๊ธฐ ์œ„ํ•ด์„ 
์กฐํšŒ ์กฐ๊ฑด์ด ์‚ฌ์šฉ์ž์ธ๊ฒŒ ์ค‘์š”ํ•œ ๊ฒŒ ์•„๋‹ˆ๋ผ
1:N ๊ตฌ์กฐ์—์„œ 1 ์— ํ•ด๋‹นํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ์˜ ID๋กœ N ์— ํ•ด๋‹นํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ๋“ค์„ ์กฐํšŒํ•˜๋Š”?
์กฐํšŒ ์กฐ๊ฑด์„ ๊ฑธ ๋•Œ, ๋ฉ”์„œ๋“œ ์ด๋ฆ„ ์ฟผ๋ฆฌ์™€ JPQL ์ค‘
๋ฌด์—‡์„ ํƒํ•˜๊ณ  ๊ทธ ์ด์œ ์™€ ํ•œ๊ณ„๊ฐ€ ๋ฌด์—‡์ผ์ง€๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•ด
์„ธ์…˜id๋กœ ๊ฑธ๋ ค์žˆ๋Š” ๋Œ€๊ธฐ๋“ค์„ ์กฐํšŒํ•˜๋Š” ์‹คํ—˜์œผ๋กœ ๋ณ€๊ฒฝ

ํŠน์ • ์„ธ์…˜(์˜ id)์˜ ๋Œ€๊ธฐ ๋ชฉ๋ก ์กฐํšŒ

๋„ค์ž„์ฟผ๋ฆฌ.

JPA๋Š” ์—ฐ๊ด€(์˜์กด/์ฐธ์กฐ๊ด€๊ณ„?) ์—”ํ‹ฐํ‹ฐ์˜ ํ•„๋“œ๊นŒ์ง€ ๊ฒฝ๋กœ ํƒ์ƒ‰ ๊ฐ€๋Šฅ.
๋ฉ”์„œ๋“œ ์ด๋ฆ„์— ํ•ด๋‹น ํ•„๋“œ(sessionId)๊ฐ€ ์—†๋”๋ผ๋„ ์ž๋™์œผ๋กœ ์ ์ ˆํ•˜๊ฒŒ(session.id) ๊ฒฝ๋กœ๋ฅผ ํ•ด์„ํ•˜๊ธฐ์—.

์ƒ์„ฑ๋˜๋Š” ์ฟผ๋ฆฌ๋Š” SELECT w FROM Waiting w WHERE w.session.id = :sessionId

  • ํ•œ๊ณ„

๋ณตํ•ฉ / ์ค‘์ฒฉ ์ฟผ๋ฆฌ ํ™œ์šฉ ์ œํ•œ.
๋ณตํ•ฉ์ ์ธ ์กฐ๊ฑด๋“ค์ด๋‚˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ๊ธธ์–ด์ง€๋ฉด ๊ฐ€๋…์„ฑ์ด ๊ธ‰๊ฒฉํ•˜๊ฒŒ ์ €ํ•˜๋จ

์ง‘๊ณ„ํ•จ์ˆ˜/๊ทธ๋ฃจํ•‘/์„œ๋ธŒ์ฟผ๋ฆฌ ๋ถˆ๊ฐ€.

  • ๋ช…ํ™•ํžˆ ์ธ์ง€ํ•˜์ง„ ๋ชปํ•œ ๋‚ด์šฉ
    JOIN FETCH ์ œ์–ด ๋ถˆ๊ฐ€
    ์œ„์—์„œ ์„ ํƒํ•œ ๋„ค์ž„์ฟผ๋ฆฌ๋กœ ์กฐํšŒํ•œ ๋Œ€๊ธฐ๋“ค์— ํฌํ•จ๋œ Session ์— ์ ‘๊ทผํ•  ๋•Œ.
    Session ์ด Lazy loading ์ด๊ธฐ์— ์ ‘๊ทผํ•  ๋•Œ ๋งˆ๋‹ค ์ถ”๊ฐ€๋กœ SELECT ๋ฐœ์ƒ. ==> N+1 ๋ฌธ์ œ.

3๋‹จ๊ณ„

  • ์ž๊ธฐ ์ง„๋‹จ
์ฒ˜์Œ์—” ๊ณต์œ ๋˜๋Š” ํ•„๋“œ๊ฐ€ ๋งŽ๊ธฐ์— ์ƒํƒœ๋งŒ ์ฃผ๊ฐ€ํ•ด ์ฃผ๋ฉด ๋˜์ง€ ์•Š์„๊นŒ ์ƒ๊ฐํ–ˆ์ง€๋งŒ 
๋Œ€๊ธฐ๋Š” ์ˆœ๋ฒˆ์ด๋ผ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ๊ณ , 
์ด๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด created_at ์œผ๋กœ ์ˆœ๋ฒˆ์„ ๊ณ„์‚ฐํ•˜๋Š” ๊ตฌ์กฐ๋ฅผ ์ƒ๊ฐ. 
์ด ๊ตฌ์กฐ์—์„œ ๊ณ„์‚ฐ๋œ ์ˆœ๋ฒˆ์„ ํ•„์š”๋กœ ํ•˜๋Š” ๊ฒƒ์€ `์˜ˆ์•ฝ` ์ด ์•„๋‹Œ `๋Œ€๊ธฐ` ์ด๊ธฐ์— 
์˜ˆ์•ฝ๊ณผ ๋Œ€๊ธฐ๊ฐ€ ๋ชฉ์ ๊ณผ ์ฑ…์ž„๊ณผ ์—ญํ• ์ด ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ๋ผ ์ƒ๊ฐํ•˜๊ณ , ๋ถ„๋ฆฌํ•ด์„œ ๋ณ„๋„ ์—”ํ‹ฐํ‹ฐ๋กœ ์„ค๊ณ„. 
  • JPQL์ด ๋ฐœํ–‰ํ•˜๋Š” SQL

@formula ๋กœ ์šฐํšŒํ•ด๋ฒ„๋ฆฌ๊ธด ํ–ˆ๋Š”๋ฐ ์—ฌํŠผ ๋‚˜์˜ค๋Š” ๊ฒฐ๊ณผ๋Š” ๋™์ผ

    @Formula("(SELECT COUNT(*) FROM waiting w2 WHERE w2.session_id = session_id AND w2.id <= id)")
    private Integer waitingNumber;

SELECT
    w.id,
    w.name,
    w.session_id,
    (SELECT COUNT(*) FROM waiting w2
     WHERE w2.session_id = w.session_id
     AND w2.id <= w.id) AS waitingNumber   โ† ๋กœ waitingNumber ๊ณ„์‚ฐ
FROM waiting w
WHERE w.session_id = ?
ORDER BY w.id ASC 
  • ๋„ค์ž„์ฟผ๋ฆฌ์˜ ํ•œ๊ณ„

๋„ค์ž„์ฟผ๋ฆฌ๋กœ๋Š” ํ‘œํ˜„ํ•  ์ˆ˜ ์—†๋Š” ์„œ๋ธŒ์ฟผ๋ฆฌ ๋“ฑ์˜ ์ฟผ๋ฆฌ๊ฐ€ ์กด์žฌ

4๋‹จ๊ณ„

image image

๋ฐœํ–‰ SQL ๋ฐœ์ทŒ

  • findById(reservationId).getTime().getStartAt()์ด ๋ฐœํ–‰ํ•˜๋Š” SQL
Hibernate: 
    select
        s1_0.id,
        s1_0.date,
        s1_0.theme_id,
        s1_0.time_id 
    from
        session s1_0 
    where
        s1_0.id=?
Hibernate: 
    select
        ts1_0.id,
        ts1_0.start_at 
    from
        time_slot ts1_0 
    where
        ts1_0.id=?
  • JPQL์ด ๋ฐœํ–‰ํ•˜๋Š” SQL
SELECT
    w.id,
    w.name,
    w.session_id,
    (SELECT COUNT(*) FROM waiting w2
     WHERE w2.session_id = w.session_id
     AND w2.id <= w.id) AS waitingNumber   โ† ๋กœ waitingNumber ๊ณ„์‚ฐ
FROM waiting w
WHERE w.session_id = ?
ORDER BY w.id ASC 

๋ง์„ค์ธ ๊ฒฐ์ • 1~2๊ฐœ

์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ ๊ตฌ์กฐ๋กœ ํ•ด๊ฒฐ๋˜๋Š” ๋ถ€๋ถ„๋„, ์ƒ๋žต ๊ฐ€๋Šฅํ•œ ๋ถ€๋ถ„๋„ ๋งŽ์•˜๋‹ค ๋ณด๋‹ˆ
์–ด๋–ค์‹์œผ๋กœ ์‹คํ—˜ํ•˜๊ณ  JPA ํ•™์Šต์„ ์ง„ํ–‰ํ•ด์•ผ ํ• ๊นŒ ์‹ถ์—ˆ๋˜ ๋ถ€๋ถ„๋“ค?

  • memberId ๋กœ ์กฐํšŒํ•˜๋Š” ๋กœ์ง์„ ๋ญ˜๋กœ ๋™์ผํ•˜๊ฒŒ ์‹คํ—˜ํ•  ๊ฒƒ์ธ๊ฐ€
  • @Formula ๋กœ ๋Œ€์ฒดํ•œ ๋ถ€๋ถ„์„ JPQL ๋กœ ๋ณ€๊ฒฝ? ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ๋™์ผํ•œ๋ฐ?

ํ”๋“ค๋ฆฐ ํ•œ ์žฅ๋ฉด.

JPA ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ๊ด€์ฐฐ ๋ฐ ์‹ ํ˜ธ ์บก์ฒ˜.
๋„์ €ํžˆ ์‹คํ—˜ํ•  ์—„๋‘ ์•ˆ๋‚˜์„œ AI ๋Œ๋ ค์„œ ํ…Œ์ŠคํŠธ.

JPA ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ

JPA ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ํ•ต์‹ฌ ๋™์ž‘ ์ •๋ฆฌ

ํ”„๋กœ์ ํŠธ: spring-roomescape-waiting (JPA ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ดํ›„ ๊ด€์ฐฐ ๊ธฐ๋ก)


์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ž€

@Transactional ๋ฒ”์œ„ ์•ˆ์—์„œ JPA๊ฐ€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” 1์ฐจ ์ €์žฅ์†Œ.
DB์™€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‚ฌ์ด์˜ ๋ฒ„ํผ ์—ญํ• ์„ ํ•˜๋ฉฐ, ํŠธ๋žœ์žญ์…˜์ด ๋‹ซํžˆ๋ฉด ํ•จ๊ป˜ ์†Œ๋ฉธ๋œ๋‹ค.

@Transactional ๋ฒ”์œ„ ์•ˆ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ (1์ฐจ ์บ์‹œ)     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ Session#1  id=1      โ”‚  โ”‚
โ”‚  โ”‚ Waiting#1  id=1      โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚          โ†• flush           โ”‚
โ”‚     H2 DATABASE            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

@Transactional ๋ฒ”์œ„ ๋ฐ–
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ (๋‹ซํž˜)         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ TimeSlot ํ”„๋ก์‹œ (๋ฏธ์ดˆ๊ธฐํ™”)โ”‚  โ”‚
โ”‚  โ”‚  โ†’ getStartAt() ํ˜ธ์ถœ ์‹œ โ”‚  โ”‚
โ”‚  โ”‚  LazyInitializationEx โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ‘  ๋”ํ‹ฐ ์ฒดํ‚น (Dirty Checking)

ํ˜„์ƒ: save() ์—†์ด ํ•„๋“œ๋งŒ ์ˆ˜์ •ํ•ด๋„ ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ UPDATE ์ž๋™ ๋ฐœํ–‰

์›๋ฆฌ: ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋กœ๋“œํ•  ๋•Œ Hibernate๊ฐ€ ๊ทธ ์‹œ์ ์˜ ํ•„๋“œ๊ฐ’์„ ์Šค๋ƒ…์ƒท์œผ๋กœ ์ €์žฅํ•œ๋‹ค.
flush/commit ์‹œ ์Šค๋ƒ…์ƒท๊ณผ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋น„๊ตํ•ด ๋ณ€๊ฒฝ์ด ์žˆ์œผ๋ฉด UPDATE๋ฅผ ์ž๋™ ๋ฐœํ–‰ํ•œ๋‹ค.

// save() ํ˜ธ์ถœ ์—†์Œ
Session managed = em.find(Session.class, 1L);
managed.

setDate(LocalDate.now().

plusDays(99));  // ํ•„๋“œ๋งŒ ๋ณ€๊ฒฝ
        em.

flush();  // โ†’ UPDATE session SET date=? WHERE id=?

์ฝ˜์†” ์ถœ๋ ฅ:

update session
set date     = ?,
    theme_id = ?,
    time_id  = ?
where id = ?
-- save() ํ˜ธ์ถœ ์—†์—ˆ์ง€๋งŒ flush ์‹œ ์ž๋™ ๋ฐœํ–‰

โ‘ก 1์ฐจ ์บ์‹œ

ํ˜„์ƒ: ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์—์„œ findById ๋‘ ๋ฒˆ ํ˜ธ์ถœ โ†’ SELECT 1ํšŒ๋งŒ ๋ฐœํ–‰, ๋™์ผ ์ธ์Šคํ„ด์Šค ๋ฐ˜ํ™˜

Session a = repo.findById(1L);  // SELECT ๋ฐœํ–‰
Session b = repo.findById(1L);  // SQL ์—†์Œ (์บ์‹œ ์ ์ค‘)

assert a ==b;  // true โ€” ๋™์ผ ์ธ์Šคํ„ด์Šค

์ฝ˜์†” ์ถœ๋ ฅ:

select s1_0.id, s1_0.date, s1_0.theme_id, s1_0.time_id
from session s1_0
where s1_0.id = 1
-- ๋‘ ๋ฒˆ์งธ ํ˜ธ์ถœ์€ SQL ๋ฏธ๋ฐœํ–‰

์ฃผ์˜: ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜(์˜์†์„ฑ ์ปจํ…์ŠคํŠธ) ์•ˆ์—์„œ๋งŒ ์œ ํšจ.
em.clear() ๋˜๋Š” ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ๋„˜์œผ๋ฉด ์บ์‹œ ์†Œ๋ฉธ.


โ‘ข ์“ฐ๊ธฐ ์ง€์—ฐ (Write-Behind)

ํ˜„์ƒ: save() ์‹œ์ ์— INSERT๊ฐ€ ์ฆ‰์‹œ ๋ฐœํ–‰๋˜์ง€ ์•Š๊ณ  flush/commit๊นŒ์ง€ ์ง€์—ฐ๋  ์ˆ˜ ์žˆ์Œ

์ „๋žต๋ณ„ ์ฐจ์ด:

์ „๋žต INSERT ๋ฐœํ–‰ ์‹œ์  ์ด์œ 
IDENTITY (ํ˜„์žฌ) save() ์ฆ‰์‹œ DB๊ฐ€ id๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์ง€์—ฐ ๋ถˆ๊ฐ€
SEQUENCE flush/commit ์‹œ ์‹œํ€€์Šค๋กœ id๋ฅผ ๋ฏธ๋ฆฌ ์ฑ„๋ฒˆํ•ด ๋ฒ„ํผ์— ์ ์žฌ ๊ฐ€๋Šฅ
// IDENTITY ์ „๋žต (ํ˜„์žฌ ํ”„๋กœ์ ํŠธ)
Reservation saved = repo.save(Reservation.transientOf("๋ธŒ๋ผ์šด", session));
// โ†‘ ์—ฌ๊ธฐ์„œ ์ฆ‰์‹œ INSERT ๋ฐœํ–‰ (id๊ฐ€ ํ•„์š”ํ•˜๋ฏ€๋กœ)
System.out.

println(saved.getId());  // null ์•„๋‹˜

// SEQUENCE ์ „๋žต์ด์—ˆ๋‹ค๋ฉด
// save() ์‹œ์  โ†’ ๋ฒ„ํผ์— ์ ์žฌ (INSERT ๋ฏธ๋ฐœํ–‰)
// em.flush() ๋˜๋Š” commit ์‹œ์  โ†’ INSERT ์ผ๊ด„ ๋ฐœํ–‰

โ‘ฃ flush ์‹œ์ 

ํ˜„์ƒ: flush()๋ฅผ ๋ช…์‹œ ํ˜ธ์ถœํ•˜์ง€ ์•Š์•„๋„ ํŠน์ • ์‹œ์ ์— ์ž๋™์œผ๋กœ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ โ†’ DB ๋™๊ธฐํ™”

์ž๋™ flush ๋ฐœ์ƒ ์กฐ๊ฑด 3๊ฐ€์ง€:

  1. JPQL / Native Query ์‹คํ–‰ ์ง์ „ (FlushMode.AUTO ๊ธฐ๋ณธ๊ฐ’)
  2. ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ (@Transactional ๋ฉ”์„œ๋“œ ์ •์ƒ ์ข…๋ฃŒ)
  3. em.flush() ๋ช…์‹œ ํ˜ธ์ถœ (์ปค๋ฐ‹ ์—†์ด DB์—๋งŒ ๋ฐ˜์˜, ๋กค๋ฐฑ ๊ฐ€๋Šฅ)
em.persist(waiting);  // 1์ฐจ ์บ์‹œ์—๋งŒ ์กด์žฌ

// JPQL ์‹คํ–‰ ์ง์ „ ์ž๋™ flush
Long count = em.createQuery("SELECT COUNT(w) FROM Waiting w", Long.class)
        .getSingleResult();
// โ†’ persistํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ COUNT์— ๋ฐ˜์˜๋จ

์ฝ˜์†” ์ถœ๋ ฅ:

insert into waiting (name, session_id)
values (?, ?) โ† ์ž๋™ flush
select count(w1_0.id)
from waiting w1_0 โ† JPQL

โ‘ค fetch ๊ธฐ๋ณธ๊ฐ’

ํ˜„์ƒ: @ManyToOne(fetch = LAZY) ๋ช…์‹œ โ†’ Session ๋กœ๋“œ ์‹œ TimeSlot์€ ํ”„๋ก์‹œ๋กœ ๋Œ€๊ธฐ

JPA ํ‘œ์ค€ ๊ธฐ๋ณธ๊ฐ’:

  • @ManyToOne, @OneToOne โ†’ EAGER (๊ธฐ๋ณธ)
  • @OneToMany, @ManyToMany โ†’ LAZY (๊ธฐ๋ณธ)

ํ˜„์žฌ ํ”„๋กœ์ ํŠธ: ๋ช…์‹œ์ ์œผ๋กœ FetchType.LAZY ์˜ค๋ฒ„๋ผ์ด๋“œ

// Session.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "time_id")
private TimeSlot timeSlot;  // ๋กœ๋“œ ์‹œ ํ”„๋ก์‹œ ๊ฐ์ฒด

์ฝ˜์†” ์ถœ๋ ฅ:

-- sessionRepo.findById(1L) ํ˜ธ์ถœ ์‹œ
select s1_0.id, s1_0.date, s1_0.theme_id, s1_0.time_id
from session s1_0
where s1_0.id = 1
-- time_slot JOIN ์—†์Œ โ† LAZY ์ ์šฉ

-- session.getTimeSlot().getStartAt() ์ ‘๊ทผ ์‹œ์ ์—
select t1_0.id, t1_0.start_at
from time_slot t1_0
where t1_0.id = ?
-- ์ด๋•Œ์„œ์•ผ ๋ณ„๋„ SELECT ๋ฐœํ–‰

โ‘ฅ LazyInitializationException

ํ˜„์ƒ: ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ(ํŠธ๋žœ์žญ์…˜)๊ฐ€ ๋‹ซํžŒ ํ›„ LAZY ํ”„๋ก์‹œ์— ์ ‘๊ทผ โ†’ ์˜ˆ์™ธ ๋ฐœ์ƒ

// @Transactional ์„œ๋น„์Šค์—์„œ Session ๋ฐ˜ํ™˜
Session session = sessionService.findById(1L);
// โ†’ ๋ฉ”์„œ๋“œ ๋ฐ˜ํ™˜ ์‹œ ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ, ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ๋‹ซํž˜

// ์ปจํŠธ๋กค๋Ÿฌ์—์„œ LAZY ํ•„๋“œ ์ ‘๊ทผ
session.

getTimeSlot().

getStartAt();
// โ†’ LazyInitializationException: could not initialize proxy - no Session

๋ฐœ์ƒ ํ๋ฆ„:

1. @Transactional ์„œ๋น„์Šค์—์„œ Session ๋กœ๋“œ
   โ†’ timeSlot์€ ๋ฏธ์ดˆ๊ธฐํ™” ํ”„๋ก์‹œ (SELECT ๋ฏธ๋ฐœํ–‰)

2. ๋ฉ”์„œ๋“œ ๋ฐ˜ํ™˜ โ†’ ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ
   โ†’ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ๋‹ซํž˜

3. ์ปจํŠธ๋กค๋Ÿฌ์—์„œ session.getTimeSlot().getStartAt()
   โ†’ ํ”„๋ก์‹œ ์ดˆ๊ธฐํ™” ์‹œ๋„ โ†’ no Session
   โ†’ LazyInitializationException

์ฃผ์˜ โ€” 1์ฐจ ์บ์‹œ๋กœ ์ธํ•œ ํ•จ์ •:

๊ฐ™์€ EntityManager์—์„œ TimeSlot์„ ์ €์žฅํ•œ ๋’ค Session์„ ์กฐํšŒํ•˜๋ฉด,
TimeSlot์ด 1์ฐจ ์บ์‹œ์— ๋‚จ์•„ ํ”„๋ก์‹œ ๋Œ€์‹  ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
์ด ๊ฒฝ์šฐ EM์„ ๋‹ซ์•„๋„ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•„ ๋ฌธ์ œ๋ฅผ ๋†“์น˜๊ธฐ ์‰ฝ๋‹ค.

์˜ˆ์™ธ๋ฅผ ์žฌํ˜„ํ•˜๋ ค๋ฉด ๋ณ„๋„ EntityManager(๋ณ„๋„ ํŠธ๋žœ์žญ์…˜)๋กœ ์กฐํšŒํ•ด์•ผ ํ”„๋ก์‹œ๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.

์‹ค์ œ ์„œ๋น„์Šค์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค:

  1. spring.jpa.open-in-view=false ์„ค์ • (๊ธฐ๋ณธ๊ฐ’ true โ†’ OSIV๊ฐ€ ์ปจํŠธ๋กค๋Ÿฌ๊นŒ์ง€ ์„ธ์…˜ ์—ฐ์žฅ)
  2. @Transactional ์—†๋Š” ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ฐ˜ํ™˜๋œ ์—”ํ‹ฐํ‹ฐ์˜ LAZY ํ•„๋“œ๋ฅผ JSON ์ง๋ ฌํ™”

๋ฉ”์„œ๋“œ ์ด๋ฆ„ ์ฟผ๋ฆฌ vs JPQL ์„ ํƒ ๊ธฐ์ค€

๋ฉ”์„œ๋“œ ์ด๋ฆ„ ์ฟผ๋ฆฌ (๋„ค์ž„์ฟผ๋ฆฌ)

Spring Data JPA๊ฐ€ ๋ฉ”์„œ๋“œ๋ช…์„ ํŒŒ์‹ฑํ•ด ์ฟผ๋ฆฌ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•œ๋‹ค.
์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ ํ•„๋“œ๊นŒ์ง€ ๊ฒฝ๋กœ ํƒ์ƒ‰(property traversal)์„ ์ง€์›ํ•œ๋‹ค.

// Waiting.session (Session) โ†’ Session.id (Long) ๊ฒฝ๋กœ ํƒ์ƒ‰
List<Waiting> findBySessionId(Long sessionId);
// โ†’ SELECT w FROM Waiting w WHERE w.session.id = :sessionId

// ์ •๋ ฌ ํฌํ•จ
List<Waiting> findBySessionOrderByIdAsc(Session session);
// โ†’ SELECT w FROM Waiting w WHERE w.session = :session ORDER BY w.id ASC

๋„ค์ž„์ฟผ๋ฆฌ์˜ ํ•œ๊ณ„

ํ•œ๊ณ„ ์„ค๋ช…
์ง‘๊ณ„/๊ทธ๋ฃนํ•‘ ๋ถˆ๊ฐ€ GROUP BY, HAVING, COUNT ๋“ฑ ํ‘œํ˜„ ๋ถˆ๊ฐ€
์„œ๋ธŒ์ฟผ๋ฆฌ ๋ถˆ๊ฐ€ WHERE id IN (SELECT ...) ๋ถˆ๊ฐ€
JOIN FETCH ๋ถˆ๊ฐ€ N+1 ๋ฌธ์ œ๋ฅผ ์ฟผ๋ฆฌ ๋ ˆ๋ฒจ์—์„œ ํ•ด๊ฒฐ ๋ถˆ๊ฐ€
๊ฐ€๋…์„ฑ ํ•œ๊ณ„ ์กฐ๊ฑด์ด 3๊ฐœ ์ด์ƒ์ด๋ฉด ๋ฉ”์„œ๋“œ๋ช…์ด ํŒŒ๊ตญ

JPQL์ด ํ•„์š”ํ•œ ์‹œ์ 

// N+1 ํ•ด๊ฒฐ โ€” JOIN FETCH
@Query("SELECT w FROM Waiting w JOIN FETCH w.session WHERE w.session.id = :id")
List<Waiting> findWithSession(@Param("id") Long id);

// ์ง‘๊ณ„
@Query("SELECT COUNT(w) FROM Waiting w WHERE w.session = :session AND w.id <= :id")
int countBefore(@Param("session") Session session, @Param("id") Long id);

// ์„œ๋ธŒ์ฟผ๋ฆฌ (WHERE/HAVING ์ ˆ๋งŒ ๊ฐ€๋Šฅ, FROM ์ ˆ ๋ถˆ๊ฐ€)
@Query("SELECT w FROM Waiting w WHERE w.session.id IN " +
        "(SELECT s.id FROM Session s WHERE s.date > :date)")
List<Waiting> findAfterDate(@Param("date") LocalDate date);

JPQL์˜ ํ•œ๊ณ„ โ†’ ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ

// FROM ์ ˆ ์„œ๋ธŒ์ฟผ๋ฆฌ, ROW_NUMBER(), ์œˆ๋„์šฐ ํ•จ์ˆ˜ ๋“ฑ์€ JPQL ๋ถˆ๊ฐ€
// โ†’ @Query(nativeQuery = true) ๋˜๋Š” @Formula ์‚ฌ์šฉ
@Formula("(SELECT COUNT(*) FROM waiting w2 WHERE w2.session_id = session_id AND w2.id <= id)")
private Integer waitingNumber;

์„ ํƒ ๊ธฐ์ค€ ์š”์•ฝ

์ƒํ™ฉ ์„ ํƒ
๋‹จ์ˆœ ์กฐ๊ฑด 1-2๊ฐœ + ์ •๋ ฌ ๋„ค์ž„์ฟผ๋ฆฌ
์กฐ๊ฑด 3๊ฐœ ์ด์ƒ / ๋ณต์žกํ•œ ์ด๋ฆ„ JPQL
N+1 ํ•ด๊ฒฐ (JOIN FETCH) JPQL
์ง‘๊ณ„, ๊ทธ๋ฃนํ•‘ JPQL
์„œ๋ธŒ์ฟผ๋ฆฌ (WHERE/HAVING) JPQL
์œˆ๋„์šฐ ํ•จ์ˆ˜, FROM ์„œ๋ธŒ์ฟผ๋ฆฌ ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ / @formula

nn98 and others added 30 commits May 31, 2026 23:26
* chore: ~ ์‚ฌ์ „ ํ•™์Šต

* docs(readme): 1๋‹จ๊ณ„ - ์˜ˆ์•ฝ ๋Œ€๊ธฐ ์‹ ์ฒญ/์ทจ์†Œ ์„ค๊ณ„/๊ตฌํ˜„ ๋‚ด์šฉ ์ •๋ฆฌ

* feat: waiting ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์ถ”๊ฐ€

* feat: Command ๊ฐ์ฒด ์ถ”๊ฐ€

* feat: waiting ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ถ”๊ฐ€, ์‚ฝ์ž… ๊ตฌํ˜„

* feat: WaitingRepository ๋‚ด๋ถ€ ์ˆœ๋ฒˆ ์กฐํšŒ, ์‚ญ์ œ ๊ตฌํ˜„

* feat: waiting ์„œ๋น„์Šค ์ถ”๊ฐ€

* feat: ์ปจํŠธ๋กค๋Ÿฌ ์ˆœ๋ฒˆ ์กฐํšŒ, ์‚ฝ์ž…, ์‚ญ์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* feat(exception): ์—†๋Š” ์˜ˆ์•ฝ ๋Œ€๊ธฐ ์‚ญ์ œ ์‹œ 404

* feat: ์˜ˆ์•ฝ ๋Œ€๊ธฐ ๊ฐœ์ˆ˜, ์˜ˆ์ƒ ์ˆœ๋ฒˆ ์กฐํšŒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

* docs: 2๋‹จ๊ณ„ - ๋‚ด ์˜ˆ์•ฝ ๋ชฉ๋ก ์กฐํšŒ (์ƒํƒœ ๊ตฌ๋ถ„) ๊ตฌํ˜„ ๋ชฉ๋ก ์ •๋ฆฌ

* feat: waiting ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์ถ”๊ฐ€

* feat: ๋Œ€๊ธฐ ์ •๋ณด๋ฅผ ๋‹ด์„ Waiting ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ถ”๊ฐ€

* feat: ๋Œ€๊ธฐ ์ •๋ณด๋ฅผ ๋‹ด์„ ๋„๋ฉ”์ธ ๊ฐ์ฒด Waiting ๋„์ž… / WaitingCommand ๋Œ€์ฒด

* feat: ์˜ˆ์•ฝ/๋Œ€๊ธฐ ์ •๋ณด๋ฅผ ๋ชจ๋‘ ๋‹ด์„ Response ๊ฐ์ฒด ์ถ”๊ฐ€ ๋ฐ ์˜ˆ์•ฝ/๋Œ€๊ธฐ ์ •๋ณด๋ฅผ ์กฐํšŒํ•  ๋ฉ”์„œ๋“œ ๊ตฌํ˜„

* feat: ์˜ˆ์•ฝ๋œ ์ƒํƒœ์—์„œ ๋Œ€๊ธฐ ๋“ฑ๋ก ๋ถˆ๊ฐ€ํ•˜๋„๋ก ์ˆ˜์ •

* refactor: Waiting DELETE API ๋ณ€๊ฒฝ

* feat: ์ง€๋‚œ ๋‚ ์งœ, ์‹œ๊ฐ„์œผ๋กœ ๋Œ€๊ธฐ ๋ถˆ๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„

* refactor(waiting): ๋ถˆ์šฉ ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ

* test: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ ํ…Œ์ŠคํŠธ ์‚ญ์ œ

* docs(readme): API ๋ช…์„ธํ‘œ ์ถ”๊ฐ€

* docs: ํ”„๋ก ํŠธ ํŽ˜์ด์ง€ ์ฃผ์†Œ ์ถ”๊ฐ€

* docs: ๊ทธ๋ฃน ๊ทœ์น™ ์ถ”๊ฐ€

* docs(requirements): ์š”๊ตฌ์‚ฌํ•ญ ์ƒ์„ธ ๋ถ„์„ ๋ฐ ๋‹ต๋ณ€, ๊ทผ๊ฑฐ ์ถ”๊ฐ€

* refactor(controller_dto): valid ๋ณด์™„/์ถ”๊ฐ€

* refactor(reservation): Booking ๋ฆฌ์†Œ์Šค ์ถ”๊ฐ€, ์ฑ…์ž„ ์ด๊ด€

* refactor(dto): ๋ถˆํ•„์š”ํ•œ PUT DTO ์ œ๊ฑฐ ๋ฐ ํ†ตํ•ฉ

* refactor(DTO): ๊ณ„์ธต๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ผ๊ด€ํ™”

* remove: ๋ถˆ์šฉ ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ

* refactor(feedback01): ์ถ”๊ฐ€ ์ฟผ๋ฆฌ JOIN ์ ์šฉ์œผ๋กœ ํšจ์œจํ™”

* refactor(feedback02): ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์˜ˆ์•ฝ์— ๋Œ€ํ•œ ๋Œ€๊ธฐ ๊ฒ€์ฆ

* refactor(feedback03): ์˜ˆ์•ฝ์˜ ๋‚ ์งœ/์‹œ๊ฐ„์— ๋Œ€ํ•œ ๊ฒ€์ฆ

* docs(feedback05): ์ค‘๋ณต ์กฐํšŒ ๋กœ์ง ์ œ๊ฑฐ

* refactor(feedback06): UPDATE ์ฟผ๋ฆฌ์˜ ๊ฒฐ๊ณผ๊ฐ’ ๋ฐ˜ํ™˜

* refactor(feedback07): SimpleJdbcInsert ์ค‘๋ณต ์ƒ์„ฑ

* refactor(feedback08): ๋ถˆํ•„์š”ํ•œ ์กฐํšŒ ๋กœ์ง ๋ฐ˜๋ณต

* docs(feedback09): ๋Œ€๊ธฐ ์ˆœ๋ฒˆ ์กฐํšŒ ๋ฐฉ์‹ ํ˜ผ์žฌ

* refactor(feedback04): JDBC ํ™˜๊ฒฝ์—์„œ ์—”ํ‹ฐํ‹ฐ์˜ ๋ถˆ๋ณ€์„ฑ

* docs(feedback11): ํ™•์žฅ๊ณผ ๊ทผ๋ณธ

---------

Co-authored-by: minzzun99 <skg06123@naver.com>
WebMvcConfigurer ๊ตฌํ˜„์ฒด WebConfig ์ถ”๊ฐ€
๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ(/**)์— ๋Œ€ํ•ด CORS ํ—ˆ์šฉ โ€” Vercel ๋“ฑ ์™ธ๋ถ€ ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™์„ ์œ„ํ•œ ์„ค์ •
allowedOriginPatterns("*")๋กœ ์ „์ฒด ์˜ค๋ฆฌ์ง„ ํ—ˆ์šฉ, ๋ฐฐํฌ URL ํ™•์ • ํ›„ ์ขํž ์ˆ˜ ์žˆ์Œ

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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