Backend/Spring

[๋ฒˆ์—ญ] How to implement a soft delete with Hibernate(์–ด๋–ป๊ฒŒ Hibernate๋ฅผ ์‚ฌ์šฉํ•ด ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”๊ฐ€)

Seyun(Marco) 2024. 1. 27. 22:43
728x90

๐Ÿ’ก ์›๋ณธ๊ธ€ : https://thorben-janssen.com/implement-soft-delete-hibernate/

 

How to implement a soft delete with Hibernate

Hibernate supports a set of annotations that allows you to implement a soft delete with minimal coding effort.

thorben-janssen.com

 

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

์‹œ์Šคํ…œ์—์„œ ์ด ์ •๋ณด๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋‘ ๊ฐ€์ง€ ๊ธฐ๋ณธ ์˜ต์…˜์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ฌธ์„œํ™”ํ•˜๋Š” ๊ฐ์‚ฌ ๋กœ๊ทธ(audit log)๋ฅผ ์œ ์ง€ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ˆจ๊ธฐ๋Š” ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ์‚ฌ ๋กœ๊ทธ(audit log) ์˜ต์…˜์— ๋Œ€ํ•œ ์„ค๋ช…์€ Hibernate Envers์— ๊ด€ํ•œ ๋‚ด ๊ธฐ์‚ฌ์—์„œ ๋‹ค๋ฃจ์—ˆ์Šต๋‹ˆ๋‹ค. ์˜ค๋Š˜์€ Hibernate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ๋“œ๋ฆฌ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๊ธฐ ์ „์— ์†Œํ”„ํŠธ ์‚ญ์ œ๊ฐ€ ๋ฌด์—‡์ธ์ง€ ๊ฐ„๋‹จํžˆ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์†Œํ”„ํŠธ ์‚ญ์ œ๋ž€ ๋ฌด์—‡์ธ๊ฐ€์š”?

์†Œํ”„ํŠธ ์‚ญ์ œ๋Š” ๋ ˆ์ฝ”๋“œ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์—์„œ ์‚ญ์ œํ•˜๋Š” ๋Œ€์‹  ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œ๋กœ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•œ ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ๋ชจ๋ธ๋งํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • ๋ ˆ์ฝ”๋“œ๊ฐ€ ํ™œ์„ฑ ๋˜๋Š” ์‚ญ์ œ๋œ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” boolean,
  • ๋ ˆ์ฝ”๋“œ ์ƒํƒœ๋ฅผ ๋ชจ๋ธ๋งํ•˜๋Š” enum,
  • ์†Œํ”„ํŠธ ์‚ญ์ œ๊ฐ€ ์ˆ˜ํ–‰๋œ ๋‚ ์งœ์™€ ์‹œ๊ฐ„์„ ์ €์žฅํ•˜๋Š” timestamp.

์ด๋Ÿฌํ•œ ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์€ ๋‹น์—ฐํžˆ ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•œ ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„์ผ ๋ฟ์ž…๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ๋ ˆ์ฝ”๋“œ๋ฅผ ์ €์žฅํ•  ๋•Œ ์ด๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•˜๋ฉฐ, ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋Œ€์‹  ์†Œํ”„ํŠธ ์‚ญ์ œ ํ‘œ์‹œ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์†Œํ”„ํŠธ ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ˆจ๊ธฐ๋ ค๋ฉด ์†Œํ”„ํŠธ ์‚ญ์ œ ํ‘œ์‹œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ œ์™ธํ•˜๋Š” ๋ชจ๋“  ์ฟผ๋ฆฌ๋ฅผ ์กฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ๋งŽ์€ ์ž‘์—…์ฒ˜๋Ÿผ ๋“ค๋ฆด ์ˆ˜ ์žˆ์ง€๋งŒ, Hibernate๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๊ทธ๋ ‡์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์–ด๋–ป๊ฒŒ Hibernate๋ฅผ ์‚ฌ์šฉํ•ด ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”๊ฐ€.

