Skip to content

Commit fa79785

Browse files
authored
Merge pull request #235 from inha-iesw/feat/#234
feat/#234: 비관적 락을 통한 댓글 관련 좋아요 동시성 해결
2 parents 797cace + fbc1208 commit fa79785

File tree

10 files changed

+246
-236
lines changed

10 files changed

+246
-236
lines changed

src/main/java/inha/git/common/code/status/ErrorStatus.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ public enum ErrorStatus implements BaseErrorCode {
124124
PROJECT_NOT_LIKE(HttpStatus.BAD_REQUEST, "PROJECT4020", "좋아요하지 않은 프로젝트입니다."),
125125
PROJECT_NOT_PUBLIC(HttpStatus.BAD_REQUEST, "PROJECT4021", "비공개 프로젝트입니다."),
126126
ALREADY_RECOMMENDED(HttpStatus.BAD_REQUEST, "PROJECT4022", "이미 추천한 프로젝트입니다."),
127-
128127
TEMPORARY_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "SERVICE4000", "일시적으로 서비스를 이용할 수 없습니다."),
129128

130129
ALREADY_LIKE(HttpStatus.BAD_REQUEST, "PROJECT4017", "이미 좋아요한 댓글입니다."),

src/main/java/inha/git/project/api/service/ProjectCommentServiceImpl.java

Lines changed: 78 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import inha.git.utils.IdempotentProvider;
2424
import lombok.RequiredArgsConstructor;
2525
import lombok.extern.slf4j.Slf4j;
26+
import org.springframework.dao.DataIntegrityViolationException;
27+
import org.springframework.dao.PessimisticLockingFailureException;
2628
import org.springframework.stereotype.Service;
2729
import org.springframework.transaction.annotation.Transactional;
2830

