์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ”„ Spring Data Envers๋กœ ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ์žฌ๋ฏธ์žˆ๊ฒŒ ๊ด€๋ฆฌํ•ด๋ณด์ž! ๐Ÿ•ฐ๏ธ

๐Ÿ”„ Spring Data Envers๋กœ ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ์žฌ๋ฏธ์žˆ๊ฒŒ ๊ด€๋ฆฌํ•ด๋ณด์ž! ๐Ÿ•ฐ๏ธ

Envers ํƒ€์ž„๋จธ์‹  ์ด๋ ฅ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค Spring Data Envers ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ ์ถ”์ 

์•ˆ๋…•! ์˜ค๋Š˜์€ Spring Data Envers๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์—”ํ‹ฐํ‹ฐ์˜ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๋Š”์ง€ ํ•จ๊ป˜ ์•Œ์•„๋ณผ ๊ฑฐ์•ผ. ๐Ÿ˜Š ๋ฐ์ดํ„ฐ์˜ ๋ณ€ํ™”๋ฅผ ์ถ”์ ํ•˜๋Š” ๊ฑด ๋งˆ์น˜ ํƒ€์ž„๋จธ์‹ ์„ ํƒ€๊ณ  ๊ณผ๊ฑฐ๋กœ ๋Œ์•„๊ฐ€๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์žฌ๋ฐŒ๋Š” ์ผ์ด์ง€! ํŠนํžˆ ์‹ค๋ฌด์—์„œ๋Š” "๋ˆ„๊ฐ€, ์–ธ์ œ, ๋ฌด์—‡์„" ๋ณ€๊ฒฝํ–ˆ๋Š”์ง€ ์ถ”์ ํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์ •๋ง ์ค‘์š”ํ•˜๊ฑฐ๋“ .

์ด ๊ธ€์„ ํ†ตํ•ด Java ๊ธฐ๋ฐ˜ Spring ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ์‰ฝ๊ณ  ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ. ๋งˆ์น˜ ์žฌ๋Šฅ๋„ท์—์„œ ๋‹ค์–‘ํ•œ ์žฌ๋Šฅ์„ ๊ฑฐ๋ž˜ํ•˜๋“ฏ์ด, ์šฐ๋ฆฌ๋„ Spring Data Envers๋ผ๋Š” ์žฌ๋Šฅ์„ ์Šต๋“ํ•ด์„œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณด์ž! ๐Ÿš€

๐Ÿ“‹ ๋ชฉ์ฐจ

  1. Spring Data Envers๋ž€ ๋ฌด์—‡์ธ๊ฐ€?
  2. ์™œ ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ• ๊นŒ?
  3. Spring Data Envers ์„ค์ •ํ•˜๊ธฐ
  4. ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
  5. ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ํ™œ์šฉํ•˜๊ธฐ
  6. ์‹ค์ „ ์˜ˆ์ œ: ์‚ฌ์šฉ์ž ์ •๋ณด ๋ณ€๊ฒฝ ์ด๋ ฅ ๊ด€๋ฆฌ
  7. ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ
  8. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
  9. ๋งˆ๋ฌด๋ฆฌ ๋ฐ ์ถ”๊ฐ€ ์ž๋ฃŒ

๐Ÿ” 1. Spring Data Envers๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

Spring Data Envers๋Š” Hibernate Envers๋ฅผ Spring Data JPA์™€ ํ†ตํ•ฉํ•œ ํ”„๋กœ์ ํŠธ์•ผ. ๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด, ์—”ํ‹ฐํ‹ฐ์˜ ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ž๋™์œผ๋กœ ๊ธฐ๋กํ•ด์ฃผ๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ๋„๊ตฌ์ง€! ๐Ÿง™โ€โ™‚๏ธ

Hibernate Envers๋Š” JPA ์—”ํ‹ฐํ‹ฐ์˜ ๊ฐ์‚ฌ(audit) ๋ฐ ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ Hibernate ๋ชจ๋“ˆ์ด์•ผ. ์ด๋ฅผ Spring Data์™€ ๊ฒฐํ•ฉํ•˜๋ฉด ๋” ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฑฐ์ง€. ๋งˆ์น˜ ์š”๋ฆฌ ์‹ค๋ ฅ์ด ์—†์–ด๋„ ์ „์ž๋ ˆ์ธ์ง€๋งŒ ์žˆ์œผ๋ฉด ๋ง›์žˆ๋Š” ์Œ์‹์„ ๋ฐ์šธ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ, ๋ณต์žกํ•œ ์ด๋ ฅ ๊ด€๋ฆฌ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ Envers๊ฐ€ ๋‹ค ํ•ด๊ฒฐํ•ด์ค˜!

Spring Data JPA Hibernate Envers Spring Data Envers Spring Data Envers ๊ตฌ์กฐ

Spring Data Envers์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•„:

  1. ์—”ํ‹ฐํ‹ฐ์˜ ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ž๋™์œผ๋กœ ๊ธฐ๋ก ๐Ÿ“
  2. ํŠน์ • ์‹œ์ ์˜ ์—”ํ‹ฐํ‹ฐ ์ƒํƒœ๋ฅผ ์‰ฝ๊ฒŒ ์กฐํšŒ ๊ฐ€๋Šฅ โฑ๏ธ
  3. ๋ˆ„๊ฐ€, ์–ธ์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ–ˆ๋Š”์ง€ ์ถ”์  ๐Ÿ•ต๏ธโ€โ™€๏ธ
  4. Spring Data JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์™€ ์™„๋ฒฝํ•˜๊ฒŒ ํ†ตํ•ฉ ๐Ÿ”„
  5. ์ตœ์†Œํ•œ์˜ ์„ค์ •์œผ๋กœ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ ์ œ๊ณต โš™๏ธ