Hibernate 6.4 ๋ฒ„์ „์—์„œ Hibernate ํŒ€์€ Hibernate ORM์„ ์œ„ํ•œ ๊ณต์‹ ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ์†Œ๊ฐœํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์—์„œ ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด 1๊ฐœ์˜ ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. Hibernate์€ ๊ทธ๋Ÿฐ ๋‹ค์Œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์†Œํ”„ํŠธ ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•œ ํ•„์š”ํ•œ SQL UPDATE ๋ฌธ์„ ์ƒ์„ฑํ•˜๊ณ  ์†Œํ”„ํŠธ ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ œ์™ธํ•œ ๋ชจ๋“  ์ฟผ๋ฆฌ ๋ฌธ์„ ์กฐ์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ์‚ฌ์˜ ๋‹ค์Œ ์„น์…˜์—์„œ ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๊ณ  ๋‹ค์–‘ํ•œ ๊ตฌ์„ฑ ์˜ต์…˜์„ ๋ณด์—ฌ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

Hibernate ๋ฒ„์ „์ด 6.3 ์ดํ•˜์ธ ๊ฒฝ์šฐ, ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ž‘์—…์—๋Š” ์•ฝ๊ฐ„์˜ ์ถ”๊ฐ€ ์ž‘์—…์ด ํ•„์š”ํ•˜์ง€๋งŒ ๋ณต์žกํ•˜์ง€ ์•Š์œผ๋ฉฐ ์—”ํ‹ฐํ‹ฐ ๋งคํ•‘์—์„œ ํ•„์š”ํ•œ ๋ชจ๋“  ๋ถ€๋ถ„์„ ์„ ์–ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋น„์ฆˆ๋‹ˆ์Šค ์ฝ”๋“œ์—์„œ ์ฒ˜๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ์‚ฌ์˜ ๋์—์„œ ๋งคํ•‘์„ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ๋ณด์—ฌ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

6.4 ์ด์ƒ Hibernate ์†Œํ”„ํŠธ ์‚ญ์ œ

Hibernate 6.4์—์„œ ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์— @SoftDelete ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ด๋ฉด Hibernate๊ฐ€ ๋‚˜๋จธ์ง€ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

Hibernate์˜ ๊ธฐ๋ณธ ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ตฌํ˜„

๋‹ค์Œ ์ฝ”๋“œ ์กฐ๊ฐ์€ Hibernate์˜ ๊ธฐ๋ณธ ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ตฌํ˜„์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ตฌํ˜„์€ ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์—์„œ boolean ์œ ํ˜•์˜ deleted ์—ด์„ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค. @SoftDelete ์–ด๋…ธํ…Œ์ด์…˜์˜ columnName ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๋‹น ์—ด์˜ ๋‹ค๋ฅธ ์ด๋ฆ„์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Entity
@SoftDelete
public class Account { ... }

Hibernate๋Š” ์ƒˆ๋กœ์šด Account ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜์†ํ™”ํ•  ๋•Œ ์ž๋™์œผ๋กœ deleted ํ•„๋“œ๋ฅผ false๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

Account a = new Account();
a.setName("thjanssen");
em.persist(a);
10:30:49,099 DEBUG SQL:135 - insert into Account (name,deleted,id) values (?,false,?)

์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ญ์ œํ•˜๋ฉด Hibernate๋Š” deleted ํ•„๋“œ๋ฅผ true๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Hibernate๋Š” ๋ชจ๋“  JPQL ๋ฐ Criteria ์ฟผ๋ฆฌ ๋ฐ ์ƒ์„ฑ๋œ ๋ชจ๋“  ๋ฌธ์— ๋Œ€ํ•ด deleted=true์ธ ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ๋ฅผ ์ œ์™ธํ•˜๋Š” ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

Account a = em.find(Account.class, a.getId());
em.remove(a);

TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
a = (Account) q.getSingleResult();

10:30:49,199 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.deleted=false and a1_0.id=?
10:30:49,211 DEBUG SQL:135 - update Account set deleted=true where id=? and deleted=false

10:30:49,234 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.name like ? escape '' and a1_0.active=true

๋‹ค๋ฅธ SoftDeleteType ์‚ฌ์šฉํ•˜๊ธฐ

