infra & network

[Lecture] ์Šคํ”„๋ง๋ถ€ํŠธ๋กœ ์ง์ ‘ ๋งŒ๋“ค๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ๋Œ€๊ทœ๋ชจ ์‹œ์Šคํ…œ ์„ค๊ณ„ - (1)

devJK93 2025. 4. 12.

๐Ÿ“Œ ๋Œ€๊ทœ๋ชจ ์‹œ์Šคํ…œ ์„œ๋ฒ„ ์ธํ”„๋ผ ๊ธฐ์ดˆ

  • Scale Up (์ˆ˜์ง์  ํ™•์žฅ) : ํ•˜๋“œ์›จ์–ด ์„ฑ๋Šฅ ํ–ฅ์ƒ (ex. RAM 16 → 64GB)
  • Scale Out (์ˆ˜ํ‰์  ํ™•์žฅ) : ์„œ๋ฒ„ ์—ฌ๋Ÿฌ๋Œ€, ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ๋กœ ์š”์ฒญ ๋ถ„์‚ฐ
  • Load Balancer : ํŠธ๋ž˜ํ”ฝ์„ ๋ผ์šฐํŒ… ๋ฐ ๋ถ„์‚ฐํ•˜๊ธฐ ์œ„ํ•œ ๋„๊ตฌ (Client๋Š” Load Balancer ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด , Load Balancer๋Š” ์š”์ฒญ์„ ์ ์ ˆํžˆ ๋ถ„์‚ฐํ•˜์—ฌ ์„œ๋ฒ„๋กœ ์ „๋‹ฌ)
  • Cache : ์„œ๋ฒ„๊ฐ€ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์ด ๊ธธ์–ด์งˆ ์ˆ˜๋ก ์‘๋‹ต์€ ๋А๋ ค์ง„๋‹ค. ๊ทธ๋ž˜์„œ ์ž์ฃผ ์š”์ฒญ๋˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋ฏธ๋ฆฌ ์ €์žฅํ•ด๋‘๊ณ , ๋‹ค์Œ์— ๋˜ ์š”์ฒญ์ด ์˜ค๋ฉด ๋น ๋ฅด๊ฒŒ ์‘๋‹ตํ•ด์ฃผ๋Š” ์ €์žฅ์†Œ๋ฅผ ๋‘”๋‹ค.
    ๐Ÿ“ ์บ์‹œ ์ข…๋ฅ˜
    ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ (ํ”„๋ก ํŠธ)
    ์„œ๋ฒ„ ์บ์‹œ (Spring์—์„œ @Cacheable)
    Redis๊ฐ™์€ ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ ์„œ๋ฒ„
    CDN ์บ์‹œ (์ •์  ๋ฆฌ์†Œ์Šค, ์ด๋ฏธ์ง€, js ๋“ฑ)

  • ์ƒค๋”ฉ (Sharding) : ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ์„œ๋ฒ„(๋˜๋Š” ํŒŒํ‹ฐ์…˜)๋กœ ๋‚˜๋ˆ„์–ด ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ• (shard ๋“ค : 1~1,000 ์„œ๋ฒ„A, 1,001~2,000 ์„œ๋ฒ„B)
    MongoDB, Cassandra ๊ฐ™์€ NoSQL DB์—์„œ ์ž์ฃผ ์‚ฌ์šฉ
  • ๋ ˆํ”Œ๋ฆฌ์ผ€์ด์…˜ (Replication) : ๋™์ผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ์„œ๋ฒ„์— ๋ณต์ œํ•ด ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•. ์ฃผ๋กœ ์ฝ๊ธฐ ์„ฑ๋Šฅ์„ ๋†’์ด๊ฑฐ๋‚˜ ์žฅ์•  ๋ณต๊ตฌ(๊ณ ๊ฐ€์šฉ์„ฑ)๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉ๋จ.
  • ๊ณ ๊ฐ€์šฉ์„ฑ : ํ•˜๋‚˜ ๋ง๊ฐ€์ ธ๋„ ๋‹ค๋ฅธ ์• ๊ฐ€ ๊ทธ ์—ญํ•  
  • ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ
  • DNS : Domain Name System → ๋„๋ฉ”์ธ ์ด๋ฆ„(์˜ˆ: google.com)์„ IP ์ฃผ์†Œ(์˜ˆ: 142.250.206.14)๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” ์‹œ์Šคํ…œ
    DNS๊ฐ€ ๋А๋ฆฌ๋ฉด → ์›น์‚ฌ์ดํŠธ ์ ‘์† ์†๋„๋„ ๋Šฆ์–ด์ง
    ๊ทธ๋ž˜์„œ OS๋‚˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ตœ๊ทผ ์ ‘์†ํ•œ ๋„๋ฉ”์ธ์€ ์บ์‹ฑํ•œ๋‹ค.
    ์„ฑ๋Šฅ ํŠœ๋‹ ์‹œ์—๋„ nslookup, dig, ping ์œผ๋กœ DNS ํ™•์ธ ์ž์ฃผ ํ•จ.
  • DNS ์ฟผ๋ฆฌ : ๋„๋ฉ”์ธ ์ด๋ฆ„์— ๋Œ€ํ•œ IP ์ฃผ์†Œ๊ฐ€ ๋ญ์•ผ?"๋ผ๊ณ  DNS ์„œ๋ฒ„์— ์งˆ๋ฌธ(์š”์ฒญ)์„ ๋ณด๋‚ด๋Š” ๊ฒƒ
  • ๋ถ„์‚ฐ๋œ ๊ฐ ์„œ๋ฒ„์— ๋Œ€ํ•œ Client ๋ผ์šฐํŒ…์„ ์œ„ํ•ด DNS ํ™œ์šฉ?
    ์‚ฌ์šฉ์ž ์š”์ฒญ์ด ์™”์„ ๋•Œ ํ•œ๊ตญ ์‚ฌ๋žŒ์€ ์„œ์šธ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด๋Š”๊ฒŒ ๋น ๋ฅด๊ณ , ๋ฏธ๊ตญ ์‚ฌ๋žŒ์€ ๋ฏธ๊ตญ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด๋Š”๊ฒŒ ๋น ๋ฅด๋‹ค.
    → ์ด๊ฒƒ์„ ๋ฐ”๋กœ Client ๋ผ์šฐํŒ…
    → ์ฆ‰, ๋‹ค์ค‘ํ™” ๋œ ์„œ๋ฒ„ ์ค‘ ์–ด๋А ์„œ๋ฒ„๋กœ ์š”์ฒญ์„ ๋ณด๋‚ผ์ง€ ๊ฒฐ์ •ํ•˜๋Š” ๊ฒƒ.
    * ๋ผ์šฐํŒ… ๋ฐฉ์‹์€ ๋‹ค์–‘ํ•˜๊ณ  ๊ทธ ์ค‘ 1. DNS 2. GSLB๊ฐ€ ์žˆ๋‹ค.
    1. DNS : ๋„๋ฉ”์ธ → IP์ฃผ์†Œ๋ฅผ ๋งคํ•‘ํ•ด์ค€๋‹ค. ์•„๋ž˜์ฒ˜๋Ÿผ ์„ค์ • ๊ฐ€๋Šฅ
    ํ•œ๊ตญ์—์„œ example.com → ์„œ์šธ ์„œ๋ฒ„ IP
    ๋ฏธ๊ตญ์—์„œ example.com → ๋ฏธ๊ตญ ์„œ๋ฒ„ IP
    ์ด๋ฅผ GeoDNS ๋˜๋Š” DNS ๊ธฐ๋ฐ˜ ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ์ด๋ผ๊ณ  ํ•œ๋‹ค.
  • ์ •๋ฐ€ํ•˜๊ณ  ๋ณต์žกํ•œ ๋ผ์šฐํŒ…์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ GSLB ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ• ํ™œ์šฉ? : Global Server Load Balancing
    GSLB๋Š” ์ข€ ๋” ๋˜‘๋˜‘ํ•œ ๋ผ์šฐํŒ… ์‹œ์Šคํ…œ. DNS์ฒ˜๋Ÿผ IP๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ฃผ์ง€๋งŒ
    '์‚ฌ์šฉ์ž์˜ ์œ„์น˜', '์„œ๋ฒ„ ์ƒํƒœ (์ฃฝ์—ˆ๋Š”์ง€ ์‚ด์•˜๋Š”์ง€)', '์‘๋‹ต ์†๋„', 'ํŠธ๋ž˜ํ”ฝ ๋ถ€ํ•˜์ •๋„' ์™€ ๊ฐ™์€ ๋‹ค์–‘ํ•œ ์กฐ๊ฑด์„ ๊ณ ๋ คํ•ด์„œ ๊ฐ€์žฅ ์ ์ ˆํ•œ ์„œ๋ฒ„๋ฅผ ๊ณจ๋ผ์ค€๋‹ค.
  • ๋‹จ์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์ปค์งˆ์ˆ˜๋ก ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ค์›Œ์งˆ ์ˆ˜ ์žˆ๋‹ค. :
    ๋‹จ์ผ ์„œ๋ฒ„๊ฐ€ ์ฒ˜๋ฆฌํ•ด์•ผํ•  ํŠธ๋ž˜ํ”ฝ์ด ๋„ˆ๋ฌด ๋งŽ์•„์ง€๋ฉด, ๋ฆฌ์†Œ์Šค๊ฐ€ ๋ถ€์กฑํ•  ์ˆ˜๋„ ์žˆ๊ณ , ์‹œ์Šคํ…œ์ด ์ปค์งˆ์ˆ˜๋ก ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ค์›Œ์งˆ ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ

  • ๋‹จ์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ธฐ๋Šฅ์— ๋งž๋Š” ์—ฌ๋Ÿฌ๊ฐœ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
    ๊ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๋‹ด๋‹นํ•œ ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด ๊ฐœ๋ณ„ ์ž‘์—…๋งŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.
    ์˜ˆ๋ฅผ ๋“ค๋ฉด, A์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๊ฒŒ์‹œ๊ธ€ ๊ธฐ๋Šฅ๋งŒ, B์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๋Œ“๊ธ€ ๊ธฐ๋Šฅ๋งŒ ์ฒ˜๋ฆฌํ•œ๋‹ค.
    ํ•„์š”ํ•˜๋‹ค๋ฉด, DB๋„ ๊ฐ ๊ธฐ๋Šฅ ๋ณ„๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๋ถ„๋ฆฌ๋œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ํ•˜๋‚˜์˜ ์„œ๋น„์Šค๋ฅผ ์ด๋ฃจ๋ ค๋ฉด, ์„œ๋กœ ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์ด ํ•„์š”ํ•˜๋‹ค.
    API๋ฅผ ํ†ตํ•ด ์ง์ ‘์ ์œผ๋กœ ํ†ต์‹ ํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ๋ฉ”์„ธ์ง€(์ด๋ฒคํŠธ)๋ฅผ ํ†ตํ•ด ๊ฐ„์ ‘์ ์œผ๋กœ ํ†ต์‹ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
    ๊ฐ ๋ฐฉ์‹์ด ๋ฌด์—‡์ด๊ณ  ์žฅ๋‹จ์ ์€ ๋ฌด์—‡์ผ์ง€, ์–ด๋–ป๊ฒŒ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ์„์ง€๋Š” ์ฐจ๊ทผ์ฐจ๊ทผ ์•Œ์•„๋ณด์ž
  • ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ณ๋ž€? :
    ์‹œ์Šคํ…œ์˜ ๊ตฌ์กฐ๋‚˜ ์„ค๊ณ„ ๋ฐฉ์‹ (์†Œํ”„ํŠธ์›จ์–ด ์‹œ์Šคํ…œ์ด ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑ๋˜๊ณ  ๋™์ž‘ํ• ์ง€์— ๋Œ€ํ•œ '์„ค๊ณ„ ๊ตฌ์กฐ๋„')
    ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด: ์ด ์‹œ์Šคํ…œ, ์–ด๋–ค ๋ถ€ํ’ˆ๋“ค๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๊ณ , ๊ทธ ๋ถ€ํ’ˆ๋“ค์ด ์–ด๋–ป๊ฒŒ ์„œ๋กœ ์—ฐ๊ฒฐ๋ผ ์žˆ๋Š”์ง€"์— ๋Œ€ํ•œ ์ฒญ์‚ฌ์ง„
    ํ™•์žฅ์„ฑ, ์œ ์ง€๋ณด์ˆ˜์„ฑ, ์„ฑ๋Šฅ ๋“ฑ ํฐ ์˜ํ–ฅ
    ๐Ÿ“ ๋Œ€ํ‘œ์ ์ธ ์•„ํ‚คํ…์ฒ˜
    Monoloithic Architecture vs Microservice Architecture
  • Monoloithic Architecture
Monoloithic Architecture

๋ชจ๋“  ๊ธฐ๋Šฅ์ด ํ•˜๋‚˜์˜ ํฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋‹ด๊ฒจ ์žˆ์Œ

- ๊ตฌ์กฐ: ํ•˜๋‚˜์˜ ํ”„๋กœ์ ํŠธ๋กœ ๋ชจ๋“  ๊ธฐ๋Šฅ ์ฒ˜๋ฆฌ

- ์žฅ์ : ๋‹จ์ˆœํ•˜๊ณ  ๋น ๋ฅด๊ฒŒ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ

- ๋‹จ์ : ๊ธฐ๋Šฅ์ด ๋งŽ์•„์ง€๋ฉด ์œ ์ง€๋ณด์ˆ˜ ์–ด๋ ค์›€

→ ex) ํšŒ์›๊ฐ€์ž…, ๊ฒŒ์‹œํŒ, ๋Œ“๊ธ€, ๊ฒฐ์ œ ๊ธฐ๋Šฅ → ๋ชจ๋‘ ํ•˜๋‚˜์˜ Spring Boot ์•ฑ์— ์žˆ์Œ

๋ชจ๋†€๋ฆฌํ‹ฑ ์•„ํ‚คํ…์ฒ˜์—์„œ ๊ฒŒ์‹œ๊ธ€ ๊ธฐ๋Šฅ๋งŒ ํŠธ๋ž˜ํ”ฝ์ด ์ƒ์Šนํ•œ ๊ฒฝ์šฐ: Scale-Up


๋ถ€ํ•˜ ๊ฐ๋‹น์„ ์œ„ํ•ด Scale-Up (๋‹จ์ผ ์„œ๋ฒ„์˜ ์„ฑ๋Šฅ ํ–ฅ์ƒ : ์„œ๋ฒ„๋ฅผ ๋ฌดํ•œ์ • Scale-Upํ•˜๋Š” ๊ฒƒ์€ ํ•œ๊ณ„๊ฐ€ ์žˆ๊ณ  ๋น„์šฉ๋„ ๋งค์šฐ ๋น„์‹ธ๋‹ค.)์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

๋ชจ๋†€๋ฆฌํ‹ฑ ์•„ํ‚คํ…์ฒ˜์—์„œ ๊ฒŒ์‹œ๊ธ€ ๊ธฐ๋Šฅ๋งŒ ํŠธ๋ž˜ํ”ฝ์ด ์ƒ์Šนํ•œ ๊ฒฝ์šฐ: Scale-Out


