데이터베이스에서 여러 트랜잭션이 공유자원에 접근하는 상황이 자주 발생한다.
여러 트랜잭션이 동시에 공유자원에 접근하게되면 데이터가 변조될 가능성이 높아진다.
이를 방지하기 위해 Lock이 사용되며, 공유자원에 접근하기 위해서는 Lock을 먼저 취득해야만 한다.
Lock에 대해 더 알아보기 이전에 트랜잭션 격리 수준에 대해 살펴보자.
트랜잭션 격리 수준은 보통 4가지로 구분된다.
- Read Uncommitted : commit 되지 않은 자원도 다른 트랜잭션에서 접근할 수 있다
- Read Committed : commit이 된 자원만 다른 트랜잭션에서 접근할 수 있다
- Repeatable Read : 한 트랜잭션 내에서 동일한 자원에 반복접근 했을 때, 항상 동일한 값이 출력된다
- Serializable : 한 트랜잭션의 작업이 끝나기 전에는 다른 트랜잭션에서 자원에 접근할 수 없다
이러한 격리 수준은 데이터베이스에서의 동시성 제어를 레벨로 나눠서 구분한 것으로, 이를 구현하는 과정에서 Lock이 사용된다.
Lock은 보통 읽기 Lock과 쓰기 Lock으로 구분된다.
읽기 Lock은 공유자원을 조회할 수 있는 권한을 획득하기 위해 필요한 Lock을 의미하며,
쓰기 Lock은 공유자원을 수정할 수 있는 권한을 획득하기 위해 필요한 Lock이다.
한 트랜잭션에서 공유자원에 대한 특정 Lock을 획득하고 있는 상황이라면, 다른 트랜잭션에서 반대에 해당하는 Lock을 얻을 수 없다.
즉, A 트랜잭션에서 read-lock 을 반환하지 않았다면 B 트랜잭션에서 write-lock을 획득할 수 없다.
Lock과 관련해서는 추가적으로 공부해볼 내용이 많으니 기회가 된다면 따로 살펴보자
부족하지만 과거에 정리했던 내용도 있긴하다
Lock의 획득과 반환으로 구분하여 페이즈를 나누는 2PL( 2-Phase Lock ),
Lock의 성능 향상을 위해 읽기<->쓰기 Lock이 서로 영향을 주지 않는 MVCC( Multi-Version Concurrency Control ) 등의 내용이 있다.MVCC는 읽기 작업에는 스냅샷 이라는 가장 유효한 데이터의 사본을 이용하고,
쓰기 작업에는 데이터의 버전을 새롭게 생성하여 새로운 트랜잭션에서 해당 데이터에 접근하는 방식으로 동작한다
Spring Data JPA에서 특정한 Lock 모드를 사용해야 한다면 @Lock을 이용하면 된다.
interface UserRepository extends Repository<User, Long> {
// Plain query method
@Lock(LockModeType.READ)
List<User> findByLastname(String lastname);
}
LockModeType를 이용하여 쿼리메서드를 대상으로 Lock 모드를 지정할 수 있다.
기본적으로는 Lock 관련 설정을 하지않으면 DB의 격리수준에 맞춰서 진행된다.
따라서, 현재 시점에서는 사용방법을 알아두는 정도로만 이해하고 넘어가도록 하자
Auditing은 Entity의 생성과 변경에 관여된 정보들을 추적할 수 있도록 정보를 제공하는 기능이다.
Spring Data JPA에서는 크게 애노테이션 기반의 설정과 인터페이스 기반의 설정이 존재한다.
Auditing을 활성화하기 위해서는 2개의 사전 설정이 필요하다.
- Entity 클래스에
@EntityListeners(AuditingEntityListener.class)를 설정@EntityListeners(AuditingEntityListener.class) public class Person { // ... }
- Config 파일에
@EnableJpaAuditing를 설정하여 Auditing을 활성화@Configuration @EnableJpaAuditing public class JavaConfig { // ... }
@Entity
@ToString
@Getter @Setter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Person {
@Id
private UUID id;
private String firstname;
private String lastname;
@CreatedDate
private LocalDate createdAt;
@Embedded
private Address address;
public Person(String firstname, String lastname, String zipCode, String city, String street){
this.id = UUID.randomUUID();
this.firstname = firstname;
this.lastname = lastname;
this.address = new Address(zipCode, city, street);
}
@Embeddable @Getter
@NoArgsConstructor
@AllArgsConstructor
static class Address{
String zipCode;
String city;
String street;
}
}
애노테이션 기반의 Auditing은 @CreatedBy, @LastModifiedBy 등의 애노테이션을 이용하여 손쉽게 설정할 수 있다.

이미지와 같이 created_at을 만들어서 넣어주는 모습을 확인할 수 있다.
Auditing 관련 애노테이션으로 @CreatedBy 나 @LastModifiedBy처럼 특정 대상에 의해 생성됨 혹은 수정됨을 나타내는 것들이 있다.
이 경우, 대상을 특정할 수 있어야하기 때문에 추가적인 설정이 필요하다
AuditorAware<T>는 @CreatedBy 나 @LastModifiedBy에서 Entity를 생성 혹은 수정한 사람을 파악하기 위한 용도로 작성해줘야한다.
@Component @Slf4j
@RequiredArgsConstructor
public class SpringSecurityAuditorAware implements AuditorAware<Member> {
private final MemberRepository memberRepository;
@Override
public Optional<Member> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(object -> memberRepository.findByUsername(object.toString()).orElseThrow());
}
}
현재 테스트 환경에서 Spring Security를 사용하고 있기 때문에 SecurityContextHolder에서 현재 인증된 사용자를 얻어내는 방식으로 구현했다.

위와 같은 요청을 서버로 보냈을 때, DB에는 아래와 같이 반영된다.

Reactive 환경이라면
ReactiveAuditorAware<T>를 구현하면 된다
인터페이스 기반의 Auditing은 Auditable<U, ID, T>를 구현해내면 된다.
@EntityListeners(AuditingEntityListener.class)
public class Person implements Auditable<Member, UUID, LocalDate> {
// ...
}

이런식으로 구현하면 되는데, 이 방법의 경우 Auditing 관련 Getter와 Setter를 모두 재정의해야해서 까다롭다