๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Spring/Test

JPA Auditing @createdDate ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

by ํ‘์‹œ๋ฐ” 2023. 10. 30.

๐Ÿ˜Ž  ๋ฐฐ๊ฒฝ

API๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ ์š”๊ตฌ์‚ฌํ•ญ์— ์กฐ๊ฑด์ด๋‚˜ ๊ธฐ๋Šฅ์— ์ƒ์„ฑ ์‹œ๊ฐ„, ์ˆ˜์ • ์‹œ๊ฐ„์ด ํฌํ•จ๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ, ์˜ฌ๋ฐ”๋ฅธ ์‹œ๊ฐ„ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค.

 

๋ณดํ†ต JPA๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์ƒ์„ฑ์ž/์ƒ์„ฑ ์ผ์ž/์ˆ˜์ •์ž/์ˆ˜์ • ์ผ์ž๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด BaseEntity, Audit ๊ธฐ๋Šฅ์„ ๋งŽ์ด ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋‹ค. ํ•„์ž๊ฐ€ ์ง„ํ–‰ ์ค‘์ธ ํ”„๋กœ์ ํŠธ์—๋„ ๋งค๋ฒˆ Audit๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์žˆ๋‹ค.

 

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

๋ฐฉ๋ฒ•

ํ•„์ž๊ฐ€ ์ฐพ์•„๋ณธ ๋ฐฉ๋ฒ• ์ค‘ ์‚ฌ์šฉํ•˜๊ธฐ ๊ฐ€์žฅ ๊ดœ์ฐฎ์€ ๋ฐฉ๋ฒ•์€ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด์—ˆ๋‹ค.

 

์ฒซ ๋ฒˆ์งธ๋Š” DateTimeProvider Mocking์„ ํ†ตํ•ด์„œ ์‹œ๊ฐ„ ๊ฐ’์„ Mocking ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

๋‘ ๋ฒˆ์งธ๋Š” JdbcTemplate์„ ์ด์šฉํ•ด์„œ ์‹œ๊ฐ„ ๊ฐ’์„ ์กฐ์ž‘ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

1. Mocking 

@SpringBootTest
@Transactional
public class AuditTest {

    @Autowired
    ShibaHolicRepository shibaHolicRepository;

    @MockBean
    private DateTimeProvider dateTimeProvider;

    @SpyBean
    private AuditingHandler handler;

    @BeforeEach
    void setUp() throws Exception {
        MockitoAnnotations.openMocks(this);
        handler.setDateTimeProvider(dateTimeProvider);
    }

    @Test
    void auditingTest() {
        // given
        LocalDateTime testTime = LocalDateTime.of(2023,10,30,0,0,0);
        when(dateTimeProvider.getNow()).thenReturn(Optional.of(testTime));

        ShibaHolic shibaHolic1 = ShibaHolic.builder()
                .name("์‹œ๋ฐ”ํ™€๋ฆญ1")
                .age(10L)
                .build();
        shibaHolicRepository.save(shibaHolic1);

        ShibaHolic shibaHolic2 = ShibaHolic.builder()
                .name("์‹œ๋ฐ”ํ™€๋ฆญ2")
                .age(20L)
                .build();
        shibaHolicRepository.save(shibaHolic2);

        // when
        List<ShibaHolic> result = shibaHolicRepository.findAll();

        // then
        assertThat(result).hasSize(2)
                .extracting("createdAt")
                .containsOnly(testTime);
    }
}

 

ํ•ด๋‹น ๋ฐฉ์‹์€ DateTimeProvider๋ฅผ Mocking ํ•˜๊ณ  AuditingHandler์— ๋“ฑ๋กํ•ด์„œ ์‚ฌ์šฉํ•œ๋‹ค.

 

    @Test
    void auditingTest() {
        // given
        LocalDateTime testTime = LocalDateTime.of(2023,10,30,0,0,0);
        when(dateTimeProvider.getNow()).thenReturn(Optional.of(testTime));

        ShibaHolic shibaHolic1 = ShibaHolic.builder()
                .name("์‹œ๋ฐ”ํ™€๋ฆญ1")
                .age(10L)
                .build();
        shibaHolicRepository.save(shibaHolic1);

        LocalDateTime testTime2 = LocalDateTime.of(2023,10,31,0,0,0);
        when(dateTimeProvider.getNow()).thenReturn(Optional.of(testTime2));

        ShibaHolic shibaHolic2 = ShibaHolic.builder()
                .name("์‹œ๋ฐ”ํ™€๋ฆญ2")
                .age(20L)
                .build();
        shibaHolicRepository.save(shibaHolic2);

        // when
        List<ShibaHolic> result = shibaHolicRepository.findAll();

        // then
        assertThat(result).hasSize(2)
                .extracting("createdAt")
                .contains(testTime, testTime2);
    }

 

