๐Ÿ’พ Soft Delete: ๋ฐ์ดํ„ฐ๋ฅผ ์ง„์งœ ์ง€์šฐ์ง€ ์•Š๋Š” ์ด์œ 

๋ฐ•์ค€ํ˜•ยท2025๋…„ 10์›” 19์ผ

์Šคํ”„๋ง ๊ฐœ๋ฐœ

๋ชฉ๋ก ๋ณด๊ธฐ
20/20
post-thumbnail

ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ๋•Œ ๊ณ ๋ฏผ์ด ๋งŽ์•˜๋‹ค. "์‚ฌ์šฉ์ž๊ฐ€ ์‚ญ์ œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด DB์—์„œ ์™„์ „ํžˆ ์ง€์›Œ๋ฒ„๋ฆด๊นŒ?" ์ฒ˜์Œ์—” ๋‹น์—ฐํžˆ ๊ทธ๋ž˜์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋Š”๋ฐ, ์กฐ๊ธˆ๋งŒ ์ƒ๊ฐํ•ด๋ณด๋‹ˆ ๋ฌธ์ œ๊ฐ€ ๋ณด์ด๊ธฐ ์‹œ์ž‘ํ–ˆ๋‹ค.

๋ฌธ์ œ ์ƒํ™ฉ

์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ๋Š” ๋ฐ์ดํ„ฐ ๋ถ„์„ ํ”Œ๋žซํผ์ด๋‹ค. ์‚ฌ์šฉ์ž๋“ค์ด ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋กœ๋“œํ•˜๊ณ , ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๋ถ„์„ ํ”„๋กœ์ ํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ํ”ผ๋“œ๋ฐฑ์„ ์ด์–ด ๋‚จ๊ธธ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋งŒ์•ฝ ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ˆ˜๋กœ ์‚ญ์ œ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €๋‹ค๋ฉด? ๋˜๋Š” ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ๋‚จ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•œ๋‹ค๋ฉด? ๋ฒ•์  ๋ฌธ์ œ๋กœ ํŠน์ • ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ด๋ ฅ ์ถ”์ ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด?

๋ฐ์ดํ„ฐ๋ฅผ ์™„์ „ํžˆ ์‚ญ์ œํ•ด๋ฒ„๋ฆฌ๋ฉด(Hard Delete) ๋ณต๊ตฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. ๋ฐฑ์—…์—์„œ ๋ณต๊ตฌํ•  ์ˆ˜๋„ ์žˆ๊ฒ ์ง€๋งŒ, ๊ทธ๋™์•ˆ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋“ค์ด ์ž‘์„ฑํ•œ ๋ฐ์ดํ„ฐ์™€์˜ ์ •ํ•ฉ์„ฑ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค.

Soft Delete๋ž€?

Soft Delete๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ฌผ๋ฆฌ์ ์œผ๋กœ ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ , "์‚ญ์ œ๋จ" ํ‘œ์‹œ๋งŒ ํ•ด๋‘๋Š” ๋ฐฉ์‹์ด๋‹ค.

// Hard Delete (๋ฌผ๋ฆฌ์  ์‚ญ์ œ)
DELETE FROM users WHERE user_id = 123;

// Soft Delete (๋…ผ๋ฆฌ์  ์‚ญ์ œ)
UPDATE users SET is_deleted = true WHERE user_id = 123;

DB์—๋Š” ๊ทธ๋Œ€๋กœ ๋‚จ์•„์žˆ์ง€๋งŒ, ์ผ๋ฐ˜์ ์ธ ์กฐํšŒ ์ฟผ๋ฆฌ์—์„œ๋Š” ๋ณด์ด์ง€ ์•Š๊ฒŒ ์ฒ˜๋ฆฌํ•œ๋‹ค.

์‚ญ์ œ๋œ ๊ฒŒ์‹œ๊ธ€, ์™œ ๋ณด์กดํ•ด์•ผ ํ• ๊นŒ?