๊ทธ ๋‹ค์Œ์€ Scale-Out (์ˆ˜ํ‰์  ํ™•์žฅ : ์„œ๋ฒ„ ์—ฌ๋Ÿฌ๋Œ€์— ๋ถ€ํ•˜ ๋ถ„์‚ฐ)์„ ๊ณ ๋ คํ•ด ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
Scale-Out ๊ตฌ์„ฑ์„ ๋ณด๋ฉด ๋ฆฌ์†Œ์Šค๊ฐ€ ๋‚ญ๋น„๋˜๋Š” ์ง€์ ์ด ์žˆ๋‹ค. (๋ถ„๋ช… ๊ฒŒ์‹œ๊ธ€ ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ๋ถ€๋‹ด๋งŒ ๋Œ€์‘ํ•ด์ฃผ๋ฉด ์ถฉ๋ถ„ํ–ˆ์„ ๊ฒƒ์ด๋‹ค.)
Monolithic Architecture์—์„œ๋Š” ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ๋‹จ์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ํ•จ๊ป˜ ๋ฐฐํฌ๋˜๋ฏ€๋กœ, ๊ฒŒ์‹œ๊ธ€๊ณผ ๊ด€๋ จ ์—†๋Š” ๋‹ค๋ฅธ ๊ธฐ๋Šฅ๋“ค๋„ ๊ฐ™์ด ๋ฐฐํฌ๋˜์–ด์„œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ ์œ ํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.

  • Microservice Architecture
    ์‹œ์Šคํ…œ์ด ์ž‘๊ณ  ๋…๋ฆฝ์ ์ธ ์„œ๋น„์Šค(Microservice)๋กœ ๊ตฌ์„ฑ
    ๊ฐ ์„œ๋น„์Šค๋Š” ๋‹จ์ผ ๊ธฐ๋Šฅ์„ ๋‹ด๋‹นํ•˜๋ฉฐ, ๋…๋ฆฝ์  ๋ฐฐํฌ ๊ฐ€๋Šฅ
    ์šฐ๋ฆฌ์˜ ์‹œ์Šคํ…œ์—์„œ๋Š”,
    ๊ฒŒ์‹œ๊ธ€, ๋Œ“๊ธ€, ์ข‹์•„์š”, ์กฐํšŒ์ˆ˜, ์ธ๊ธฐ๊ธ€, ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ
    ๊ฐ ์š”๊ตฌ์‚ฌํ•ญ(๊ธฐ๋Šฅ)์„ ๋…๋ฆฝ๋œ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋กœ ๋ณผ ์ˆ˜๋„ ์žˆ๋‹ค.
    ์„œ๋น„์Šค ๋‹จ์œ„๋กœ ์œ ์—ฐํ•œ ํ™•์žฅ ๊ฐ€๋Šฅ
    ํ•˜์ง€๋งŒ, ์„œ๋น„์Šค ๊ฐ„ ๋ณต์žกํ•œ ํ†ต์‹  ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ํ•„์š”
    ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ฐ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ์˜ ์–ด๋ ค์›€
    ์–ด๋ ค์šด ๊ฐœ๋ฐœ ๋‚œ์ด๋„
  • Docker
    Docker๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ปจํ…Œ์ด๋„ˆ(container)๋ผ๋Š” ๋‹จ์œ„๋กœ ํฌ์žฅํ•ด์„œ, ์–ด๋””์„œ๋“  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ์ˆ 
    : ์–ด? ๋‚ด ์ปดํ“จํ„ฐ์—์„œ๋Š” ๋˜๋Š”๋ฐ? ๊ทผ๋ฐ ๋‹ˆ ์ปดํ“จํ„ฐ๋ž‘ ๋‚˜๋ž‘ ํ™˜๊ฒฝ์ด ๋‹ค๋ฅด์ž–์•„.. (OS, ์„ค์ •, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „ ๋“ฑ๋“ฑ)
    ๊ทธ๋ž˜์„œ ๊ฐœ๋ฐœํ™˜๊ฒฝ๊ณผ ์‹คํ–‰ํ™˜๊ฒฝ์„ ๋˜‘๊ฐ™์ด ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ–ˆ๋‹ค.
    → ์ปจํ…Œ์ด๋„ˆ ์•ˆ์— ์•ฑ๊ณผ ์‹คํ–‰ํ™˜๊ฒฝ์„ ํ†ต์งธ๋กœ ๋‹ด์•„์„œ ๋ฐฐํฌ

๐Ÿ“ ๊ธฐ๋Šฅ

 

์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ๊ทธ ํ™˜๊ฒฝ์„ ๋ถ„๋ฆฌ๋œ ๊ณต๊ฐ„์—์„œ ์‹คํ–‰
์ด๋ฏธ์ง€ ์ƒ์„ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰์— ํ•„์š”ํ•œ ๋ชจ๋“  ๊ฒƒ(OS, ์ฝ”๋“œ ๋“ฑ)์„ ์ด๋ฏธ์ง€๋กœ ํŒจํ‚ค์ง•
์ด์‹์„ฑ ๋ณด์žฅ ์–ด๋””์—์„œ๋“  ๋™์ผํ•˜๊ฒŒ ์‹คํ–‰ ๊ฐ€๋Šฅ (๋ฆฌ๋ˆ…์Šค/์œˆ๋„์šฐ/Mac/ํด๋ผ์šฐ๋“œ ๋“ฑ)
๊ฒฝ๋Ÿ‰ ์‹คํ–‰ ๊ฐ€์ƒ๋จธ์‹ ๋ณด๋‹ค ๊ฐ€๋ณ๊ณ  ๋น ๋ฆ„

๐Ÿ“ ํŽธ๋ฆฌํ•œ ์ 

โ˜๏ธ ์–ด๋””์„œ๋“  ๋™์ผํ•˜๊ฒŒ ์‹คํ–‰ ํ™˜๊ฒฝ์ด ๋‹ฌ๋ผ๋„ Docker ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ ๋™์ผ
๐Ÿ› ๏ธ ๋น ๋ฅธ ๊ฐœ๋ฐœ ๋ฐ ํ…Œ์ŠคํŠธ ๊ฐœ๋ฐœ์ž๊ฐ€ Dockerfile๋กœ ์‹คํ–‰ ํ™˜๊ฒฝ์„ ํ†ต์ผ
๐Ÿš€ ๋ฐฐํฌ ์ž๋™ํ™”์™€ CI/CD ์šฉ์ด ๋นŒ๋“œ/๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ์— ์‰ฝ๊ฒŒ ํ†ตํ•ฉ๋จ
๐Ÿ’ก ๊ฐ€์ƒ๋จธ์‹ ๋ณด๋‹ค ๊ฐ€๋ณ๊ณ  ๋น ๋ฆ„ OS ์ „์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ, ์•ฑ ์ˆ˜์ค€๋งŒ ๊ฒฉ๋ฆฌ

 

  • Redis
    ๐Ÿ“Œ 1. ๊ฐœ๋…
    Redis๋Š” ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ
    ์ •ํ™•ํžˆ๋Š” Key-Value ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ด์ž, ์บ์‹œ ์„œ๋ฒ„์ด์ž, ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ์—ญํ• ๋„ ๊ฐ€๋Šฅ.
    ๐Ÿ“Œ 2. ๋“ฑ์žฅ ๋ฐฐ๊ฒฝ
    ๊ธฐ์กด RDB๋‚˜ ํŒŒ์ผ ๊ธฐ๋ฐ˜ DB๋Š” ์†๋„๊ฐ€ ๋А๋ ค์„œ ๊ณ ์†์˜ ์ฝ๊ธฐ/์“ฐ๊ธฐ ์„ฑ๋Šฅ์ด ํ•„์š”ํ•œ ์ƒํ™ฉ์—์„œ๋Š” ์ ํ•ฉํ•˜์ง€ ์•Š์•˜์Œ.
    → ๊ทธ๋ž˜์„œ ๋ฉ”๋ชจ๋ฆฌ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ด์„œ ๋น ๋ฅด๊ฒŒ ์ฝ๊ณ  ์“ฐ๋Š” ์ €์žฅ์†Œ๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค.
    → Redis๋Š” ์ด๋ฅผ ์œ„ํ•ด ๋“ฑ์žฅ
    ๐Ÿ“Œ 3. ๊ธฐ๋Šฅ
Key-Value ์ €์žฅ ๋ฌธ์ž์—ด, ๋ฆฌ์ŠคํŠธ, ํ•ด์‹œ, ์…‹ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ตฌ์กฐ ์ง€์›
TTL(๋งŒ๋ฃŒ์‹œ๊ฐ„) ์ž๋™ ์บ์‹œ ๋งŒ๋ฃŒ ๊ธฐ๋Šฅ
Pub/Sub ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰/๊ตฌ๋… ๊ธฐ๋Šฅ
Atomic ์—ฐ์‚ฐ ๋™์‹œ์„ฑ ๋ฌธ์ œ ์—†์ด ์•ˆ์ „ํ•œ ์—ฐ์‚ฐ ๊ฐ€๋Šฅ
Persistence ์˜ต์…˜ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ๋˜์ง€๋งŒ ๋””์Šคํฌ์—๋„ ์ €์žฅ ๊ฐ€๋Šฅ (RDB, AOF ๋ฐฉ์‹)

 

  • Kafka
    ๐Ÿ“Œ 1. ๊ฐœ๋…
    Kafka๋Š” ๋ถ„์‚ฐํ˜• ๋ฉ”์‹œ์ง€ ์ŠคํŠธ๋ฆฌ๋ฐ ํ”Œ๋žซํผ.
    ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ „๋‹ฌํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ณ ์„ฑ๋Šฅ ๋ฉ”์‹œ์ง€ ํ ์‹œ์Šคํ…œ.
    ๐Ÿ“Œ 2. ๋“ฑ์žฅ ๋ฐฐ๊ฒฝ
    ๊ธฐ์กด ๋ฉ”์‹œ์ง€ ํ ์‹œ์Šคํ…œ(RabbitMQ ๋“ฑ)์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•œ๊ณ„๋ฅผ ๊ฐ€์กŒ์Œ:
    - ๋Œ€์šฉ๋Ÿ‰ ์ฒ˜๋ฆฌ์— ์–ด๋ ค์›€
    - ๋ฐ์ดํ„ฐ ์†์‹ค ๊ฐ€๋Šฅ์„ฑ
    - ๋А๋ฆฐ ์†Œ๋น„์ž ์ฒ˜๋ฆฌ
    → LinkedIn์ด ๋น…๋ฐ์ดํ„ฐ ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ์„ ์œ„ํ•ด Kafka๋ฅผ ๋งŒ๋“ค์—ˆ๊ณ , ์ง€๊ธˆ์€ ๊ฑฐ์˜ ํ‘œ์ค€ ์‹ค์‹œ๊ฐ„ ๋ฉ”์‹œ์ง€ ํ”Œ๋žซํผ์ด ๋๋‹ค.
    ๐Ÿ“Œ 3. ํ•ต์‹ฌ ๊ธฐ๋Šฅ
Publisher → Broker → Subscriber ๊ตฌ์กฐ ๋ฐ์ดํ„ฐ ๋ฐœํ–‰ → Kafka์— ์ €์žฅ → ๊ตฌ๋…์ž๊ฐ€ ์ฝ์Œ
Topic ๊ธฐ๋ฐ˜ ๋ฉ”์‹œ์ง€ ํ ๋ฐ์ดํ„ฐ๋Š” Topic์ด๋ผ๋Š” ๋‹จ์œ„๋กœ ๋‚˜๋ˆ  ๊ด€๋ฆฌ
๋‚ด๊ตฌ์„ฑ ๋†’์€ ๋กœ๊ทธ ์ €์žฅ ๋””์Šคํฌ์— ๋กœ๊ทธ๋ฅผ ์ €์žฅํ•ด์„œ ์†์‹ค ์—†์ด ์ „๋‹ฌ
๋ถ„์‚ฐ ์ฒ˜๋ฆฌ ์—ฌ๋Ÿฌ ๋ธŒ๋กœ์ปค๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜๋ˆ ์„œ ์ €์žฅํ•˜๊ณ  ์ฒ˜๋ฆฌ
Consumer ๊ทธ๋ฃน ์—ฌ๋Ÿฌ Consumer๊ฐ€ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ

 

  • ์ธ๋ฉ”๋ชจ๋ฆฌ ์ €์žฅ์†Œ๋ž€?
    ๋ฐ์ดํ„ฐ๋ฅผ ํ•˜๋“œ๋””์Šคํฌ ๊ฐ™์€ ์ €์žฅ์žฅ์น˜๊ฐ€ ์•„๋‹ˆ๋ผ, RAM(๋ฉ”์ธ ๋ฉ”๋ชจ๋ฆฌ)์— ์ €์žฅํ•˜๋Š” ์ €์žฅ์†Œ
    ์™œ RAM(๋ฉ”๋ชจ๋ฆฌ)์— ์ €์žฅํ•˜๋‚˜?
    RAM์€ ๋น ๋ฅด๊ณ  ํ•˜๋“œ๋Š” ๋А๋ฆฌ๊ธฐ ๋•Œ๋ฌธ.
    → ๊ทธ๋ž˜์„œ ์†๋„๊ฐ€ ์ค‘์š”ํ•œ ์‹œ์Šคํ…œ์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ RAM์— ์ €์žฅํ•ด์„œ ๋น ๋ฅด๊ฒŒ ์ฝ๊ณ  ์“ฐ๋Š” ๋ฐฉ์‹์„ ์“ด๋‹ค.
    ์ด๊ฒŒ ๋ฐ”๋กœ ์ธ๋ฉ”๋ชจ๋ฆฌ ์ €์žฅ์†Œ

    Redis ๊ฐ™์€ ์ธ๋ฉ”๋ชจ๋ฆฌ ์ €์žฅ์†Œ๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜๋Š” ๋ณ„๋„ ํ”„๋กœ์„ธ์Šค.
    ์ด ํ”„๋กœ์„ธ์Šค๋Š” ์„œ๋ฒ„์˜ ๋ฉ”๋ชจ๋ฆฌ(RAM)์— ๋ฐ์ดํ„ฐ๋ฅผ ์˜ฌ๋ ค์„œ ๋ณด๊ด€.
    ์ฆ‰, "๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ฆฐ๋‹ค"๋Š” ๊ฑด ๋””์Šคํฌ๊ฐ€ ์•„๋‹Œ RAM์—์„œ ๋ฐ”๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š”๋‹ค๋Š” ๋œป.

    ๐Ÿ“ ๊ทธ๋ž˜์„œ ์†๋„๊ฐ€ ์—„์ฒญ ๋น ๋ฅธ ๋Œ€์‹ , ์„œ๋ฒ„๊ฐ€ ๊บผ์ง€๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋‚ด์šฉ์€ ๋‚ ์•„๊ฐ„๋‹ค.
    → ํ•˜์ง€๋งŒ Redis๋Š” ๋””์Šคํฌ์— ๋ฐฑ์—…์„ ๋‚จ๊ฒจ์„œ ๋ณต๊ตฌ ๊ฐ€๋Šฅํ•˜๊ฒŒ๋„ ํ•  ์ˆ˜ ์žˆ์Œ

  • RDB vs Redis ๊ตฌ์กฐ ์ฐจ์ด
    ๐Ÿ—ƒ๏ธ RDB (Relational Database, ์˜ˆ: MySQL, MariaDB)