Hibernate๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋œ boolean์˜ ์˜๋ฏธ๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ๋‹ค๋ฅธ SoftDeleteType์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

  1. SoftDeleteType.ACTIVE: ๊ฐ’์ด true๋Š” ๋ ˆ์ฝ”๋“œ๋ฅผ ํ™œ์„ฑ์œผ๋กœ ํ‘œ์‹œํ•˜๋ฉฐ Hibernate์€ ๊ธฐ๋ณธ์ ์œผ๋กœ active๋ฅผ ์—ด ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  2. SoftDeleteType.DELETED: ๊ฐ’์ด true๋Š” ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œ๋กœ ํ‘œ์‹œํ•˜๋ฉฐ Hibernate์€ ๊ธฐ๋ณธ์ ์œผ๋กœ deleted๋ฅผ ์—ด ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒŒ ๊ธฐ๋ณธ๊ฐ’์ž…๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์—์„œ SoftDeleteType์„ ACTIVE๋กœ ์„ค์ •ํ•˜๋Š” ๊ฐ„๋‹จํ•œ ๋งคํ•‘์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Entity
@SoftDelete(strategy = SoftDeleteType.ACTIVE)
public class Account { ... }

์ด์ œ ์ด์ „๊ณผ ๋™์ผํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด Hibernate๊ฐ€ ์—”ํ‹ฐํ‹ฐ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ active ์—ด์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ญ์ œํ•˜๋ฉด Hibernate๊ฐ€ ํ•ด๋‹น ํ•„๋“œ๋ฅผ false๋กœ ์„ค์ •ํ•˜๊ณ  ๋ชจ๋“  ์ฟผ๋ฆฌ๋Š” active=true์ธ ๋ ˆ์ฝ”๋“œ๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

// persist a new Account
Account a = new Account();
a.setName("thjanssen");
em.persist(a);

// find and remove an Account
a = em.find(Account.class, a.getId());
em.remove(a);

// query an Account
TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
a = (Account) q.getSingleResult();

// persist a new Account
10:46:26,099 DEBUG SQL:135 - insert into Account (name,active,id) values (?,true,?)

// find and remove an Account
10:46:26,199 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.active=true and a1_0.id=?
10:46:26,211 DEBUG SQL:135 - update Account set active=false where id=? and active=true

// query an Account
10:46:26,234 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.name like ? escape '' and a1_0.active=true

๋‹ค๋ฅธ ์ปฌ๋Ÿผ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๊ธฐ

Hibernate์˜ ๊ธฐ๋ณธ ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ตฌํ˜„์€ ๊ฐ ๋ ˆ์ฝ”๋“œ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐ boolean ์œ ํ˜•์˜ ์—ด์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‚ด๋ถ€์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” Boolean์„ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์œ ํ˜•์— ๋งคํ•‘ํ•˜๋Š” AttributeConverter๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์ด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ๋ ˆ๊ฑฐ์‹œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์ž‘์—…ํ•˜๋Š” ๊ฒฝ์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ ๊ฐ ๋ ˆ์ฝ”๋“œ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” String ๋˜๋Š” ์—ด๊ฑฐํ˜•์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์—์„œ ์—”ํ‹ฐํ‹ฐ ๋งคํ•‘์„ ๋ณผ ์ˆ˜ ์žˆ์œผ๋ฉฐ Hibernate์—๊ฒŒ ํ˜„์žฌ ๋ ˆ์ฝ”๋“œ์˜ ์ƒํƒœ๋ฅผ state ์—ด์— ์ €์žฅํ•˜๋„๋ก ์ง€์‹œํ•˜๊ณ  StateConverter AttributeConverter๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์†์„ฑ์„ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค.

@Entity
@SoftDelete(strategy = SoftDeleteType.ACTIVE, columnName = "state", converter = StateConverter.class)
public class Account { ... }

StateConverter ๊ตฌํ˜„์€ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. AttributeConverter ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉฐ convertToDatabaseColumn ๋ฐ convertToEntityAttribute ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Hibernate์˜ ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ตฌํ˜„์€ ๋ ˆ์ฝ”๋“œ๊ฐ€ ํ™œ์„ฑ ์ƒํƒœ์ธ์ง€ ์‚ญ์ œ ์ƒํƒœ์ธ์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” Boolean์— ์˜์กดํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ AttributeConverter์˜ ์ฒซ ๋ฒˆ์งธ ์œ ํ˜• ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” Boolean์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๋ฒˆ์งธ ์œ ํ˜• ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๋ ค๋Š” ์œ ํ˜•์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ์—์„œ๋Š” Boolean์„ String๊ฐ’์ธ "active" ๋˜๋Š” "inactive"์— ๋งคํ•‘ํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