๐Ÿค” 2. ์™œ ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ• ๊นŒ?

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

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

๐ŸŒŸ ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ ๊ด€๋ฆฌ์˜ ์ฃผ์š” ์ด์ 

1. ๊ทœ์ • ์ค€์ˆ˜ ๋ฐ ๊ฐ์‚ฌ: ๊ธˆ์œต, ์˜๋ฃŒ, ๋ฒ•๋ฅ  ๋“ฑ ๋งŽ์€ ์‚ฐ์—…์—์„œ๋Š” ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ๋ณด๊ด€ํ•˜๋Š” ๊ฒƒ์ด ๋ฒ•์  ์š”๊ตฌ์‚ฌํ•ญ์ด์•ผ. ๐Ÿ›๏ธ

2. ๋ฌธ์ œ ํ•ด๊ฒฐ: ๋ฐ์ดํ„ฐ์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ, ์–ธ์ œ ์–ด๋–ค ๋ณ€๊ฒฝ์ด ์žˆ์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋ฉด ์›์ธ์„ ์ฐพ๊ธฐ ์‰ฌ์›Œ์ ธ. ๐Ÿ”

3. ์‹ค์ˆ˜ ๋ณต๊ตฌ: ๋ˆ„๊ตฐ๊ฐ€ ์‹ค์ˆ˜๋กœ ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ–ˆ๋‹ค๋ฉด? ์ด์ „ ์ƒํƒœ๋กœ ์‰ฝ๊ฒŒ ๋˜๋Œ๋ฆด ์ˆ˜ ์žˆ์–ด! โช

4. ์‚ฌ์šฉ์ž ํ–‰๋™ ๋ถ„์„: ์‚ฌ์šฉ์ž๋“ค์ด ์–ด๋–ค ๋ฐ์ดํ„ฐ๋ฅผ ์ž์ฃผ ๋ณ€๊ฒฝํ•˜๋Š”์ง€ ๋ถ„์„ํ•˜๋ฉด UX ๊ฐœ์„ ์— ๋„์›€์ด ๋ผ. ๐Ÿ“Š

5. ๋ณ€๊ฒฝ ๋‚ด์—ญ ์‹œ๊ฐํ™”: ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ๋ฐ์ดํ„ฐ ๋ณ€ํ™”๋ฅผ ์‹œ๊ฐํ™”ํ•˜๋ฉด ํŠธ๋ Œ๋“œ๋ฅผ ํŒŒ์•…ํ•˜๊ธฐ ์‰ฌ์›Œ์ ธ. ๐Ÿ“ˆ

์˜ˆ๋ฅผ ๋“ค์–ด, ์˜จ๋ผ์ธ ์‡ผํ•‘๋ชฐ์—์„œ ์ƒํ’ˆ ๊ฐ€๊ฒฉ์ด ๊ฐ‘์ž๊ธฐ ๋ณ€๊ฒฝ๋˜์—ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ด. ๊ณ ๊ฐ์ด "์–ด์ œ๋Š” ์ด ๊ฐ€๊ฒฉ์ด ์•„๋‹ˆ์—ˆ๋Š”๋ฐ์š”?"๋ผ๊ณ  ๋ฌธ์˜ํ–ˆ์„ ๋•Œ, ๋ณ€๊ฒฝ ์ด๋ ฅ์ด ์—†๋‹ค๋ฉด ํ™•์ธํ•˜๊ธฐ ์–ด๋ ต๊ฒ ์ง€? ํ•˜์ง€๋งŒ Envers๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด "๋„ค, ์–ด์ œ ์˜คํ›„ 3์‹œ์— ํ”„๋กœ๋ชจ์…˜์ด ์ ์šฉ๋˜์–ด ๊ฐ€๊ฒฉ์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"๋ผ๊ณ  ์ •ํ™•ํžˆ ๋‹ต๋ณ€ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ˜Ž

โš™๏ธ 3. Spring Data Envers ์„ค์ •ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Spring Data Envers๋ฅผ ์„ค์ •ํ•ด๋ณด์ž! ์ƒ๊ฐ๋ณด๋‹ค ์ •๋ง ๊ฐ„๋‹จํ•ด. ๋งˆ์น˜ ๋ ˆ๊ณ  ๋ธ”๋ก ์กฐ๋ฆฝํ•˜๋“ฏ์ด ๋ช‡ ๊ฐ€์ง€ ๋ถ€ํ’ˆ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ผ. ๐Ÿงฉ

3.1 ์˜์กด์„ฑ ์ถ”๊ฐ€ํ•˜๊ธฐ

๋จผ์ € Maven์ด๋‚˜ Gradle ํ”„๋กœ์ ํŠธ์— ํ•„์š”ํ•œ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด:

Maven (pom.xml)

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-envers</artifactId>
    <version>2.7.0</version>
</dependency>

Gradle (build.gradle)

implementation 'org.springframework.data:spring-data-envers:2.7.0'

๋ฒ„์ „์€ Spring Boot ๋ฒ„์ „์— ๋งž๊ฒŒ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„. Spring Boot๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ์ž๋™์œผ๋กœ ํ•ด์ฃผ๋‹ˆ ๋ฒ„์ „์„ ์ƒ๋žตํ•ด๋„ ๋ผ!