๊ตฌ์กฐ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜, ํ–‰-์—ด ๊ตฌ์กฐ
์ฟผ๋ฆฌ ๋ฐฉ์‹ SQL ์‚ฌ์šฉ (SELECT * FROM user WHERE id = 1)
๋ชฉ์  ์ •ํ˜•ํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๊ตฌ ์ €์žฅ
์˜ˆ์‹œ ์œ ์ € ํ…Œ์ด๋ธ”, ๊ฒŒ์‹œ๊ธ€ ํ…Œ์ด๋ธ” ๋“ฑ

     

      โšก Redis (In-memory Key-Value Store)

๊ตฌ์กฐ Key → Value ๊ตฌ์กฐ (์˜ˆ: "session:user123" → "{username: 'kim'}")
์ฟผ๋ฆฌ ๋ฐฉ์‹ ๋ช…๋ น์–ด ๊ธฐ๋ฐ˜ (GET key, SET key value)
๋ชฉ์  ๋น ๋ฅธ ์ฝ๊ธฐ/์“ฐ๊ธฐ, ์บ์‹ฑ, ์„ธ์…˜ ๋“ฑ

 

  • ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋ž€?
    ํ•œ ์‹œ์Šคํ…œ์—์„œ ๋‹ค๋ฅธ ์‹œ์Šคํ…œ์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ค‘๊ฐ„์—์„œ ์ „๋‹ฌํ•ด์ฃผ๋Š” ์ค‘๊ณ„์ž ์—ญํ• 
    ๋Œ€ํ‘œ์ ์ธ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค: Kafka, RabbitMQ, Redis (๊ฒฝ๋Ÿ‰์šฉ)

    ๐Ÿ“จ Redis์˜ Pub/Sub ๊ธฐ๋Šฅ์ด๋ž€?
    Publisher๊ฐ€ ์–ด๋–ค ์ฑ„๋„์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด, Subscriber๊ฐ€ ๊ทธ ์ฑ„๋„์„ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋‹ค๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ›์Œ

# A ์‹œ์Šคํ…œ (Publisher)
PUBLISH news "์ƒˆ ๊ฒŒ์‹œ๊ธ€ ์˜ฌ๋ผ์™”์Šต๋‹ˆ๋‹ค!"

# B ์‹œ์Šคํ…œ (Subscriber)
SUBSCRIBE news
→ "์ƒˆ ๊ฒŒ์‹œ๊ธ€ ์˜ฌ๋ผ์™”์Šต๋‹ˆ๋‹ค!" ์ˆ˜์‹ 

 

        โœ… Redis๊ฐ€ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋กœ ์“ฐ์ผ ๋•Œ์˜ ํŠน์ง•

์žฅ์  ๋น ๋ฅด๊ณ  ์„ค์ • ์‰ฌ์›€, ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ์— ์ ํ•ฉ
๋‹จ์  ๋ฉ”์‹œ์ง€ ๋ˆ„๋ฝ ๊ฐ€๋Šฅ (์ง€์† ์ €์žฅ ์•ˆ ํ•จ), ์˜์†์„ฑ ์—†์Œ
์‚ฌ์šฉ ์˜ˆ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…, ์•Œ๋ฆผ, ๊ฒฝ๋Ÿ‰ ์ด๋ฒคํŠธ ์ „๋‹ฌ
  • ๋‚ด๊ฐ€ ๋“ฃ๊ณ  ์žˆ๋Š” ๊ฐ•์˜์—์„œ MySQL, Redis, Kakfa๋ฅผ ์„ค์น˜ํ•˜๊ธฐ ์œ„ํ•ด Docker๋ฅผ ์‚ฌ์šฉํ• ๊ฑฐ ๋ผ๋Š”๋ฐ ๋ฌด์Šจ ์˜๋ฏธ?
    → ๊ทธ๋Ÿฌ๋‹ˆ๊น ๋‚ด ํ˜ธ์ŠคํŠธ PC์™€ ์™„์ „ํžˆ ๋ถ„๋ฆฌ๋œ ์ƒˆ๋กœ์šด ๊ฐ€์ƒํ™˜๊ฒฝ์—์„œ ์„ธํŒ…ํ•˜๊ฒ ๋‹ค๋Š”๊ฑฐ์ง€? ๊ทธ๋Ÿฌ๋‹ˆ๊น ๋‚ด ๊ธฐ์กด ์„ค์ •๊ณผ ์ถฉ๋Œ๋„ ์—†๊ณ 
  • docker์—์„œ ํ”„๋กœ๊ทธ๋žจ์€ ์ด๋ฏธ์ง€๋กœ ํŒจํ‚ค์ง•๋˜๊ณ , ์ปจํ…Œ์ด๋„ˆ๋กœ ์‹คํ–‰๋œ๋‹ค๋Š”๋ฐ ๋ฌด์Šจ ์˜๋ฏธ์ง€?
    ํ”„๋กœ๊ทธ๋žจ-> ์ด๋ฏธ์ง€ ๊ณผ ํ”„๋กœ์„ธ์Šค -> ์ปจํ…Œ์ด๋„ˆ๋กœ ์ดํ•ดํ•˜๋ฉด ๋˜๊ฒ ๋„ค ๊ทธ์ง€?
  • ๋ฉ€ํ‹ฐ ๋ชจ๋“ˆ ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ์„ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ธํŒ…ํ–ˆ๋Š”๋ฐ ์˜๋ฏธ๋“ค์„ ์ž˜ ๋ชจ๋ฅด๊ฒ ๋‹ค.
    ์˜๋ฏธ๋ฅผ ์ข€ ์„ค๋ช…์„ ์ข€ ํ•ด์ค˜

    service/article/build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

 

        build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.2'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'kuke'
version = '1'

allprojects {
    repositories {
        mavenCentral()
    }

    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    java {
        sourceCompatibility = JavaVersion.VERSION_21
        targetCompatibility = JavaVersion.VERSION_21
    }

    dependencies {
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'

        testCompileOnly 'org.projectlombok:lombok'
        testAnnotationProcessor 'org.projectlombok:lombok'

        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }

    tasks.named('test') {
        useJUnitPlatform()
    }
}

 

      settings.gradle

rootProject.name = 'kuke-board'

include 'common'
include 'service'
include 'service:article'

 

  • ์ˆ˜์ง ์ƒค๋”ฉ & ์ˆ˜ํ‰ ์ƒค๋”ฉ
    ์ˆ˜์ง ์ƒค๋”ฉ (Vertical Sharding) : ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง์œผ๋กœ ๋ถ„ํ• ํ•˜๋Š” ๋ฐฉ์‹ (์ปฌ๋Ÿผ ๋‹จ์œ„) : PK ์‹๋ณ„์ž๋ฅผ ํ™œ์šฉํ•ด์„œ ์ขŒ์ธก์ƒค๋“œ, ์šฐ์ธก์ƒค๋“œ ๋ถ„์‚ฐ
    ์ˆ˜ํ‰ ์ƒค๋”ฉ (Horizontal Sharding) : ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜ํ‰์œผ๋กœ ๋ถ„ํ• ํ•˜๋Š” ๋ฐฉ์‹ (ํ–‰ ๋‹จ์œ„) : 1~5,000, 5,001~10,000
  • 2PC (2-Phase Commit)
    ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜์—์„œ ์—ฌ๋Ÿฌ DB(์ƒค๋“œ)์— ์ผ๊ด€๋œ ์ปค๋ฐ‹/๋กค๋ฐฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋กœํ† ์ฝœ.
    Phase 1 : Prepare
    Coordinator๊ฐ€ ๊ฐ ์ฐธ์—ฌ DB์—๊ฒŒ "์ด๊ฑฐ ์ปค๋ฐ‹ํ•ด๋„ ๋˜๋ƒ?"๋ผ๊ณ  ๋ฌผ์–ด๋ด„.
    ๊ฐ DB๋Š” "OK, ์ค€๋น„๋จ" or "NO, ๋ฌธ์ œ ์žˆ์Œ" ์‘๋‹ต.
    Phase 2 : Commit or Rollback
    ๋ชจ๋‘ OK๋ฉด → ๋ชจ๋“  DB์— Commit
    ํ•˜๋‚˜๋ผ๋„ NO๋ฉด → ๋ชจ๋“  DB์— Rollback
๊ตฌ๋ถ„ ์ˆ˜์ง ์ƒค๋”ฉ (Vertical) ์ˆ˜ํ‰ ์ƒค๋”ฉ (Horizontal)
๊ฐœ๋… ์ปฌ๋Ÿผ(์†์„ฑ) ๋‹จ์œ„ ๋ถ„๋ฆฌ
ex) article ํ…Œ์ด๋ธ”์„ ์ ˆ๋ฐ˜์€ A DB์—, ์ ˆ๋ฐ˜์€ B DB์— ์ €์žฅ
ํ–‰(row) ๋‹จ์œ„ ๋ถ„๋ฆฌ
ex) article_id 11000์€ A DB, 10012000์€ B DB
ํŠธ๋žœ์žญ์…˜ ๋ณต์žก๋„ ๋†’์Œ
ํ•˜๋‚˜์˜ ๋…ผ๋ฆฌ ํ…Œ์ด๋ธ”์ด ์—ฌ๋Ÿฌ DB์— ๊ฑธ์ณ ์žˆ์Œ → ์กฐ์ธ, ํŠธ๋žœ์žญ์…˜ ๋งค๋ฒˆ ๋ถ„์‚ฐ๋จ
์ƒ๋Œ€์ ์œผ๋กœ ๋‚ฎ์Œ
ํ•˜๋‚˜์˜ ๋กœ์šฐ๋Š” ํ•˜๋‚˜์˜ ์ƒค๋“œ์—๋งŒ ์กด์žฌํ•˜๋ฏ€๋กœ, ๋Œ€๋ถ€๋ถ„ ๋‹จ์ผ DB ํŠธ๋žœ์žญ์…˜
์žฅ์  ์ปฌ๋Ÿผ ๋‹จ์œ„๋กœ ๋ถ„๋ฆฌ → ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๊ฐ€๋Šฅ ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ, ํ™•์žฅ์„ฑ ↑
๋‹จ์  JOIN/UPDATE ์–ด๋ ค์›€
ํŠธ๋žœ์žญ์…˜ ๋ณต์žก
์ƒค๋”ฉ ํ‚ค ์„ค๊ณ„ ์–ด๋ ค์›€
  • Range-based Sharding (๋ฒ”์œ„ ๊ธฐ๋ฐ˜ ์ƒค๋”ฉ)
    ๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ๊ฐ’(Shard Key)์˜ ํŠน์ • ๋ฒ”์œ„์— ๋”ฐ๋ผ ๋ถ„ํ• ํ•˜๋Š” ๊ธฐ๋ฒ•
    ex. ์ขŒ์ธก ์ƒค๋“œ article_id : 1 ~ 5,000, ์šฐ์ธก ์ƒค๋“œ article_id : 5,001 ~ 10,000

Shard Key : article_id

๋ฒ”์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์œ ๋ฆฌ

→ article_id 100๋ถ€ํ„ฐ 30๊ฐœ ์กฐํšŒํ•œ๋‹ค๋ฉด ์ขŒ์ธก ์ƒค๋“œ์—์„œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ

๋ฐ์ดํ„ฐ ์ ๋ฆผ ํ˜„์ƒ ๋ฐœ์ƒ ๊ฐ€๋Šฅ
→ ๋ฐ์ดํ„ฐ๊ฐ€ 6,000๊ฐœ ์žˆ๋‹ค๋ฉด, ์šฐ์ธก ์ƒค๋“œ์—๋Š” 1,000๊ฐœ๋งŒ ์ €์žฅ

  • Hash-based Sharding (ํ•ด์‹œ ๊ธฐ๋ฐ˜ ์ƒค๋”ฉ)
    ๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ๊ฐ’(Shard Key)์˜ ํ•ด์‹œ ํ•จ์ˆ˜์— ๋”ฐ๋ผ ๋ถ„ํ• ํ•˜๋Š” ๊ธฐ๋ฒ•
    ex. hash_function = article_id → article_id % 2 ๋ผ๋ฉด?
    ์ขŒ์ธก ์ƒค๋“œ article_id = 1,3,5 ...
    ์šฐ์ธก ์ƒค๋“œ article_id = 2,4,6 ...

Shard Key : article_id % 2

๋ฒ”์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋ถˆ๋ฆฌ
๋ฐ์ดํ„ฐ ์ ๋ฆผ ํ˜„์ƒ ๋ฐฉ์ง€ ๊ฐ€๋Šฅ

  • Directory-based Sharding (๋””๋ ‰ํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ์ƒค๋”ฉ)
    ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ค ์ƒค๋“œ์— ์ €์žฅํ• ์ง€๋ฅผ "์ƒค๋”ฉ ํ‚ค ๊ธฐ๋ฐ˜ ํ•ด์‹œ"๋กœ ์ž๋™ ๊ณ„์‚ฐํ•˜๋Š” ๋Œ€์‹ ,
    ๋ช…์‹œ์ ์ธ ๋งคํ•‘ ํ…Œ์ด๋ธ”(๋””๋ ‰ํ† ๋ฆฌ)์„ ์‚ฌ์šฉํ•ด์„œ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹
  • ์ด ์™ธ ๋‹ค์–‘ํ•œ ์ƒค๋”ฉ ๊ธฐ๋ฒ•์ด ์กด์žฌ
  • ๋ฌผ๋ฆฌ์  ์ƒค๋“œ & ๋…ผ๋ฆฌ์  ์ƒค๋“œ

Shard ํ™•์žฅ ์‹œ : ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๋“ฑ ์ƒˆ๋กœ์šด Shard์˜ ์ •๋ณด๋ฅผ ์•Œ์•„์•ผ ํ•œ๋‹ค.

DB์˜ ํ™•์žฅ (2Shard → 4Shard)๋กœ ์ธํ•ด Client์˜ ์ˆ˜์ •๋„ ํ•„์š”ํ–ˆ๋‹ค. Client์™€ DB๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๊ตฌ์„ฑ ๋ฐ ์šด์˜๋  ์ˆ˜ ์žˆ๋Š” ์‹œ์Šคํ…œ์ด๋‹ค.
Client์˜ ์ˆ˜์ • ์—†์ด DB๋งŒ ์œ ์—ฐํ•˜๊ฒŒ ํ™•์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์—†๋Š”๊ฐ€?
→ ๋…ผ๋ฆฌ์  ์ƒค๋“œ์˜ ๋“ฑ์žฅ