@@ -251,23 +253,17 @@ public ReplyCommentResponse deleteReply(User user, Integer replyCommentIdx) {
251253
*/
252254
@Override
253255
public String projectCommentLike(User user, CommentLikeRequest commentLikeRequest) {
254-
255-
idempotentProvider.isValidIdempotent(List.of("projectCommentLike", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
256-
257-
258-
ProjectComment projectComment = getProjectComment(commentLikeRequest);
259-
Project project = projectComment.getProject();
260-
261-
if (!hasAccessToProject(project, user)) {
262-
throw new BaseException(PROJECT_NOT_PUBLIC);
256+
ProjectComment projectComment = getProjectComment(user, commentLikeRequest);
257+
try {
258+
validLike(projectComment, user, projectCommentLikeJpaRepository.existsByUserAndProjectComment(user, projectComment));
259+
projectCommentLikeJpaRepository.save(projectMapper.createProjectCommentLike(user, projectComment));
260+
projectComment.setLikeCount(projectComment.getLikeCount() + 1);
261+
log.info("프로젝트 댓글 좋아요 완료 - 사용자: {} 프로젝트 댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectComment.getLikeCount());
262+
return commentLikeRequest.idx() + "번 프로젝트 댓글 좋아요 완료";
263+
}catch(DataIntegrityViolationException e) {
264+
log.error("프로젝트 댓글 좋아요 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
265+
throw new BaseException(ALREADY_RECOMMENDED);
263266
}
264-
265-
266-
validLike(projectComment, user, projectCommentLikeJpaRepository.existsByUserAndProjectComment(user, projectComment));
267-
projectCommentLikeJpaRepository.save(projectMapper.createProjectCommentLike(user, projectComment));
268-
projectComment.setLikeCount(projectComment.getLikeCount() + 1);
269-
log.info("프로젝트 댓글 좋아요 완료 - 사용자: {} 프로젝트 댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectComment.getLikeCount());
270-
return commentLikeRequest.idx() + "번 프로젝트 댓글 좋아요 완료";
271267
}
272268

273269
/**
@@ -279,25 +275,20 @@ public String projectCommentLike(User user, CommentLikeRequest commentLikeReques
279275
*/
280276
@Override
281277
public String projectCommentLikeCancel(User user, CommentLikeRequest commentLikeRequest) {
282-
283-
idempotentProvider.isValidIdempotent(List.of("projectCommentLikeCancel", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
284-
285-
286-
ProjectComment projectComment = getProjectComment(commentLikeRequest);
287-
288-
Project project = projectComment.getProject();
289-
290-
if (!hasAccessToProject(project, user)) {
291-
throw new BaseException(PROJECT_NOT_PUBLIC);
278+
ProjectComment projectComment = getProjectComment(user, commentLikeRequest);
279+
try {
280+
validLikeCancel(projectComment, user, projectCommentLikeJpaRepository.existsByUserAndProjectComment(user, projectComment));
281+
projectCommentLikeJpaRepository.deleteByUserAndProjectComment(user, projectComment);
282+
if (projectComment.getLikeCount() <= 0) {
283+
projectComment.setLikeCount(0);
284+
}
285+
projectComment.setLikeCount(projectComment.getLikeCount() - 1);
286+
log.info("프로젝트 댓글 좋아요 취소 완료 - 사용자: {} 프로젝트 댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectComment.getLikeCount());
287+
return commentLikeRequest.idx() + "번 프로젝트 댓글 좋아요 취소 완료";
288+
}catch (DataIntegrityViolationException e) {
289+
log.error("프로젝트 댓글 좋아요 취소 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
290+
throw new BaseException(PROJECT_NOT_LIKE);
292291
}
293-
validLikeCancel(projectComment, user, projectCommentLikeJpaRepository.existsByUserAndProjectComment(user, projectComment));
294-
projectCommentLikeJpaRepository.deleteByUserAndProjectComment(user, projectComment);
295-
if (projectComment.getLikeCount() <= 0) {
296-
projectComment.setLikeCount(0);
297-
}
298-
projectComment.setLikeCount(projectComment.getLikeCount() - 1);
299-
log.info("프로젝트 댓글 좋아요 취소 완료 - 사용자: {} 프로젝트 댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectComment.getLikeCount());
300-
return commentLikeRequest.idx() + "번 프로젝트 댓글 좋아요 취소 완료";
301292
}
302293

303294
/**
@@ -309,22 +300,17 @@ public String projectCommentLikeCancel(User user, CommentLikeRequest commentLike
309300
*/
310301
@Override
311302
public String projectReplyCommentLike(User user, CommentLikeRequest commentLikeRequest) {
312-
idempotentProvider.isValidIdempotent(List.of("projectReplyCommentLike", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
313-
314-
ProjectReplyComment projectReplyComment = projectReplyCommentJpaRepository.findByIdAndState(commentLikeRequest.idx(), ACTIVE)
315-
.orElseThrow(() -> new BaseException(PROJECT_COMMENT_REPLY_NOT_FOUND));
316-
317-
Project project = projectReplyComment.getProjectComment().getProject();
318-
319-
if (!hasAccessToProject(project, user)) {
320-
throw new BaseException(PROJECT_NOT_PUBLIC);
303+
ProjectReplyComment projectReplyComment = getProjectReplyComment(user, commentLikeRequest);
304+
try {
305+
validReplyLike(projectReplyComment, user, projectReplyCommentLikeJpaRepository.existsByUserAndProjectReplyComment(user, projectReplyComment));
306+
projectReplyCommentLikeJpaRepository.save(projectMapper.createProjectReplyCommentLike(user, projectReplyComment));
307+
projectReplyComment.setLikeCount(projectReplyComment.getLikeCount() + 1);
308+
log.info("프로젝트 대댓글 좋아요 완료 - 사용자: {} 프로젝트 대댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectReplyComment.getLikeCount());
309+
return commentLikeRequest.idx() + "번 프로젝트 대댓글 좋아요 완료";
310+
} catch (DataIntegrityViolationException e) {
311+
log.error("프로젝트 대댓글 좋아요 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
312+
throw new BaseException(ALREADY_RECOMMENDED);
321313
}
322-
323-
validReplyLike(projectReplyComment, user, projectReplyCommentLikeJpaRepository.existsByUserAndProjectReplyComment(user, projectReplyComment));
324-
projectReplyCommentLikeJpaRepository.save(projectMapper.createProjectReplyCommentLike(user, projectReplyComment));
325-
projectReplyComment.setLikeCount(projectReplyComment.getLikeCount() + 1);
326-
log.info("프로젝트 대댓글 좋아요 완료 - 사용자: {} 프로젝트 대댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectReplyComment.getLikeCount());
327-
return commentLikeRequest.idx() + "번 프로젝트 대댓글 좋아요 완료";
328314
}
329315

330316
/**
@@ -336,37 +322,22 @@ public String projectReplyCommentLike(User user, CommentLikeRequest commentLikeR
336322
*/
337323
@Override
338324
public String projectReplyCommentLikeCancel(User user, CommentLikeRequest commentLikeRequest) {
339-
idempotentProvider.isValidIdempotent(List.of("projectReplyCommentLikeCancel", user.getId().toString(), user.getName(), commentLikeRequest.idx().toString()));
340-
341-
ProjectReplyComment projectReplyComment = projectReplyCommentJpaRepository.findByIdAndState(commentLikeRequest.idx(), ACTIVE)
342-
.orElseThrow(() -> new BaseException(PROJECT_COMMENT_REPLY_NOT_FOUND));
343-
344-
Project project = projectReplyComment.getProjectComment().getProject();
345-
346-
if (!hasAccessToProject(project, user)) {
347-
throw new BaseException(PROJECT_NOT_PUBLIC);
325+
ProjectReplyComment projectReplyComment = getProjectReplyComment(user, commentLikeRequest);
326+
try{
327+
validReplyLikeCancel(projectReplyComment, user, projectReplyCommentLikeJpaRepository.existsByUserAndProjectReplyComment(user, projectReplyComment));
328+
projectReplyCommentLikeJpaRepository.deleteByUserAndProjectReplyComment(user, projectReplyComment);
329+
if (projectReplyComment.getLikeCount() <= 0) {
330+
projectReplyComment.setLikeCount(0);
331+
}
332+
projectReplyComment.setLikeCount(projectReplyComment.getLikeCount() - 1);
333+
log.info("프로젝트 대댓글 좋아요 취소 완료 - 사용자: {} 프로젝트 대댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectReplyComment.getLikeCount());
334+
return commentLikeRequest.idx() + "번 프로젝트 대댓글 좋아요 취소 완료";
335+
} catch (DataIntegrityViolationException e) {
336+
log.error("프로젝트 대댓글 좋아요 취소 중복 발생 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
337+
throw new BaseException(PROJECT_NOT_LIKE);
348338
}
349-
350-
validReplyLikeCancel(projectReplyComment, user, projectReplyCommentLikeJpaRepository.existsByUserAndProjectReplyComment(user, projectReplyComment));
351-
projectReplyCommentLikeJpaRepository.deleteByUserAndProjectReplyComment(user, projectReplyComment);
352-
if (projectReplyComment.getLikeCount() <= 0) {
353-
projectReplyComment.setLikeCount(0);
354-
}
355-
projectReplyComment.setLikeCount(projectReplyComment.getLikeCount() - 1);
356-
log.info("프로젝트 대댓글 좋아요 취소 완료 - 사용자: {} 프로젝트 대댓글 식별자: {} 좋아요 개수: {}", user.getName(), commentLikeRequest.idx(), projectReplyComment.getLikeCount());
357-
return commentLikeRequest.idx() + "번 프로젝트 대댓글 좋아요 취소 완료";
358339
}
359340

360-
361-
362-
363-
/**
364-
* 댓글 좋아요 정보 유효성 검사
365-
*
366-
* @param projectComment 댓글 정보
367-
* @param user 사용자 정보
368-
* @param commentLikeJpaRepository 댓글 좋아요 레포지토리
369-
*/
370341
private void validLike(ProjectComment projectComment, User user, boolean commentLikeJpaRepository) {
371342
if (projectComment.getUser().getId().equals(user.getId())) {
372343
log.error("프로젝트 댓글 좋아요 실패 - 사용자: {} 자신의 댓글에 좋아요를 할 수 없습니다.", user.getName());
@@ -378,13 +349,6 @@ private void validLike(ProjectComment projectComment, User user, boolean comment
378349
}
379350
}
380351

381-
/**
382-
* 대댓글 좋아요 정보 유효성 검사
383-
*
384-
* @param projectReplyComment 대댓글 정보
385-
* @param user 사용자 정보
386-
* @param commentLikeJpaRepository 대댓글 좋아요 레포지토리
387-
*/
388352
private void validReplyLike(ProjectReplyComment projectReplyComment, User user, boolean commentLikeJpaRepository) {
389353
if (projectReplyComment.getUser().getId().equals(user.getId())) {
390354
log.error("프로젝트 대댓글 좋아요 실패 - 사용자: {} 자신의 대댓글에 좋아요를 할 수 없습니다.", user.getName());
@@ -396,13 +360,6 @@ private void validReplyLike(ProjectReplyComment projectReplyComment, User user,
396360
}
397361
}
398362

399-
/**
400-
* 댓글 좋아요 취소
401-
*
402-
* @param user 사용자 정보
403-
* @param projectComment 좋아요 취소할 댓글 정보
404-
* @param commentLikeJpaRepository 댓글 좋아요 레포지토리
405-
*/
406363
private void validLikeCancel(ProjectComment projectComment, User user, boolean commentLikeJpaRepository) {
407364
if (projectComment.getUser().getId().equals(user.getId())) {
408365
log.error("프로젝트 댓글 좋아요 취소 실패 - 사용자: {} 자신의 댓글에 좋아요를 취소할 수 없습니다.", user.getName());
@@ -425,14 +382,35 @@ private void validReplyLikeCancel(ProjectReplyComment projectReplyComment, User
425382
}
426383

427384
}
428-
/**
429-
* 댓글 좋아요 정보 조회
430-
*
431-
* @param commentLikeRequest 댓글 좋아요 정보
432-
* @return 댓글 좋아요 정보
433-
*/
434-
private ProjectComment getProjectComment(CommentLikeRequest commentLikeRequest) {
435-
return projectCommentJpaRepository.findByIdAndState(commentLikeRequest.idx(), ACTIVE)
436-
.orElseThrow(() -> new BaseException(PROJECT_NOT_FOUND));
385+
386+
private ProjectComment getProjectComment(User user, CommentLikeRequest commentLikeRequest) {
387+
ProjectComment projectComment;
388+
try {
389+
projectComment = projectCommentJpaRepository.findByIdAndStateWithPessimisticLock(commentLikeRequest.idx(), ACTIVE)
390+
.orElseThrow(() -> new BaseException(PROJECT_COMMENT_NOT_FOUND));
391+
} catch (PessimisticLockingFailureException e) {
392+
log.error("프로젝트 댓글 추천 락 획득 실패 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
393+
throw new BaseException(TEMPORARY_UNAVAILABLE);
394+
}
395+
396+
if (!hasAccessToProject(projectComment.getProject(), user)) {
397+
throw new BaseException(PROJECT_NOT_PUBLIC);
398+
}
399+
return projectComment;
400+
}
401+
402+
private ProjectReplyComment getProjectReplyComment(User user, CommentLikeRequest commentLikeRequest) {
403+
ProjectReplyComment projectReplyComment;
404+
try {
405+
projectReplyComment = projectReplyCommentJpaRepository.findByIdAndStateWithPessimisticLock(commentLikeRequest.idx(), ACTIVE)
406+
.orElseThrow(() -> new BaseException(PROJECT_COMMENT_REPLY_NOT_FOUND));
407+
} catch (PessimisticLockingFailureException e) {
408+
log.error("프로젝트 대댓글 추천 락 획득 실패 - 사용자: {}, 프로젝트 ID: {}", user.getName(), commentLikeRequest.idx());
409+
throw new BaseException(TEMPORARY_UNAVAILABLE);
410+
}
411+
if (!hasAccessToProject(projectReplyComment.getProjectComment().getProject(), user)) {
412+
throw new BaseException(PROJECT_NOT_PUBLIC);
413+
}
414+
return projectReplyComment;
437415
}
438416
}

0 commit comments

Comments
 (0)