public class StateConverter implements AttributeConverter<Boolean, String>{

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return attribute ? "active" : "inactive";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return dbData.equals("active");
    }

}

When I now execute the same test as before, Hibernate stores the values “active” or “inactive” in the state column. When I remove the entity, Hibernate sets that field to “inactive”, and all queries only return records where state=active.

// persist a new Account
Account a = new Account();
a.setName("thjanssen");
em.persist(a);

// find and remove an Account
a = em.find(Account.class, a.getId());
em.remove(a);

// query an Account
TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
a = (Account) q.getSingleResult();
// persist a new Account
10:46:26,099 DEBUG SQL:135 - insert into Account (name,state,id) values (?,'active',?)

// find and remove an Account
11:18:39,857 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.state='active' and a1_0.id=?
11:18:39,867 DEBUG SQL:135 - update Account set state='inactive' where id=? and state='active'

// query an Account
11:18:39,889 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.name like ? escape '' and a1_0.state='active'

6.4 ๋ฏธ๋งŒ Hibernate ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ตฌํ˜„

Hibernate 6.4 ๋ฏธ๋งŒ์—์„œ ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์€ ๊ทธ๋ฆฌ ์–ด๋ ต์ง€ ์•Š์ง€๋งŒ ์•ฝ๊ฐ„์˜ ์ถ”๊ฐ€ ์ž‘์—…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  1. ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๋ฅผ ์ œ๊ฑฐํ•  ๋•Œ ์‚ญ์ œ ์ž‘์—… ๋Œ€์‹  SQL UPDATE๋ฅผ ์ˆ˜ํ–‰ํ•˜๋„๋ก Hibernate์— ์ง€์‹œํ•˜๊ณ 
  2. ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ์—์„œ ๋ชจ๋“  ์†Œํ”„ํŠธ ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ์˜ˆ์ œ์—์„œ ์ด๋ฅผ ์‰ฝ๊ฒŒ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ๋Š” ๋ชจ๋‘ Account ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ AccountState ์ƒํƒœ ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ณ„์ •์ด INACTIVE, ACTIVE, ๋˜๋Š” DELETED ์ธ์ง€๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

@Entity
@NamedQuery(name = "Account.FindByName", query = "SELECT a FROM Account a WHERE name like :name")
public class Account {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;

    @Column
    private String name;

    @Column
    @Enumerated(EnumType.STRING)
    private AccountState state;

    …

}

๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋Œ€์‹  ๋ ˆ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ.

์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด Hibernate์˜ ๊ธฐ๋ณธ์ ์ธ ์ œ๊ฑฐ ์ž‘์—…์„ ์žฌ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ @SQLDelete ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ญ์ œํ•  ๋•Œ Hibernate๊ฐ€ ์‹คํ–‰ํ•  ์‚ฌ์šฉ์ž ์ •์˜ ๋„ค์ดํ‹ฐ๋ธŒ SQL ์ฟผ๋ฆฌ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์ฝ”๋“œ ์กฐ๊ฐ์—์„œ ์˜ˆ์ œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Entity
@SQLDelete(sql = "UPDATE account SET state = ‘DELETED’ WHERE id = ?", check = ResultCheckStyle.COUNT)
public class Account { … }