๋…ผ๋ฆฌ์  ์ƒค๋“œ

์‹ค์ œ ๋ฌผ๋ฆฌ์  ์ƒค๋“œ์˜ ๊ฐœ์ˆ˜์™€ ๊ด€๊ณ„์—†์ด, ๊ฐœ๋…์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„ํ• ํ•˜๋Š” ๊ฐ€์ƒ์˜ ์ƒค๋“œ 4๊ฐœ๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ž.
๋ฌผ๋ฆฌ์  ์ƒค๋“œ๋Š” 2๊ฐœ์ด์ง€๋งŒ, Client๋Š” 4๊ฐœ์˜ ์ƒค๋“œ๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ๋ถ„ํ• ํ•˜๋Š” ๊ฒƒ.
์ด๋Ÿฐ ๊ฐ€์ƒ์˜ ์ƒค๋“œ๋ฅผ ๋…ผ๋ฆฌ์  ์ƒค๋“œ๋ผ๊ณ  ํ•œ๋‹ค. ๋ฌผ๋ฆฌ์ ์œผ๋กœ 2๊ฐœ์˜ ์ƒค๋“œ๋กœ ๋ถ„์‚ฐ๋˜์ง€๋งŒ, Client๋Š” 4๊ฐœ์˜ ์ƒค๋“œ๋กœ ๋ถ„์‚ฐ๋œ ๊ฒƒ์œผ๋กœ ์ธ์ง€ํ•œ๋‹ค.
Client๋Š” ๋…ผ๋ฆฌ์  ์ƒค๋“œ์˜ ์ ‘๊ทผ ๋ฐฉ๋ฒ•์œผ๋กœ๋งŒ DB์— ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
* hash_function = article_id → article_id % 4
๊ทธ๋ ‡๋‹ค๋ฉด Client๊ฐ€ ์š”์ฒญํ•œ ๋…ผ๋ฆฌ์  ์ƒค๋“œ๊ฐ€ ์–ด๋–ค ๋ฌผ๋ฆฌ์  ์ƒค๋“œ์— ์†ํ•ด์žˆ๋Š”์ง€ ์•Œ์•„์•ผํ•œ๋‹ค. ๊ทธ๋ž˜์•ผ ์ง์ ‘์ ์œผ๋กœ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋‹ค.
์ด๋Ÿฌํ•œ ๋ผ์šฐํŒ…์„ ์œ„ํ•œ ๋ผ์šฐํ„ฐ๋Š” DB์‹œ์Šคํ…œ ๋‚ด๋ถ€ ๋˜๋Š” DB์™€ Client๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š” ์ค‘๊ฐ„์— ๋…๋ฆฝ์ ์œผ๋กœ ๋งŒ๋“ค์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค.


์ดˆ๊ธฐ ์ƒํƒœ:

๋…ผ๋ฆฌ ์ƒค๋“œ ๋ฌผ๋ฆฌ ์ƒค๋“œ
0, 1 DB A
2, 3 DB B

→ ๋…ผ๋ฆฌ์  ์ƒค๋“œ 4๊ฐœ, ๋ฌผ๋ฆฌ์  DB 2๊ฐœ์— ๋‚˜๋ˆ  ์ €์žฅ
→ ํด๋ผ์ด์–ธํŠธ๋Š” ์—ฌ์ „ํžˆ articleId % 4๋งŒ ํ•˜๋ฉด ๋จ

 

DB๋ฅผ ํ™•์žฅํ•œ๋‹ค๊ณ  ๊ฐ€์ •:

๋…ผ๋ฆฌ ์ƒค๋“œ ๋ฌผ๋ฆฌ ์ƒค๋“œ
0 DB A
1 DB B
2 DB C โฌ…๏ธ ์ƒˆ๋กœ ์ถ”๊ฐ€
3 DB D โฌ…๏ธ ์ƒˆ๋กœ ์ถ”๊ฐ€

ํด๋ผ์ด์–ธํŠธ๋Š” ์—ฌ์ „ํžˆ articleId % 4๋งŒ ํ•˜๋ฉด ๋จ → ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์Œ

* ๋…ผ๋ฆฌ ์ƒค๋“œ์™€ ๋ฌผ๋ฆฌ ์ƒค๋“œ ๋งคํ•‘๋งŒ ๋ผ์šฐํ„ฐ๊ฐ€ ๋ฐ”๊ฟ”์ฃผ๋ฉด ๋จ

  • ๋ ˆํ”Œ๋ฆฌ์นด
     Primary & Replica / Leader & Follower / Master & Slave / Main & Standby

๋ณต์ œ

์ด๋Ÿฐ ๋ณต์ œ๋Š” ๋™๊ธฐ์  ๋˜๋Š” ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋  ์ˆ˜ ์žˆ๋‹ค.
Synchronous(๋™๊ธฐ์ ) : ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜๋‚˜, ์“ฐ๊ธฐ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋œ๋‹ค.

Asynchronous(๋น„๋™๊ธฐ์ ) : ์“ฐ๊ธฐ ์„ฑ๋Šฅ์ด ์œ ์ง€๋˜๋‚˜, ๋ณต์ œ๋ณธ์— ์ตœ์‹  ๋ฐ์ดํ„ฐ๊ฐ€ ์ฆ‰์‹œ ๋ฐ˜์˜๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค.

  • Article์˜ Shard Key๋Š” board_id๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.
    ์™œ๋ƒํ•˜๋ฉด ๋งŒ์•ฝ article_id๋ฅผ Shard Key๋กœ ํ•˜๋ฉด (ex. article_id % 2) ๊ทธ๋Ÿฌ๋ฉด ๊ฐ™์€ ๊ฒŒ์‹œํŒ์— ์žˆ๋Š” ๊ฒŒ์‹œ๊ธ€์ธ๋ฐ๋„ ์—ฌ๋Ÿฌ ์ƒค๋“œ์—์„œ ์กฐํšŒํ•ด ์™€์•ผํ•œ๋‹ค. Shard Key๊ฐ€ board_id ๋ฉด ๊ทธ๋Ÿด ์ผ์ด ์—†๊ฒ ์ง€.
  • Article ํ…Œ์ด๋ธ”์˜ PK๋Š” AUTO_INCREMENT ๋˜๋Š” UUID๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
    ๋ถ„์‚ฐ ์‹œ์Šคํ…œ๊ณผ INDEX ๊ตฌ์กฐ์— ๋Œ€ํ•œ ์ดํ•ด๊ฐ€ ํ•„์š”ํ•˜๋‹ค. (์ฐจํ›„ ์„ค๋ช…)
    ์˜ค๋ฆ„์ฐจ์ˆœ UNIQUE ์ˆซ์ž๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜? → ์ด๋ฏธ ๋‹ค์–‘ํ•˜๋‹ค (ex. Snowflake, TSID ๋“ฑ) ์ด ์ค‘ Snowflake๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์ž

  • Snowflake ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ๋ถ„์‚ฐ์‹œ์Šคํ…œ์—์„œ๋„ ๋น ๋ฅด๊ฒŒ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ์˜ค๋ฆ„์ฐจ์ˆœ ์•Œ๊ณ ๋ฆฌ์ฆ˜
    ๋ถ„์„ํ•˜๋ฉด <<,>> (๋น„ํŠธ ์‰ฌํ”„ํŠธ), |,& (๋น„ํŠธ ์—ฐ์‚ฐ์ž), synchronized(๋™๊ธฐํ™”) ๋“ฑ์— ๋Œ€ํ•ด ํ•™์Šต
<< (๋น„ํŠธ ์™ผ์ชฝ ์‹œํ”„ํŠธ)
1L << n : 1์„ 2โฟ๋งŒํผ ์‹œํ”„ํŠธ
ex. 1 << 10 : 10000000000โ‚โ‚‚โ‚Ž

| (๋น„ํŠธ ์—ฐ์‚ฐ์ž)
11001โ‚โ‚‚โ‚Ž | 10011โ‚โ‚‚โ‚Ž : 11011โ‚โ‚‚โ‚Ž

& (๋น„ํŠธ ์—ฐ์‚ฐ์ž)
11001โ‚โ‚‚โ‚Ž | 10011โ‚โ‚‚โ‚Ž : 10001โ‚โ‚‚โ‚Ž

// ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์ ‘๊ทผํ•˜๋ฉด ์•ˆ ๋˜๋Š” ์ฝ”๋“œ์— 'ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ'๋งŒ ๋“ค์–ด์˜ค๋„๋ก
public synchronized long nextId() {
    // ์ด ์•ˆ์—๋Š” ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๋งŒ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ์Œ
}
  • open-in-view: false๋Š” lazy loading์— ๊ด€ํ•œ ์„ค์ •
    : Lazy Loading์€ ๊ด€๊ณ„๊ฐ€ ๊ฑธ๋ฆฐ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ”๋กœ ๋ถˆ๋Ÿฌ์˜ค์ง€ ์•Š๊ณ , "์ง„์งœ ํ•„์š”ํ•œ ์ˆœ๊ฐ„๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ๊ทธ๋•Œ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฌ๋Š” ๊ฒƒ"
@Entity
class Post {
	@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
	private List<Comment> comments; // ๋Œ“๊ธ€๋“ค (์ฒ˜์Œ์—” ์•ˆ ๋ถˆ๋Ÿฌ์˜ด)
}

 

์œ„ ์ฝ”๋“œ์ฒ˜๋Ÿผ Post๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ, comments๋Š” ์•„์ง ์•ˆ ๋ถˆ๋Ÿฌ์™€์ง.
post.getComments()๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ๊ทธ์ œ์•ผ DB์—์„œ ๊ฐ€์ ธ์˜จ๋‹ค.

@GetMapping("/post/{id}")
public Post getPost(@PathVariable Long id) {
  Post post = postRepository.findById(id).orElseThrow();

  // ์ด ์‹œ์ ์—” ํŠธ๋žœ์žญ์…˜ ๋๋‚ฌ๊ณ , DB ์„ธ์…˜๋„ ๋‹ซํž˜
  // ์•„๋ž˜ ์ฝ”๋“œ๊ฐ€ Lazy Loading์„ ์œ ๋ฐœํ•˜๋ฉด...
  List<Comment> comments = post.getComments();  // โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ

  return post;
}

 

โ— LazyLoading์€ Hibernate Session์ด ์—ด๋ ค ์žˆ์–ด์•ผ ๊ฐ€๋Šฅํ•จ

ํ•˜์ง€๋งŒ ๋ณดํ†ต Controller์—์„œ๋Š” ์ด๋ฏธ ํŠธ๋žœ์žญ์…˜์ด ๋๋‚ฌ๊ธฐ ๋•Œ๋ฌธ์—
"Lazyํ•˜๊ฒŒ ๋ถˆ๋Ÿฌ์˜ค๋ ค๋‹ค๊ฐ€ ์˜ค๋ฅ˜ ํ„ฐ์ง" (์˜ˆ: LazyInitializationException)

๐Ÿ”ง ๊ทธ๋ž˜์„œ open-in-view: true๋ฉด?

Spring์ด ํŠธ๋žœ์žญ์…˜์ด ๋๋‚˜๋„ DB ์—ฐ๊ฒฐ์„ ๋Š์ง€ ์•Š๊ณ , ๋ทฐ ๋ Œ๋”๋งํ•  ๋•Œ๊นŒ์ง€ ์œ ์ง€ํ•ด์ค˜

  • ์ฆ‰, Controller, View ๊ณ„์ธต์—์„œ๋„ Lazy Loading์ด ๊ฐ€๋Šฅํ•ด์ง
  • ํŽธํ•˜๊ธด ํ•˜์ง€๋งŒ, ์ปค๋„ฅ์…˜์„ ์˜ค๋ž˜ ์ ์œ ํ•ด์„œ ์„ฑ๋Šฅ, ์•ˆ์ •์„ฑ ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Œ

๐Ÿ“› open-in-view: false๋Š”?

ํŠธ๋žœ์žญ์…˜ ๋๋‚˜๋ฉด ๋ฐ”๋กœ DB ์„ธ์…˜์„ ๋‹ซ์•„๋ฒ„๋ฆผ

  • Controller์—์„œ Lazy Loading ์‹œ๋„ → โŒ ์˜ค๋ฅ˜
  • ๋Œ€์‹ , Service ๊ณ„์ธต์—์„œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ๋‹ค ๋ฏธ๋ฆฌ ๋กœ๋”ฉํ•ด์•ผ ํ•จ
@PostService
public Post getPostWithComments(Long id) {
    return postRepository.findByIdWithComments(id); // JOIN FETCH ์‚ฌ์šฉ
}
์„ค์ • ์„ค๋ช… Lazy Loading ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์œ„์น˜ ํŠน์ง•
true (๊ธฐ๋ณธ) ๋ทฐ ๋ Œ๋”๋ง ์‹œ๊นŒ์ง€ DB ์„ธ์…˜ ์œ ์ง€ Controller, View๊นŒ์ง€ ๊ฐ€๋Šฅ ํŽธํ•˜์ง€๋งŒ ์œ„ํ—˜์„ฑ ์žˆ์Œ
false ํŠธ๋žœ์žญ์…˜ ๋๋‚˜๋ฉด ์„ธ์…˜ ์ข…๋ฃŒ Service ๊ณ„์ธต๊นŒ์ง€๋งŒ ๊ฐ€๋Šฅ ์•ˆ์ „ํ•˜๊ณ  ๋ช…ํ™•ํ•œ ์„ค๊ณ„ ์œ ๋„

  • ์™œ ๋น„์Šทํ•œ Articleํด๋ž˜์Šค๊ฐ€ 4๊ฐœ๋‚˜ ์žˆ์„๊นŒ?
    4๊ฐœ์˜ ํด๋ž˜์Šค๋Š” ๊ณ„์ธต์˜ ์—ญํ•  ๋ถ„๋ฆฌ(Clean Architecture) ๊ด€์ ์—์„œ ์•„์ฃผ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•ด.
kuke.board.article
โ”œโ”€โ”€ entity
โ”‚   โ””โ”€โ”€ Article.java                    ← JPA ์—”ํ‹ฐํ‹ฐ
โ”‚
โ”œโ”€โ”€ service
โ”‚   โ”œโ”€โ”€ request
โ”‚   โ”‚   โ”œโ”€โ”€ ArticleCreateRequest.java   ← ์ƒ์„ฑ ์š”์ฒญ DTO
โ”‚   โ”‚   โ””โ”€โ”€ ArticleUpdateRequest.java   ← ์ˆ˜์ • ์š”์ฒญ DTO
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ response
โ”‚       โ””โ”€โ”€ ArticleResponse.java        ← ์‘๋‹ต DTO
@Getter
public class ArticleCreateRequest {
  private String title;
  private String content;
  private Long writerId;
  private Long boardId;
}