"๊ทธ๋ƒฅ ์ง€์šฐ๋ฉด ๋˜๋Š”๋ฐ ๋ญ๊ฐ€ ๋ฌธ์ œ์•ผ?"๋ผ๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ œ ์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•˜๋‹ค ๋ณด๋ฉด ์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์กดํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด ์ƒ๊ฐ๋ณด๋‹ค ๋งŽ๋‹ค.

1. ๋ฒ•์  ์ฆ๊ฑฐ ๋ณด์กด

์–ด๋–ค ์‚ฌ์šฉ์ž๊ฐ€ ์•…์˜์ ์ธ ๊ฒŒ์‹œ๊ธ€์„ ์˜ฌ๋ฆฌ๊ณ  ๋ฐ”๋กœ ์‚ญ์ œํ•ด๋ฒ„๋ ธ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ณด์ž.

1. ์‚ฌ์šฉ์ž A๊ฐ€ ๋ช…์˜ˆํ›ผ์†์„ฑ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ
2. ํ”ผํ•ด์ž B๊ฐ€ ์‹ ๊ณ  ์ „์— A๊ฐ€ ๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ
3. ์ฆ๊ฑฐ๊ฐ€ ์‚ฌ๋ผ์ง โ†’ ๋ฒ•์  ๋Œ€์‘ ๋ถˆ๊ฐ€๋Šฅ

Hard Delete๋ฅผ ์“ฐ๋ฉด ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ ์†์ˆ˜๋ฌด์ฑ…์ด๋‹ค. ํ•˜์ง€๋งŒ Soft Delete๋กœ ๊ตฌํ˜„ํ•˜๋ฉด, ๊ฒ‰์œผ๋กœ๋Š” ์‚ญ์ œ๋์ง€๋งŒ DB์—๋Š” ๋‚จ์•„์žˆ์–ด์„œ ํ•„์š”ํ•  ๋•Œ ์ฆ๊ฑฐ๋กœ ์ œ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ถ€์ ์ ˆํ•œ ๋ฐ์ดํ„ฐ์…‹ ์„ค๋ช…์„ ์˜ฌ๋ฆฌ๊ณ  ์‚ญ์ œํ•œ ์ผ€์ด์Šค๊ฐ€ ์žˆ์„ ๊ฒƒ ๊ฐ™๊ธฐ์—, Soft Delete ๋•๋ถ„์— ์šด์˜ ๊ณผ์ •์—์„œ ํ•ด๋‹น ๋‚ด์šฉ์„ ํ™•์ธํ•˜๊ณ  ๊ฒฝ๊ณ  ์กฐ์น˜๋ฅผ ์ทจํ•  ์ˆ˜ ์žˆ๋‹ค.

2. ์•…์˜์ ์ธ ์‚ญ์ œ ๊ณต๊ฒฉ ๋ฐฉ์–ด

๊ณ„์ •์ด ํ•ดํ‚น๋‹นํ–ˆ๊ฑฐ๋‚˜, ํ‡ด์‚ฌํ•˜๋Š” ์ง์›์ด ์•™์‹ฌ์„ ํ’ˆ๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๋Œ€๋Ÿ‰ ์‚ญ์ œํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ์ƒ๊ฐํ•ด๋ณด์ž.

// ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์˜ ๋ชจ๋“  ๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ
for (Post post : userPosts) {
    post.delete();  // Soft Delete
}

Hard Delete์˜€๋‹ค๋ฉด ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์˜๊ตฌ ์‚ญ์ œ๋˜์ง€๋งŒ, Soft Delete๋Š” ๋ณต๊ตฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค. ๊ด€๋ฆฌ์ž๊ฐ€ ์ด์ƒ ์ง•ํ›„๋ฅผ ๋ฐœ๊ฒฌํ•˜๋ฉด ๋ฐ”๋กœ ๋ณต๊ตฌํ•  ์ˆ˜ ์žˆ๋‹ค.

// ๊ด€๋ฆฌ์ž๊ฐ€ ํ•œ ๋ฒˆ์— ๋ณต๊ตฌ
deletedPosts.forEach(Post::restore);

3. ์„œ๋น„์Šค ์šด์˜๊ณผ ๋ณด์•ˆ