3.2 Spring Boot ์„ค์ •

Spring Boot๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์„ค์ •์ด ๋” ๊ฐ„๋‹จํ•ด์ ธ. @EnableJpaRepositories ์–ด๋…ธํ…Œ์ด์…˜์— repositoryFactoryBeanClass ์†์„ฑ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ผ:

import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Spring Data Envers๊ฐ€ Spring Data JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์™€ ํ†ตํ•ฉ๋˜์–ด ์ž‘๋™ํ•ด! ๐ŸŽฏ

3.3 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”

Envers๋Š” ์ž๋™์œผ๋กœ ๊ฐ์‚ฌ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ด. ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ฐ ์—”ํ‹ฐํ‹ฐ ํ…Œ์ด๋ธ”์— ๋Œ€ํ•ด [ํ…Œ์ด๋ธ”๋ช…]_AUD๋ผ๋Š” ์ด๋ฆ„์˜ ๊ฐ์‚ฌ ํ…Œ์ด๋ธ”๊ณผ REVINFO๋ผ๋Š” ๋ฆฌ๋น„์ „ ์ •๋ณด ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ด.

USER id (PK) name email created_at USER_AUD id (PK) name email created_at REV (FK) REVTYPE Envers ํ…Œ์ด๋ธ” ๊ตฌ์กฐ

์œ„ ๊ทธ๋ฆผ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด, USER_AUD ํ…Œ์ด๋ธ”์€ ์›๋ณธ USER ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ์„ ํฌํ•จํ•˜๊ณ , ์ถ”๊ฐ€๋กœ REV(๋ฆฌ๋น„์ „ ๋ฒˆํ˜ธ)์™€ REVTYPE(๋ณ€๊ฒฝ ์œ ํ˜•: 0=์ถ”๊ฐ€, 1=์ˆ˜์ •, 2=์‚ญ์ œ)์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด.

๐Ÿš€ 4. ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•

์ด์ œ Spring Data Envers๋ฅผ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•ด๋ณด์ž! ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•์€ ์ •๋ง ๊ฐ„๋‹จํ•ด. ๋‹จ์ง€ ๋ช‡ ๊ฐœ์˜ ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ผ! ๐Ÿ˜ƒ

4.1 ์—”ํ‹ฐํ‹ฐ์— @Audited ์ถ”๊ฐ€ํ•˜๊ธฐ

๋ณ€๊ฒฝ ์ด๋ ฅ์„ ์ถ”์ ํ•˜๊ณ  ์‹ถ์€ ์—”ํ‹ฐํ‹ฐ์— @Audited ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ด:

import org.hibernate.envers.Audited;
import javax.persistence.*;

@Entity
@Audited
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private String email;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    // ์ƒ์„ฑ์ž, ๊ฒŒํ„ฐ, ์„ธํ„ฐ ๋“ฑ
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด User ์—”ํ‹ฐํ‹ฐ์˜ ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ž๋™์œผ๋กœ ๊ธฐ๋ก๋ผ. ๋งŒ์•ฝ ํŠน์ • ํ•„๋“œ๋งŒ ์ถ”์ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๊ทธ ํ•„๋“œ์—๋งŒ @Audited๋ฅผ ๋ถ™์ด๋ฉด ๋ผ! ๐ŸŽฏ

4.2 RevisionRepository ์ธํ„ฐํŽ˜์ด์Šค ์‚ฌ์šฉํ•˜๊ธฐ

์ด์ œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. Spring Data JPA์˜ ์ผ๋ฐ˜์ ์ธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์™€ ๋น„์Šทํ•˜์ง€๋งŒ, RevisionRepository ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์†๋ฐ›์•„์•ผ ํ•ด:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.history.RevisionRepository;

public interface UserRepository extends JpaRepository<User, Long>, RevisionRepository<User, Long, Integer> {
    // ์ถ”๊ฐ€ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ์—ฌ๊ธฐ์— ์ •์˜
}

RevisionRepository์˜ ์ œ๋„ค๋ฆญ ํƒ€์ž… ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•„:

  1. ์—”ํ‹ฐํ‹ฐ ํƒ€์ž… (User)
  2. ์—”ํ‹ฐํ‹ฐ์˜ ID ํƒ€์ž… (Long)
  3. ๋ฆฌ๋น„์ „ ๋ฒˆํ˜ธ ํƒ€์ž… (๊ธฐ๋ณธ์ ์œผ๋กœ Integer)

4.3 ๊ธฐ๋ณธ์ ์ธ ์ด๋ ฅ ์กฐํšŒ

์ด์ œ RevisionRepository๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ด๋ ฅ์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์–ด:

// ์„œ๋น„์Šค ํด๋ž˜์Šค ๋‚ด๋ถ€
@Autowired
private UserRepository userRepository;

// ํŠน์ • ์—”ํ‹ฐํ‹ฐ์˜ ๋ชจ๋“  ๋ฆฌ๋น„์ „ ์กฐํšŒ
Revisions<Integer, User> revisions = userRepository.findRevisions(userId);

// ํŠน์ • ์‹œ์ ์˜ ์—”ํ‹ฐํ‹ฐ ์ƒํƒœ ์กฐํšŒ
Optional<Revision<Integer, User>> revision = userRepository.findRevision(userId, revisionNumber);