๐Ÿ“Œ ๋ชฉ์ 

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ƒˆ ๊ฒŒ์‹œ๊ธ€์„ ์ƒ์„ฑํ•  ๋•Œ ๋ณด๋‚ด๋Š” JSON ๋ฐ์ดํ„ฐ๋ฅผ ๋งคํ•‘
  • HTTP ์š”์ฒญ ๋ฐ”๋””(Request Body)์— ๋‹ด๊ธด ๋‚ด์šฉ์„ ๋ฐ›๋Š” ์—ญํ• 

โœ… ์žฅ์ 

  • ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๊ฐ€๋Šฅ (@Valid, @NotNull, ๋“ฑ)
  • ์™ธ๋ถ€ ์š”์ฒญ์—๋งŒ ์ง‘์ค‘ → ์ž…๋ ฅ์˜ ์•ˆ์ •์„ฑ ํ™•๋ณด
@Getter
public class ArticleUpdateRequest {
  private String title;
  private String content;
}

๐Ÿ“Œ ๋ชฉ์ 

  • ๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • ์‹œ ์‚ฌ์šฉํ•  ์š”์ฒญ ๊ฐ์ฒด
  • ์ˆ˜์ •์—์„œ๋Š” title, content๋งŒ ํ•„์š”ํ•˜๋ฏ€๋กœ, ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ํฌํ•จ

โœ… ์žฅ์ 

  • API ๋ช…์„ธ๊ฐ€ ๋ช…ํ™•ํ•จ (ํ•„๋“œ๊ฐ€ ์ ์„์ˆ˜๋ก ์ข‹์Œ)
  • "์ˆ˜์ • ์‹œ์—๋Š” ์ด๊ฒƒ๋งŒ ํ•„์š”ํ•˜๋‹ค"๋Š” ์˜๋„๋ฅผ ์ฝ”๋“œ๋กœ ํ‘œํ˜„
@Entity
public class Article {
  @Id
  private Long articleId;
  private String title;
  ...
}

๐Ÿ“Œ ๋ชฉ์ 

  • ์ง„์งœ DB์™€ ์—ฐ๊ฒฐ๋œ ๋ชจ๋ธ
  • JPA๊ฐ€ ํ…Œ์ด๋ธ”๊ณผ ๋งคํ•‘ํ•ด์„œ ์˜์†์„ฑ ๊ด€๋ฆฌ
  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋“ค์–ด๊ฐ€๋Š” ๊ณณ (update(), create() ๋“ฑ)

โœ… ์žฅ์ 

  • DB์™€ ์ง์ ‘ ์—ฐ๋™ → ์ €์žฅ/์กฐํšŒ/์ˆ˜์ • ๊ฐ€๋Šฅ
  • ๋„๋ฉ”์ธ ๋กœ์ง ํฌํ•จ ๊ฐ€๋Šฅ (update(), delete(), isOwner() ๋“ฑ)
@Getter
public class ArticleResponse {
  private Long articleId;
  private String title;
  ...
  public static ArticleResponse from(Article article) { ... }
}

๐Ÿ“Œ ๋ชฉ์ 

  • ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์‘๋‹ตํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” DTO
  • Entity ๊ทธ๋Œ€๋กœ ๋„˜๊ธฐ์ง€ ์•Š๊ธฐ ์œ„ํ•ด ์กด์žฌํ•จ (๋ณด์•ˆ, ์•ˆ์ •์„ฑ)

โœ… ์žฅ์ 

  • ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ๊ณจ๋ผ์„œ ์ค„ ์ˆ˜ ์žˆ์Œ
  • JSON ์ง๋ ฌํ™”์— ์•ˆ์ „ํ•จ (Lazy ๋กœ๋”ฉ ํ•„๋“œ๋‚˜ ๋ฌดํ•œ์ˆœํ™˜ ๋ฌธ์ œ ์—†์Œ)
  • ํ•„์š”ํ•˜๋ฉด ํ•„๋“œ๋ช…๋„ ๋ฐ”๊ฟ”์„œ ์ค„ ์ˆ˜ ์žˆ์Œ (title → articleTitle ๋“ฑ)

๐Ÿ“Œ "์™œ ๋น„์Šทํ•œ๋ฐ๋„ ๊ตณ์ด ๋‚˜๋ˆŒ๊นŒ?"

์ด์œ  ์„ค๋ช…
๊ณ„์ธต ๊ฐ„ ์—ญํ•  ๋ถ„๋ฆฌ ์š”์ฒญ DTO, ์‘๋‹ต DTO, ๋„๋ฉ”์ธ(Entity)์„ ๋‚˜๋ˆ„๋ฉด ์ฑ…์ž„์ด ๋ช…ํ™•
๋ณด์•ˆ ๋ฐ ์•ˆ์ •์„ฑ Entity๋ฅผ ๊ทธ๋Œ€๋กœ ์‘๋‹ตํ•˜๊ฑฐ๋‚˜, ์š”์ฒญ๋ฐ›์œผ๋ฉด ์œ„ํ—˜ (๋…ธ์ถœ/์ˆ˜์ • ๋ฌธ์ œ ๋“ฑ)
์œ ์—ฐํ•œ ๋ณ€๊ฒฝ ์š”์ฒญ/์‘๋‹ต ํฌ๋งท ๋ฐ”๋€Œ์–ด๋„ Entity๋Š” ๊ทธ๋Œ€๋กœ ๋‘˜ ์ˆ˜ ์žˆ์Œ
ํ…Œ์ŠคํŠธ ์‰ฌ์›€ ๊ฐ ๊ณ„์ธต์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง
ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜ ์ฒ ํ•™ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ "์ž…๋ ฅ → ์ฒ˜๋ฆฌ → ์ถœ๋ ฅ"์œผ๋กœ ๋ช…ํ™•ํžˆ ๋‚˜๋ˆ”

๐Ÿ’ก ๋น„์œ ๋กœ ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•˜๋ฉด...

Article = "ํšŒ์‚ฌ ๋‚ด๋ถ€ ์ „์‚ฐ DB ํ…Œ์ด๋ธ”"

  • ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค ๋“ค์–ด์žˆ๊ณ , ๋ฏผ๊ฐํ•œ ์ •๋ณด๋„ ์žˆ์Œ
  • ์•„๋ฌด๋‚˜ ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ๋ฐ”๊พธ๋ฉด ์•ˆ ๋จ

Create/Update Request = "๊ณ ๊ฐ์ด ์ž‘์„ฑํ•œ ์‹ ์ฒญ์„œ"

  • ๊ณ ๊ฐ์ด ์ฑ„์›Œ์•ผ ํ•  ํ•ญ๋ชฉ๋งŒ ์žˆ์Œ

Response = "๊ณ ๊ฐ์—๊ฒŒ ๋ณด์—ฌ์ฃผ๋Š” ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ"

  • ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ์˜ˆ์˜๊ฒŒ ๊ฐ€๊ณตํ•ด์„œ ๋ณด์—ฌ์คŒ

  • JSON ์ง๋ ฌํ™”์— ์•ˆ์ „ํ•จ (Lazy ๋กœ๋”ฉ ํ•„๋“œ๋‚˜ ๋ฌดํ•œ์ˆœํ™˜ ๋ฌธ์ œ ์—†์Œ) ์ด๊ฒŒ ๋ฌด์Šจ ๋ง์ด์•ผ?

์ข‹์•„! ์ด๊ฑด JPA์—์„œ ์ •๋ง ์ž์ฃผ ๋‚˜์˜ค๋Š” "Lazy ๋กœ๋”ฉ" + "๋ฌดํ•œ ์ˆœํ™˜" ๋ฌธ์ œ์— ๋Œ€ํ•œ ์„ค๋ช…์ด์•ผ.
์•„์ฃผ ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ์‹ค์ˆ˜์ธ๋ฐ, JSON ์ง๋ ฌํ™”ํ•  ๋•Œ ์™œ Entity ๋Œ€์‹  Response DTO๋ฅผ ์จ์•ผ ํ•˜๋Š”์ง€ ์ดํ•ดํ•˜๋Š” ํ•ต์‹ฌ์ด๊ธฐ๋„ ํ•ด.

โœ… ์šฉ์–ด ๋จผ์ € ์งš์ž

๐Ÿ“Œ ์ง๋ ฌํ™”(Serialization)?

  • ์ž๋ฐ” ๊ฐ์ฒด → JSON, XML, ๋ฌธ์ž์—ด ๋“ฑ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ
  • ์˜ˆ: Article ๊ฐ์ฒด → JSON ์‘๋‹ต์œผ๋กœ ๋ณ€ํ™˜
{ 
  "articleId": 1, 
  "title": "Hello" 
}

๐Ÿข Lazy ๋กœ๋”ฉ์ด ๋ญ์˜€๋”๋ผ?

์˜ˆ๋ฅผ ๋“ค์–ด ๊ฒŒ์‹œ๊ธ€ → ๋Œ“๊ธ€ ๊ตฌ์กฐ:

@Entity
public class Article {
  @OneToMany(mappedBy = "article", fetch = FetchType.LAZY)
  private List<Comment> comments;
}
  • fetch = LAZY → comments๋Š” ์‹ค์ œ๋กœ ์กฐํšŒํ•  ๋•Œ๊นŒ์ง€ DB์—์„œ ์•ˆ ๋ถˆ๋Ÿฌ์˜ด
article.getComments(); // ์ด ๋•Œ DB์—์„œ SELECT ์‹คํ–‰

๐Ÿ”ฅ ๋ฌธ์ œ ์ƒํ™ฉ

์ปจํŠธ๋กค๋Ÿฌ์—์„œ Entity๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฆฌํ„ดํ•˜๋ฉด:

@GetMapping("/article/{id}")
public Article getArticle(@PathVariable Long id) {
  return articleRepository.findById(id).orElseThrow();
}

→ Spring MVC๊ฐ€ ์ด๊ฑธ JSON์œผ๋กœ ์ž๋™ ๋ณ€ํ™˜(์ง๋ ฌํ™”)ํ•  ๋•Œ, → ๋‚ด๋ถ€์ ์œผ๋กœ article.getComments()๋ฅผ ํ˜ธ์ถœํ•จ
→ ๊ทธ๋Ÿฐ๋ฐ ์ด ์‹œ์ ์— DB ์„ธ์…˜์€ ์ด๋ฏธ ๋‹ซํ˜€ ์žˆ์Œ → ๐Ÿ’ฅ LazyInitializationException

๐Ÿ” ๋ฌดํ•œ ์ˆœํ™˜ ๋ฌธ์ œ

๊ด€๊ณ„๊ฐ€ ์ด๋ ‡๊ฒŒ ๋˜์–ด ์žˆ๋‹ค๋ฉด?

Article → List<Comment>  
Comment → Article (์–‘๋ฐฉํ–ฅ ๋งคํ•‘)

JSON ์ง๋ ฌํ™”ํ•  ๋•Œ:

Article → comments → each Comment → article → comments → ...

→ JSON ๋ณ€ํ™˜ ๋„์ค‘์— ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐœ์ƒ
→ ๐Ÿ’ฅ StackOverflowError, Jackson ์ˆœํ™˜ ์ฐธ์กฐ ์˜ค๋ฅ˜ ๋ฐœ์ƒ

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•: DTO ๋ถ„๋ฆฌ

ArticleResponse ๊ฐ™์€ DTO๋Š” ๊ทธ๋ƒฅ ํ‰๋ฉด ๊ตฌ์กฐ๋กœ ํ•„๋“œ๋ฅผ ๋ณต์‚ฌํ•˜๊ธฐ ๋•Œ๋ฌธ์—,

  • Lazy ๋กœ๋”ฉ๋œ ํ•„๋“œ ์•ˆ ๊ฑด๋“œ๋ฆผ
  • ์ˆœํ™˜ ์ฐธ์กฐ ๊ตฌ์กฐ ์—†์Œ
  • JSON ๋ณ€ํ™˜ ์‹œ ์•ˆ์ „

โœ… ์š”์•ฝ ํ‘œ

์ƒํ™ฉ ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
Entity ๊ทธ๋Œ€๋กœ ๋ฆฌํ„ด Lazy ํ•„๋“œ ์กฐํšŒ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ DTO๋กœ ๋ณ€ํ™˜
Entity ์–‘๋ฐฉํ–ฅ ๊ด€๊ณ„ ์ˆœํ™˜ ์ฐธ์กฐ ๋ฌดํ•œ ๋ฃจํ”„ DTO์—์„œ ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ์‚ฌ์šฉ
์„ฑ๋Šฅ ์ตœ์ ํ™” ํ•„์š” ์—†๋Š” ํ•„๋“œ๊นŒ์ง€ ์กฐํšŒ๋จ ํ•„์š”ํ•œ ์ •๋ณด๋งŒ DTO์— ๋‹ด์Œ

๐Ÿ“Œ ๊ทธ๋ž˜์„œ ์‹ค๋ฌด์—์„œ๋Š” Entity → DTO ๋ณ€ํ™˜ํ•ด์„œ ์‘๋‹ต ๋ณด๋‚ด๋Š” ๊ฒŒ "์•ˆ์ „ํ•˜๊ณ  ๊ถŒ์žฅ๋˜๋Š” ๋ฐฉ์‹"์ด์•ผ.

์›ํ•˜๋ฉด @JsonIgnore, @JsonManagedReference, @JsonBackReference ๊ฐ™์€ ์ˆœํ™˜ ์ฐธ์กฐ ํ•ด๊ฒฐ ์• ๋„ˆํ…Œ์ด์…˜๋“ค๋„ ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ์–ด!

 

You said:

→ Spring MVC๊ฐ€ ์ด๊ฑธ JSON์œผ๋กœ ์ž๋™ ๋ณ€ํ™˜(์ง๋ ฌํ™”)ํ•  ๋•Œ, → ๋‚ด๋ถ€์ ์œผ๋กœ article.getComments()๋ฅผ ํ˜ธ์ถœํ•จ ์–ด์ผ€ ์ด๊ฒŒ ๊ฐ€๋Šฅํ•ด
 

ChatGPT said:

์ง„์งœ ์ข‹์€ ์งˆ๋ฌธ์ด์•ผ!
"๋‚ด๊ฐ€ getComments()๋ฅผ ํ˜ธ์ถœํ•œ ์ ๋„ ์—†๋Š”๋ฐ, ์–ด๋–ป๊ฒŒ Spring์ด ๊ทธ๊ฑธ ํ˜ธ์ถœํ•˜๋ƒ๊ณ ?" ์‹ถ์ง€?
๊ทผ๋ฐ ์ด๊ฑด Spring์ด ์ž๋™์œผ๋กœ ์ง๋ ฌํ™”๋ฅผ ํ•ด์ฃผ๋ฉด์„œ ๋ฒŒ์–ด์ง€๋Š” ์ผ์ด์•ผ. ํ•˜๋‚˜์”ฉ ํ’€์–ด์„œ ์„ค๋ช…ํ•ด๋ณผ๊ฒŒ.

โœ… ์ƒํ™ฉ ์ •๋ฆฌ

@GetMapping("/article/{id}")
public Article getArticle(@PathVariable Long id) {
  return articleRepository.findById(id).orElseThrow();
}