์ด์ „ ์ฝ”๋“œ ์กฐ๊ฐ์˜ @SQLDelete ์–ด๋…ธํ…Œ์ด์…˜์€ Hibernate์—๊ฒŒ ๊ธฐ๋ณธ SQL DELETE ๋ฌธ ๋Œ€์‹  ์ง€์ •๋œ SQL UPDATE ๋ฌธ์„ ์‹คํ–‰ํ•˜๋„๋ก ์ง€์‹œํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๊ณ„์ •์˜ ์ƒํƒœ๋ฅผ DELETED๋กœ ๋ณ€๊ฒฝํ•˜๋ฉฐ ์‚ญ์ œ๋œ ๊ณ„์ •์„ ์ œ์™ธํ•˜๋ ค๋ฉด ๋ชจ๋“  ์ฟผ๋ฆฌ์—์„œ state ์†์„ฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Account a = em.find(Account.class, a.getId());
em.remove(a);
16:07:59,511 DEBUG SQL:92 – select account0_.id as id1_0_0_, account0_.name as name2_0_0_, account0_.state as state3_0_0_ from Account account0_ where account0_.id=? and ( account0_.state <> 'DELETED')
16:07:59,534 DEBUG SQL:92 – UPDATE account SET state = 'DELETED' WHERE id = ?

๊ธฐ๋ณธ์ ์ธ ์†Œํ”„ํŠธ ์‚ญ์ œ ๊ตฌํ˜„์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๊ฒƒ์€ ์ด๊ฒƒ๋ฟ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ๋‹ค๋ฅธ ๋‘ ๊ฐ€์ง€ ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  1. Account ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ญ์ œํ•  ๋•Œ Hibernate๋Š” ํ˜„์žฌ ์„ธ์…˜์—์„œ state ์†์„ฑ์˜ ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  2. ์‚ญ์ œ๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ œ์™ธํ•˜๊ธฐ ์œ„ํ•ด ๋ชจ๋“  ์ฟผ๋ฆฌ๋ฅผ ์กฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ์„ธ์…˜์—์„œ state ์†์„ฑ์„ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ.

Hibernate๋Š” @SQLDelete ์–ด๋…ธํ…Œ์ด์…˜์— ์ œ๊ณตํ•˜๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌ๋ฌธ ๋ถ„์„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ์ € ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฐ’์„ ์„ค์ •ํ•˜๊ณ  ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ @SQLDelete ์–ด๋…ธํ…Œ์ด์…˜์— DELETE ๋ฌธ ๋Œ€์‹  SQL UPDATE ๋ฌธ์„ ์ œ๊ณตํ–ˆ๋‹ค๋Š” ์‚ฌ์‹ค์„ ์ธ์‹ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ์‚ญ์ œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ ํ›„ state ์†์„ฑ์˜ ๊ฐ’์ด ์˜ˆ์ „ ๊ฐ’์ด๋ผ๋Š” ๊ฒƒ์„ ์•Œ์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, ์ด๋Š” ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Hibernate๊ฐ€ SQL ๋ฌธ์„ ์‹คํ–‰ํ•  ๋•Œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ ˆ์ฝ”๋“œ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๊ณ  ๋ชจ๋“  ์ฟผ๋ฆฌ๊ฐ€ ์ƒˆ๋กœ์šด state ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ EntityManager.remove(Object entity) ์ž‘์—…์— ์ œ๊ณตํ•œ Account ์—”ํ‹ฐํ‹ฐ๋Š” ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”?

๊ทธ ์—”ํ‹ฐํ‹ฐ์˜ state ์†์„ฑ์€ ์˜ˆ์ „ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ์‚ญ์ œํ•œ ํ›„ ์ฆ‰์‹œ ์ฐธ์กฐ๋ฅผ ํ•ด์ œํ•˜๋Š” ๊ฒฝ์šฐ ํฐ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ ์†์„ฑ์„ ์ง์ ‘ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ ์ฝ”๋“œ ์กฐ๊ฐ์—์„œ์ฒ˜๋Ÿผ ๋ผ์ดํ”„์‚ฌ์ดํด ์ฝœ๋ฐฑ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. @PreRemove ์–ด๋…ธํ…Œ์ด์…˜์€ deleteUser ๋ฉ”์„œ๋“œ์— ์žˆ์œผ๋ฉฐ Hibernate์—๊ฒŒ ์ œ๊ฑฐ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์ „์—์ด ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ์ง€์‹œํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ state ์†์„ฑ์˜ ๊ฐ’์„ DELETED๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

@Entity
@SQLDelete(sql = "UPDATE account SET state = 'DELETED' WHERE id = ?", check = ResultCheckStyle.COUNT)
public class Account {

	...

