Home > Backend Development > πŸ“š[Backend Development] λŒ€κ·œλͺ¨ λ°μ΄ν„°μ—μ„œ κ²Œμ‹œκΈ€ λͺ©λ‘ μ‘°νšŒκ°€ λ³΅μž‘ν•œ 이유

πŸ“š[Backend Development] λŒ€κ·œλͺ¨ λ°μ΄ν„°μ—μ„œ κ²Œμ‹œκΈ€ λͺ©λ‘ μ‘°νšŒκ°€ λ³΅μž‘ν•œ 이유
Backend Ddevelopment

β€œπŸ“š[Backend Development] λŒ€κ·œλͺ¨ λ°μ΄ν„°μ—μ„œ κ²Œμ‹œκΈ€ λͺ©λ‘ μ‘°νšŒκ°€ λ³΅μž‘ν•œ 이유

πŸ“ Intro

  • λŒ€κ·œλͺ¨ λ°μ΄ν„°μ—μ„œ κ²Œμ‹œκΈ€ λͺ©λ‘ μ‘°νšŒκ°€ λ³΅μž‘ν•œ μ΄μœ λŠ” μ—¬λŸ¬ 가지가 μžˆμŠ΅λ‹ˆλ‹€.
  • 일반적으둜 μ†Œκ·œλͺ¨ λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œλŠ” λ‹¨μˆœν•œ SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 같은 쿼리둜 μ‰½κ²Œ ν•΄κ²°ν•  수 μžˆμ§€λ§Œ, 수백만~μˆ˜μ–΅ 개의 데이터λ₯Ό 닀뀄야 ν•˜λŠ” μ‹œμŠ€ν…œμ—μ„œλŠ” μ—¬λŸ¬ λ³΅μž‘ν•œ λ¬Έμ œκ°€ λ°œμƒν•©λ‹ˆλ‹€.

βœ…1️⃣ λŒ€κ·œλͺ¨ λ°μ΄ν„°μ—μ„œ κ²Œμ‹œκΈ€ λͺ©λ‘ μ‘°νšŒκ°€ λ³΅μž‘ν•œ 이유

1️⃣ λ°μ΄ν„°μ˜ 양이 λ°©λŒ€ν•¨

  • κ²Œμ‹œκΈ€μ΄ λ§Žμ•„μ§ˆμˆ˜λ‘ ORDER BY와 LIMIT μ—°μ‚°μ˜ 뢀담이 컀짐
  • 인덱슀λ₯Ό μ‚¬μš©ν•˜λ”λΌλ„ λ””μŠ€ν¬μ˜ I/O λΉ„μš©μ΄ μ¦κ°€ν•˜κ³  캐싱이 μ–΄λ ΅κ²Œ 됨

πŸ“ 예제 쿼리

SELECT * FROM posts ORDER BY created_at DESC LIMIT 10;

❌ 문제점

  • ORDER BY create_at DESC μ‹€ν–‰ μ‹œ λͺ¨λ“  κ²Œμ‹œκΈ€μ„ μ •λ ¬ν•΄μ•Ό 함 (데이터가 클수둝 느렀짐)
  • μ΅œμ‹  κ²Œμ‹œκΈ€μ΄ 많으면 μƒˆλ‘œμš΄ 데이터가 계속 μΆ”κ°€λ˜λ©° μΈλ±μŠ€κ°€ 자주 변경됨

2️⃣ νŽ˜μ΄μ§€λ„€μ΄μ…˜ (Pagination) μ„±λŠ₯ μ €ν•˜

  • κ²Œμ‹œνŒμ€ 보톡 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ„ 지원해야 ν•©λ‹ˆλ‹€.
  • 즉, LIMITκ³Ό OFFSET을 μ‚¬μš©ν•΄ νŠΉμ • νŽ˜μ΄μ§€μ˜ κ²Œμ‹œκΈ€μ„ μ‘°νšŒν•΄μ•Ό ν•˜λŠ”λ°, λŒ€κ·œλͺ¨ λ°μ΄ν„°μ—μ„œλŠ” OFFSET이 클수둝 μ„±λŠ₯이 μ €ν•˜λ©λ‹ˆλ‹€.
    SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 OFFSET 1000000;
    