// ๊ฐ€์žฅ ์ตœ๊ทผ ๋ฆฌ๋น„์ „ ์กฐํšŒ
Optional<Revision<Integer, User>> latestRevision = userRepository.findLastChangeRevision(userId);

Revision ๊ฐ์ฒด์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ •๋ณด๊ฐ€ ํฌํ•จ๋ผ:

  1. ๋ฆฌ๋น„์ „ ๋ฒˆํ˜ธ (getRevisionNumber())
  2. ๋ฆฌ๋น„์ „ ๋‚ ์งœ/์‹œ๊ฐ„ (getRevisionDate())
  3. ํ•ด๋‹น ์‹œ์ ์˜ ์—”ํ‹ฐํ‹ฐ (getEntity())

์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•˜๊ฒŒ ์—”ํ‹ฐํ‹ฐ์˜ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์–ด! ๋งˆ์น˜ ํƒ€์ž„๋จธ์‹ ์„ ํƒ€๊ณ  ๊ณผ๊ฑฐ๋กœ ๋Œ์•„๊ฐ€๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ง์ด์•ผ. โฐ

๐Ÿ”ฅ 5. ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ํ™œ์šฉํ•˜๊ธฐ

๊ธฐ๋ณธ ๊ธฐ๋Šฅ์— ์ต์ˆ™ํ•ด์กŒ๋‹ค๋ฉด, ์ด์ œ Spring Data Envers์˜ ๋” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์‚ดํŽด๋ณด์ž! ์ด๋Ÿฐ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค์„ ํ™œ์šฉํ•˜๋ฉด ์žฌ๋Šฅ๋„ท๊ณผ ๊ฐ™์€ ๋ณต์žกํ•œ ํ”Œ๋žซํผ์—์„œ๋„ ํšจ๊ณผ์ ์œผ๋กœ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ’ช

5.1 ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”์ ํ•˜๊ธฐ

๋ณ€๊ฒฝ ์ด๋ ฅ์— "๋ˆ„๊ฐ€" ๋ณ€๊ฒฝํ–ˆ๋Š”์ง€ ๊ธฐ๋กํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ? Envers์˜ RevisionListener๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ!

import org.hibernate.envers.RevisionListener;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

public class UserRevisionListener implements RevisionListener {
    @Override
    public void newRevision(Object revisionEntity) {
        UserRevisionEntity revision = (UserRevisionEntity) revisionEntity;
        
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            revision.setUsername(auth.getName());
        } else {
            revision.setUsername("system");
        }
    }
}

๊ทธ๋ฆฌ๊ณ  ์ปค์Šคํ…€ ๋ฆฌ๋น„์ „ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด:

import org.hibernate.envers.DefaultRevisionEntity;
import org.hibernate.envers.RevisionEntity;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "REVINFO")
@RevisionEntity(UserRevisionListener.class)
public class UserRevisionEntity extends DefaultRevisionEntity {
    private String username;
    
    // ๊ฒŒํ„ฐ, ์„ธํ„ฐ
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
}

์ด์ œ ๊ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ์— ์‚ฌ์šฉ์ž ์ด๋ฆ„์ด ํ•จ๊ป˜ ๊ธฐ๋ก๋  ๊ฑฐ์•ผ! ๐Ÿ‘ค

5.2 ํŠน์ • ํ•„๋“œ๋งŒ ๊ฐ์‚ฌํ•˜๊ธฐ

๋ชจ๋“  ํ•„๋“œ๊ฐ€ ์•„๋‹Œ ํŠน์ • ํ•„๋“œ๋งŒ ์ถ”์ ํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์–ด:

// ๋ฐฉ๋ฒ• 1: ํŠน์ • ํ•„๋“œ์—๋งŒ @Audited ์ ์šฉ
@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    
    @Audited
    private String name;
    
    @Audited
    private BigDecimal price;
    
    // ์ด ํ•„๋“œ๋Š” ๊ฐ์‚ฌ๋˜์ง€ ์•Š์Œ
    private Integer stockQuantity;
}

// ๋ฐฉ๋ฒ• 2: @NotAudited ์‚ฌ์šฉ
@Entity
@Audited
public class Customer {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    private String email;
    
    @NotAudited
    private String internalNotes; // ์ด ํ•„๋“œ๋Š” ๊ฐ์‚ฌ์—์„œ ์ œ์™ธ
}

5.3 ์กฐ๊ฑด๋ถ€ ๊ฐ์‚ฌ

ํŠน์ • ์กฐ๊ฑด์—์„œ๋งŒ ๊ฐ์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด @ConditionalAudited๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ ์ด ๊ธฐ๋Šฅ์€ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•ด:

๐Ÿ”ง ์กฐ๊ฑด๋ถ€ ๊ฐ์‚ฌ ๊ตฌํ˜„ ์˜ˆ์‹œ

1. ๋จผ์ € ์ปค์Šคํ…€ ์–ด๋…ธํ…Œ์ด์…˜์„ ๋งŒ๋“ค์–ด:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface ConditionalAudited {
    String condition();
}

2. ๊ทธ๋ฆฌ๊ณ  ์ด๋ฅผ ์ฒ˜๋ฆฌํ•  ๋ฆฌ์Šค๋„ˆ๋ฅผ ๊ตฌํ˜„ํ•ด:

@Component
public class ConditionalAuditEventListener {
    @PrePersist
    @PreUpdate
    @PreRemove
    public void checkAuditCondition(Object entity) {
        // ์—”ํ‹ฐํ‹ฐ์˜ @ConditionalAudited ์–ด๋…ธํ…Œ์ด์…˜์„ ํ™•์ธํ•˜๊ณ 
        // ์กฐ๊ฑด์— ๋”ฐ๋ผ ๊ฐ์‚ฌ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๋กœ์ง ๊ตฌํ˜„
    }
}

5.4 ๋ณ€๊ฒฝ ์ด๋ ฅ ์ฟผ๋ฆฌ ์ตœ์ ํ™”

๋Œ€๋Ÿ‰์˜ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ํšจ์œจ์ ์œผ๋กœ ์ฟผ๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ ํŒ์„ ์†Œ๊ฐœํ• ๊ฒŒ:

๐Ÿ“Š ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ

Page<Revision<Integer, User>> userRevisions = userRepository.findRevisions(
    userId, PageRequest.of(0, 10, Sort.by("revisionNumber").descending()));

๐Ÿ” ํŠน์ • ๊ธฐ๊ฐ„ ์กฐํšŒ

// ์ปค์Šคํ…€ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ ํ•„์š”
List<Revision<Integer, User>> revisions = userRepository.findRevisionsInPeriod(
    userId, startDate, endDate);

์ด๋Ÿฐ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค์„ ํ™œ์šฉํ•˜๋ฉด ๋” ์„ธ๋ฐ€ํ•˜๊ณ  ํšจ์œจ์ ์ธ ๋ณ€๊ฒฝ ์ด๋ ฅ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ ธ! ๐Ÿš€

๐Ÿ’ผ 6. ์‹ค์ „ ์˜ˆ์ œ: ์‚ฌ์šฉ์ž ์ •๋ณด ๋ณ€๊ฒฝ ์ด๋ ฅ ๊ด€๋ฆฌ

์ด์ œ ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ†ตํ•ด Spring Data Envers๋ฅผ ์ ์šฉํ•ด๋ณด์ž! ์‚ฌ์šฉ์ž ์ •๋ณด์˜ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ๊ด€๋ฆฌํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ. ์ด๋Ÿฐ ๊ธฐ๋Šฅ์€ ์žฌ๋Šฅ๋„ท๊ณผ ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์‚ฌ์šฉ์ž ๊ณ„์ • ๊ด€๋ฆฌ์— ๋งค์šฐ ์œ ์šฉํ•  ๊ฑฐ์•ผ. ๐Ÿ‘ฅ

6.1 ์—”ํ‹ฐํ‹ฐ ์„ค๊ณ„

import org.hibernate.envers.Audited;
import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
@Audited
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;
    private String email;
    private String fullName;
    private String phoneNumber;
    
    @Column(name = "last_login_at")
    private LocalDateTime lastLoginAt;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    // ์ƒ์„ฑ์ž, ๊ฒŒํ„ฐ, ์„ธํ„ฐ ๋“ฑ
}

6.2 ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.history.RevisionRepository;

public interface UserRepository extends 
    JpaRepository<User, Long>, 
    RevisionRepository<User, Long, Integer> {
    
    // ์ถ”๊ฐ€ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ
}

6.3 ์„œ๋น„์Šค ๋ ˆ์ด์–ด ๊ตฌํ˜„

import org.springframework.data.history.Revision;
import org.springframework.data.history.Revisions;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class UserService {
    
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Transactional
    public User createUser(User user) {
        user.setCreatedAt(LocalDateTime.now());
        user.setUpdatedAt(LocalDateTime.now());
        return userRepository.save(user);
    }
    
    @Transactional
    public User updateUser(Long userId, User userDetails) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new RuntimeException("User not found"));
        
        user.setUsername(userDetails.getUsername());
        user.setEmail(userDetails.getEmail());
        user.setFullName(userDetails.getFullName());
        user.setPhoneNumber(userDetails.getPhoneNumber());
        user.setUpdatedAt(LocalDateTime.now());
        
        return userRepository.save(user);
    }
    
    public List<User> getUserHistory(Long userId) {
        Revisions<Integer, User> revisions = userRepository.findRevisions(userId);
        
        return revisions.getContent().stream()
            .map(Revision::getEntity)
            .collect(Collectors.toList());
    }
    
    public Optional<User> getUserAtRevision(Long userId, Integer revisionNumber) {
        return userRepository.findRevision(userId, revisionNumber)
            .map(Revision::getEntity);
    }
    
    public Optional<User> getPreviousUserState(Long userId) {
        Revisions<Integer, User> revisions = userRepository.findRevisions(userId);
        List<Revision<Integer, User>> revisionList = revisions.getContent();
        
        if (revisionList.size() <= 1) {
            return Optional.empty(); // ์ด์ „ ์ƒํƒœ๊ฐ€ ์—†์Œ
        }
        
        // ์ตœ์‹  ์ƒํƒœ ๋ฐ”๋กœ ์ด์ „์˜ ์ƒํƒœ ๋ฐ˜ํ™˜
        return Optional.of(revisionList.get(revisionList.size() - 2).getEntity());
    }
}