์ŠคํŒธ/์–ด๋ทฐ์ง• ํŒจํ„ด ๋ถ„์„

์‚ญ์ œ๋œ ๊ฒŒ์‹œ๊ธ€์„ ๋ถ„์„ํ•˜๋ฉด ์ŠคํŒธ ํŒจํ„ด์„ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • "์ด ์‚ฌ์šฉ์ž๋Š” ํ•ญ์ƒ ๊ด‘๊ณ  ๊ฒŒ์‹œ๊ธ€์„ ์˜ฌ๋ฆฌ๊ณ  30๋ถ„ ๋’ค ์‚ญ์ œํ•˜๋„ค?"
  • "ํŠน์ • ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ๊ฒŒ์‹œ๊ธ€์ด ์ž์ฃผ ์‚ญ์ œ๋˜๋„ค?"

์ด๋Ÿฐ ์ธ์‚ฌ์ดํŠธ๋Š” Hard Delete๋กœ๋Š” ์–ป์„ ์ˆ˜ ์—†๋‹ค. Soft Delete๋กœ ๋ณด์กดํ•˜๋ฉด ์ŠคํŒธ์„ ์‚ฌ์ „์— ์ฐจ๋‹จํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

์–ด๋ทฐ์ง• ์‚ฌ์šฉ์ž ์ถ”์ 

-- ์ตœ๊ทผ 1์ฃผ์ผ๊ฐ„ 50๊ฐœ ์ด์ƒ ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•˜๊ณ  ์‚ญ์ œํ•œ ์‚ฌ์šฉ์ž
SELECT user_id, COUNT(*) as deleted_count
FROM posts
WHERE is_deleted = true
  AND deleted_at > NOW() - INTERVAL '7 days'
GROUP BY user_id
HAVING COUNT(*) > 50;

์ด๋Ÿฐ ์ฟผ๋ฆฌ๋กœ ์–ด๋ทฐ์ง• ์‚ฌ์šฉ์ž๋ฅผ ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค.

4. ์‚ญ์ œ ์ด์œ  ๋ถ„์„

"์™œ ์‚ฌ์šฉ์ž๋“ค์ด ๊ฒŒ์‹œ๊ธ€์„ ์‚ญ์ œํ• ๊นŒ?"๋ฅผ ๋ถ„์„ํ•˜๋ฉด ์„œ๋น„์Šค ๊ฐœ์„  ํฌ์ธํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค.

  • ๋Œ€๋ถ€๋ถ„ ์—…๋กœ๋“œ ์งํ›„ 5๋ถ„ ์ด๋‚ด์— ์‚ญ์ œ๋จ โ†’ UI/UX๊ฐ€ ๋ถˆํŽธํ•ด์„œ ๋‹ค์‹œ ์˜ฌ๋ฆฌ๋Š” ๊ฑฐ๊ตฌ๋‚˜
  • ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ๋… ๋งŽ์ด ์‚ญ์ œ๋จ โ†’ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„๋ฅ˜๊ฐ€ ํ—ท๊ฐˆ๋ฆฌ๋Š”๊ตฌ๋‚˜

์ด๋Ÿฐ ๊ฑธ ๋ฐœ๊ฒฌํ•˜๊ณ  ์—…๋กœ๋“œ ํ”Œ๋กœ์šฐ๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

5. ์‚ฐ์—…๋ณ„ ๊ทœ์ œ ์ค€์ˆ˜

์ด์ „์—๋„ ์—ฌ๋Ÿฌ ํ”Œ๋žซํผ์„ ์ด์šฉํ•ด๋ณด๋ฉด ์•ฝ๊ด€์—์„œ ๋ฐ์ดํ„ฐ ๋ณด์กด๊ณผ ๊ด€๋ จ๋œ ๋‚ด์šฉ์„ ๋ดค์—ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ์‚ฐ์—…์— ๋”ฐ๋ผ์„œ๋Š” ๋ฒ•์ ์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ณด์กด ์˜๋ฌด๊ฐ€ ์žˆ๋‹ค.

  • ๊ธˆ์œต: ๊ธˆ์œต๊ฑฐ๋ž˜ ๊ธฐ๋ก 5๋…„ ๋ณด์กด ์˜๋ฌด
  • ์˜๋ฃŒ: ์ง„๋ฃŒ ๊ธฐ๋ก 10๋…„ ๋ณด์กด ์˜๋ฌด
  • ์ „์ž์ƒ๊ฑฐ๋ž˜: ์†Œ๋น„์ž ๋ณดํ˜ธ๋ฒ•์— ๋”ฐ๋ฅธ ๊ฑฐ๋ž˜ ๊ธฐ๋ก ๋ณด์กด