❌ 문제점

  • OFFSET 1000000을 μ‚¬μš©ν•˜λ©΄ μ•žμ˜ 100만 개 데이터λ₯Ό μŠ€μΊ”ν•˜κ³  버린 ν›„ κ·Έ λ‹€μŒ 10개λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
  • 데이터가 λ§Žμ„μˆ˜λ‘ λΆˆν•„μš”ν•œ 연산이 λ§Žμ•„μ§€κ³  μ„±λŠ₯이 κΈ‰κ²©νžˆ μ €ν•˜λ©λ‹ˆλ‹€.

βœ… ν•΄κ²° 방법

  • Keyset pagination (Seek 방식) ν™œμš© ➞ OFFSET 없이 WHERE 쑰건을 ν™œμš©
  • LIMIT을 ν™œμš©ν•΄ 이전 쑰회된 λ§ˆμ§€λ§‰ IDλ₯Ό κΈ°μ€€μœΌλ‘œ λ‹€μŒ 데이터λ₯Ό κ°€μ Έμ˜€κΈ°
    SELECT * FROM posts WHERE created_at < '2024-03-11 10:00:00' ORDER BY created_at DESC LIMIT 10;
    

3️⃣ 인덱슀 μ‚¬μš© μ΅œμ ν™” 문제

πŸ“Œ μ™œ 인덱슀만으둜 ν•΄κ²°λ˜μ§€ μ•Šμ„κΉŒ?

  • 인덱슀λ₯Ό μ‚¬μš©ν•˜λ©΄ 정렬이 λΉ¨λΌμ§€μ§€λ§Œ, 데이터가 λ§Žμ„ κ²½μš°μ—λ„ μ—¬μ „νžˆ λ””μŠ€ν¬ I/O λΉ„μš©μ΄ 증가
  • μƒˆλ‘œμš΄ κ²Œμ‹œκΈ€μ΄ 좔가될 λ•Œλ§ˆλ‹€ B-Tree μΈλ±μŠ€κ°€ κ°±μ‹ λ˜λ©° μ„±λŠ₯에 뢀담을 쀌

βœ… ν•΄κ²° 방법

  • 컀버링 인덱슀 (Covering Index) ν™œμš© ➞ SELECT에 ν¬ν•¨λœ λͺ¨λ“  μ»¬λŸΌμ„ μΈλ±μŠ€μ— 미리 포함
  • νŒŒν‹°μ…”λ‹ (Partitioning) ➞ λ‚ μ§œλ³„ ν…Œμ΄λΈ” 뢄리 (posts_202403 같은 ν…Œμ΄λΈ”)
    CREATE INDEX idx_posts_created ON posts(created_at, title, content);
    

4️⃣ νŠΈλž˜ν”½ λΆ„μ‚° 문제

  • κ²Œμ‹œκΈ€ λͺ©λ‘μ€ λŒ€λΆ€λΆ„ μ„œλΉ„μŠ€μ—μ„œ 쑰회 νŠΈλž˜ν”½μ΄ 많고, μ“°κΈ° νŠΈλž˜ν”½λ„ κΎΈμ€€νžˆ λ°œμƒν•˜λŠ” κ΅¬μ‘°μž…λ‹ˆλ‹€.
  • 즉, 읽기(READ)κ°€ λ§Žμ§€λ§Œ, λ™μ‹œμ— μƒˆλ‘œμš΄ κ²Œμ‹œκΈ€μ΄ μΆ”κ°€λ˜λ©° μ •λ ¬ μˆœμ„œκ°€ 계속 λ°”λ€λ‹ˆλ‹€.

