많은 데이터베이스의 테이블에는 항상 해당 데이터의 생성시간을 기록한다. 가장 최신부터 조회한다던가하는 등의 목적으로 다양하게 활용이 가능하기 때문이다. PK를 내림차순으로 정렬하는 전략도 많이 쓰이지만, 모든 PK를 숫자로 사용하는 것은 아니다. UUID를 쓰는 경우도 있고, 회원의 아이디같은 경우에는 순수하게 String 을 PK 로 사용해 중복을 방지하는 경우도 있다.
그래서, 많은 어플리케이션이 생성시각, 그리고 더불어 수정 상황을 추적하기 위해 해당 로우의 수정시간을 기록한다.
데이터는 입력부터 목적지인 데이터베이스까지 흘러간다. 웹 페이지에서 액트가 실행되어 자바스크립트, 하이퍼링크, 폼이 실행되고, 해당 데이터가 API 서버에 도착하면, 라우팅을 거치고 역직렬화된 뒤 비지니스 로직을 수행하고 데이터베이스에 기록된다. 이 중 한 군데에서라도 생성시간을 입력해준다면, 해당 데이터는 생성시간을 가질 수 있다.
보통 방법은 많을수록 문제가 된다.
웹 페이지에서, 프론트엔드에서 생성해서 보내면 어떨까?
function sumbit() {
fetch('/api/common/blog/post', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: this.title,
description: this.description,
createdAt: new Date()
})
})
.then(response => response.json())
.then(data => {
console.log('전송완료')
})
.catch((error) => {
console.error('Error:', error);
});
}
사실 자세히 다룰 필요도 없이 이런 전략을 취할 이유는 별로 없을 것이다. 위변조의 가능성이 있기 때문이다. 그리고, 시간 타입의 역직렬화는 지독하게 귀찮기 때문에, 하고 싶은 이유도 없을 것이다.
// PostServiceImpl.java
@Service
@RequiredArgsConstructor
@Transactional
public class PostServiceImpl implements PostService {
private final PostRepository postRepository
@Override
public PostResponse writePost(PostRequest request) {
Post entity = Post.create(request);
return postRepository.save(entity);
}
}
// Post.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String title;
@Column
private String description;
@Column
private LocalDateTime createdAt;
@Column
private LocalDateTime updatedAt;
public static Post create(PostRequest request) {
Post entity = new Post();
entity.title = request.getTitle;
entity.description = request.getDscription;
entity.createdAt = LocalDateTime.now();
entity.updatedAt = LocalDateTime.now();
}
}
DDD 기반의 비지니스 로직에서 직접 날짜를 생성해봤다. 심플하게 데이터를 입력하기 전에 넣어주는 것이다. 오류의 가능성도 적고 구현도 단순하다.
ORM 기능 등에 있는등으로 생성, 수정 날짜를 감시하는 것이다. 대표적으로 JPA Auditing 이나 Spring AOP 등을 활용하는 것이다. 수레바퀴를 새롭게 개발할 필요는 없다는 이야기다.
// Post.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post extends BaseTimeEntity { // 추상 클래스 상속
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String title;
@Column
private String description;
public static Post create(PostRequest request) {
Post entity = new Post();
entity.title = request.getTitle;
entity.description = request.getDscription;
}
}
// BaseTimeEntity.java
@Entity
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // Auditing 추가
public class Post extends BaseTimeEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
많이들 사용하는 예시다. 간편하고, 한번 적용해두면 상속만으로 구현이 완료되니 상당히 간편하다.
어차피 데이터베이스에 입력되는 값이다.
-- MYSQL 기준
INSERT INTO post
(
title
, description
, createdAt
, updatedAt
VALUES
(
'글 제목'
, '글 내용입니다.'
, NOW()
, NOW()
)
자주 보게 되는 방식이다. 개인적으로는 이 방식은 무척 안 좋아한다. 실제 데이터의 값을 다루는 곳과 데이터가 실제로 생성되는 곳이 너무 멀기 때문이다. 이것 말고도 테이블을 정의할 때 DEFAULT 로 NOW() 를 사용하는 일도 있다.
웹 페이지에서 넣는 것은 그냥 가능은 하다 정도로 받아들이자.
단, "사용자의 제어권"을 강하게 준다는 의미에서 아주 실용성이 없는 것은 아니다. 근무하면서 "이 글의 작성시간을 3개월 전으로 바꿔주세요" 라는 요청을 받는 일도 자주 있었다. 이 경우엔 생성시간/수정시간이라기보단, 사용자의 입력을 받아들이는 의미가 더 크다. 개발자에겐 "날짜의 위변조"가 데이터의 신뢰성을 깨는 행위일 수 있지만, 공시 의무 게시 기간을 놓친 담당자에겐 중요한 일일 수도 있다.
미리 말했지만 쿼리로 제어하는 방식은 무척 안 좋아한다. ORM 을 사용한다면 일부러 쿼리를 칠 이유가 없을 것이다.
테이블에 정의하는 것은 다소 의미가 있을 수 있다. 이것은 실제로 코드를 제어하는 것에 의미를 가진다기보단 "이 테이블의 이 값이 지니는 의미"를 명시하는 의미가 있기 때문이다. 하지만 명시성을 중요시한다면, 비지니스 로직에서 시간의 입력을 배제할 이유도 없을 것이다.
남는 것은 자동화와 비지니스 로직인데, 사실 나는 비지니스 로직을 사용하는 경우가 대부분의 경우 더 좋은 결과를 가져온다고 생각한다.
'this.createdAt=' 이나 'this.updatedAt=' 으로 검색하는 것으로 해당 데이터의 변경 시점을 명확히 알 수 있다. DDD로 개발할 경우에는 엔티티 클래스 하나만 확인하면 된다.
원하든 원치 않든, 데이터가 수정이 필요한 경우가 있다. 아직도 많은 데이터베이스에서 날짜를 문자열로 저장한다. 유감스러운 일이지만 항상 리팩터링이 가능한 것은 아니다. 여러 개의 서버에서 사용하는 데이터인데 서버마다 타임존이 각각 다른 경우도 있다. 이런 일을 대응하기 위해 라이브러리를 재구현하거나 혹은 어느 부분을 오버라이딩한다거나 하는 수고를 들이는 것보단, 대부분의 경우에는 한 줄 더 치는 게 낫다.
관리자 기능으로 "데이터 수정일"을 변경하고 싶을 때는?
직접 입력한 값과 Auditing 값 중 어떤 것이 우선되는지는 나중에 확인해보자.
물론 어플리케이션의 규모나, 데이터를 다루는 태도에 따라서 선택 기준이 달라질 수 있다. 나라면 AI 학습을 위한 데이터를 수집할 때라면 비지니스 로직에 의해 값이 수정될 수 있는 방식으로는 구현하지 못하게 할 것이다. 그런 값들은 Object 보단 Raw 데이터에 가깝다.
MVC 패턴을 지키면 된다.