Skip to content

Commit 57f5ce8

Browse files
authored
Merge pull request #322 from prgrms-web-devcourse-final-project/refactor/EA3-211-study-currentcount
[EA3-211] refactor: 스터디 인원수 증감 로직 수정
2 parents 14f2600 + 5e5f476 commit 57f5ce8

File tree

11 files changed

+232
-102
lines changed

11 files changed

+232
-102
lines changed

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ dependencies {
9797

9898
// s3
9999
implementation 'software.amazon.awssdk:s3:2.25.61'
100+
101+
// aop
102+
implementation 'org.springframework.retry:spring-retry'
103+
implementation 'org.springframework.boot:spring-boot-starter-aop'
100104
}
101105

102106
tasks.named('test') {

src/main/java/grep/neogulcoder/NeogulCoderApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import org.springframework.boot.SpringApplication;
77
import org.springframework.boot.autoconfigure.SpringBootApplication;
88
import org.springframework.scheduling.annotation.EnableAsync;
9+
import org.springframework.retry.annotation.EnableRetry;
910
import org.springframework.scheduling.annotation.EnableScheduling;
1011

1112
@EnableAsync
1213
@EnableScheduling
14+
@EnableRetry
1315
@SpringBootApplication
1416
public class NeogulCoderApplication {
1517

src/main/java/grep/neogulcoder/domain/alram/controller/AlarmController.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44
import grep.neogulcoder.domain.alram.service.AlarmService;
55
import grep.neogulcoder.global.auth.Principal;
66
import grep.neogulcoder.global.response.ApiResponse;
7-
import java.util.List;
87
import lombok.RequiredArgsConstructor;
98
import org.springframework.http.ResponseEntity;
109
import org.springframework.security.core.annotation.AuthenticationPrincipal;
11-
import org.springframework.web.bind.annotation.GetMapping;
12-
import org.springframework.web.bind.annotation.PathVariable;
13-
import org.springframework.web.bind.annotation.PostMapping;
14-
import org.springframework.web.bind.annotation.RequestMapping;
15-
import org.springframework.web.bind.annotation.RestController;
10+
import org.springframework.web.bind.annotation.*;
11+
12+
import java.util.List;
1613

1714
@RestController
1815
@RequiredArgsConstructor

src/main/java/grep/neogulcoder/domain/study/controller/StudyManagementController.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import grep.neogulcoder.domain.study.controller.dto.response.ExtendParticipationResponse;
66
import grep.neogulcoder.domain.study.controller.dto.response.StudyExtensionResponse;
77
import grep.neogulcoder.domain.study.service.StudyManagementService;
8+
import grep.neogulcoder.domain.study.service.StudyManagementServiceFacade;
89
import grep.neogulcoder.global.auth.Principal;
910
import grep.neogulcoder.global.response.ApiResponse;
1011
import jakarta.validation.Valid;
@@ -21,6 +22,7 @@
2122
public class StudyManagementController implements StudyManagementSpecification {
2223

2324
private final StudyManagementService studyManagementService;
25+
private final StudyManagementServiceFacade studyManagementServiceFacade;
2426

2527
@GetMapping("/extension")
2628
public ResponseEntity<ApiResponse<StudyExtensionResponse>> getStudyExtension(@PathVariable("studyId") Long studyId) {
@@ -37,7 +39,7 @@ public ResponseEntity<ApiResponse<List<ExtendParticipationResponse>>> getExtendP
3739
@DeleteMapping("/me")
3840
public ResponseEntity<ApiResponse<Void>> leaveStudy(@PathVariable("studyId") Long studyId,
3941
@AuthenticationPrincipal Principal userDetails) {
40-
studyManagementService.leaveStudy(studyId, userDetails.getUserId());
42+
studyManagementServiceFacade.leaveStudyWithRetry(studyId, userDetails.getUserId());
4143
return ResponseEntity.ok(ApiResponse.noContent());
4244
}
4345

@@ -60,7 +62,7 @@ public ResponseEntity<ApiResponse<Long>> extendStudy(@PathVariable("studyId") Lo
6062
@PostMapping("/extension/participations")
6163
public ResponseEntity<ApiResponse<Void>> registerExtensionParticipation(@PathVariable("studyId") Long studyId,
6264
@AuthenticationPrincipal Principal userDetails) {
63-
studyManagementService.registerExtensionParticipation(studyId, userDetails.getUserId());
65+
studyManagementServiceFacade.registerExtensionParticipationWithRetry(studyId, userDetails.getUserId());
6466
return ResponseEntity.ok(ApiResponse.noContent());
6567
}
6668

src/main/java/grep/neogulcoder/domain/study/repository/StudyRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package grep.neogulcoder.domain.study.repository;
22

33
import grep.neogulcoder.domain.study.Study;
4+
import jakarta.persistence.LockModeType;
45
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Lock;
57
import org.springframework.data.jpa.repository.Modifying;
68
import org.springframework.data.jpa.repository.Query;
79
import org.springframework.data.repository.query.Param;
@@ -28,4 +30,8 @@ public interface StudyRepository extends JpaRepository<Study, Long> {
2830
List<Study> findStudiesToBeFinished(@Param("now") LocalDateTime now);
2931

3032
int countByUserIdAndActivatedTrueAndFinishedFalse(Long userId);
33+
34+
@Lock(LockModeType.OPTIMISTIC)
35+
@Query("SELECT s FROM Study s WHERE s.id = :id")
36+
Optional<Study> findByIdWithLock(@Param("id") Long id);
3137
}

src/main/java/grep/neogulcoder/domain/study/service/StudyManagementService.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ public class StudyManagementService {
4141
private final StudyMemberQueryRepository studyMemberQueryRepository;
4242
private final UserRepository userRepository;
4343
private final ApplicationEventPublisher eventPublisher;
44-
private final StudyManagementServiceFacade studyManagementServiceFacade;
4544
private final RecruitmentPostRepository recruitmentPostRepository;
4645

4746
public StudyExtensionResponse getStudyExtension(Long studyId) {
@@ -59,7 +58,8 @@ public List<ExtendParticipationResponse> getExtendParticipations(Long studyId) {
5958

6059
@Transactional
6160
public void leaveStudy(Long studyId, Long userId) {
62-
Study study = getStudyById(studyId);
61+
Study study = studyRepository.findByIdWithLock(studyId)
62+
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
6363
StudyMember studyMember = getStudyMemberById(studyId, userId);
6464

6565
if (isLastMember(study)) {
@@ -74,7 +74,7 @@ public void leaveStudy(Long studyId, Long userId) {
7474
}
7575

7676
studyMember.delete();
77-
studyManagementServiceFacade.decreaseMemberCount(studyId, userId);
77+
study.decreaseMemberCount();
7878
}
7979

8080
@Transactional
@@ -112,7 +112,7 @@ public void deleteUserFromStudies(Long userId) {
112112
}
113113

114114
myMember.delete();
115-
studyManagementServiceFacade.decreaseMemberCount(study.getId(), userId);
115+
study.decreaseMemberCount();
116116
}
117117
}
118118

@@ -140,7 +140,8 @@ public Long extendStudy(Long studyId, ExtendStudyRequest request, Long userId) {
140140

141141
@Transactional
142142
public void registerExtensionParticipation(Long studyId, Long userId) {
143-
getStudyById(studyId);
143+
Study study = studyRepository.findByIdWithLock(studyId)
144+
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
144145
StudyMember studyMember = getStudyMemberById(studyId, userId);
145146

146147
if (studyMember.isParticipated()) {
@@ -154,7 +155,7 @@ public void registerExtensionParticipation(Long studyId, Long userId) {
154155

155156
StudyMember extendMember = StudyMember.createMember(extendedStudy, userId);
156157
studyMemberRepository.save(extendMember);
157-
studyManagementServiceFacade.increaseMemberCount(studyId, userId);
158+
study.decreaseMemberCount();
158159
}
159160

160161
@Transactional
@@ -175,6 +176,14 @@ public void reactiveStudy(Long studyId) {
175176
study.reactive();
176177
}
177178

179+
@Transactional
180+
public void increaseMemberCount(Long studyId, Long userId) {
181+
Study study = studyRepository.findByIdWithLock(studyId)
182+
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
183+
184+
study.increaseMemberCount();
185+
}
186+
178187
private Study getStudyById(Long studyId) {
179188
return studyRepository.findById(studyId)
180189
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,70 @@
11
package grep.neogulcoder.domain.study.service;
22

3-
import grep.neogulcoder.domain.study.Study;
4-
import grep.neogulcoder.domain.study.repository.StudyRepository;
53
import grep.neogulcoder.global.exception.business.BusinessException;
6-
import grep.neogulcoder.global.exception.business.NotFoundException;
74
import jakarta.persistence.OptimisticLockException;
85
import lombok.RequiredArgsConstructor;
96
import lombok.extern.slf4j.Slf4j;
107
import org.springframework.orm.ObjectOptimisticLockingFailureException;
8+
import org.springframework.retry.annotation.Backoff;
9+
import org.springframework.retry.annotation.Recover;
10+
import org.springframework.retry.annotation.Retryable;
1111
import org.springframework.stereotype.Service;
12-
import org.springframework.transaction.annotation.Transactional;
1312

14-
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.*;
13+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_MEMBER_COUNT_UPDATE_FAILED;
1514

15+
@Slf4j
1616
@Service
1717
@RequiredArgsConstructor
1818
public class StudyManagementServiceFacade {
1919

20-
private final StudyMemberCountService studyMemberCountService;
20+
private final StudyManagementService studyManagementService;
2121

22+
@Retryable(
23+
retryFor = {OptimisticLockException.class, ObjectOptimisticLockingFailureException.class},
24+
maxAttempts = 3,
25+
backoff = @Backoff(delay = 50)
26+
)
2227
public void increaseMemberCount(Long studyId, Long userId) {
23-
studyMemberCountService.increaseMemberCount(studyId, userId);
28+
studyManagementService.increaseMemberCount(studyId, userId);
2429
}
2530

26-
public void decreaseMemberCount(Long studyId, Long userId) {
27-
studyMemberCountService.decreaseMemberCount(studyId, userId);
31+
@Retryable(
32+
retryFor = {OptimisticLockException.class, ObjectOptimisticLockingFailureException.class},
33+
maxAttempts = 3,
34+
backoff = @Backoff(delay = 50)
35+
)
36+
public void leaveStudyWithRetry(Long studyId, Long userId) {
37+
studyManagementService.leaveStudy(studyId, userId);
38+
System.out.println("leaveStudyWithRetry 실행 (studyId={}, userId={})");
39+
}
40+
41+
@Retryable(
42+
retryFor = {OptimisticLockException.class, ObjectOptimisticLockingFailureException.class},
43+
maxAttempts = 3,
44+
backoff = @Backoff(delay = 50)
45+
)
46+
public void deleteUserFromStudiesWithRetry(Long userId) {
47+
studyManagementService.deleteUserFromStudies(userId);
48+
}
49+
50+
@Retryable(
51+
retryFor = {OptimisticLockException.class, ObjectOptimisticLockingFailureException.class},
52+
maxAttempts = 3,
53+
backoff = @Backoff(delay = 50)
54+
)
55+
public void registerExtensionParticipationWithRetry(Long studyId, Long userId) {
56+
studyManagementService.registerExtensionParticipation(studyId, userId);
57+
}
58+
59+
@Recover
60+
public void recover(OptimisticLockException e, Long studyId, Long userId) {
61+
log.warn("스터디 currentCount 증감 실패 (studyId={}, userId={})", studyId, userId, e);
62+
throw new BusinessException(STUDY_MEMBER_COUNT_UPDATE_FAILED);
63+
}
64+
65+
@Recover
66+
public void recover(ObjectOptimisticLockingFailureException e, Long studyId, Long userId) {
67+
log.warn("스터디 currentCount 증감 실패 (studyId={}, userId={})", studyId, userId, e);
68+
throw new BusinessException(STUDY_MEMBER_COUNT_UPDATE_FAILED);
2869
}
2970
}

src/main/java/grep/neogulcoder/domain/study/service/StudyMemberCountService.java

Lines changed: 0 additions & 77 deletions
This file was deleted.

src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public void approveApplication(Long applicationId, Long userId) {
9595

9696
StudyMember studyMember = StudyMember.createMember(study, application.getUserId());
9797
studyMemberRepository.save(studyMember);
98-
studyManagementServiceFacade.increaseMemberCount(study.getId(), userId);
98+
studyManagementServiceFacade.increaseMemberCount(study.getId(), application.getUserId());
9999

100100
eventPublisher.publishEvent(new ApplicationStatusChangedEvent(applicationId, AlarmType.STUDY_APPLICATION_APPROVED));
101101
}

src/main/java/grep/neogulcoder/domain/users/service/UserService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import grep.neogulcoder.domain.prtemplate.service.LinkService;
99
import grep.neogulcoder.domain.prtemplate.service.PrTemplateService;
1010
import grep.neogulcoder.domain.study.service.StudyManagementService;
11+
import grep.neogulcoder.domain.study.service.StudyManagementServiceFacade;
1112
import grep.neogulcoder.domain.users.controller.dto.request.SignUpRequest;
1213
import grep.neogulcoder.domain.users.controller.dto.response.AllUserResponse;
1314
import grep.neogulcoder.domain.users.controller.dto.response.UserResponse;
@@ -42,6 +43,8 @@ public class UserService {
4243
private final LinkService linkService;
4344
private final StudyManagementService studyManagementService;
4445
private final BuddyEnergyService buddyEnergyService;
46+
private final EmailVerificationService verificationService;
47+
private final StudyManagementServiceFacade studyManagementServiceFacade;
4548

4649
@Transactional(readOnly = true)
4750
public User get(Long id) {
@@ -112,7 +115,7 @@ public void deleteUser(Long userId, String password, String passwordCheck) {
112115
throw new PasswordNotMatchException(UserErrorCode.PASSWORD_UNCHECKED);
113116
}
114117

115-
studyManagementService.deleteUserFromStudies(userId);
118+
studyManagementServiceFacade.deleteUserFromStudiesWithRetry(userId);
116119
prTemplateService.deleteByUserId(user.getId());
117120
linkService.deleteByUserId(userId);
118121

0 commit comments

Comments
 (0)