❌ 문제점

  • μΊμ‹œ μ‚¬μš©μ΄ μ–΄λ ΅λ‹€ ➞ μƒˆλ‘œμš΄ κ²Œμ‹œκΈ€μ΄ μΆ”κ°€λ˜λ©΄ μ •λ ¬ μˆœμ„œκ°€ λ°”λ€Œμ–΄ κΈ°μ‘΄ μΊμ‹œ λ¬΄νš¨ν™”λ¨
  • λ™μ‹œμ„±μ΄ μ¦κ°€ν•˜λ©΄ DB λΆ€ν•˜κ°€ 컀짐 ➞ λ§Žμ€ μœ μ €κ°€ 같은 λͺ©λ‘μ„ μš”μ²­ν•˜λ©΄ DB λΆ€ν•˜ 증가

βœ… ν•΄κ²° 방법

1️⃣ 캐싱 (Redis, Memcached) ν™œμš©

  • μ΅œμ‹  κ²Œμ‹œκΈ€ λͺ©λ‘μ„ Redis에 μ €μž₯ν•˜κ³ , 데이터가 변경될 λ•Œλ§Œ μ—…λ°μ΄νŠΈ.
    redisTemplate.opsForValue().set("latest_posts", posts, Duration.ofSeconds(60));
    

2️⃣ 읽기 μ „μš© λ°μ΄ν„°λ² μ΄μŠ€(Read Replica) ν™œμš©

  • Master-Slave Replication을 μ‚¬μš©ν•˜μ—¬ 읽기 νŠΈλž˜ν”½μ„ Slave DB둜 λΆ„μ‚°
    SELECT * FROM posts ORDER BY created_at DESC LIMIT 10;
    -- 이 쿼리λ₯Ό Slave DBμ—μ„œ μ‹€ν–‰ν•˜μ—¬ Master DB λΆ€ν•˜ κ°μ†Œ
    

3️⃣ CQRS νŒ¨ν„΄ 적용

  • κ²Œμ‹œκΈ€ μ‘°νšŒμ™€ 생성/μˆ˜μ •μ„ λ‹€λ₯Έ DB둜 뢄리 (읽기 μ „μš© DB, μ“°κΈ° μ „μš© DB)

5️⃣ JOIN μ—°μ‚° μ„±λŠ₯ 문제

  • κ²Œμ‹œκΈ€ λͺ©λ‘μ„ μ‘°νšŒν•  λ•Œ 보톡 μž‘μ„±μž 정보, μ’‹μ•„μš” 수, λŒ“κΈ€ 수 등을 ν•¨κ»˜ μ‘°νšŒν•΄μ•Ό ν•©λ‹ˆλ‹€.
  • 즉, JOIN을 μˆ˜ν–‰ν•΄μ•Ό ν•˜λŠ”λ°, 데이터가 λ§Žμ„μˆ˜λ‘ μ„±λŠ₯이 μ €ν•˜λ©λ‹ˆλ‹€.
    SELECT p.id, p.title, p.content, u.name, COUNT(c.id) as comment_count
    FROM posts p
    LEFT JOIN users u ON p.user_id = u.id
    LEFT JOIN comments c ON p.id = c.post_id
    GROUP BY p.id, u.name
    ORDER BY p.created_at DESC LIMIT 10;
    

❌ 문제점

  • JOIN을 μˆ˜ν–‰ν•  λ•Œ 데이터가 많으면 λ©”λͺ¨λ¦¬μ™€ CPUκ°€ ν•„μš”
  • GROUP BYλ₯Ό μˆ˜ν–‰ν•˜λ©΄ μ •λ ¬ 및 집계 연산이 ν•„μš”ν•˜μ—¬ μ„±λŠ₯이 더 느렀짐

βœ… ν•΄κ²° 방법

  • NoSQL(예: Redis, Elasticsearch) μΊμ‹œ ν™œμš© ➞ μ’‹μ•„μš” 수, λŒ“κΈ€ μˆ˜λŠ” 미리 μ €μž₯ν•΄λ‘ 
  • CQRS νŒ¨ν„΄ 적용 ➞ 별도 ν…Œμ΄λΈ”μ— 미리 κ³„μ‚°λœ 카운트 값을 μ €μž₯
    SELECT p.id, p.title, p.title, u.name, p.comment_count
    FROM posts p
    LEFT JOIN users u ON p.user_id = u.id
    ORDER BY p.created_at DESC LIMIT 10;
    
  • p.comment_countλŠ” 미리 κ³„μ‚°λœ κ°’μ΄λ―€λ‘œ JOIN을 쀄여 μ„±λŠ₯을 κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

