문제이슈
내편(내 마음을 편지로) 서비스에서 롤링페이퍼의 메세지에 좋아요를 누를 수 있는 기능이 있습니다.
- 메세지 좋아요에 엄청난 연타를 누르거나
- PC, 모바일에서 좋아요를 동시에 눌렀을 때
다음과같은 이슈가 발생하였습니다.
중복된 데이터가 쌓였습니다!
- 메세지 좋아요 테이블에는 같은 member_id, message_id가 공존하면 안됩니다.
기존 코드
public MessageLikeResponseDto likeMessage(Long memberId, Long rollingpaperId, Long messageId) {
// 메세지 좋아요 대상 "메세지"를 찾는다.
final Message message = messageRepository.findById(messageId)
.orElseThrow(() -> new NotFoundMessageException(messageId));
// "메세지 좋아요" 테이블에 존재하면 에러를 던진다.
if (messageLikeRepository.existsByMemberIdAndMessageId(memberId, messageId)) {
throw new InvalidLikeMessageException(messageId, messageId);
}
// 메세지 좋아요 추가
message.like();
messageLikeRepository.save(new MessageLike(memberId, rollingpaperId, messageId));
return new MessageLikeResponseDto(message.getLikes(), true);
}
- 메세지 좋아요 대상 "메세지"를 찾습니다.
- "메세지 좋아요" 테이블에 기록이 존재하면 에러를 던집니다.
- 메세지 좋아요를 추가한다.
어떤 점이 문제점이었을까요?
문제원인
시퀀스 | Tx1 | Tx2 |
1 | 메시지 A 찾음 | |
2 | 메시지 A찾음 | |
3 | 메세지 좋아요 기록 존재하지 않는가? true | |
4 | 메세지 좋아요 기록 존재하지 않는가? true | |
5 | 메세지 좋아요 저장 | |
6 | 메세지 좋아요 저장 | |
7 | Tx1 Commit | |
8 | Tx2 Commit |
원인은 바로 3, 4번에 있습니다.
두 개의 트랜잭션이 검사를 했을 때 기록이 존재하지않는가? 에서 둘다 TRUE를 반환했기 때문이었습니다.
해결방법
기존 격리수준은 repeatable read이며 MySQL을 사용하고 있었습니다.
기존 격리수준을 변경하지 않고 격리성을 증진할 수 있는 방법으로 우리는 락(Lock) 기능을 사용할 수 있습니다.
락은 트랜잭션을 순차적으로 처리하도록 도와주고, 데이터의 일관성을 유지할 수 있도록 도와줍니다.
락에는 낙관적 락, 비관적 락이 있는데 어떤 것을 적용해야 할까요?
낙관적 락 vs 비관적 락
- 낙관적 락은 대부분의 경우 트랜잭션 충돌이 일어나지 않는다는 낙관적 가정을 하는 기법입니다. DB 자체에서 제공하는 것이 아닌 JPA 즉 어플리케이션에서 제공하는 기법입니다. 또한 낙관적 락은 충돌을 가정하지 않아 커밋 된 이후에 충돌여부를 알 수 있습니다.
- 비관적 락은 대부분의 경우 트랜젝션에서 충돌이 발생할 것이라 가정하고, 일단 락을 걸고 보는 기법이다. 비관적 락은 데이터베이스의 락 알고리즘을 통해 구현된다.
메세지 좋아요 충돌(엄청난 연타…이슈)은 대부분의 경우 트랜잭션 충돌이 발생하지 않는다로 보아 낙관적 락을 적용하기로 결정하였습니다.
JPA에서 낙관적락
JPA를 사용하고 있고 @Version 어노테이션을 사용하면 낙관적락을 적용할 수 있습니다.
낙관적 락은 어떻게 진행될까요? 버전 관리 기능을 사용하면 다음의 순서로 진행됩니다.
- 엔티티를 조회
- 데이터 수정 또는 저장
- version 비교
- 맞다면 트랜잭션 커밋
- 다르다면 롤백
적용해봅시다!
- 엔티티 클래스에 @Version 어노테이션을 추가해 버전 관리 기능을 하도록 도와줍니다.
- 낙관적 락 옵션으로 가장 많이 사용하는 두가지입니다.
- NONE: 락 옵션을 적용하지 않아도 @Version만 있으면 낙관적 락이 적용됩니다.
- 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경되지 않아야 함
- 조회 시점부터 수정 시점까지를 보장
- race condition을 예방
- OPTIMISTIC: 조회만 해도 버전을 체크합니다.
- 용도: 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야 함
- 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장
- 동작: 트랜잭션을 커밋할 때 버전 정보를 조회해서 SELECT, 현재 엔티티의 버전과 같은지 검증함 만약 같지 않으면 예외 발생, 트랜잭션을 커밋할 때 SELECT 쿼리로 조회해서 처음에 조회한 엔티티의 버전정보와 비교함, 엔티티를 수정하지 않고 단순히 조회만 해도 버전을 확인
메세지를 조회한 시점부터 버전을 체크하기 위해 OPTIMISTIC을 적용하였습니다.
적용한 소스코드
- 엔티티에 version 추가
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Message extends BaseEntity {
// ...
@Version
private Integer version;
}
- Repository에 OPTIMISTIC 적용
public interface MessageRepositoryCustom {
@Lock(LockModeType.OPTIMISTIC)
Optional<Message> findByIdForUpdate(Long id);
}
낙관적 락을 적용한 뒤 트랜잭션 변화
시퀀스 | Tx1 | Tx2 |
1 | Tx1 start | |
2 | Tx2 start | |
3 | 메시지 A 찾음, version = 0 | |
4 | 메시지 A찾음, version = 0 | |
5 | 메세지 좋아요 기록 존재하지 않는가? true | |
6 | 메세지 좋아요 기록 존재하지 않는가? true | |
7 | 좋아요 수 증가, 메세지 좋아요 저장 | |
8 | 좋아요 수 증가, 메세지 좋아요 저장 | |
9 | version == 0 true, Tx1 Commit, version += 1 | |
10 | version == 0 false, Tx2 Rollback |
낙관적 락을 걸어 중복되는 수정으로 부터 일관성을 유지할 수 있게 되었습니다.
참고
- https://effectivesquid.tistory.com/entry/Optimistic-Lock%EA%B3%BC-Pessimistic-Lock
- https://sup2is.github.io/2020/10/22/jpa-optimistic-lock-and-pessimistic-lock.html
'공부 > 프로젝트' 카테고리의 다른 글
restdocs를 사용해보자 (0) | 2022.07.29 |
---|---|
Flyway를 사용하는 이유 (0) | 2022.07.29 |
젠킨스 pipeline를 설정해보자 (0) | 2022.07.26 |
[지원플랫폼] 플래닝포커를 해보자 (0) | 2022.07.25 |
DB 서버를 연동해보자 (0) | 2022.07.25 |
댓글