6.4 ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        return ResponseEntity.ok(userService.createUser(user));
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
        return ResponseEntity.ok(userService.updateUser(id, user));
    }
    
    @GetMapping("/{id}/history")
    public ResponseEntity<List<User>> getUserHistory(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getUserHistory(id));
    }
    
    @GetMapping("/{id}/revision/{revisionNumber}")
    public ResponseEntity<User> getUserAtRevision(
            @PathVariable Long id, 
            @PathVariable Integer revisionNumber) {
        return userService.getUserAtRevision(id, revisionNumber)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    @GetMapping("/{id}/previous")
    public ResponseEntity<User> getPreviousUserState(@PathVariable Long id) {
        return userService.getPreviousUserState(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
}

์ด ์˜ˆ์ œ์—์„œ๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด์˜ ์ƒ์„ฑ, ์ˆ˜์ •, ๊ทธ๋ฆฌ๊ณ  ๋ณ€๊ฒฝ ์ด๋ ฅ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ์–ด. ํŠนํžˆ getUserHistory, getUserAtRevision, getPreviousUserState ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ด๋ ฅ์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์ง€! ๐Ÿ•ต๏ธโ€โ™€๏ธ

ํด๋ผ์ด์–ธํŠธ ์ปจํŠธ๋กค๋Ÿฌ ์„œ๋น„์Šค ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์‚ฌ์šฉ์ž ์ด๋ ฅ ๊ด€๋ฆฌ ํ๋ฆ„๋„ API ์š”์ฒญ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ฐ์ดํ„ฐ ์ ‘๊ทผ

โšก 7. ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

Spring Data Envers๋Š” ๊ฐ•๋ ฅํ•˜์ง€๋งŒ, ์ž˜๋ชป ์‚ฌ์šฉํ•˜๋ฉด ์„ฑ๋Šฅ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด. ์—ฌ๊ธฐ ๋ช‡ ๊ฐ€์ง€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ์„ ์†Œ๊ฐœํ• ๊ฒŒ! ๐Ÿš€

7.1 ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ๊ฐ์‚ฌํ•˜๊ธฐ

๋ชจ๋“  ํ•„๋“œ๋ฅผ ๊ฐ์‚ฌํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณต๊ฐ„์„ ๋งŽ์ด ์ฐจ์ง€ํ•˜๊ณ  ์ฟผ๋ฆฌ ์„ฑ๋Šฅ๋„ ์ €ํ•˜๋  ์ˆ˜ ์žˆ์–ด. ๊ผญ ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ @Audited๋กœ ํ‘œ์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„!

@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    
    @Audited // ์ค‘์š”ํ•œ ํ•„๋“œ๋งŒ ๊ฐ์‚ฌ
    private String name;
    
    @Audited // ๊ฐ€๊ฒฉ ๋ณ€๊ฒฝ์€ ์ค‘์š”ํ•˜๋ฏ€๋กœ ๊ฐ์‚ฌ
    private BigDecimal price;
    
    // ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๋Š” ์žฌ๊ณ  ์ˆ˜๋Ÿ‰์€ ๊ฐ์‚ฌํ•˜์ง€ ์•Š์Œ
    private Integer stockQuantity;
}

7.2 ์ธ๋ฑ์Šค ํ™œ์šฉํ•˜๊ธฐ

๊ฐ์‚ฌ ํ…Œ์ด๋ธ”์— ์ ์ ˆํ•œ ์ธ๋ฑ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์ฟผ๋ฆฌ ์„ฑ๋Šฅ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด:

@Entity
@Table(name = "REVINFO", indexes = {
    @Index(name = "idx_revinfo_revtstmp", columnList = "REVTSTMP")
})
@RevisionEntity(UserRevisionListener.class)
public class UserRevisionEntity extends DefaultRevisionEntity {
    // ...
}

7.3 ๊ฐ์‚ฌ ๋ฐ์ดํ„ฐ ์ •๋ฆฌํ•˜๊ธฐ

์˜ค๋ž˜๋œ ๊ฐ์‚ฌ ๋ฐ์ดํ„ฐ๋Š” ์ฃผ๊ธฐ์ ์œผ๋กœ ์ •๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„. ์˜ˆ๋ฅผ ๋“ค์–ด, 1๋…„ ์ด์ƒ ๋œ ๊ฐ์‚ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์•„์นด์ด๋ธŒํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•˜๋Š” ๋ฐฐ์น˜ ์ž‘์—…์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด:

@Scheduled(cron = "0 0 1 * * ?") // ๋งค์ผ ์ƒˆ๋ฒฝ 1์‹œ์— ์‹คํ–‰
public void cleanupOldAuditData() {
    LocalDateTime oneYearAgo = LocalDateTime.now().minusYears(1);
    long timestamp = oneYearAgo.toEpochSecond(ZoneOffset.UTC) * 1000;
    
    // ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ๋กœ ์˜ค๋ž˜๋œ ๊ฐ์‚ฌ ๋ฐ์ดํ„ฐ ์‚ญ์ œ
    entityManager.createNativeQuery(
        "DELETE FROM REVINFO WHERE REVTSTMP < :timestamp")
        .setParameter("timestamp", timestamp)
        .executeUpdate();
}

7.4 ์ง€์—ฐ ๋กœ๋”ฉ ํ™œ์šฉํ•˜๊ธฐ

๊ฐ์‚ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•  ๋•Œ ์ง€์—ฐ ๋กœ๋”ฉ(Lazy Loading)์„ ํ™œ์šฉํ•˜๋ฉด ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด:

@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private List<UserAddress> addresses;

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ฃผ์†Œ ์ •๋ณด๊ฐ€ ํ•„์š”ํ•  ๋•Œ๋งŒ ๋กœ๋”ฉ๋˜์–ด ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์กฐํšŒ๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์–ด!

7.5 ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ™œ์šฉํ•˜๊ธฐ

๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ๋Š” ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„:

@Transactional
public void processLargeDataSet() {
    int batchSize = 100;
    List<User> allUsers = userRepository.findAll();
    
    for (int i = 0; i < allUsers.size(); i += batchSize) {
        List<User> batch = allUsers.subList(
            i, Math.min(i + batchSize, allUsers.size()));
        
        // ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋กœ์ง
        processBatch(batch);
        
        // ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ์ดˆ๊ธฐํ™”๋กœ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๊ด€๋ฆฌ
        entityManager.clear();
    }
}

์ด๋Ÿฐ ์ตœ์ ํ™” ๊ธฐ๋ฒ•๋“ค์„ ์ ์šฉํ•˜๋ฉด Spring Data Envers๋ฅผ ๋Œ€๊ทœ๋ชจ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋„ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ์„ฑ๋Šฅ๊ณผ ๊ธฐ๋Šฅ ์‚ฌ์ด์˜ ๊ท ํ˜•์„ ์ž˜ ๋งž์ถ”๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด. ๐ŸŽฏ

โ“ 8. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

Spring Data Envers๋ฅผ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋ฉด ๋ช‡ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด. ์—ฌ๊ธฐ์„œ๋Š” ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์™€ ๊ทธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ! ๐Ÿ› ๏ธ

8.1 ๋ฌธ์ œ: ๊ฐ์‚ฌ ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋˜์ง€ ์•Š์Œ

์ฆ์ƒ: @Audited ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ–ˆ๋Š”๋ฐ ๊ฐ์‚ฌ ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋˜์ง€ ์•Š์•„.

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

  1. Hibernate ์„ค์ •์—์„œ hibernate.hbm2ddl.auto ๊ฐ’์ด create, create-drop, ๋˜๋Š” update๋กœ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด.
  2. Spring Boot ์„ค์ •์—์„œ spring.jpa.hibernate.ddl-auto ๊ฐ’์„ ํ™•์ธํ•ด.
  3. @EnableJpaRepositories ์–ด๋…ธํ…Œ์ด์…˜์—
    repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class
    ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด.

8.2 ๋ฌธ์ œ: ๋ฆฌ๋น„์ „ ๋ฒˆํ˜ธ๊ฐ€ ์ฆ๊ฐ€ํ•˜์ง€ ์•Š์Œ

์ฆ์ƒ: ์—”ํ‹ฐํ‹ฐ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์ˆ˜์ •ํ•ด๋„ ๋ฆฌ๋น„์ „ ๋ฒˆํ˜ธ๊ฐ€ ์ฆ๊ฐ€ํ•˜์ง€ ์•Š์•„.

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

  1. ํŠธ๋žœ์žญ์…˜์ด ์ œ๋Œ€๋กœ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด. @Transactional ์–ด๋…ธํ…Œ์ด์…˜์ด ํ•„์š”ํ•ด.
  2. ๋ณ€๊ฒฝ ๊ฐ์ง€๊ฐ€ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ด. ์—”ํ‹ฐํ‹ฐ์˜ ํ•„๋“œ ๊ฐ’์ด ์‹ค์ œ๋กœ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ด.
  3. ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ํ”Œ๋Ÿฌ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•ด.
@Transactional
public void updateUser(User user) {
    // ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
    User existingUser = userRepository.findById(user.getId()).orElseThrow();
    if (!existingUser.getName().equals(user.getName())) {
        existingUser.setName(user.getName());
        userRepository.save(existingUser);
    }
}

8.3 ๋ฌธ์ œ: ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ ๊ฐ์‚ฌ ๋ฌธ์ œ

์ฆ์ƒ: ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ๊ฐ์‚ฌ๋˜์ง€ ์•Š์•„.

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

  1. ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ์—๋„ @Audited ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ด.
  2. @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)๋ฅผ ์‚ฌ์šฉํ•ด ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ๋Š” ๊ฐ์‚ฌํ•˜์ง€ ์•Š์œผ๋ฉด์„œ ๊ด€๊ณ„๋งŒ ๊ฐ์‚ฌํ•  ์ˆ˜ ์žˆ์–ด.