๋งŒ์•ฝ ๋“ฑ๋กํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ์— ๋”ฐ๋ผ์„œ ์‹œ๊ฐ„ ์ฐจ์ด๋ฅผ ์ฃผ๊ณ  ์‹ถ๋‹ค๋ฉด, Repository save()๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์ „์— when() ์ ˆ์„ ํ•œ ๋ฒˆ ๋” ์„ ์–ธํ•ด์„œ ๋‹ค๋ฅธ ์‹œ๊ฐ„์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค.

2. JdbcTemplate

@SpringBootTest
@Transactional
public class AuditTest2 {

    @Autowired
    ShibaHolicRepository shibaHolicRepository;

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    EntityManager entityManager;

    @Test
    void auditingTest() {
        // given
        ShibaHolic shibaHolic1 = ShibaHolic.builder()
                .name("์‹œ๋ฐ”ํ™€๋ฆญ1")
                .age(10L)
                .build();
        ShibaHolic savedShibaHolic1 = shibaHolicRepository.save(shibaHolic1);

        // 10์›” 29์ผ
        String sql = "update tb_shiba_holic set created_at = DATE_SUB('2023-10-30 00:00:00', INTERVAL 1 DAY) where id = " + savedShibaHolic1.getId() + " ";
        jdbcTemplate.execute(sql);

        ShibaHolic shibaHolic2 = ShibaHolic.builder()
                .name("์‹œ๋ฐ”ํ™€๋ฆญ2")
                .age(20L)
                .build();
        ShibaHolic savedShibaHolic2 = shibaHolicRepository.save(shibaHolic2);
        // 10์›” 31์ผ
        String sql2 = "update tb_shiba_holic set created_at = DATE_ADD('2023-10-30 00:00:00', INTERVAL 1 DAY) where id = " + savedShibaHolic2.getId() + " ";
        jdbcTemplate.execute(sql2);

        entityManager.flush();
        entityManager.clear();

        // when
        List<ShibaHolic> result = shibaHolicRepository.findAll();

        // then
        assertThat(result).hasSize(2)
                .extracting("createdAt")
                .contains(LocalDateTime.of(2023,10,29,0,0,0),
                        LocalDateTime.of(2023,10,31,0,0,0));
    }
}

 

ํ•ด๋‹น ๋ฐฉ์‹์€ jdbcTemplate์„ ์‚ฌ์šฉํ•ด์„œ ์ง์ ‘ SQL ๋ฌธ์„ ์ž‘์„ฑํ•ด์„œ ์‹œ๊ฐ„์„ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

 

๊ทธ๋Ÿฐ๋ฐ ํ•ด๋‹น ๋ฐฉ๋ฒ•์€ ๊ทธ๋ƒฅ ์—”ํ‹ฐํ‹ฐ์—์„œ ์‹œ๊ฐ„์„ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ๋˜์ง€ ์•Š์„๊นŒ?๋ผ๋Š” ๊ณ ๋ฏผ์ด ๋“ค๊ฒŒ ํ•œ๋‹ค.

 

 

๋ณดํ†ต BaseEntity๋Š” updatable=false ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒŒ ์ผ๋ฐ˜์ ์ด๊ธฐ ๋•Œ๋ฌธ์— ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ•ด๋‹น ์˜ต์…˜์„ ํ’€์–ด์•ผ ํ•œ๋‹ค. ๊ณผ์—ฐ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ ์ด๋Ÿฌํ•œ ์ œ์•ฝ ์กฐ๊ฑด ํ•ด์ œ๊นŒ์ง€ ํ•ด์•ผ ํ• ๊นŒ? ํŒ๋‹จํ•˜๋Š” ๊ฑด ๊ฐœ๋ฐœ์ž์˜ ๋ชซ์ด๋‹ค.