์—ฌ๊ธฐ์„œ Article ๊ฐ์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ–ˆ์–ด.
๊ทธ๋Ÿฌ๋ฉด Spring์€ ๋ญ˜ ํ•˜๋ƒ๋ฉด...

๐Ÿ”„ Spring MVC๊ฐ€ ์ž๋™์œผ๋กœ ํ•ด์ฃผ๋Š” ๊ณผ์ •

  1. Article ๊ฐ์ฒด → ํด๋ผ์ด์–ธํŠธ์— JSON์œผ๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•จ
  2. ์ด๋•Œ Spring์€ ๋‚ด๋ถ€์ ์œผ๋กœ Jackson(ObjectMapper) ์„ ์‚ฌ์šฉํ•ด์„œ ์ง๋ ฌํ™”ํ•ด
ObjectMapper mapper = new ObjectMapper();
mapper.writeValueAsString(article);
  1. Jackson์€ Article ํด๋ž˜์Šค์˜ ๋ชจ๋“  ํ•„๋“œ์— ๋Œ€ํ•ด getter()๋ฅผ ํ˜ธ์ถœํ•ด์„œ ๊ฐ’์„ ์ถ”์ถœํ•จ
article.getArticleId();
article.getTitle();
article.getComments(); ← ์—ฌ๊ธฐ๊ฐ€ ๋ฌธ์ œ!

 

๐Ÿงจ ์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ํ„ฐ์ง

๐Ÿ’ฅ Lazy ๋กœ๋”ฉ ํ•„๋“œ๋Š” ์‹ค์ œ DB ์กฐํšŒ๋ฅผ "๊ทธ๋•Œ" ์‹œ๋„ํ•จ

@OneToMany(fetch = FetchType.LAZY)
private List<Comment> comments;
  • ์ด ํ•„๋“œ๋Š” ์ฒ˜์Œ์—” ํ”„๋ก์‹œ ๊ฐ์ฒด๋กœ๋งŒ ๋“ค์–ด ์žˆ๊ณ ,
  • ์ง„์งœ getComments()๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ์ˆœ๊ฐ„ DB ์กฐํšŒ ์‹œ๋„ํ•จ
  • ๊ทธ๋Ÿฐ๋ฐ ์ด๋ฏธ DB ์„ธ์…˜์€ ๋‹ซํ˜€ ์žˆ์–ด์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session

 

๐Ÿ“ฆ ๊ทธ๋ž˜์„œ DTO๊ฐ€ ์ค‘์š”ํ•œ ์ด์œ 

public class ArticleResponse {
    private Long articleId;
    private String title;
    // comments ์—†์Œ
}
  • DTO๋Š” ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ๋ฝ‘์•„์™€์„œ getComments() ๊ฐ™์€ ํ˜ธ์ถœ์ด ์•„์˜ˆ ์•ˆ ์ผ์–ด๋‚จ
  • ๊ทธ๋ž˜์„œ Lazy ํ•„๋“œ, ์ˆœํ™˜ ์ฐธ์กฐ ๋ฌธ์ œ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋ฒ—์–ด๋‚  ์ˆ˜ ์žˆ์Œ

โœ… ์š”์•ฝ

๋‹จ๊ณ„๋‚ด์šฉ
1 Spring์ด Controller ๋ฆฌํ„ด๊ฐ’์„ JSON์œผ๋กœ ๋ฐ”๊พธ๋ ค ํ•จ
2 ๋‚ด๋ถ€์ ์œผ๋กœ Jackson์ด getter()๋ฅผ ํ˜ธ์ถœ
3 Lazy ํ•„๋“œ์˜ getter ํ˜ธ์ถœ → DB ์กฐํšŒ ์‹œ๋„
4 ์ด ์‹œ์ ์—” DB ์„ธ์…˜์ด ๋‹ซํ˜€ ์žˆ์–ด์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ

๐Ÿ”’ ์ด๊ฑธ ๋ฐฉ์ง€ํ•˜๋ ค๊ณ  Entity ๋Œ€์‹  DTO๋ฅผ ๋งŒ๋“ค์–ด์„œ
์ง๋ ฌํ™”์— ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ "๋ช…์‹œ์ ์œผ๋กœ" ํฌํ•จํ•˜๋Š” ๊ฒŒ ์‹ค๋ฌด์˜ ์ •์„์ด์•ผ.


  • JPA์˜ ์˜์†์„ฑ์ปจํ…์ŠคํŠธ๊ฐ€ update์‹œ ํ•ด์ฃผ๋Š” ์ผ
  @Transactional
  public ArticleResponse update(Long articleId, ArticleUpdateRequest request) {
    Article article = articleRepository.findById(articleId).orElseThrow();
    article.update(request.getTitle(), request.getContent());
    return ArticleResponse.from(article);
  }

 

Q: ์ด๊ฑฐ DB์— ๋ฐ˜์˜๋˜๋Š”๊ฑฐ์•ผ JPA๊ฐ€ ํ•ด์ฃผ๋Š”๊ฑฐ์•ผ?

 

์ •ํ™•ํžˆ ์งš์—ˆ์–ด ๐Ÿ‘
๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜๋ฉด:

โœ… DB์— ๋ฐ˜์˜๋˜๋Š” ๊ฑฐ ๋งž๊ณ , ๊ทธ ๋ฐ˜์˜์€ JPA๊ฐ€ ์•Œ์•„์„œ ํ•ด์ฃผ๋Š” ๊ฒƒ์ด์•ผ.

๐Ÿ” ์ฝ”๋“œ ๋‹ค์‹œ ๋ณด๊ธฐ

@Transactional
public ArticleResponse update(Long articleId, ArticleUpdateRequest request) {
    Article article = articleRepository.findById(articleId).orElseThrow();
    article.update(request.getTitle(), request.getContent()); // ← ์—ฌ๊ธฐ์„œ ํ•„๋“œ๋งŒ ๋ฐ”๊ฟˆ
    return ArticleResponse.from(article); // ์ €์žฅ ์•ˆ ํ–ˆ๋Š”๋ฐ??
}

โ“๊ทผ๋ฐ save()๋„ ์•ˆ ํ–ˆ์ž–์•„?

  • articleRepository.save(article) ์ด๋Ÿฐ ๊ฑฐ ์•ˆ ํ•ด๋„…
  • ์‹ค์ œ๋กœ DB์— ๊ฐ’์ด ๋ฐ˜์˜๋ผ!

๐Ÿค– ์™œ ๊ทธ๋Ÿด๊นŒ? → JPA์˜ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ(Persistence Context) ๋•๋ถ„

1. findById()๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋ฉด

→ JPA๋Š” ํ•ด๋‹น ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜์† ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•˜๊ฒŒ ๋ผ

2. ๊ทธ ํ›„์— ์—”ํ‹ฐํ‹ฐ ํ•„๋“œ๋ฅผ ๋ฐ”๊พธ๋ฉด

→ JPA๋Š” "์ด ๊ฐ์ฒด๊ฐ€ ๋ฐ”๋€Œ์—ˆ๊ตฌ๋‚˜"๋ผ๊ณ  ์ถ”์ ํ•˜๊ณ  ์žˆ๋‹ค๊ฐ€

3. @Transactional์ด ๋๋‚˜๋Š” ์‹œ์  (ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ)

→ flush()๊ฐ€ ์ž๋™ ํ˜ธ์ถœ๋˜๋ฉด์„œ DB์— ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ฐ˜์˜ํ•จ
(์ฆ‰, UPDATE ์ฟผ๋ฆฌ ์ž๋™ ์‹คํ–‰๋จ)

โœ… ํ๋ฆ„ ์š”์•ฝ

1. findById()๋กœ DB์—์„œ ์—”ํ‹ฐํ‹ฐ ์กฐํšŒ → ์˜์† ์ƒํƒœ๋กœ ๊ด€๋ฆฌ ์‹œ์ž‘
2. article.update(...)๋กœ ํ•„๋“œ ๋ณ€๊ฒฝ
3. ํŠธ๋žœ์žญ์…˜ ๋๋‚˜๋Š” ์‹œ์ ์— → JPA๊ฐ€ ๋ณ€๊ฒฝ ๊ฐ์ง€(dirty checking)
4. UPDATE SQL ์‹คํ–‰๋จ

 

๐Ÿ’ฌ ๊ทธ๋ž˜์„œ save()๋Š” ์–ธ์ œ ์“ฐ๋ƒ?

์ƒํ™ฉ ์‚ฌ์šฉ ์˜ˆ์‹œ save() ํ•„์š” ์—ฌ๋ถ€
์ƒˆ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ new Article() → save() โœ… ํ•„์š”
์ด๋ฏธ ์˜์† ์ƒํƒœ์ธ ๊ฐ์ฒด ์ˆ˜์ • findById() ํ›„ ํ•„๋“œ ์ˆ˜์ • โŒ ํ•„์š” ์—†์Œ (JPA๊ฐ€ ์ž๋™ ๋ฐ˜์˜)
๋น„์˜์† ์ƒํƒœ ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ • detached ๊ฐ์ฒด โœ… merge/save ํ•„์š”

๐Ÿ’ก ๊ฒฐ๋ก 

  • @Transactional ๋•๋ถ„์— ๋ณ€๊ฒฝ ๊ฐ์ง€ + ์ž๋™ DB ๋ฐ˜์˜์ด ์ผ์–ด๋‚˜๋Š” ๊ฑฐ๊ณ ,
  • ์ด๊ฒŒ JPA์˜ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜์•ผ:
    ๐Ÿ‘‰ "์ˆ˜์ •ํ•  ๋•Œ ๊ตณ์ด save() ์•ˆ ํ•ด๋„ ๋œ๋‹ค"

ํ•„์š”ํ•˜๋ฉด Dirty Checking์ด ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ๋‚ด๋ถ€ ๊ตฌ์กฐ๋„ ๊ทธ๋ฆผ์œผ๋กœ ์ •๋ฆฌํ•ด์ค„๊ฒŒ!

 

You said:

dirty checking ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•ด? ๋‚ด๋ถ€ ๊ตฌ์กฐ๋„ ๊ทธ๋ ค์ฃผ๊ณ 
 

ChatGPT said:

์ข‹์•„!
Dirty Checking(๋”ํ‹ฐ ์ฒดํ‚น)์€ JPA์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜์ธ๋ฐ,
**"์˜์† ์ƒํƒœ(Entity๊ฐ€ DB์™€ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š” ์ƒํƒœ)์—์„œ ํ•„๋“œ๊ฐ€ ๋ฐ”๋€Œ๋ฉด JPA๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ด์„œ UPDATE ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฌ๋Š” ๊ฒƒ"**์ด์•ผ.

๐Ÿง  Dirty Checking ํ๋ฆ„ ์„ค๋ช… (with ๊ทธ๋ฆผ)

โœ… ์˜ˆ์‹œ ์ฝ”๋“œ

@Transactional
public void updateArticle(Long id, String newTitle) {
    Article article = articleRepository.findById(id).orElseThrow();
    article.setTitle(newTitle); // ํ•„๋“œ ๋ณ€๊ฒฝ
    // save() ํ˜ธ์ถœ ์•ˆ ํ•จ
} // ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ ์‹œ์ ์— DB ๋ฐ˜์˜๋จ

๐Ÿ“ฆ ๋‚ด๋ถ€ ๊ตฌ์กฐ: JPA๊ฐ€ ๋™์ž‘ํ•˜๋Š” ๋ฐฉ์‹

1. ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ (@Transactional)
2. findById → DB์—์„œ ์กฐํšŒ → 1์ฐจ ์บ์‹œ์— Article ์ €์žฅ
3. Article.setTitle("new title") ← ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ
4. ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ → flush()
5. JPA ๋‚ด๋ถ€ ๋น„๊ต → ๋ณ€๊ฒฝ ๊ฐ์ง€
6. UPDATE SQL ์‹คํ–‰

๐ŸŽจ ๊ทธ๋ฆผ์œผ๋กœ ๋ณด๊ธฐ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         JPA ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ       โ”‚ ← ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์œ ์ง€๋˜๋Š” ์บ์‹œ
โ”‚                               โ”‚
โ”‚  Article ์—”ํ‹ฐํ‹ฐ (ID=1)          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ title = "old title"  โ”‚     โ”‚ ← ์Šค๋ƒ…์ƒท
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚        โ–ฒ              โ–ฒ       โ”‚
โ”‚        โ”‚              โ”‚       โ”‚
โ”‚        โ”‚   Article.setTitle("new title") ํ˜ธ์ถœ
โ”‚        โ”‚              โ”‚       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ title = "new title"  โ”‚     โ”‚ ← ์‹ค์ œ ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚ flush() ์‹œ์ 
               โ–ผ
      ๋ณ€๊ฒฝ ๊ฐ์ง€(Diff ๋น„๊ต)
      → title ๋ณ€๊ฒฝ๋จ ๊ฐ์ง€
      → UPDATE article SET title = 'new title' WHERE id = 1 ์‹คํ–‰

๐Ÿ” ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ๊ฐ์ง€ํ•˜๋ƒ?

JPA๋Š” ์Šค๋ƒ…์ƒท(snapshot) ์„ ๋งŒ๋“ค์–ด ๋‘๊ณ ,
ํŠธ๋žœ์žญ์…˜์ด ๋๋‚˜๊ธฐ ์ „์— flush() ํ•  ๋•Œ → ์Šค๋ƒ…์ƒท๊ณผ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋น„๊ต(diff) ํ•ด.

if (!originalTitle.equals(currentTitle)) {
    generateUpdateSql();
}

๐Ÿ’ก ์ •๋ฆฌ ์š”์•ฝ

ํ•ญ๋ชฉ ์„ค๋ช…
์–ธ์ œ? @Transactional ๋‚ด์—์„œ ์—”ํ‹ฐํ‹ฐ ํ•„๋“œ ๋ณ€๊ฒฝ ์‹œ
์–ด๋–ป๊ฒŒ? ์ดˆ๊ธฐ ์ƒํƒœ(์Šค๋ƒ…์ƒท)์™€ ํ˜„์žฌ ์ƒํƒœ ๋น„๊ต
๋ญ๊ฐ€ ๋ฐœ์ƒ? ์ž๋™์œผ๋กœ UPDATE SQL ์ƒ์„ฑ๋จ
save() ํ•„์š”? โŒ ํ•„์š” ์—†์Œ (์ด๋ฏธ ์˜์† ์ƒํƒœ์ด๋ฏ€๋กœ)

์ด๊ฒŒ JPA๊ฐ€ "๋„๋ฉ”์ธ ๋ชจ๋ธ ์ค‘์‹ฌ ๊ฐœ๋ฐœ"์„ ์‰ฝ๊ฒŒ ํ•ด์ฃผ๋Š” ์ด์œ  ์ค‘ ํ•˜๋‚˜์•ผ.
์ˆ˜์ •์€ ๊ทธ๋ƒฅ ๊ฐ์ฒด์ฒ˜๋Ÿผ ๋‹ค๋ฃจ๊ธฐ๋งŒ ํ•˜๋ฉด ๋˜๊ณ , DB์— ๋ฐ˜์˜์€ JPA๊ฐ€ ์•Œ์•„์„œ ํ•ด์ฃผ๋‹ˆ๊นŒ.

