Skip to content

Commit 95cef65

Browse files
authored
Merge pull request #133 from Dugout-Developers/fix/#132
[FIX] 직관 신청시 발생하는 동시성 이슈 해결
2 parents ca6fa21 + dd9491d commit 95cef65

File tree

6 files changed

+44
-12
lines changed

6 files changed

+44
-12
lines changed

src/main/java/com/back/catchmate/domain/board/repository/BoardRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.back.catchmate.domain.board.repository;
22

33
import com.back.catchmate.domain.board.entity.Board;
4+
import jakarta.persistence.LockModeType;
45
import org.springframework.data.domain.Page;
56
import org.springframework.data.domain.Pageable;
67
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Lock;
79
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
811

912
import java.util.Optional;
1013

@@ -20,4 +23,8 @@ public interface BoardRepository extends JpaRepository<Board, Long>, BoardReposi
2023
Optional<Board> findTopByUserIdAndIsCompletedIsFalseAndDeletedAtIsNullOrderByCreatedAtDesc(Long userId);
2124

2225
long countByDeletedAtIsNullAndIsCompletedIsTrue();
26+
27+
@Lock(LockModeType.PESSIMISTIC_WRITE)
28+
@Query("SELECT b FROM Board b WHERE b.id = :boardId")
29+
Optional<Board> findByIdWithLock(@Param("boardId") Long boardId);
2330
}

src/main/java/com/back/catchmate/domain/chat/repository/ChatRoomRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.back.catchmate.domain.chat.repository;
22

33
import com.back.catchmate.domain.chat.entity.ChatRoom;
4+
import jakarta.persistence.LockModeType;
45
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Lock;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
59

610
import java.util.Optional;
711

@@ -10,5 +14,9 @@ public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long> {
1014

1115
Optional<ChatRoom> findByBoardIdAndDeletedAtIsNull(Long boardId);
1216

17+
@Lock(LockModeType.PESSIMISTIC_WRITE)
18+
@Query("SELECT c FROM ChatRoom c WHERE c.board.id = :boardId AND c.deletedAt IS NULL")
19+
Optional<ChatRoom> findByBoardIdWithLock(@Param("boardId") Long boardId);
20+
1321
boolean existsByBoardId(Long boardId);
1422
}

src/main/java/com/back/catchmate/domain/enroll/repository/EnrollRepository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package com.back.catchmate.domain.enroll.repository;
22

3+
import com.back.catchmate.domain.enroll.entity.AcceptStatus;
34
import com.back.catchmate.domain.enroll.entity.Enroll;
5+
import jakarta.persistence.LockModeType;
46
import org.springframework.data.domain.Page;
57
import org.springframework.data.domain.Pageable;
68
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Lock;
10+
import org.springframework.data.jpa.repository.Modifying;
711
import org.springframework.data.jpa.repository.Query;
812
import org.springframework.data.repository.query.Param;
913

@@ -33,4 +37,12 @@ public interface EnrollRepository extends JpaRepository<Enroll, Long> {
3337
int countNewEnrollListByUserId(@Param("userId") Long userId);
3438

3539
boolean existsByUserIdAndBoardIdAndDeletedAtIsNull(Long userId, Long boardId);
40+
41+
@Modifying
42+
@Query("UPDATE Enroll e SET e.acceptStatus = :status WHERE e.id = :enrollId AND e.acceptStatus = 'PENDING'")
43+
int updateEnrollStatus(@Param("enrollId") Long enrollId, @Param("status") AcceptStatus status);
44+
45+
@Lock(LockModeType.PESSIMISTIC_WRITE)
46+
@Query("SELECT e FROM Enroll e WHERE e.id = :enrollId")
47+
Optional<Enroll> findByIdWithLock(@Param("enrollId") Long enrollId);
3648
}

src/main/java/com/back/catchmate/domain/enroll/service/EnrollServiceImpl.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,39 +163,44 @@ public UpdateEnrollInfo acceptEnroll(Long enrollId, Long userId) throws IOExcept
163163
User loginUser = userRepository.findById(userId)
164164
.orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND));
165165

166-
Enroll enroll = enrollRepository.findById(enrollId)
166+
Enroll enroll = enrollRepository.findByIdWithLock(enrollId)
167167
.orElseThrow(() -> new BaseException(ErrorCode.ENROLL_NOT_FOUND));
168168

169169
User boardWriter = enroll.getBoard().getUser();
170170
User enrollApplicant = userRepository.findById(enroll.getUser().getId())
171171
.orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND));
172172

173-
Board board = enroll.getBoard();
173+
Board board = boardRepository.findByIdWithLock(enroll.getBoard().getId())
174+
.orElseThrow(() -> new BaseException(ErrorCode.BOARD_NOT_FOUND));
175+
174176
if (board.canIncrementCurrentPerson()) {
175177
board.incrementCurrentPerson();
178+
} else {
179+
throw new BaseException(ErrorCode.FULL_PERSON);
176180
}
177181

178-
// 게시글 작성자와 로그인한 사용자가 다를 경우 예외 발생
179182
if (loginUser.isDifferentUserFrom(boardWriter)) {
180183
throw new BaseException(ErrorCode.ENROLL_ACCEPT_INVALID);
181184
}
182185