์ด๋Ÿฐ ์„œ๋น„์Šค๋Š” Soft Delete๊ฐ€ ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜์ด๋‹ค.

6. ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ

๊ฒŒ์‹œ๊ธ€์„ Hard Deleteํ•˜๋ฉด ์—ฐ๊ด€๋œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ๊ณจ์น˜์•„ํ”„๋‹ค.

๊ฒŒ์‹œ๊ธ€ (Hard Delete)
  โ†“
๋Œ“๊ธ€ 50๊ฐœ๋Š” ์–ด๋–ป๊ฒŒ ํ•˜์ง€?
์ข‹์•„์š” 100๊ฐœ๋Š”?
์•Œ๋ฆผ ๊ธฐ๋ก์€?
ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ๋Š”?

CASCADE DELETE๋กœ ๋‹ค ์ง€์šธ ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ๊ทธ๋Ÿผ ์‚ฌ์šฉ์ž๊ฐ€ ์“ด ๋Œ“๊ธ€๊นŒ์ง€ ์ „๋ถ€ ์‚ฌ๋ผ์ง„๋‹ค. "๋‚ด๊ฐ€ ๋ถ„๋ช…ํžˆ ๋Œ“๊ธ€ ๋‹ฌ์•˜๋Š”๋ฐ ์™œ ์—†์–ด์š”?" ๊ฐ™์€ ๋ฌธ์˜๊ฐ€ ๋“ค์–ด์˜จ๋‹ค.

Soft Delete๋Š” ์ฐธ์กฐ ๊ด€๊ณ„๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ์กฐํšŒ์—์„œ๋งŒ ์ œ์™ธ๋˜๋‹ˆ ํ›จ์”ฌ ๊น”๋”ํ•˜๋‹ค.

// ๊ฒŒ์‹œ๊ธ€์ด ์‚ญ์ œ๋˜์–ด๋„ ๋Œ“๊ธ€์€ ๊ทธ๋Œ€๋กœ
@Entity
public class Comment {
    @ManyToOne
    @JoinColumn(name = "post_id")
    private Post post;  // ์‚ญ์ œ๋œ ๊ฒŒ์‹œ๊ธ€๋„ ์ฐธ์กฐ ๊ฐ€๋Šฅ
}

์‹ค์ˆ˜๋กœ ์ง€์šด ๊ฑด ์ •๋ง ๋งŽ๋‹ค

์†”์งํžˆ ๋งํ•˜๋ฉด, ์ œ์ผ ๋งŽ์€ ์ผ€์ด์Šค๋Š” "์‚ฌ์šฉ์ž ์‹ค์ˆ˜"๋‹ค.

  • "ํœด๋Œ€ํฐ ํ™”๋ฉด ์ž‘์•„์„œ ์ž˜๋ชป ๋ˆŒ๋ €์–ด์š”"
  • "์‚ญ์ œ ํ™•์ธ ํŒ์—…์ด ์•ˆ ๋–ด์–ด์š”" (์‹ค์ œ๋ก  ๋–ด์Œ...)
  • "์ด๊ฑฐ ์ค‘์š”ํ•œ ๊ฑด๋ฐ ์™œ ์ง€์› ์„๊นŒ์š”?" (๋ณธ์ธ์ด ์ง€์›€)