ํ•„์š”ํ•˜๋ฉด Dirty Checking ๋ฌดํšจํ™”๋˜๋Š” ๊ฒฝ์šฐ๋„ ์ •๋ฆฌํ•ด์ค„๊ฒŒ (์˜ˆ: detached ์ƒํƒœ, flush ์‹œ์  ๋“ฑ).


๐Ÿ“ API ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” RestClient

RestClient ๊ฐ€ ํ™”๋ฉด์—†์ด API test์— ์‚ฌ์šฉ๋˜๋˜๋ฐ ๋ญํ•˜๋Š”๊ฑฐ์ง€
 

ChatGPT said:

์ข‹์•„, ์•„์ฃผ ์ข‹์€ ์งˆ๋ฌธ์ด์•ผ!
RestClient๋Š” ์ตœ๊ทผ Spring์—์„œ ๋„์ž…๋œ HTTP ํด๋ผ์ด์–ธํŠธ ๋„๊ตฌ ์ค‘ ํ•˜๋‚˜์ธ๋ฐ,
๋„Œ "ํ™”๋ฉด ์—†์ด API ํ…Œ์ŠคํŠธํ•  ๋•Œ ์“ฐ์ธ๋‹ค"๊ณ  ๋ดค๋‹ค๊ณ  ํ–ˆ์ง€?
→ ๊ทธ๊ฑด ์ •ํ™•ํ•ด. ์ง€๊ธˆ๋ถ€ํ„ฐ ์ •๋ฆฌํ•ด์ค„๊ฒŒ.

โœ… RestClient๋ž€?

Spring 6.1๋ถ€ํ„ฐ ๋„์ž…๋œ ์ƒˆ๋กœ์šด HTTP ํด๋ผ์ด์–ธํŠธ
๊ธฐ์กด์˜ RestTemplate, WebClient๋ณด๋‹ค ๊ฐ„๊ฒฐํ•˜๊ณ  ํ…Œ์ŠคํŠธ ์นœํ™”์ ์ธ API ์ œ๊ณต

๐Ÿ“ฆ ์–ธ์ œ ์“ฐ์ด๋ƒ๋ฉด?

  • ํ”„๋ก ํŠธ ํ™”๋ฉด ์—†์ด ๋ฐฑ์—”๋“œ API๋งŒ ํ…Œ์ŠคํŠธํ•˜๊ฑฐ๋‚˜ ํ˜ธ์ถœํ•˜๊ณ  ์‹ถ์„ ๋•Œ
  • ์˜ˆ๋ฅผ ๋“ค์–ด, "๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ API", "๋กœ๊ทธ์ธ API"๊ฐ€ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•  ๋•Œ
  • ๋˜๋Š” ๋‹ค๋ฅธ ์™ธ๋ถ€ API๋ฅผ ํ˜ธ์ถœํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ฌ ๋•Œ๋„ ์‚ฌ์šฉ

โœ… ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ (Spring Boot)

RestClient client = RestClient.create();

ArticleCreateRequest request = new ArticleCreateRequest("์ œ๋ชฉ", "๋‚ด์šฉ", 1L, 2L);

ArticleResponse response = client.post()
    .uri("http://localhost:8080/api/articles")
    .body(request)
    .retrieve()
    .body(ArticleResponse.class);

๐Ÿง  ์˜๋ฏธ

  • RestClient.create() → HTTP ์š”์ฒญ์„ ๋ณด๋‚ผ ์ค€๋น„
  • .post().uri(...) → POST ์š”์ฒญ์„ ์ด URL๋กœ ๋ณด๋ƒ„
  • .body(request) → JSON์œผ๋กœ ๋ณ€ํ™˜๋˜์–ด ์ „์†ก๋จ
  • .retrieve().body(...) → ์‘๋‹ต์„ ํŠน์ • ๊ฐ์ฒด๋กœ ํŒŒ์‹ฑํ•ด์„œ ๋ฐ›์Œ

๐Ÿ’ก ์™œ RestClient๊ฐ€ ์ฃผ๋ชฉ๋ฐ›๋ƒ๋ฉด

๋น„๊ต ๋Œ€์ƒ ํŠน์ง•
RestTemplate ์˜›๋‚  ๋ฐฉ์‹. ๊ณง deprecated ๋  ์˜ˆ์ •
WebClient ๋น„๋™๊ธฐ ์ง€์›๋จ. ๊ทธ๋Ÿฌ๋‚˜ ์•ฝ๊ฐ„ ๋ณต์žกํ•จ
RestClient ๊ฐ€์žฅ ๊ฐ„๊ฒฐํ•˜๊ณ  ์ง๊ด€์ ์ž„. ํ…Œ์ŠคํŠธ๋‚˜ ๊ฐ„๋‹จ ํ˜ธ์ถœ์— ์ข‹์•„

๐Ÿงช ๊ทธ๋Ÿผ "API ํ…Œ์ŠคํŠธ์šฉ"์œผ๋กœ ์–ด๋–ป๊ฒŒ ์“ฐ๋ƒ?

์˜ˆ: API ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์—์„œ ์ง์ ‘ API ํ˜ธ์ถœํ•˜๊ณ  ๊ฒฐ๊ณผ ๊ฒ€์ฆ

@Test
void ๊ฒŒ์‹œ๊ธ€_์ƒ์„ฑ_API_ํ…Œ์ŠคํŠธ() {
    ArticleCreateRequest request = new ArticleCreateRequest("์ œ๋ชฉ", "๋‚ด์šฉ", 1L, 2L);

    ArticleResponse response = RestClient.create()
        .post()
        .uri("http://localhost:8080/api/articles")
        .body(request)
        .retrieve()
        .body(ArticleResponse.class);

    assertEquals("์ œ๋ชฉ", response.getTitle());
}
  • ํ™”๋ฉด ์—†์ด API๊ฐ€ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆ ๊ฐ€๋Šฅ
  • Postman์ด๋‚˜ Swagger ์—†์ด ์ฝ”๋“œ๋กœ ํ…Œ์ŠคํŠธ

โœ… ์š”์•ฝ

ํ•ญ๋ชฉ ์„ค๋ช…
์ด๋ฆ„ RestClient
์šฉ๋„ API ํ˜ธ์ถœ, ํ…Œ์ŠคํŠธ, ์™ธ๋ถ€ ์„œ๋ฒ„ ํ†ต์‹ 
์žฅ์  ๊ฐ„๊ฒฐํ•˜๊ณ  ์ง๊ด€์ ์ธ ๋ฌธ๋ฒ•, ํ…Œ์ŠคํŠธ ์นœํ™”์ 
๋„์ž… Spring 6.1๋ถ€ํ„ฐ ์ง€์›

์›ํ•˜๋ฉด RestTemplate, WebClient, RestClient 3๊ฐ€์ง€ ๋น„๊ตํ‘œ๋„ ๋งŒ๋“ค์–ด์ค„๊ฒŒ!
๋˜๋Š” ์‹ค์ œ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ๊ตฌ์กฐ๋„ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์–ด.


๐Ÿ“ @SpringBootTest ์˜ ์˜๋ฏธ

@SpringBootTest๋Š” Spring Boot์—์„œ ํ…Œ์ŠคํŠธํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์šฉ ์• ๋„ˆํ…Œ์ด์…˜์ด์•ผ.
๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ณด๋‹ค ํ•œ ๋‹จ๊ณ„ ๋” ์˜ฌ๋ผ๊ฐ„ ์ˆ˜์ค€์œผ๋กœ, ์Šคํ”„๋ง ์ปจํ…์ŠคํŠธ ์ „์ฒด๋ฅผ ๋กœ๋”ฉํ•ด์„œ ์‹ค์ œ์ฒ˜๋Ÿผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด.

โœ… ํ•œ ์ค„ ์š”์•ฝ

Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด๋ฅผ ์‹คํ–‰ํ•œ ์ƒํƒœ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ์• ๋„ˆํ…Œ์ด์…˜

๐Ÿง  ์–ธ์ œ ์“ฐ์ด๋ƒ?

  • @Service, @Repository, @Controller ๊ฐ™์€ ๋นˆ๋“ค์ด ์‹ค์ œ์ฒ˜๋Ÿผ ๋‹ค ๋“ฑ๋ก๋œ ์ƒํƒœ์—์„œ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์„ ๋•Œ
  • ์˜ˆ: DB ์—ฐ๋™, ํŠธ๋žœ์žญ์…˜, ์˜์กด์„ฑ ์ฃผ์ž…, API ํ…Œ์ŠคํŠธ ๋“ฑ

๐Ÿงช ์˜ˆ์‹œ ์ฝ”๋“œ

@SpringBootTest
public class ArticleServiceTest {

    @Autowired
    ArticleService articleService;

    @Test
    void createArticle() {
        articleService.create(...);
    }
}
  • SpringBootTest ๋•๋ถ„์— ArticleService๊ฐ€ @Autowired ๋จ
  • ํŠธ๋žœ์žญ์…˜, JPA, ์Šคํ”„๋ง ์„ค์ • ๋‹ค ์ ์šฉ๋จ

โš™๏ธ ๋‚ด๋ถ€์ ์œผ๋กœ ํ•˜๋Š” ์ผ

@SpringBootTest๋Š” ๋‹ค์Œ์„ ํฌํ•จํ•ด:

  • @ExtendWith(SpringExtension.class)
  • ์Šคํ”„๋ง ApplicationContext ์ „์ฒด ๋กœ๋“œ
  • @Autowired, @Transactional ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • ํ•„์š”ํ•˜๋ฉด application.yml๋„ ์ ์šฉ๋จ

โœ… ์ถ”๊ฐ€ ์˜ต์…˜

์˜ต์…˜ ์„ค๋ช…
webEnvironment = NONE ์›น ํ™˜๊ฒฝ ์—†์Œ (๊ธฐ๋ณธ๊ฐ’)
webEnvironment = MOCK MockServlet ํ™˜๊ฒฝ ์‚ฌ์šฉ
webEnvironment = RANDOM_PORT ์‹ค์ œ HTTP ์„œ๋ฒ„ ์‹คํ–‰ (๋žœ๋ค ํฌํŠธ)
webEnvironment = DEFINED_PORT ์‹ค์ œ HTTP ์„œ๋ฒ„ ์‹คํ–‰ (application.yml์— ์ •์˜๋œ ํฌํŠธ ์‚ฌ์šฉ)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

→ ์ด๋Ÿฌ๋ฉด ์ง„์งœ ์„œ๋ฒ„ ๋„์›Œ์„œ RestTemplate, RestClient, TestRestTemplate๋กœ API ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ

โœ… ์–ธ์ œ ์•ˆ ์“ฐ๋Š” ๊ฒŒ ์ข‹์„๊นŒ?

  • ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ๋Š” ๋„ˆ๋ฌด ๋ฌด๊ฑฐ์›€ (์ปจํ…์ŠคํŠธ ์ „์ฒด ๋กœ๋”ฉํ•˜๋‹ˆ๊นŒ ๋А๋ฆผ)
  • ๋‹จ์ˆœ ๋กœ์ง ํ…Œ์ŠคํŠธ๋Š” @WebMvcTest, @DataJpaTest, @MockBean ๋“ฑ์„ ์จ์„œ ๊ฐ€๋ณ๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒŒ ์ข‹์Œ

๐Ÿ” ์ •๋ฆฌ

ํ•ญ๋ชฉ ์„ค๋ช…
์—ญํ•  ์Šคํ”„๋ง ์ปจํ…์ŠคํŠธ ์ „์ฒด ๋กœ๋”ฉํ•ด์„œ ํ…Œ์ŠคํŠธ
์ฃผ๋กœ ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ์„œ๋น„์Šค, ๋ฆฌํฌ์ง€ํ† ๋ฆฌ, API, ํ†ตํ•ฉ ์‹œ๋‚˜๋ฆฌ์˜ค
์žฅ์  ์‹ค์ œ ์‹คํ–‰ ํ™˜๊ฒฝ๊ณผ ๊ฑฐ์˜ ๋™์ผํ•˜๊ฒŒ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
๋‹จ์  ๋А๋ฆผ. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ณด๋‹ค ๋ฆฌ์†Œ์Šค ๋งŽ์ด ์”€

๐Ÿ“ @PersistenceContext, EntityManager, TransactionTemplate, CountDownLatch, ExecuterService, Executers.newFixedThreadPool() ์„ค๋ช…

package kuke.board.article.data;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import kuke.board.article.entity.Article;
import kuke.board.common.snowflake.Snowflake;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@SpringBootTest
public class DataInitializer {
  @PersistenceContext
  private EntityManager entityManager;
  @Autowired
  TransactionTemplate transactionTemplate;
  Snowflake snowflake = new Snowflake();
  CountDownLatch latch = new CountDownLatch(EXCUTE_COUNT);

  static final int BULK_INIT_SIZE = 2000;
  static final int EXCUTE_COUNT = 6000;

  @Test
  void initialize() throws InterruptedException {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    for (int i = 0; i < EXCUTE_COUNT; i++) {
      executorService.submit(() -> {
        insert();
        latch.countDown();
        System.out.println("latch.getCount() = " + latch.getCount());
      });
    }
    latch.await();
    executorService.shutdown();
  }

  void insert() {
    transactionTemplate.executeWithoutResult(status -> {
      for (int i = 0; i < BULK_INIT_SIZE; i++) {
        Article article = Article.create(
            snowflake.nextId(),
            "title" + i,
            "content" + i,
            1L,
            1L
        );
        entityManager.persist(article);
      }
    });
  }
}

 

์™„์ „ ์ข‹์€ ํฌ์ธํŠธ๋“ค์ด์•ผ ๐Ÿ‘
์ด๊ฑด ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์ฝ”๋“œ์ธ๋ฐ,
๋งํ•œ ํ‚ค์›Œ๋“œ๋“ค์ด ์ „๋ถ€ ํ•ต์‹ฌ ์š”์†Œ๋“ค์ด๋ผ ํ•˜๋‚˜์”ฉ ๊น”๋”ํ•˜๊ฒŒ ์„ค๋ช…ํ•ด์ค„๊ฒŒ!

โœ… @PersistenceContext + EntityManager

๐Ÿ”ธ @PersistenceContext

  • JPA๊ฐ€ ์ œ๊ณตํ•˜๋Š” EntityManager๋ฅผ ์ฃผ์ž…ํ•ด์ฃผ๋Š” ์• ๋„ˆํ…Œ์ด์…˜
  • @Autowired๋ž‘ ๋น„์Šทํ•˜์ง€๋งŒ, JPA ํ‘œ์ค€ ๋ฐฉ์‹ (์Šคํ”„๋ง ๋ถ€ํŠธ์—์„  ๊ฑฐ์˜ ๊ฐ™๊ฒŒ ๋™์ž‘ํ•จ)