@Entity
@Audited
public class Order {
    @Id
    @GeneratedValue
    private Long id;
    
    // Customer ์—”ํ‹ฐํ‹ฐ๋„ ๊ฐ์‚ฌ๋จ
    @ManyToOne
    @Audited
    private Customer customer;
    
    // Product ์—”ํ‹ฐํ‹ฐ๋Š” ๊ฐ์‚ฌ๋˜์ง€ ์•Š์ง€๋งŒ, ๊ด€๊ณ„๋Š” ๊ฐ์‚ฌ๋จ
    @ManyToMany
    @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
    private List<Product> products;
}

8.4 ๋ฌธ์ œ: ์„ฑ๋Šฅ ์ €ํ•˜

์ฆ์ƒ: Envers๋ฅผ ์ ์šฉํ•œ ํ›„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋จ.

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

  1. ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ๊ฐ์‚ฌํ•˜๋„๋ก ์„ค์ •ํ•ด.
  2. ๊ฐ์‚ฌ ํ…Œ์ด๋ธ”์— ์ ์ ˆํ•œ ์ธ๋ฑ์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด.
  3. ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ํ™œ์šฉํ•ด.
  4. ์˜ค๋ž˜๋œ ๊ฐ์‚ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ •๋ฆฌํ•ด.

8.5 ๋ฌธ์ œ: ์ปค์Šคํ…€ ๋ฆฌ๋น„์ „ ์—”ํ‹ฐํ‹ฐ ๋ฌธ์ œ