์ด๋Ÿฐ ๋ฌธ์˜๊ฐ€ ๋“ค์–ด์˜ฌ ๊ฐ€๋Šฅ์„ฑ๋„ ์žˆ๋‹ค. ๋ณต๊ตฌ ๊ธฐ๋Šฅ์ด ์—†์œผ๋ฉด ๊ณ ๊ฐ์„ผํ„ฐ ๋ถ€๋‹ด์ด ์—„์ฒญ๋‚˜๋‹ค.

์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ ์ ์šฉ ์‚ฌ๋ก€

1. Entity ์„ค๊ณ„

๋ชจ๋“  ์ฃผ์š” ์—”ํ‹ฐํ‹ฐ์— isDeleted ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

@Entity
@Table(name = "data")
@SQLRestriction("is_deleted = false")
public class DataEntity extends BaseTimeEntity {
    // ... ๋‹ค๋ฅธ ํ•„๋“œ๋“ค

    @Column(name = "is_deleted", nullable = false)
    @Builder.Default
    private Boolean isDeleted = false;

    public void delete() {
        this.isDeleted = true;
    }

    public void restore() {
        this.isDeleted = false;
    }
}

์—ฌ๊ธฐ์„œ ํฌ์ธํŠธ๋Š” @SQLRestriction("is_deleted = false") ์ด๋‹ค.

2. @SQLRestriction ๋งˆ๋ฒ•

Hibernate์˜ @SQLRestriction (๊ตฌ๋ฒ„์ „์—์„œ๋Š” @Where)์„ ์‚ฌ์šฉํ•˜๋ฉด, ์ด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•  ๋•Œ ์ž๋™์œผ๋กœ WHERE ์กฐ๊ฑด์ด ๋ถ™๋Š”๋‹ค.
-> ์ด์ „ Where์€ deprecated

// ๊ฐœ๋ฐœ์ž๊ฐ€ ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๋ฉด
dataRepository.findById(1L);

// ์‹ค์ œ๋กœ๋Š” ์ด๋Ÿฐ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋จ
SELECT * FROM data WHERE data_id = 1 AND is_deleted = false;

๋•๋ถ„์— ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฝ”๋“œ๋งˆ๋‹ค ์ผ์ผ์ด isDeleted = false ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค. ์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ๋Š” ์•Œ์•„์„œ ์กฐํšŒ๋˜์ง€ ์•Š๋Š”๋‹ค.

3. ์‚ญ์ œ ๋กœ์ง ๊ตฌํ˜„

@Repository
@RequiredArgsConstructor
public class SoftDeleteDataDbAdapter implements SoftDeleteDataPort {
    private final DataJpaRepository dataJpaRepository;

    @Override
    public void deleteData(Long dataId) {
        DataEntity data = dataJpaRepository.findById(dataId)
            .orElseThrow(() -> new DataException(DataErrorStatus.NOT_FOUND_DATA));

        data.delete();  // isDeleted = true
        dataJpaRepository.save(data);
    }
}

์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ง€์šฐ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, delete() ๋ฉ”์„œ๋“œ๋กœ ํ”Œ๋ž˜๊ทธ๋งŒ ๋ณ€๊ฒฝํ•œ๋‹ค.

4. ๋ณต๊ตฌ ๊ธฐ๋Šฅ๋„ ๋ณด๋„ˆ์Šค

public void restore() {
    this.isDeleted = false;
}

Soft Delete์˜ ํฐ ์žฅ์  ์ค‘ ํ•˜๋‚˜๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ "์•— ์‹ค์ˆ˜๋กœ ์ง€์› ์–ด์š”!"๋ผ๊ณ  ํ•˜๋ฉด, ๋ฐ”๋กœ ๋ณต๊ตฌํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

5. Elasticsearch์™€์˜ ๋™๊ธฐํ™”

์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ๋Š” ๊ฒ€์ƒ‰์„ ์œ„ํ•ด Elasticsearch๋„ ์‚ฌ์šฉํ•œ๋‹ค. DB์—์„œ Soft Delete ํ–ˆ์œผ๋ฉด, ES์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.

@Component
@RequiredArgsConstructor
public class SoftDeleteDataEsAdapter implements SoftDeleteDataPort {
    private final ElasticsearchClient client;