@PersistenceContext
private EntityManager entityManager;

๐Ÿ”ธ EntityManager

  • JPA์˜ ํ•ต์‹ฌ ํด๋ž˜์Šค
  • persist, find, remove, flush ๋“ฑ DB ์ž‘์—…์„ ์ง์ ‘ ์ˆ˜ํ–‰ํ•จ
  • entityManager.persist(entity) → INSERT SQL ์‹คํ–‰๋จ

โœ… TransactionTemplate

๐Ÿ“Œ ์„ค๋ช…

  • ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์œผ๋กœ ํŠธ๋žœ์žญ์…˜์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” Spring ํด๋ž˜์Šค (๊ฐœ๋ฐœ์ž๊ฐ€ ์ฝ”๋“œ๋กœ ํŠธ๋žœ์žญ์…˜์˜ ์‹œ์ž‘๊ณผ ๋์„ ์ง์ ‘ ์ œ์–ด)
  • ์–ด๋…ธํ…Œ์ด์…˜(@Transactional) ๋Œ€์‹  ์ฝ”๋“œ๋กœ ์ง์ ‘ ํŠธ๋žœ์žญ์…˜์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ

๐Ÿ“Œ ์˜ˆ์‹œ

transactionTemplate.executeWithoutResult(status -> {
    // ์—ฌ๊ธฐ๋Š” ํŠธ๋žœ์žญ์…˜ ์•ˆ
    entityManager.persist(...);
});
  • ๋‚ด๋ถ€ ์ฝ”๋“œ๊ฐ€ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์‹คํ–‰๋˜๊ณ , ์˜ˆ์™ธ๊ฐ€ ๋‚˜๋ฉด ์ž๋™ ๋กค๋ฐฑ๋จ

๐Ÿ” ๋น„๊ตํ•ด๋ณผ๊ฒŒ:

โœ… ์„ ์–ธ์  ๋ฐฉ์‹ (@Transactional)

  • ์ฝ”๋“œ ์œ„์— @Transactional๋งŒ ๋ถ™์ด๋ฉด, Spring์ด ์•Œ์•„์„œ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•˜๊ณ  ์ปค๋ฐ‹/๋กค๋ฐฑํ•ด์ค˜.
@Transactional
public void doSomething() {
    // ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์‹คํ–‰๋จ
    entityManager.persist(...);
}
  • ๊ฐ„ํŽธํ•˜์ง€๋งŒ, ๋ณต์žกํ•œ ํŠธ๋žœ์žญ์…˜ ํ๋ฆ„์„ ์ง์ ‘ ์กฐ์ ˆํ•˜๊ธฐ๋Š” ์–ด๋ ค์›€

โœ… ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹ (TransactionTemplate)

  • ์ง์ ‘ ์ฝ”๋“œ๋กœ ํŠธ๋žœ์žญ์…˜์˜ ๋ฒ”์œ„๋ฅผ ์ •์˜ํ•˜๊ณ , ๊ทธ ์•ˆ์—์„œ ์›ํ•˜๋Š” ์ฒ˜๋ฆฌ๋ฅผ ํ•จ
transactionTemplate.executeWithoutResult(status -> {
    // ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘๋จ
    entityManager.persist(...);
    // ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ์ž๋™ ๋กค๋ฐฑ
});
  • ์ด ๋ฐฉ์‹์˜ ์žฅ์ :
    • ํŠน์ • ์กฐ๊ฑด์— ๋”ฐ๋ผ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Œ
    • try-catch๋กœ ์ •๊ตํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
    • ๋‚ด๋ถ€์—์„œ rollback ์ „ํ™˜๋„ ๊ฐ€๋Šฅ
status.setRollbackOnly();

โœ… CountDownLatch

๐Ÿ“Œ ์„ค๋ช…

  • ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์ž‘์—…์„ ์‹คํ–‰ํ•˜๊ณ ,
  • ๋ชจ๋“  ์ž‘์—…์ด ๋๋‚  ๋•Œ๊นŒ์ง€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ํ•˜๋Š” ๋„๊ตฌ

๐Ÿ“Œ ์ž‘๋™ ๋ฐฉ์‹

CountDownLatch latch = new CountDownLatch(6000);
  • ์Šค๋ ˆ๋“œ๊ฐ€ ์ž‘์—…ํ•  ๋•Œ๋งˆ๋‹ค latch.countDown();
  • ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋Š” latch.await(); ๋กœ ๊ธฐ๋‹ค๋ฆผ

์ฆ‰, 6000๊ฐœ์˜ insert ์ž‘์—…์ด ๋๋‚  ๋•Œ๊นŒ์ง€ initialize()๊ฐ€ ๋๋‚˜์ง€ ์•Š์Œ

โœ… ExecutorService + Executors.newFixedThreadPool()

๐Ÿ”ธ ExecutorService

  • ์ž๋ฐ”์˜ ์Šค๋ ˆ๋“œ ํ’€ ๊ด€๋ฆฌ ๋„๊ตฌ
  • ์Šค๋ ˆ๋“œ๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ , ํ’€์—์„œ ๊บผ๋‚ด์„œ ์‹คํ–‰ ๊ฐ€๋Šฅ
ExecutorService executorService = Executors.newFixedThreadPool(10);
  • ์ตœ๋Œ€ 10๊ฐœ์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์‹คํ–‰ ๊ฐ€๋Šฅ
  • executorService.submit(() -> {...}) → ๋น„๋™๊ธฐ๋กœ ์ž‘์—… ์‹คํ–‰

โœ… ์ด ์ฝ”๋“œ์˜ ์ „์ฒด ๊ตฌ์กฐ๋ฅผ ์ •๋ฆฌํ•˜๋ฉด?

๐Ÿš€ ๋ชฉํ‘œ:

2000๊ฐœ์”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š” insert()๋ฅผ 6000๋ฒˆ ๋ณ‘๋ ฌ๋กœ ๋Œ๋ ค์„œ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”

๐Ÿงฑ ๊ตฌ์„ฑ:

๊ตฌ์„ฑ ์š”์†Œ ์—ญํ• 
EntityManager JPA๋กœ INSERT ์‹คํ–‰
TransactionTemplate ํŠธ๋žœ์žญ์…˜ ์ง์ ‘ ์ œ์–ด (@Transactional ์—†์ด๋„ ์•ˆ์ „ํ•˜๊ฒŒ)
ExecutorService 10๊ฐœ์˜ ์Šค๋ ˆ๋“œ๋กœ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
CountDownLatch ๋ชจ๋“  ์ž‘์—… ๋๋‚  ๋•Œ๊นŒ์ง€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋Œ€๊ธฐ
Snowflake ID ์ค‘๋ณต ์—†์ด ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๊ณ ์œ  ID ์ƒ์„ฑ๊ธฐ

โ˜‘๏ธ ๊ฐ„๋‹จ ์š”์•ฝ

ํ‚ค์›Œ๋“œ ์„ค๋ช…
@PersistenceContext JPA์˜ EntityManager ์ฃผ์ž…
EntityManager JPA์˜ DB ์ž‘์—… ๋„๊ตฌ (persist, find, etc)
TransactionTemplate ๋ช…์‹œ์ ์œผ๋กœ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ์ œ์–ด
CountDownLatch ๋ชจ๋“  ์Šค๋ ˆ๋“œ ์ž‘์—… ์™„๋ฃŒ ๋Œ€๊ธฐ์šฉ
ExecutorService ์Šค๋ ˆ๋“œ ํ’€๋กœ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ์ฒ˜๋ฆฌ
Executors.newFixedThreadPool(10) ์ตœ๋Œ€ 10๊ฐœ์˜ ์Šค๋ ˆ๋“œ๋กœ ๋™์‹œ์— ์‹คํ–‰

ํ•„์š”ํ•˜๋ฉด ์ด ๊ตฌ์กฐ๋ฅผ @Transactional, @Async, TestContainers, Benchmark ์ชฝ์œผ๋กœ ํ™•์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์•Œ๋ ค์ค„๊ฒŒ!


๐Ÿ“ transactionTemplate ์ด ์—†์œผ๋ฉด entityManager.persist(); ๊ฐ€ ํšจ๊ณผ ์—†๋‚˜?

  void insert() {
    transactionTemplate.executeWithoutResult(status -> {
      for (int i = 0; i < BULK_INSERT_SIZE; i++) {
        Article article = Article.create(
            snowflake.nextId(),
            "title" + i,
            "content" + i,
            1L,
            1L
        );
        entityManager.persist(article);
      }
    });
  }

 

์ข‹์•„, ์ด ์งˆ๋ฌธ ํ•ต์‹ฌ์ด์•ผ.
๋ฐ”๋กœ ๋Œ€๋‹ตํ•˜๋ฉด:

โŒ transactionTemplate์ด ์—†๋‹ค๊ณ  entityManager.persist()๋ฅผ ๋ชป ์“ฐ๋Š” ๊ฑด ์•„๋‹ˆ์•ผ.
ํ•˜์ง€๋งŒ → ํŠธ๋žœ์žญ์…˜์ด ์—†์œผ๋ฉด persist()๋งŒ์œผ๋กœ๋Š” ์‹ค์ œ๋กœ DB์— ์•ˆ์ „ํ•˜๊ฒŒ insert๋˜์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ์–ด.

์ด๊ฑธ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๋ ค๋ฉด, JPA์—์„œ ํŠธ๋žœ์žญ์…˜์ด ์–ด๋–ค ์—ญํ• ์„ ํ•˜๋Š”์ง€ ๋จผ์ € ๋ด์•ผ ํ•ด.

โœ… JPA์—์„œ persist()์˜ ๋ณธ์งˆ

entityManager.persist(entity);

 

์ด๊ฑด ๋‹จ์ˆœํžˆ **"์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ๋“ฑ๋ก"**ํ•œ๋‹ค๋Š” ๋œป์ด์•ผ.
์ฆ‰, ๋ฉ”๋ชจ๋ฆฌ ์ˆ˜์ค€์˜ ์ž‘์—…์ด์•ผ.

ํ•˜์ง€๋งŒ ์ง„์งœ๋กœ DB์— INSERT SQL์ด ๋‚ ์•„๊ฐ€๊ณ  ์ปค๋ฐ‹๋˜๋Š” ๊ฑด:

โœ… ํŠธ๋žœ์žญ์…˜์ด ์—ด๋ ค ์žˆ์„ ๋•Œ + ์ปค๋ฐ‹๋  ๋•Œ

โŒ ํŠธ๋žœ์žญ์…˜ ์—†์ด persist()๋งŒ ํ˜ธ์ถœํ•˜๋ฉด?

  • EntityManager.persist()๋Š” ํ˜ธ์ถœ๋˜์ง€๋งŒ,
  • flush()๋‚˜ commit()์ด ํ˜ธ์ถœ๋˜์ง€ ์•Š์œผ๋ฉด ์‹ค์ œ DB์—๋Š” ๋ฐ˜์˜๋˜์ง€ ์•Š์Œ
  • ์ฆ‰, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ์•„๋ฌด ์ผ๋„ ์•ˆ ์ผ์–ด๋‚œ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ผ ์ˆ˜ ์žˆ์Œ

๐ŸŽฏ ๊ทธ๋ž˜์„œ transactionTemplate์ด ํ•„์š”ํ•œ ์ด์œ ๋Š”?

๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ๋Š” @Transactional์„ ์“ฐ๊ธฐ ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์—
→ TransactionTemplate์„ ์จ์„œ ๋ช…์‹œ์ ์œผ๋กœ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ์ •ํ•ด์ฃผ๋Š” ๊ฑฐ์•ผ.

transactionTemplate.executeWithoutResult(status -> {
    entityManager.persist(...); // ์ด์ œ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์‹คํ–‰๋จ
});

๐Ÿ“Œ ๋น„๊ต ์š”์•ฝ

์ผ€์ด์Šค persist() ์‹คํ–‰ ๊ฐ€๋Šฅ? ์‹ค์ œ DB ๋ฐ˜์˜๋จ? ๋น„๊ณ 
ํŠธ๋žœ์žญ์…˜ ์—†์Œ ๊ฐ€๋Šฅ (์˜ค๋ฅ˜ ์•„๋‹˜) โŒ ์•ˆ ๋จ flush/commit ์—†์Œ
ํŠธ๋žœ์žญ์…˜ ์žˆ์Œ (@Transactional ๋˜๋Š” TransactionTemplate) โœ… ๊ฐ€๋Šฅ โœ… DB ๋ฐ˜์˜๋จ ํŠธ๋žœ์žญ์…˜ ๋์— flush๋จ

โœ… ์ •๋ฆฌ

  • entityManager.persist()๋Š” ํŠธ๋žœ์žญ์…˜์ด ์žˆ์–ด์•ผ ์˜๋ฏธ ์žˆ์Œ
  • TransactionTemplate ์—†์ด๋„ ํ˜ธ์ถœ์€ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, DB ๋ฐ˜์˜์€ ์•ˆ ๋จ
  • ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ์—์„œ๋Š” @Transactional์ด ์ ์šฉ ์•ˆ ๋˜๊ธฐ ๋•Œ๋ฌธ์—, TransactionTemplate์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”

๊ถ๊ธˆํ•˜๋ฉด flush(), commit(), rollback์˜ ์ฐจ์ด๋„ ๋” ์„ค๋ช…ํ•ด์ค„๊ฒŒ!
๋˜๋Š” persist() vs merge() ์ฐจ์ด๋„ ์ž์ฃผ ๋ฌป๋Š” ํฌ์ธํŠธ์•ผ.


๐Ÿ“ docker exec -it kuke_board_mysql bash ๋ช…๋ น์–ด ์˜๋ฏธ

์ข‹์•„! ์ด ๋ช…๋ น์–ด๋Š” Docker์—์„œ ์‹คํ–‰ ์ค‘์ธ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์— ๋“ค์–ด๊ฐ€๋Š” ๋ช…๋ น์–ด์•ผ.

๐Ÿงพ docker exec -it kuke_board_mysql bash

 

โœ… ๊ตฌ์„ฑ ์š”์†Œ๋ณ„ ํ•ด์„

ํŒŒํŠธ ์˜๋ฏธ
docker Docker CLI ๋ช…๋ น์–ด
exec ์‹คํ–‰ ์ค‘์ธ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๊ฒ ๋‹ค๋Š” ๋œป
-i interactive ๋ชจ๋“œ — ์ž…๋ ฅ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ
-t pseudo-TTY — ํ„ฐ๋ฏธ๋„์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ํ•ด์คŒ (ํ™”๋ฉด ์ถœ๋ ฅ ์˜ˆ์˜๊ฒŒ)
kuke_board_mysql ์ปจํ…Œ์ด๋„ˆ ์ด๋ฆ„ (๋˜๋Š” ID)
bash ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ ์‹คํ–‰ํ•  ๋ช…๋ น์–ด (์—ฌ๊ธฐ์„  bash ์‰˜)

 

๋Œ“๊ธ€