엔티티를 만들고 데이터를 삽입하고 조작할 때 create date와 last modified date를 별도로 업데이트 해주면서 관리하였다.

하지만 이번에 JPA를 공부하면서 별도의 작업 없이 JPA의 Auditing 기능을 사용하면 데이터를 삽입하고 수정할 때 자동으로 날짜를 수정하도록 할 수 있는 기능이 있는 것을 확인했다.


1. Configuration

JPA Auditing을 사용하기 위해서는 기능을 자동으로 활성화 해주는 어노테이션을 붙혀주면 된다. 처음에는 @Configuration을 사용하는 클래스에 함께 선언해주었는데 정상적으로 적용이 되지 않아서 @SpringBootApplication을 사용하는 곳에 적용했더니 성공적으로 적용되었다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.wedul.springboottest;
 
import com.wedul.common.exception.ValidationException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.web.context.request.WebRequest;
 
import java.util.Map;
 
@SpringBootApplication
@EnableJpaAuditing
public class SpringboottestApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringboottestApplication.class, args);
    }
 
    @Bean
    public ErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes() {
 
            @Override
            public Map<String, Object> getErrorAttributes(WebRequest webRequest,
                                                          boolean includeStackTrace) {
                Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
                Throwable error = getError(webRequest);
 
                // validatijon Exception에 경우 별도의 처리를 진행한 에러 데이터 추가
                if (error instanceof ValidationException) {
                    errorAttributes.put("errors", ((ValidationException)error).getErrors());
                }
                return errorAttributes;
            }
 
        };
    }
}
 
cs


2. Entity에 CreatedDate, LastModifiedDate

Auditing을 사용할 엔티티에는 몇가지 어노테이션이 사용된다.

먼저 설정을위해서 사용되는 @MappedSuperclass, @EntityListeners(AuditingEntityListener.class)이다. 첫 번재 어노테이션은 이후에 사용될 createdDate, modifiedDate와 같은 필드들을 컬럼으로 인식하게 도와주는 역할을 하고 두 번째 어노테이션은 해당 Entity에 Auditing기능을 포함한다라는 명시를 한다.

처음에는 필요한 Entity에만 CreatedDate, lastModifiedDate가 포함하도록 Entity마다 기재해주었다.하지만 많은 Entity에서 필요로 할 것 같고 필요할 때마다 새로 써주기가 비효율적인 것 같아서 추상클래스로 만들고 필요한 Entity에서 이를 상속받아서 사용하도록 하였다.



- 추상클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.wedul.common.dto;
 
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
 
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
 
/**
 * 모든 Entity들의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리
 *
 * @author wedul
 * @since 2018. 8. 14.
 **/
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeEntity {
 
    @CreatedDate
    private LocalDateTime createdDate;
 
    @LastModifiedDate
    private LocalDateTime modifiedDate;
 
}
cs


- 상속받아서 사용하는 Product Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.wedul.springboottest.product.dto;
 
import com.wedul.common.dto.BaseTimeEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
 
import javax.persistence.*;
import java.io.Serializable;
import java.sql.Timestamp;
 
/**
 * 상품 정보
 *
 * @author wedul
 * @since 2018. 08. 12
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity // class (hibernate)
@Table(name = "product")
public class ProductDto extends TimeEntity implements Serializable {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long productId;
 
    @Column(nullable = false, unique = true)
    private String productName;
 
    @Column(nullable = false)
    private long price;
 
    public ProductDto(ProductRequestDto req) {
        this.productName = req.getProductName();
        this.price = req.getPrice();
    }
 
}
 
cs


테스트 코드를 작성하여 정상적으로 시간값이 들어가고 또 변경되는지 확인해보자.

1. 처음 데이터 삽입시 시간값 입력 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
 * Product Test
 *
 * @author wedul
 * @since 2018. 8. 14.
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest // rolleback 설정
@Rollback(value=true)
public class ProductTest {
 
    private MockMvc mockMvc;
    private final MediaType mediaType = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
 
    @Autowired
    ProductCtrl productCtrl;
 
    @Autowired
    ProductServiceI productService;
 
    @Before // before 클래스는 한번만 실해되고 before는 각 테스트마다 실행된다.
    public void beforeClass() {
        this.mockMvc = standaloneSetup(productCtrl).build();
    }
 
    @Test
    public void insertTest_reqestBody() throws Exception {
 
        // Mock Request Builder
        MockHttpServletRequestBuilder req =
                post("/api/product/new")
                        .content(CommonUtil.getJsonStrFromObject(new ProductRequestDto("i-mac", 2220011L)))
                        .contentType(mediaType);
 
        // 테스트
        MvcResult result = mockMvc.perform(req).andExpect(status().isOk()).andReturn();
        ResultDto resultDto = CommonUtil.getObjectFromJsonStr(result.getResponse().getContentAsString(), ResultDto.class);
 
        assertTrue(resultDto.isResult());
    }
}
cs


정상적으로 날짜값이 들어가 있는 것을 확인 할 수있다. 이제 수정 테스트를 진행해보자.


2. 수정 후 last modified date 시간 변경여부 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void updateTest_requestBody() throws Exception {
    // Mock Request Builder
    MockHttpServletRequestBuilder req =
           put("/api/product/edit")
           .content(CommonUtil.getJsonStrFromObject(new ProductRequestDto(1,"macbook pro", 2312111L)))
           .contentType(mediaType);
 
    // 테스트
    MvcResult result = mockMvc.perform(req).andExpect(status().isOk()).andReturn();
    ResultDto resultDto = CommonUtil.getObjectFromJsonStr(result.getResponse().getContentAsString(), ResultDto.class);
 
    assertTrue(resultDto.isResult());
}
cs


업데이트 후에 정상적으로 modified_date만 변경된 것을 확인할 수있다.

좋은 기능인것같다. JPA를 공부하고 있는데 Mybatis보다 솔직히 불편하다 그런데 한번 잘 구축해놓으면 편하기는 하다. 그래도 무엇하나 변경된다면 결국 너무 손이 많이가고 러닝커브가 좀 심하다.

흠 더 사용해보고 실무에서 써봐야 이걸 왜 요새 사용하는지 더 이해할 수 있을 것 같다.

자세한 코드는 github 참조
https://github.com/weduls/spring_boot_test


  1. Happy 2019.05.22 17:27

    안녕하세요 글 잘보았습니다. :) 몇가지 궁금증이 생겨서 글을 쓰게 되었습니다.

    만일 특정 사용자가 서비스 로그인 이후에 로그아웃을 하고 다시 로그인을 하면 변경사항은 전혀 없습니다.

    그렇다면 임의로 제가 특정 컬럼을 변경시켜서 (더미컬럼 ?) 수정시간이 jpa auditing 에 의해서 변경되어야 하는건지. 궁금하네요.

    한편으로는 사용자에 해당하는 테이블에 현재 로그인 여부를 컬럼으로 추가해서 그렇게 수정하는 방법도 생각해볼 수 있습니다만 어떻게 하는게 효율적일지 몰라 이렇게 장문으로 글을 남깁니다.

    • Favicon of https://wedul.site BlogIcon 위들 wedul 2019.05.22 18:45 신고

      안녕하세요!

      사용자의 마지막 로그인 시간을 별도의 컬럼으로 관리하시면 어떨까 싶습니다.

      그리고 현재 로그인중인 사용자를 테이블의 컬럼으로 관리하는건 힘들지 않을까 싶습니다.

+ Recent posts