    @Override
    public void deleteData(Long dataId) {
        DataDeletedUpdate update = DataDeletedUpdate.deleted();
        client.update(u ->
            u.index("data_index")
             .id(dataId.toString())
             .doc(update),
            DataSearchDocument.class
        );
    }
}

ES ๋ฌธ์„œ๋„ ์‚ญ์ œํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, isDeleted ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.



์‹ค์ œ๋กœ ์ ์šฉํ•ด๋ณด๋‹ˆ

์žฅ์ 

1. ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ ๊ฐ€๋Šฅ
์‹ค์ˆ˜๋กœ ์ง€์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณต๊ตฌํ•  ์ˆ˜ ์žˆ๋‹ค. ์‚ฌ์šฉ์ž ๋งŒ์กฑ๋„๊ฐ€ ์˜ฌ๋ผ๊ฐ„๋‹ค.

2. ๊ฐ์‚ฌ(Audit) ์ถ”์ 
"์ด ๋ฐ์ดํ„ฐ๊ฐ€ ์–ธ์ œ ๋ˆ„๊ฐ€ ์ง€์› ๋Š”์ง€" ์ด๋ ฅ์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๋‹ค. ๋ฒ•์  ๋ฌธ์ œ๋‚˜ ๋ถ„์Ÿ ์‹œ ์œ ์šฉํ•˜๋‹ค.

3. ํ†ต๊ณ„ ๋ถ„์„
์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ๋„ ํฌํ•จํ•ด์„œ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ๋‹ค. "์–ผ๋งˆ๋‚˜ ๋งŽ์€ ์œ ์ €๊ฐ€ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง€์› ๋Š”์ง€", "์–ด๋–ค ๋ฐ์ดํ„ฐ๊ฐ€ ์ž์ฃผ ์‚ญ์ œ๋˜๋Š”์ง€" ๊ฐ™์€ ์ธ์‚ฌ์ดํŠธ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

4. ์ฐธ์กฐ ๋ฌด๊ฒฐ์„ฑ ์œ ์ง€
๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์—์„œ FK๋กœ ์ฐธ์กฐํ•˜๊ณ  ์žˆ์–ด๋„ ๋ฌธ์ œ์—†๋‹ค. Hard Deleteํ•˜๋ฉด CASCADE ์„ค์ •์„ ์–ด๋–ป๊ฒŒ ํ• ์ง€ ๊ณ ๋ฏผํ•ด์•ผ ํ•˜๋Š”๋ฐ, Soft Delete๋Š” ๊ทธ๋Ÿฐ ๊ฑฑ์ •์ด ์—†๋‹ค.

5. ์•ˆ์ „ํ•œ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ
ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚ ์•„๊ฐˆ ๊ฑฑ์ •์„ ๋œ ์ˆ˜ ์žˆ๋‹ค.

๋‹จ์  (์†”์งํžˆ ๋งํ•˜๋ฉด)

1. ์Šคํ† ๋ฆฌ์ง€ ๋น„์šฉ
์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ๋„ ๊ณ„์† ์Œ“์ธ๋‹ค. ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด DB ์šฉ๋Ÿ‰์ด ์ปค์ง„๋‹ค. ์ฃผ๊ธฐ์ ์œผ๋กœ ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ •๋ง๋กœ ์‚ญ์ œํ•˜๋Š” ๋ฐฐ์น˜ ์ž‘์—…์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ๋‹ค. -> ์•„์ง์€ ๊ตฌํ˜„ ์•ˆํ•จ.

2. ์ฟผ๋ฆฌ ์„ฑ๋Šฅ ์ €ํ•˜
ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก WHERE is_deleted = false ์กฐ๊ฑด์ด ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋  ์ˆ˜ ์žˆ๋‹ค. is_deleted ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๋ฅผ ๊ฑธ์–ด์•ผ ํ• ์ˆ˜๋„??

CREATE INDEX idx_is_deleted ON data(is_deleted);