βœ…2️⃣ λŒ€κ·œλͺ¨ κ²Œμ‹œκΈ€ 쑰회 μ‹œ μ΅œμ ν™” 방법

1️⃣ Keyset Pagination μ‚¬μš©

SELECT * FROM posts WHERE created_at < '2024-03-11 10:00:00' ORDER BY created_at DESC LIMIT 10;
  • OFFSET 없이 이전 쑰회된 λ§ˆμ§€λ§‰ IDλ₯Ό κΈ°μ€€μœΌλ‘œ λ‹€μŒ 데이터λ₯Ό κ°€μ Έμ˜€λŠ” 방식

2️⃣ 캐싱 ν™œμš© (Redis, Elasticsearch)

redisTemplate.opsForValue().set(CACHE_KEY, posts, Duration.ofSeconds(60))
  • μ΅œμ‹  κ²Œμ‹œκΈ€μ„ μΊμ‹œμ— μ €μž₯ν•˜μ—¬ DB λΆ€ν•˜λ₯Ό μ€„μž„

3️⃣ 읽기 μ „μš© DB(Read Replica) ν™œμš©

SELECT * FROM posts ORDER BY created_at DESC LIMIT 10;
  • 읽기 νŠΈλž˜ν”½μ„ Replica DB둜 λΆ„μ‚°ν•˜μ—¬ μ„±λŠ₯ μ΅œμ ν™”

4️⃣ 미리 κ³„μ‚°λœ κ°’ μ‚¬μš©

SELECT p.id, p.title, p.content, u.name, p.comment_count
FROM posts p
LEFT JOIN users u ON p.user_id = u.id
ORDER BY p.created_at DESC LIMIT 10;
  • λŒ“κΈ€ 개수, μ’‹μ•„μš” 수 등을 미리 κ³„μ‚°λœ κ°’μœΌλ‘œ μ €μž₯ν•˜μ—¬ JOIN μ—°μ‚° 쀄이기

βœ…3️⃣ κ²°λ‘ 

βœ… λŒ€κ·œλͺ¨ λ°μ΄ν„°μ—μ„œ κ²Œμ‹œκΈ€ λͺ©λ‘ μ‘°νšŒκ°€ λ³΅μž‘ν•œ μ΄μœ λŠ”?

  • 데이터 양이 λ§Žμ•„ ORDER BY LIMITκ°€ λΉ„νš¨μœ¨μ 
  • OFFSET이 클수둝 μ„±λŠ₯ μ €ν•˜ (νŽ˜μ΄μ§• 문제)
  • JOIN 연산이 λ§Žμ„μˆ˜λ‘ μ„±λŠ₯ μ €ν•˜
  • μ“°κΈ° νŠΈλž˜ν”½μ΄ λ§Žμ•„μ§€λ©΄ 인덱슀 관리 λΆ€λ‹΄ 증가
  • μΊμ‹œκ°€ 자주 λ¬΄νš¨μ™€λ˜μ–΄ DB λΆ€ν•˜κ°€ 컀짐

βœ… μ΅œμ ν™” 방법
1️⃣ Keyset Pagination ➞ OFFSET λŒ€μ‹  λ§ˆμ§€λ§‰ ID 기반 쑰회
2️⃣ Redis, Elasticsearch 캐싱 ➞ μ΅œμ‹  데이터 미리 μ €μž₯
3️⃣ 읽기 μ „μš© DB(Read Replica) ν™œμš© ➞ 쑰회 νŠΈλž˜ν”½ λΆ„μ‚°
4️⃣ 미리 κ³„μ‚°λœ 데이터 ν™œμš© ➞ JOIN μ΅œμ†Œν™”