์žฅ/๋‹จ์ 

ํ•„์ž๋Š” ๊ฐ ๋ฐฉ์‹์— ์žฅ/๋‹จ์ ์ด ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

 

Mocking ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ํ†ต์‹  ๊ณผ์ •์ด ์—†์œผ๋ฏ€๋กœ ์†๋„๊ฐ€ ๋น ๋ฅด๊ณ , SQL๋ฌธ ์ž‘์„ฑ ์—†์ด ๋‚ด๊ฐ€ ํ•„์š”ํ•œ ์‹œ๊ฐ„์œผ๋กœ ์‰ฝ๊ฒŒ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ์–ด์„œ ํŽธ๋ฆฌํ•˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

 

ํ•˜์ง€๋งŒ, ๋งŒ์•ฝ ์ƒ์œ„(๋ถ€๋ชจ) ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์—์„œ BaseEntity๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋‹ค๋ฅธ ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค๋ฉด ์‚ฌ์šฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

(๋ณ„๋„๋กœ ๋ถ„๋ฆฌํ•œ๋‹ค๋ฉด ์Šคํ”„๋ง ์ปจํ…์ŠคํŠธ๊ฐ€ 1๊ฐœ ๋” ๋„์›Œ์ง€๊ธฐ ๋•Œ๋ฌธ์—, ์ „์ฒด ํ…Œ์ŠคํŠธ์—์„œ๋Š” ์‹œ๊ฐ„์  ์†ํ•ด๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.)

 

JdbcTemplate ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ’์„ ์ง์ ‘ ์กฐ์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„ Mocking ์ž‘์—…์ด ํ•„์š” ์—†๊ณ , ์ƒ์œ„(๋ถ€๋ชจ) ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

 

ํ•˜์ง€๋งŒ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ”Œ๋žซํผ์— ๋งž๊ฒŒ Update SQL๋ฌธ์„ ์ž‘์„ฑํ•ด์•ผ ํ•˜๊ณ , ๋งŒ์•ฝ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋ณ€๊ฒฝํ•œ๋‹ค๋ฉด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ์‰ฝ๊ฒŒ ๊นจ์งˆ ์ˆ˜ ์žˆ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

๊ฒฐ๋ก 

์–ด๋–ค ๊ฒฝ์šฐ์— ์–ด๋–ป๊ฒŒ ์“ฐ๋ฉด ์ข‹์„๊นŒ?

 

ํ•„์ž๋Š” Auditing์„ ๊ฒ€์ฆํ•˜๋Š” ๊ณผ์ •์ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์— ๋งŽ์ด ์‚ฌ์šฉ๋˜๊ฑฐ๋‚˜, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ์‚ฌ์šฉ๋˜๊ฑฐ๋‚˜, ์ƒ์œ„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์™€ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ๋Š” ๋ฐฉ์‹์ด๋ผ๋ฉด Mocking ๋ฐฉ์‹ ์‚ฌ์šฉ์„ ์ถ”์ฒœํ•˜๊ณ  ์‹ถ๋‹ค.

 

ํ•˜์ง€๋งŒ ๋ช‡ ๋ฒˆ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ์ผ๋ถ€ ์ผ€์ด์Šค์—๋งŒ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด JdbcTemplate ์‚ฌ์šฉ์„ ์ถ”์ฒœํ•˜๊ณ  ์‹ถ๋‹ค.

 

๊ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ…Œ์ŠคํŠธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์— ์•Œ๋งž์€ ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•ด์„œ ์‚ฌ์šฉํ•ด ๋ณด๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

REFERENCE

https://mariadb.com/kb/en/date_add/

https://mariadb.com/kb/en/date_sub/

https://mkyong.com/spring-boot/mocking-spring-data-datetimeprovider/

https://stackoverflow.com/questions/42374387/how-to-set-createddate-in-the-past-for-testing

๋Œ“๊ธ€