개발자
안녕하세요 글쓰기 커뮤니티 프로젝트를 개발하는 중입니다 회원이 소설을 찜하는 기능(좋아요)을 구현 중에 있는데요 찜 객체는 아래 첨부한 코드와 같이 memberId와 novelId를 외래키로 갖습니다 동시성 이슈를 다뤄보고자 500명의 사용자가 동시에 novelId가 1인 소설을 찜하는 요청을 보낸다는 가정 하에 jmeter로 테스트를 돌려보니 동시성 이슈가 아니라 데드락 문제가 발생해서요 favoriteJDBCRepository.save(memberId, novelId); 시 novelId가 외래키라 외래키 정합성 검사 때문에 db 내부적으로 novelId가 1인 레코드에 read lock이 걸리는 것 같습니다 favorite 객체 save 시 트랜잭션 A가 novelId가 1인 레코드에 대해 read lock을 얻고, novelId가 3인 novel의 favoriteCount(찜 개수)를 +1하고자 write lock 요청 -> favorite 객체 save 시 다른 트랜잭션 B도 novelId가 1인 레코드에 대해 readlock을 얻고, novelId가 1인 novel의 favoriteCount(찜 개수)를 +1하고자 write lock 요청 -> write lock은 read lock이 전부 해제되어야 얻을 수 있음. 트랜잭션 A는 B가 read lock을 release하기를 대기하고 B도 A가 read lock을 release하기를 대기하면서 데드락 발생 일단 novelId로 novel을 비관적 락을 사용해 조회해서 favorite 객체를 생성한 뒤에 저장하는 방식으로 데드락+동시성 이슈 둘 다 해결이 되는데요(chooseFavoriteV2) 이 방법 말고도 더 좋은 대안은 없을까요??
1/ **
2찜 엔티티
3**/
4
5
6@Entity
7public class Favorite {
8
9 @Id
10 @GeneratedValue(strategy = GenerationType.IDENTITY)
11 private Long favoriteId;
12
13 @ManyToOne(fetch = FetchType.LAZY)
14 @JoinColumn(name = "member_id")
15 @NotNull
16 private Member member;
17
18 @ManyToOne(fetch = FetchType.LAZY)
19 @JoinColumn(name = "novel_id")
20 @NotNull
21 private Novel novel;
22}
23
24/////////////////////////////////////////////////////////////////////////////////////////////////////////
25
26/ **
27FavoriteService 에 찜 기능 구현
28**/
29
30@Transactional
31 public FavoriteResponseDTO chooseFavorite(Long novelId, Long memberId) {
32 favoriteJDBCRepository.save(memberId, novelId);
33 novelRepository.updateFavoriteCount(novelId, +1L);
34
35 return FavoriteResponseDTO
36 .builder()
37 ....
38
39 }
40
41/////////////////////////////////////////////////////////////////////////////////////////////////////////
42
43/ **
44favoriteJDBCRepository.save(memberId, novelId)
45**/
46
47public int save(Long memberId, Long novelId) {
48 String query = "INSERT INTO favorite (member_id, novelId) VALUES (?, ?)";
49 return jdbcTemplate.update(query, memberId, novelId);
50 }
51
52
53/////////////////////////////////////////////////////////////////////////////////////////////////////////
54
55/ **
56chooseFavoriteV2
57**/
58
59@Transactional
60 public FavoriteResponseDTO chooseFavoriteV2(Long novelId, Long memberId) {
61 Novel foundNovel = novelRepository.findByIdWithPessimisticLock(novelId).orElseThrow(...);
62 Member foundMember = memberRepository.findById(memberId).orElseThrow(...);
63
64 Favorite favorite = Favorite.builder()
65 .member(foundMember)
66 .novel(foundNovel)
67 .build();
68
69 favoriteRepository.save(favorite);
70 novelRepository.updateFavoriteCount(novelId, +1L);
71
72 return FavoriteResponseDTO
73 .builder()
74 ....
75
76 }
답변 0
첫 답변을 기다리고 있어요.
지금 가입하면 모든 질문의 답변을 볼 수 있어요!
현직자들의 명쾌한 답변을 얻을 수 있어요.
이미 회원이신가요?
커리어리 AI 봇
GPT가 달아주는 답변입니다.
2024년 09월 06일
먼저, 질문에 대해 감사드립니다. 분명히 잘 이해한 것처럼 비관적 락을 사용하여 동시성 문제와 데드락을 해결하였습니다. `chooseFavoriteV2` 메서드를 통해 동일한 작업의 다른 인스턴스가 해당 레코드를 변경하지 못하도록 데이터베이스 수준에서 락을 걸고 있습니다. 다만, 비관적인 잠금은 동시성을 보장하는 방법입니다만, 시스템의 성능에 영향을 줄 수 있습니다. 따라서 다양한 요구 사항과 상황에 따라 적절한 방식을 선택하는 것이 중요합니다. 추가적으로 고려해보면 아래와 같은 방법들이 있습니다: 1. **낙관적 잠금(Optimistic Locking) 사용:** JPA는 버전(Version) 필드를 이용한 낙관적 잠금(Optimistic Locking) 기능을 제공합니다. 엔티티를 조회할 때는 잠금을 걸지 않고, 실제로 업데이트 하는 순간에 Version 값이 다르면 예외를 발생시켜 충돌이 일어나지 않게 합니다. 2. **비동기 처리:** 찜 개수 증가와 같은 카운터 업데이트를 별도의 비동기 프로세스로 처리하면 데드락 문제를 피할 수 있습니다. 예를 들어, 카프카(Kafka)와 같은 메시징 시스템을 사용하여 이벤트를 보내고, 별도의 서비스 또는 스레드가 이를 소비하여 DB에 반영하는 방식입니다. 3. **Sharding:** 각각의 작업이 다른 데이터베이스 인스턴스에서 실행되도록 하는 데이터 샤딩이 유용할 수 있습니다. 하지만 이 방법은 구현이 상당히 복잡하고 대규모 애플리케이션에 주로 적용됩니다. 4. **Rate limiting:** 사용자 요청에 대해 rate limiting을 적용하여 초당 처리할 수 있는 요청의 수를 제한하면 동시성 문제를 완화할 수 있습니다. 다양한 선택지 중에서 가장 최적의 해결책을 선택하기 위해서는 여러 가지 요소들을 고려해야 합니다: 현재 시스템의 규모, 서버의 자원, 개발 및 유지 관리 편의성 등입니다.
지금 가입하면 모든 질문의 답변을 볼 수 있어요!