반응형

테이블을 설계할때 항상 빠지지 않고 들어있는 컬럼이 있다. 바로 생성일자, 수정일자, 생성자, 수정자 컬럼이다.

거의 모든 테이블에 디폴트로 들어있는 컬럼이고 상당히 중요한(?) 정보이다. 그렇다 보니 도메인에도 항상 똑같은 컬럼이 존재하게 된다. 그런데 spring-data-jpa에 재미난 기능이 있다. 바로 생성일자, 수정일자, 생성자, 수정자 컬럼에 값을 자동으로 넣어주는 기능이다. 


Audit : 감시하다.


바로 spring-data-jpa 에서는 audit 기능을 제공하고 있다. 방법은 여러가지가 있다. 각 도메인에 @PrePersist, @PreUpdate 등을 붙여서 할수도 있다. 그런데 이게 단점이 각각의 도메인에 같은 컬럼이 정의되어있다는 것은 변하지 않는다는 것이다. 모든 도메인에 똑같이 createDate, modifyDate, createUser, modifyUser가 있어야 한다. 그래서 이런 부분을 피하기 위해 다른 방법을 택했다.


1
2
3
4
5
@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class})
public class AuditableDomain extends AbstractAuditable<Account, Long> {
    private static final long serialVersionUID = 1L;
}
cs


위 소스에 보면 @EntityListenersAuditingEntityListener를 넣어줬다. 그리고 지금 구현한 Class는  AbstractAuditable을 상속받고 있다. AbstractAuditable 클래스의 Account는 실제 User 정보를 담고 있는 Domain 이고 Long은 그 도메인의 Key 값을 의미한다. 

AbstractAuditable 클래스의 필드를 보면 다음과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@MappedSuperclass
public abstract class AbstractAuditable<U, PK extends Serializable> extends AbstractPersistable<PK> implements
        Auditable<U, PK> {
 
    private static final long serialVersionUID = 141481953116476081L;
 
    @ManyToOne
    private U createdBy;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
 
    @ManyToOne
    private U lastModifiedBy;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
 
    .. 중략
}
cs


이렇게 createBy, createDate, lastModifedBy, lastModifiedDate 필드를 가지고 있기 때문에 더이상 하위 Domain 에서는 이 필드들을 정의할 필요가 없어진다. 그리고 다시 AbstractAuditable  클래스는 AbstractPersistable을 상속받고 있다. 보면 PK 를 정의하고 있다.


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
@MappedSuperclass
public abstract class AbstractPersistable<PK extends Serializable> implements Persistable<PK> {
 
    private static final long serialVersionUID = -5554308939380869754L;
 
    @Id @GeneratedValue private PK id;
 
    /*
     * (non-Javadoc)
     * @see org.springframework.data.domain.Persistable#getId()
     */
    public PK getId() {
        return id;
    }
 
    /**
     * Sets the id of the entity.
     * 
     * @param id the id to set
     */
    protected void setId(final PK id) {
        this.id = id;
    }
 
    .. 중략
}
cs


결론적으로 이 AbstractPersistable 클래스에서 PK 를 정의하고 있기 때문에 각각의 도메인들은 더이상 id를 정의할 필요가 없어진다. 

그런데 내가 이걸 테스트 하다보니 문제가 생겼다. 

사용자 정보를 담은 도메인인 Account 에는 id를 String 으로 loginId 라는 이름으로 넣어놨다. AbstractPersistable  클래스에서 정의하는 ID 랑 다르기 때문에 @AttributeOverride  를 써서 재정의를 해보았는데 이상한 현상이 발생했다. DB 테이블에 loginId 말고 id라는 컬럼이 따로 생기고 각각의 필드들도 id랑 loginId랑 2개씩 생기는 현상이 발견됐다. createById, createByLoginId 이런 형태로 2개씩 생겼다. 

그리고 두번째로 ID를 set을 할 수가 없다. 그렇다 보니 ID를 정의할 수 없어서 결국 domain의 Id는 @generateValue 가 가능한 Long 형태로 변경했다. 혹시라도 다시 방법을 찾게 된다면 다시 포스팅을 하겠다..


1
2
3
4
5
6
7
8
9
@EnableJpaAuditing
@Configuration
public class AuditingConfig {
 
    @Bean
    CustomAuditorAware auditorAware() {
        return new CustomAuditorAware();
    }
}
cs

두번째로 해야할 일은 Config 등록이다. @EnableJpaAuditing을 선언해주고 JavaConfig로 등록해준다. 그런데 이때에 createDate, modifyDate는 상관이 없는데 createBy, modifiedBy 같은 경우는 대상이 필요하기 때문에 재정의가 필요하다. 


1
2
3
4
5
6
7
8
9
10
public class CustomAuditorAware implements AuditorAware<Account>{
 
    @Override
    public Account getCurrentAuditor() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (null == authentication || !authentication.isAuthenticated()) {
            return null;
        }
        return (Account) authentication.getPrincipal(); }
}
cs


그래서 이렇게 getCurrentAuditor method를 재정의 해준다. 지금 작성하고 있는 프로젝트는 spring security 가 적용된 프로젝트이기 때문에 SecurityConextHolder에서 정보를 가져와서 보여준다. 


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
(SpringRunner.class)
@SpringBootTest(classes = StudySpringApplication.class)
public class AccountServiceTest {
 
    private static Logger logger = LoggerFactory.getLogger(AccountServiceTest.class);
 
 
    @MockBean
    private CustomAuditorAware customAuditorAware;
 
    @Autowired
    private AccountService accountService;
 
    @Autowired
    private RoleService roleService;
 
    @Before
    public void setup(){
        Account sessionAccount = accountService.get("admin");
        Mockito.when(customAuditorAware.getCurrentAuditor()).thenReturn(sessionAccount);
    }
 
    @Test
    public void createAccount(){
        Account newAccount = new Account();
        newAccount.setLoginId("admin1");
        newAccount.setUsername("admin user");
        newAccount.setPassword("admin1!");
        newAccount.setEmail("admin@spring.com");
        newAccount.setEnable(true);
 
        accountService.create(newAccount, null);
 
        Account account = accountService.get("admin1");
        Assert.assertEquals("admin1", account.getLoginId());
        Assert.assertNotNull(account.getCreatedBy());
    }
}
cs

결과를 확인해보기 위해 Test 코드를 돌려보면 잘 적용이 되는것을 확인 할수 있다.

728x90
반응형

+ Recent posts