티스토리 뷰

문제 상황

동아리 면접 중, 동시성 문제와 관련해서 내 로직에서는 연관 엔티티의 아이디가 중복 생성 가능해보인다.

어떻게 하면 중복 생성을 막을 수 있을까? 라는 질문을 받았다.

 

정확히는 모르겠지만, 낙관적 락을 사용하면 해결할 수 있을거 같다 답했다.

 

면접관은 낙관적 락을 사용해서도 해결할 수 있지만, 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 제약조건 설정 후 동시성 테스트를 다시 해보았다.

그 결과, 한 개의 좋아요만 생성되었다.

 

 

참고

https://crazy-horse.tistory.com/entry/%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95

 

 

https://crazy-horse.tistory.com/entry/%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
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
글 보관함