티스토리 뷰
문제 상황
동아리 면접 중, 동시성 문제와 관련해서 내 로직에서는 연관 엔티티의 아이디가 중복 생성 가능해보인다.
어떻게 하면 중복 생성을 막을 수 있을까? 라는 질문을 받았다.
정확히는 모르겠지만, 낙관적 락을 사용하면 해결할 수 있을거 같다 답했다.
면접관은 낙관적 락을 사용해서도 해결할 수 있지만, UNIQUE 제약조건을 통해서도 해결할 수 있다고 했다.
그래서 UNIQUE 제약조건을 통해 직접 중복 생성을 막아보려 한다!
낙관적 락은 좀 더 공부한 뒤 추후 포스팅해 볼 예정이다.
문제 상황 재현
예를 들면, 아래와 같은 상황이다.
userId와 articleId를 갖고 있는 Like 엔티티가 존재한다고 하자.
@Entity
@NoArgsConstructor
@Table(name = "likes")
public class Like extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private Long articleId;
}
userId와 articleId에 해당하는 Like 데이터가 이미 존재한다면, exception을 발생하도록 로직을 구현했다.
즉, 한 명의 회원이 같은 게시글에 대해 좋아요를 2개 이상 누를 수 없다.
@Service
@RequiredArgsConstructor
public class LikeService {
private final LikeRepository likeRepository;
@Transactional
public void create(Long userId, Long articleId) {
if (likeRepository.existsByUserIdAndArticleId(userId, articleId)) {
throw new RuntimeException("이미 존재하는 좋아요");
}
likeRepository.save(new Like(userId, articleId));
}
}
하지만 좋아요를 반복해 누른다면, 찰나의 순간에 좋아요가 중복 생성될 수 있다.
100개의 스레드를 생성해 동시성 테스트를 해보자.
한 명의 유저가 같은 게시글에 대해 좋아요를 100번 누르는 상황이다.
@Test
void 동시에_100개_요청() throws InterruptedException {
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for(int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
try {
likeService.create(1L, 1L);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
Assertions.assertThat(likeRepository.count()).isEqualTo(1);
}
좋아요 데이터가 1개만 생성될 것이라 예상했지만, 아래와 같이 10개의 좋아요가 생성되었다.
문제 원인
상황을 표로 정리해보면 아래와 같다. (두 개 이상의 트랜잭션이 동시에 진행되는 상황)
트랜잭션 격리레벨은 디폴트로 Repeatable Read이다. Tx1은 트랜잭션1, Tx2는 트랜잭션2 이라고 하자.
시퀀스 | Tx1 | Tx2 |
1 | 해당 유저와 게시글에 대한 좋아요가 존재하지 않는가? True | |
2 | 해당 유저와 게시글에 대한 좋아요가 존재하지 않는가? True | |
3 | 좋아요 저장 | |
4 | 좋아요 저장 | |
5 | Tx1 Commit | |
6 | Tx2 Commit |
DB 조회에는 락이 필요없다. SELECT 쿼리를 통해 데이터를 조회할 때, Lock을 얻는 과정이 발생하지 않는다.
따라서 동시에 게시글 좋아요를 누르는 경우 '좋아요 기록이 존재하지 않는가?' 를 확인할 때, 두 트랜잭션에서 모두 True가 나오게 돼 DB에 좋아요가 중복으로 저장되는 것이다.
문제 해결 : UNIQUE 제약조건 사용
UNIQUE 제약조건을 걸면 중복된 값 저장 시 DB에서 에러를 발생시킨다.
userId와 articleId 컬럼에 대한 UNIQUE 제약조건을 설정한 뒤 다시 테스트해보자.
1. JPA를 통해 여러 컬럼에 대한 UNIQUE 제약조건 설정
@Table(name = "likes", uniqueConstraints = {
@UniqueConstraint(name = "UniqueUserAndArticle", columnNames = {"userId", "articleId"})
})
해당 속성은 DDL을 자동으로 생성할 때만 적용된다. 실행 로직에는 아무런 영향을 주지 않는다.
따라서 'spring.jpa.hibernate.ddl-auto=create' 을 사용하지 않는 경우 제약조건이 적용되지 않는다.
2. SQL로 UNIQUE 제약조건 설정
ALTER TABLE likes
ADD CONSTRAINT myUniqueConstraint UNIQUE(userId, articleId);
UNIQUE 제약조건 설정 후 동시성 테스트를 다시 해보았다.
그 결과, 한 개의 좋아요만 생성되었다.
참고
'Computer Science > Database' 카테고리의 다른 글
[DB] Redis 분산락으로 동시성 이슈를 해결해보자 (0) | 2023.02.16 |
---|---|
[DB] UNIQUE 제약 조건 (0) | 2023.02.06 |