	@PreRemove
	public void deleteUser() {
		this.state = AccountState.DELETED;
	}

}

์ฟผ๋ฆฌ์—์„œ ์†Œํ”„ํŠธ ์‚ญ์ œ๋œ ์—”ํ‹ฐํ‹ฐ ์ œ์™ธ

์‚ญ์ œ๋œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ์—์„œ ์ œ์™ธํ•˜๋ ค๋ฉด ๋ชจ๋“  ์ฟผ๋ฆฌ์—์„œ state ์†์„ฑ์„ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ž‘์—…์€ ์ˆ˜๋™์œผ๋กœ ์ˆ˜ํ–‰ํ•  ๊ฒฝ์šฐ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์œผ๋ฉฐ ๋ชจ๋“  ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. EntityManager.find(Class entityClass, Object primaryKey) ๋ฉ”์„œ๋“œ์™€ Hibernate์˜ Session์— ํ•ด๋‹นํ•˜๋Š” ๋ฉ”์„œ๋“œ๋Š” state ์†์„ฑ์˜ ์˜๋ฏธ๋ฅผ ์•Œ์ง€ ๋ชปํ•˜๋ฉฐ ์ด๋ฅผ ๊ณ ๋ คํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Hibernate์˜ @Where ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋“  ์‚ญ์ œ๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ œ์™ธํ•˜๋Š” ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด Hibernate์ด ๋ชจ๋“  ์ฟผ๋ฆฌ์˜ WHERE ์ ˆ์— ์ถ”๊ฐ€ํ•  SQL ์Šค๋‹ˆํŽซ์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์ฝ”๋“œ ์กฐ๊ฐ์€ state๊ฐ€ DELETED์ธ ๊ฒฝ์šฐ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ œ์™ธํ•˜๋Š” @Where ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

@Entity
@SQLDelete(sql = "UPDATE account SET state = 'DELETED' WHERE id = ?", check = ResultCheckStyle.COUNT)
@Where(clause = "state <> 'DELETED'")
@NamedQuery(name = "Account.FindByName", query = "SELECT a FROM Account a WHERE name like :name")
public class Account { ... }

๋‹ค์Œ ์ฝ”๋“œ ์กฐ๊ฐ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด Hibernate์€ JPQL ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ฑฐ๋‚˜ EntityManager.find(Class entityClass, Object primaryKey) ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์ •์˜๋œ WHERE ์ ˆ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
Account a = q.getSingleResult();
16:07:59,511 DEBUG SQL:92 – select account0_.id as id1_0_, account0_.name as name2_0_, account0_.state as state3_0_ from Account account0_ where ( account0_.state <> 'DELETED') and (account0_.name like ?)
Account a = em.find(Account.class, a.getId());
16:07:59,540 DEBUG SQL:92 – select account0_.id as id1_0_0_, account0_.name as name2_0_0_, account0_.state as state3_0_0_ from Account account0_ where account0_.id=? and ( account0_.state <> 'DELETED')

๊ฒฐ๋ก 

๋ณด์•˜๋“ฏ์ด Hibernate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

Hibernate ๋ฒ„์ „์ด 6.4 ์ด์ƒ์ด๋ผ๋ฉด ์—”ํ„ฐํ‹ฐ ํด๋ž˜์Šค์— @SoftDelete ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. Hibernate๋Š” ๊ฐ ๋ ˆ์ฝ”๋“œ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ๋ชจ๋“  ์ฟผ๋ฆฌ๋ฅผ ์†Œํ”„ํŠธ ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ œ์™ธํ•˜๋„๋ก ์ž๋™์œผ๋กœ ์กฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

Hibernate ๋ฒ„์ „์ด 6.3 ์ดํ•˜์ธ ๊ฒฝ์šฐ ์†Œํ”„ํŠธ ์‚ญ์ œ๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ์—”ํ„ฐํ‹ฐ ํด๋ž˜์Šค์— @SQLDelete ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๋ ˆ์ฝ”๋“œ์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” SQL UPDATE ๋ฌธ์„ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ Hibernate์˜ @Where ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ์†Œํ”„ํŠธ ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ œ์™ธํ•˜๋Š” ์กฐ๊ฑด์„ ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

728x90
728x90