186+
int updatedRows = enrollRepository.updateEnrollStatus(enrollId, AcceptStatus.ACCEPTED);
187+
if (updatedRows == 0) {
188+
throw new BaseException(ErrorCode.ENROLL_ALREADY_ACCEPTED);
189+
}
190+
183191
enterChatRoom(enrollApplicant, board);
184192

185193
String title = ENROLLMENT_ACCEPT_TITLE;
186194
String body = ENROLLMENT_ACCEPT_BODY;
187195

188-
// 직관 신청자에게 수락 푸시 알림 메세지 전송
189196
fcmService.sendMessageByToken(enrollApplicant.getFcmToken(), title, body, enroll.getBoard().getId(), AcceptStatus.ACCEPTED);
190-
// 데이터베이스에 저장
191197
notificationService.createNotification(title, body, boardWriter.getProfileImageUrl(), enroll.getBoard().getId(), enrollApplicant.getId(), AcceptStatus.ACCEPTED);
192198

193-
enroll.respondToEnroll(AcceptStatus.ACCEPTED);
194199
return enrollConverter.toUpdateEnrollInfo(enroll, AcceptStatus.ACCEPTED);
195200
}
196201

197202
private void enterChatRoom(User user, Board board) {
198-
ChatRoom chatRoom = chatRoomRepository.findByBoardIdAndDeletedAtIsNull(board.getId())
203+
ChatRoom chatRoom = chatRoomRepository.findByBoardIdWithLock(board.getId())
199204
.orElseThrow(() -> new BaseException(ErrorCode.CHATROOM_NOT_FOUND));
200205

201206
chatRoom.incrementParticipantCount();
@@ -213,30 +218,28 @@ public UpdateEnrollInfo rejectEnroll(Long enrollId, Long userId) throws IOExcept
213218
User loginUser = userRepository.findById(userId)
214219
.orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND));
215220

216-
Enroll enroll = enrollRepository.findById(enrollId)
221+
Enroll enroll = enrollRepository.findByIdWithLock(enrollId)
217222
.orElseThrow(() -> new BaseException(ErrorCode.ENROLL_NOT_FOUND));
218223

219224
User boardWriter = enroll.getBoard().getUser();
220225
User enrollApplicant = userRepository.findById(enroll.getUser().getId())
221226
.orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND));
222227

223-
// 게시글 작성자와 로그인한 사용자가 다를 경우 예외 발생
224228
if (loginUser.isDifferentUserFrom(boardWriter)) {
225229
throw new BaseException(ErrorCode.ENROLL_REJECT_INVALID);
226230
}
227231

228232
String title = ENROLLMENT_REJECT_TITLE;
229233
String body = ENROLLMENT_REJECT_BODY;
230234

231-
// 직관 신청자에게 거절 푸시 알림 메세지 전송
232235
fcmService.sendMessageByToken(enrollApplicant.getFcmToken(), title, body, enroll.getBoard().getId(), AcceptStatus.REJECTED);
233-
// 데이터베이스에 저장
234236
notificationService.createNotification(title, body, boardWriter.getProfileImageUrl(), enroll.getBoard().getId(), enrollApplicant.getId(), AcceptStatus.REJECTED);
235237

236238
enroll.respondToEnroll(AcceptStatus.REJECTED);
237239
return enrollConverter.toUpdateEnrollInfo(enroll, AcceptStatus.REJECTED);
238240
}
239241

242+
240243
@Override
241244
public EnrollDescriptionInfo getEnrollDescriptionById(Long boardId, Long userId) {
242245
User user = userRepository.findById(userId)

src/main/java/com/back/catchmate/domain/user/entity/BlockedUser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
@Builder
2121
@NoArgsConstructor(access = AccessLevel.PROTECTED)
2222
@AllArgsConstructor
23-
@Table(name = "blocked_users")
23+
@Table(name = "blocked_user")
2424
public class BlockedUser extends BaseTimeEntity {
2525
@Id
2626
@GeneratedValue(strategy = GenerationType.IDENTITY)

src/main/java/com/back/catchmate/global/error/ErrorCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public enum ErrorCode {
2727
ENROLL_ACCEPT_INVALID(HttpStatus.BAD_REQUEST, "직관 신청을 수락할 권한이 없습니다."),
2828
ENROLL_REJECT_INVALID(HttpStatus.BAD_REQUEST, "직관 신청을 거절할 권한이 없습니다."),
2929
ENROLL_GET_INVALID(HttpStatus.BAD_REQUEST, "직관 신청을 조회할 권한이 없습니다."),
30+
ENROLL_ALREADY_ACCEPTED(HttpStatus.BAD_REQUEST, "이미 수락된 신청입니다."),
3031

3132
// 게시글
3233
BOARD_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 게시글입니다."),
@@ -42,6 +43,7 @@ public enum ErrorCode {
4243
ALREADY_BOOKMARK(HttpStatus.BAD_REQUEST, "이미 찜한 게시글입니다."),
4344
BOOKMARK_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 찜입니다."),
4445
BOOKMARK_BAD_REQUEST(HttpStatus.BAD_REQUEST, "본인 게시글은 찜할 수 없습니다."),
46+
FULL_PERSON(HttpStatus.BAD_REQUEST, "해당 게시글은 마감되었습니다."),
4547

4648
// 알림
4749
NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 알림입니다."),

0 commit comments

Comments
 (0)