์ฆ์ƒ: ์ปค์Šคํ…€ ๋ฆฌ๋น„์ „ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์Œ.

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

  1. ์ปค์Šคํ…€ ๋ฆฌ๋น„์ „ ์—”ํ‹ฐํ‹ฐ๊ฐ€ DefaultRevisionEntity๋ฅผ ์ƒ์†๋ฐ›๋Š”์ง€ ํ™•์ธํ•ด.
  2. @RevisionEntity ์–ด๋…ธํ…Œ์ด์…˜์ด ์ œ๋Œ€๋กœ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด.
  3. ๋ฆฌ๋น„์ „ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์ œ๋Œ€๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด.
@Entity
@Table(name = "REVINFO")
@RevisionEntity(UserRevisionListener.class)
public class UserRevisionEntity extends DefaultRevisionEntity {
    private String username;
    
    // ๋ฐ˜๋“œ์‹œ ๊ฒŒํ„ฐ์™€ ์„ธํ„ฐ๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•จ
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
}

์ด๋Ÿฐ ๋ฌธ์ œ๋“ค์„ ๋ฏธ๋ฆฌ ์•Œ๊ณ  ๋Œ€๋น„ํ•˜๋ฉด Spring Data Envers๋ฅผ ๋” ํšจ๊ณผ์ ์œผ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ๋‹นํ™ฉํ•˜์ง€ ๋ง๊ณ  ์ฐจ๊ทผ์ฐจ๊ทผ ํ•ด๊ฒฐํ•ด๋ณด์ž. ๐Ÿ˜Š

๐ŸŽ 9. ๋งˆ๋ฌด๋ฆฌ ๋ฐ ์ถ”๊ฐ€ ์ž๋ฃŒ

์ง€๊ธˆ๊นŒ์ง€ Spring Data Envers๋ฅผ ์‚ฌ์šฉํ•ด ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ดค์–ด! ์ด์ œ ๋„ˆ๋„ ๋ฐ์ดํ„ฐ์˜ ๊ณผ๊ฑฐ์™€ ํ˜„์žฌ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์˜ค๊ฐ€๋Š” ํƒ€์ž„๋จธ์‹ ์˜ ์ฃผ์ธ์ด ๋œ ๊ฑฐ์•ผ. ๐Ÿš€

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

๐Ÿ”‘ ํ•ต์‹ฌ ์š”์•ฝ

  1. ์„ค์ •์ด ๊ฐ„๋‹จํ•ด: ๋ช‡ ๊ฐœ์˜ ์–ด๋…ธํ…Œ์ด์…˜๊ณผ ์ธํ„ฐํŽ˜์ด์Šค๋งŒ์œผ๋กœ ๊ฐ•๋ ฅํ•œ ์ด๋ ฅ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด.
  2. ์ž๋™ํ™”๋œ ์ด๋ ฅ ๊ด€๋ฆฌ: ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ด๋ ฅ์ด ๊ธฐ๋ก๋ผ.
  3. ์œ ์—ฐํ•œ ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ: ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ด๋ ฅ์„ ์กฐํšŒํ•˜๊ณ  ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์–ด.
  4. ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ฐ€๋Šฅ: ํ•„์š”์— ๋”ฐ๋ผ ๊ฐ์‚ฌ ๋ฐฉ์‹์„ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์–ด.
  5. ์„ฑ๋Šฅ ์ตœ์ ํ™”: ์ ์ ˆํ•œ ์„ค์ •๊ณผ ์ธ๋ฑ์‹ฑ์œผ๋กœ ๋Œ€๊ทœ๋ชจ ์‹œ์Šคํ…œ์—์„œ๋„ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด.
์„ค์ • ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๋ฌธ์ œํ•ด๊ฒฐ Envers Spring Data Envers ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

Spring Data Envers๋Š” ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ ฅ ๊ด€๋ฆฌ๋ผ๋Š” ๋ณต์žกํ•œ ๋ฌธ์ œ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•ด๊ฒฐํ•ด์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์•ผ. ์ด ๊ธ€์ด ๋„ค ํ”„๋กœ์ ํŠธ์— Envers๋ฅผ ๋„์ž…ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด! ๐Ÿ˜Š

๋‹ค์Œ์—๋Š” ๋” ์‹ฌํ™”๋œ ์ฃผ์ œ๋‚˜ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ์˜ ์ ์šฉ ์‚ฌ๋ก€๋ฅผ ๋‹ค๋ค„๋ณผ ์ˆ˜๋„ ์žˆ์„ ๊ฒƒ ๊ฐ™์•„. ํ˜น์‹œ ๋” ์•Œ๊ณ  ์‹ถ์€ ๋‚ด์šฉ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ์žฌ๋Šฅ๋„ท์—์„œ ๊ด€๋ จ ์ง€์‹์„ ์ฐพ์•„๋ณด๊ฑฐ๋‚˜ ์งˆ๋ฌธํ•ด๋ด! ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด. ๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ‘ฉโ€๐Ÿ’ป

๊ทธ๋Ÿผ ๋‹ค์Œ์— ๋˜ ๋งŒ๋‚˜์ž! Happy coding! ๐Ÿš€

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.