3. UNIQUE ์ œ์•ฝ ์กฐ๊ฑด ์ด์Šˆ
์˜ˆ๋ฅผ ๋“ค์–ด email์— UNIQUE ์ œ์•ฝ์ด ์žˆ๋‹ค๋ฉด, ์‚ญ์ œ๋œ ์‚ฌ์šฉ์ž์˜ ์ด๋ฉ”์ผ์„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

  • Partial Index ์‚ฌ์šฉ: CREATE UNIQUE INDEX idx_email ON users(email) WHERE is_deleted = false;
  • ๋˜๋Š” ์‚ญ์ œ ์‹œ ์ด๋ฉ”์ผ์„ ๋ณ€์กฐ: deleted_20231019_user@example.com

-> ํ˜„์žฌ๋Š” soft delete ์‚ญ์ œ ์ฒ˜๋ฆฌ๋งŒ ํ–ˆ๋Š”๋ฐ ๋‘˜ ์ค‘ ์–ด๋–ค ๋ฐฉ์‹์ด ๋” ๋‚˜์€ ๋ฐฉ์‹์ผ์ง€๋Š” ๊ณ ๋ฏผ์ด ์ข€ ๋” ํ•„์š” ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

4. ์ฝ”๋“œ ๋ณต์žก๋„ ์ฆ๊ฐ€
์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ •๋ง๋กœ ์กฐํšŒํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ(๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋“ฑ), ๋ณ„๋„์˜ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

// ์‚ญ์ œ๋œ ๊ฒƒ ํฌํ•จ ์ „์ฒด ์กฐํšŒ
@Query("SELECT d FROM DataEntity d")
List<DataEntity> findAllIncludingDeleted();


๊ฒฐ๋ก 

Soft Delete๋Š” ๋Œ€๋ถ€๋ถ„์˜ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์œ ์šฉํ•˜๋‹ค. ํŠนํžˆ:

  • ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ˆ˜ํ•  ์ˆ˜ ์žˆ๋Š” ์„œ๋น„์Šค
  • ๋ฐ์ดํ„ฐ ์ด๋ ฅ ์ถ”์ ์ด ์ค‘์š”ํ•œ ์„œ๋น„์Šค
  • ๋ฒ•์  ์ฑ…์ž„์ด ๋”ฐ๋ฅด๋Š” ์„œ๋น„์Šค

์ด๋Ÿฐ ๊ฒฝ์šฐ๋ผ๋ฉด ๋ฌด์กฐ๊ฑด Soft Delete๋ฅผ ๊ถŒ์žฅํ•œ๋‹ค.

๋‹ค๋งŒ ์Šคํ† ๋ฆฌ์ง€์™€ ์„ฑ๋Šฅ ์ด์Šˆ๋Š” ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค. ์ธ๋ฑ์Šค ์„ค๊ณ„๋ฅผ ์ž˜ํ•˜๊ณ , ํ•„์š”ํ•˜๋‹ค๋ฉด ์˜ค๋ž˜๋œ ์‚ญ์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์•„์นด์ด๋น™ํ•˜๋Š” ๋ฐฐ์น˜ ์ž‘์—…์„ ๊ตฌํ˜„ํ•˜์ž.

Soft Delete ๋•๋ถ„์— ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ˆ˜๋กœ ์ง€์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณต๊ตฌํ•˜๊ณ , ์•…์˜์ ์ธ ์‚ญ์ œ ์‹œ๋„๋„ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.


์ฐธ๊ณ ํ•œ ๊ฒƒ๋“ค:

  • Hibernate @SQLRestriction ๋ฌธ์„œ
  • ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๊ฒช์€ ๊ฒฝํ—˜๋“ค
profile
๋งค์ผ ๋งค์ผ ์„ฑ์žฅํ•˜๊ธฐ

1๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2025๋…„ 10์›” 19์ผ

soft delete์—์„œ ์œ ๋‹ˆํฌ ๊ณ ๋ ค ์ค‘์š”ํ•˜์ง€

deletion token์ด๋ผ๋Š” ๊ฐœ๋…๋„ ์žˆ์œผ๋‹ˆ ํ•œ๋ฒˆ ๋ด๋ด

https://tech.endeepak.com/blog/2015/10/19/soft-delete-and-unique-constraint/

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