
ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ๋ฐ์ดํฐ ์ญ์ ๋ก์ง์ ๊ตฌํํ ๋ ๊ณ ๋ฏผ์ด ๋ง์๋ค. "์ฌ์ฉ์๊ฐ ์ญ์ ๋ฒํผ์ ๋๋ฅด๋ฉด DB์์ ์์ ํ ์ง์๋ฒ๋ฆด๊น?" ์ฒ์์ ๋น์ฐํ ๊ทธ๋์ผ ํ๋ค๊ณ ์๊ฐํ๋๋ฐ, ์กฐ๊ธ๋ง ์๊ฐํด๋ณด๋ ๋ฌธ์ ๊ฐ ๋ณด์ด๊ธฐ ์์ํ๋ค.
์ฐ๋ฆฌ ํ๋ก์ ํธ๋ ๋ฐ์ดํฐ ๋ถ์ ํ๋ซํผ์ด๋ค. ์ฌ์ฉ์๋ค์ด ๋ฐ์ดํฐ๋ฅผ ์ ๋ก๋ํ๊ณ , ๋ฐ์ดํฐ์ ๋ํ ๋ถ์ ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ณ , ํผ๋๋ฐฑ์ ์ด์ด ๋จ๊ธธ ์ ์๋ค. ๊ทธ๋ฐ๋ฐ ๋ง์ฝ ์ฌ์ฉ์๊ฐ ์ค์๋ก ์ญ์ ๋ฒํผ์ ๋๋ ๋ค๋ฉด? ๋๋ ์ ์์ ์ธ ์ฌ์ฉ์๊ฐ ๋จ์ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ค๋ฉด? ๋ฒ์ ๋ฌธ์ ๋ก ํน์ ๋ฐ์ดํฐ์ ๋ํ ์ด๋ ฅ ์ถ์ ์ด ํ์ํ๋ค๋ฉด?
๋ฐ์ดํฐ๋ฅผ ์์ ํ ์ญ์ ํด๋ฒ๋ฆฌ๋ฉด(Hard 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. ์ฌ์ฉ์ A๊ฐ ๋ช
์ํผ์์ฑ ๊ฒ์๊ธ ์์ฑ
2. ํผํด์ B๊ฐ ์ ๊ณ ์ ์ A๊ฐ ๊ฒ์๊ธ ์ญ์
3. ์ฆ๊ฑฐ๊ฐ ์ฌ๋ผ์ง โ ๋ฒ์ ๋์ ๋ถ๊ฐ๋ฅ
Hard Delete๋ฅผ ์ฐ๋ฉด ์ด๋ฐ ์ํฉ์์ ์์๋ฌด์ฑ ์ด๋ค. ํ์ง๋ง Soft Delete๋ก ๊ตฌํํ๋ฉด, ๊ฒ์ผ๋ก๋ ์ญ์ ๋์ง๋ง DB์๋ ๋จ์์์ด์ ํ์ํ ๋ ์ฆ๊ฑฐ๋ก ์ ์ถํ ์ ์๋ค.
๋ถ์ ์ ํ ๋ฐ์ดํฐ์ ์ค๋ช ์ ์ฌ๋ฆฌ๊ณ ์ญ์ ํ ์ผ์ด์ค๊ฐ ์์ ๊ฒ ๊ฐ๊ธฐ์, Soft Delete ๋๋ถ์ ์ด์ ๊ณผ์ ์์ ํด๋น ๋ด์ฉ์ ํ์ธํ๊ณ ๊ฒฝ๊ณ ์กฐ์น๋ฅผ ์ทจํ ์ ์๋ค.
๊ณ์ ์ด ํดํน๋นํ๊ฑฐ๋, ํด์ฌํ๋ ์ง์์ด ์์ฌ์ ํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋๋ ์ญ์ ํ๋ ๊ฒฝ์ฐ๋ฅผ ์๊ฐํด๋ณด์.
// ์
์์ ์ธ ์ฌ์ฉ์๊ฐ ์์ ์ ๋ชจ๋ ๊ฒ์๊ธ ์ญ์
for (Post post : userPosts) {
post.delete(); // Soft Delete
}
Hard Delete์๋ค๋ฉด ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ์๊ตฌ ์ญ์ ๋์ง๋ง, Soft Delete๋ ๋ณต๊ตฌ๊ฐ ๊ฐ๋ฅํ๋ค. ๊ด๋ฆฌ์๊ฐ ์ด์ ์งํ๋ฅผ ๋ฐ๊ฒฌํ๋ฉด ๋ฐ๋ก ๋ณต๊ตฌํ ์ ์๋ค.
// ๊ด๋ฆฌ์๊ฐ ํ ๋ฒ์ ๋ณต๊ตฌ
deletedPosts.forEach(Post::restore);
์ญ์ ๋ ๊ฒ์๊ธ์ ๋ถ์ํ๋ฉด ์คํธ ํจํด์ ํ์ ํ ์ ์๋ค.
์ด๋ฐ ์ธ์ฌ์ดํธ๋ 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;
์ด๋ฐ ์ฟผ๋ฆฌ๋ก ์ด๋ทฐ์ง ์ฌ์ฉ์๋ฅผ ์ฝ๊ฒ ์ฐพ์ ์ ์๋ค.
"์ ์ฌ์ฉ์๋ค์ด ๊ฒ์๊ธ์ ์ญ์ ํ ๊น?"๋ฅผ ๋ถ์ํ๋ฉด ์๋น์ค ๊ฐ์ ํฌ์ธํธ๋ฅผ ์ฐพ์ ์ ์๋ค.
์ด๋ฐ ๊ฑธ ๋ฐ๊ฒฌํ๊ณ ์ ๋ก๋ ํ๋ก์ฐ๋ฅผ ๊ฐ์ ํ ์ ์๋ค.
์ด์ ์๋ ์ฌ๋ฌ ํ๋ซํผ์ ์ด์ฉํด๋ณด๋ฉด ์ฝ๊ด์์ ๋ฐ์ดํฐ ๋ณด์กด๊ณผ ๊ด๋ จ๋ ๋ด์ฉ์ ๋ดค์๋ ๊ฒ ๊ฐ๋ค. ์ฐ์ ์ ๋ฐ๋ผ์๋ ๋ฒ์ ์ผ๋ก ๋ฐ์ดํฐ ๋ณด์กด ์๋ฌด๊ฐ ์๋ค.
์ด๋ฐ ์๋น์ค๋ Soft Delete๊ฐ ์ ํ์ด ์๋๋ผ ํ์์ด๋ค.
๊ฒ์๊ธ์ Hard Deleteํ๋ฉด ์ฐ๊ด๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๊ฐ ๊ณจ์น์ํ๋ค.
๊ฒ์๊ธ (Hard Delete)
โ
๋๊ธ 50๊ฐ๋ ์ด๋ป๊ฒ ํ์ง?
์ข์์ 100๊ฐ๋?
์๋ฆผ ๊ธฐ๋ก์?
ํต๊ณ ๋ฐ์ดํฐ๋?
CASCADE DELETE๋ก ๋ค ์ง์ธ ์๋ ์์ง๋ง, ๊ทธ๋ผ ์ฌ์ฉ์๊ฐ ์ด ๋๊ธ๊น์ง ์ ๋ถ ์ฌ๋ผ์ง๋ค. "๋ด๊ฐ ๋ถ๋ช ํ ๋๊ธ ๋ฌ์๋๋ฐ ์ ์์ด์?" ๊ฐ์ ๋ฌธ์๊ฐ ๋ค์ด์จ๋ค.
Soft Delete๋ ์ฐธ์กฐ ๊ด๊ณ๋ฅผ ์ ์งํ๋ฉด์๋ ์กฐํ์์๋ง ์ ์ธ๋๋ ํจ์ฌ ๊น๋ํ๋ค.
// ๊ฒ์๊ธ์ด ์ญ์ ๋์ด๋ ๋๊ธ์ ๊ทธ๋๋ก
@Entity
public class Comment {
@ManyToOne
@JoinColumn(name = "post_id")
private Post post; // ์ญ์ ๋ ๊ฒ์๊ธ๋ ์ฐธ์กฐ ๊ฐ๋ฅ
}
์์งํ ๋งํ๋ฉด, ์ ์ผ ๋ง์ ์ผ์ด์ค๋ "์ฌ์ฉ์ ์ค์"๋ค.
์ด๋ฐ ๋ฌธ์๊ฐ ๋ค์ด์ฌ ๊ฐ๋ฅ์ฑ๋ ์๋ค. ๋ณต๊ตฌ ๊ธฐ๋ฅ์ด ์์ผ๋ฉด ๊ณ ๊ฐ์ผํฐ ๋ถ๋ด์ด ์์ฒญ๋๋ค.
๋ชจ๋ ์ฃผ์ ์ํฐํฐ์ 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") ์ด๋ค.
Hibernate์ @SQLRestriction (๊ตฌ๋ฒ์ ์์๋ @Where)์ ์ฌ์ฉํ๋ฉด, ์ด ์ํฐํฐ๋ฅผ ์กฐํํ ๋ ์๋์ผ๋ก WHERE ์กฐ๊ฑด์ด ๋ถ๋๋ค.
-> ์ด์ Where์ deprecated
// ๊ฐ๋ฐ์๊ฐ ์ด๋ ๊ฒ ์์ฑํ๋ฉด
dataRepository.findById(1L);
// ์ค์ ๋ก๋ ์ด๋ฐ ์ฟผ๋ฆฌ๊ฐ ์คํ๋จ
SELECT * FROM data WHERE data_id = 1 AND is_deleted = false;
๋๋ถ์ ๋น์ฆ๋์ค ๋ก์ง ์ฝ๋๋ง๋ค ์ผ์ผ์ด isDeleted = false ์กฐ๊ฑด์ ์ถ๊ฐํ ํ์๊ฐ ์๋ค. ์ญ์ ๋ ๋ฐ์ดํฐ๋ ์์์ ์กฐํ๋์ง ์๋๋ค.
@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() ๋ฉ์๋๋ก ํ๋๊ทธ๋ง ๋ณ๊ฒฝํ๋ค.
public void restore() {
this.isDeleted = false;
}
Soft Delete์ ํฐ ์ฅ์ ์ค ํ๋๋ค. ์ฌ์ฉ์๊ฐ "์ ์ค์๋ก ์ง์ ์ด์!"๋ผ๊ณ ํ๋ฉด, ๋ฐ๋ก ๋ณต๊ตฌํด์ค ์ ์๋ค.
์ฐ๋ฆฌ ํ๋ก์ ํธ๋ ๊ฒ์์ ์ํด 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 ์ ์ฝ์ด ์๋ค๋ฉด, ์ญ์ ๋ ์ฌ์ฉ์์ ์ด๋ฉ์ผ์ ์ฌ์ฌ์ฉํ ์ ์๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ:
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 ๋๋ถ์ ์ฌ์ฉ์๊ฐ ์ค์๋ก ์ง์ด ๋ฐ์ดํฐ๋ฅผ ๋ณต๊ตฌํ๊ณ , ์ ์์ ์ธ ์ญ์ ์๋๋ ์ถ์ ํ ์ ์์๋ค.
์ฐธ๊ณ ํ ๊ฒ๋ค:
soft delete์์ ์ ๋ํฌ ๊ณ ๋ ค ์ค์ํ์ง
deletion token์ด๋ผ๋ ๊ฐ๋ ๋ ์์ผ๋ ํ๋ฒ ๋ด๋ด
https://tech.endeepak.com/blog/2015/10/19/soft-delete-and-unique-constraint/