Skip to content

Commit 646339c

Browse files
authored
Merge pull request #131 from TeamPINGLE/feat/130
[feat] 분산락을 통한 참여 로직 동시성 제어
2 parents 1989924 + d71aa3c commit 646339c

20 files changed

+542
-20
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ application-secret.properties
4040
application-local.yml
4141
application-dev.yml
4242
application-prod.yml
43+
application-test.yml
4344

4445
### Java ###
4546
# Compiled class file
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.pingle.pingleserver.config;
2+
3+
import javax.sql.DataSource;
4+
import org.springframework.beans.factory.annotation.Qualifier;
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
7+
import org.springframework.boot.jdbc.DataSourceBuilder;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
import org.springframework.context.annotation.Primary;
11+
12+
@Configuration
13+
@EnableConfigurationProperties
14+
public class DataSourceConfiguration {
15+
16+
@Bean
17+
@Primary
18+
@ConfigurationProperties(prefix = "spring.datasource.jpa")
19+
DataSource jpaDataSource() {
20+
return DataSourceBuilder
21+
.create()
22+
.build();
23+
}
24+
25+
@Bean
26+
@Qualifier("lockDataSource")
27+
@ConfigurationProperties(prefix = "spring.datasource.lock")
28+
DataSource lockDataSource() {
29+
return DataSourceBuilder
30+
.create()
31+
.build();
32+
}
33+
}

src/main/java/org/pingle/pingleserver/controller/MeetingController.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.pingle.pingleserver.dto.response.ParticipantsResponse;
1717
import org.pingle.pingleserver.dto.response.SearchResponse;
1818
import org.pingle.pingleserver.dto.type.SuccessMessage;
19+
import org.pingle.pingleserver.service.MeetingParticipateFacade;
1920
import org.pingle.pingleserver.service.MeetingService;
2021
import org.pingle.pingleserver.service.PinService;
2122

@@ -28,6 +29,7 @@
2829
public class MeetingController implements MeetingApi {
2930

3031
private final MeetingService meetingService;
32+
private final MeetingParticipateFacade meetingParticipateFacade;
3133
private final UserMeetingService userMeetingService;
3234
private final PinService pinService;
3335

@@ -43,7 +45,7 @@ public ApiResponse<?> createMeeting(@Valid @RequestBody MeetingRequest request,
4345

4446
@PostMapping("/{meetingId}/join")
4547
public ApiResponse<?> participateMeeting (@UserId Long userId, @PathVariable("meetingId") Long meetingId) {
46-
Long userMeetingId = userMeetingService.participateMeeting(userId, meetingId);
48+
meetingParticipateFacade.participateWithLock(userId, meetingId);
4749
return ApiResponse.success(SuccessMessage.CREATED);
4850
}
4951

src/main/java/org/pingle/pingleserver/domain/Meeting.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
package org.pingle.pingleserver.domain;
22

3-
import jakarta.persistence.*;
3+
import java.time.LocalDateTime;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import jakarta.persistence.CascadeType;
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.EnumType;
9+
import jakarta.persistence.Enumerated;
10+
import jakarta.persistence.FetchType;
11+
import jakarta.persistence.GeneratedValue;
12+
import jakarta.persistence.GenerationType;
13+
import jakarta.persistence.Id;
14+
import jakarta.persistence.JoinColumn;
15+
import jakarta.persistence.ManyToOne;
16+
import jakarta.persistence.OneToMany;
17+
import org.pingle.pingleserver.domain.enums.MCategory;
418
import lombok.AccessLevel;
519
import lombok.Builder;
620
import lombok.Getter;
721
import lombok.NoArgsConstructor;
8-
import org.pingle.pingleserver.domain.enums.MCategory;
9-
10-
import java.time.LocalDateTime;
11-
import java.util.ArrayList;
12-
import java.util.List;
1322

1423
@Entity
1524
@Getter

src/main/java/org/pingle/pingleserver/domain/UserMeeting.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99

1010
@Entity
1111
@Getter
12+
@Table(
13+
uniqueConstraints = {
14+
@UniqueConstraint(name = "unique_user_meeting", columnNames = {"user_id", "meeting_id"})
15+
}
16+
)
1217
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1318
public class UserMeeting extends BaseTimeEntity implements Comparable<UserMeeting> {
1419

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.pingle.pingleserver.service;
2+
3+
public interface LockManager {
4+
void executeWithLock(String key, Runnable runnable);
5+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.pingle.pingleserver.service;
2+
3+
import org.springframework.stereotype.Service;
4+
import lombok.RequiredArgsConstructor;
5+
6+
@Service
7+
@RequiredArgsConstructor
8+
public class MeetingParticipateFacade {
9+
private final UserMeetingService userMeetingService;
10+
private final LockManager lockManager;
11+
12+
public void participateWithLock(Long userId, Long meetingId) {
13+
lockManager.executeWithLock(meetingId.toString(),
14+
() -> userMeetingService.participateMeeting(userId, meetingId));
15+
}
16+
}

src/main/java/org/pingle/pingleserver/service/MeetingService.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package org.pingle.pingleserver.service;
22

3-
import lombok.RequiredArgsConstructor;
3+
import java.time.LocalDateTime;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import java.util.Optional;
7+
import java.util.stream.Collectors;
48
import org.pingle.pingleserver.domain.Meeting;
59
import org.pingle.pingleserver.domain.Pin;
610
import org.pingle.pingleserver.domain.UserMeeting;
@@ -20,12 +24,7 @@
2024
import org.pingle.pingleserver.utils.SlackUtil;
2125
import org.springframework.stereotype.Service;
2226
import org.springframework.transaction.annotation.Transactional;
23-
24-
import java.time.LocalDateTime;
25-
import java.util.ArrayList;
26-
import java.util.List;
27-
import java.util.Optional;
28-
import java.util.stream.Collectors;
27+
import lombok.RequiredArgsConstructor;
2928

3029
@Service
3130
@RequiredArgsConstructor

src/main/java/org/pingle/pingleserver/service/UserMeetingService.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.pingle.pingleserver.service;
22

3+
import jakarta.persistence.EntityManager;
34
import lombok.RequiredArgsConstructor;
45
import org.pingle.pingleserver.domain.Meeting;
56
import org.pingle.pingleserver.domain.Team;
@@ -9,13 +10,14 @@
910
import org.pingle.pingleserver.dto.type.ErrorMessage;
1011
import org.pingle.pingleserver.exception.CustomException;
1112
import org.pingle.pingleserver.repository.*;
13+
import org.springframework.dao.DataIntegrityViolationException;
1214
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Propagation;
1316
import org.springframework.transaction.annotation.Transactional;
1417

1518

1619
@Service
1720
@RequiredArgsConstructor
18-
@Transactional(readOnly = true)
1921
public class UserMeetingService {
2022
private final UserRepository userRepository;
2123
private final TeamRepository teamRepository;
@@ -34,23 +36,26 @@ public Long addOwnerToMeeting(Long userId, Meeting meeting) {
3436
.build()).getId();
3537
}
3638

37-
//유저가 그룹에 있는지
3839
public void verifyUser(Long userId, Long groupId) {
3940
User user = userRepository.findByIdOrThrow(userId);
4041
Team team = teamRepository.findByIdOrThrow(groupId);
4142
userTeamRepository.findByUserAndTeam(user, team)
4243
.orElseThrow(() -> new CustomException(ErrorMessage.GROUP_PERMISSION_DENIED));
4344
}
4445

45-
@Transactional
46+
@Transactional(propagation = Propagation.REQUIRES_NEW)
4647
public Long participateMeeting(Long userId, Long meetingId) {
4748
Meeting meeting = meetingRepository.findById(meetingId).orElseThrow(() -> new CustomException(ErrorMessage.MEETING_NOT_FOUND));
48-
if(isParticipating(userId, meeting))
49-
throw new CustomException(ErrorMessage.RESOURCE_CONFLICT);
5049
if((getCurParticipants(meeting)) >= meeting.getMaxParticipants())
5150
throw new CustomException(ErrorMessage.RESOURCE_CONFLICT);
5251
User user = userRepository.findByIdOrThrow(userId);
53-
return userMeetingRepository.save(new UserMeeting(user, meeting, MRole.PARTICIPANTS)).getId();
52+
Long participateId;
53+
try{
54+
participateId = userMeetingRepository.save(new UserMeeting(user, meeting, MRole.PARTICIPANTS)).getId();
55+
} catch (DataIntegrityViolationException e) {
56+
throw new CustomException(ErrorMessage.RESOURCE_CONFLICT);
57+
}
58+
return participateId;
5459
}
5560

5661
@Transactional
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.pingle.pingleserver.service.lock;
2+
3+
import java.sql.Connection;
4+
import java.sql.PreparedStatement;
5+
import java.sql.SQLException;
6+
import javax.sql.DataSource;
7+
import org.pingle.pingleserver.dto.type.ErrorMessage;
8+
import org.pingle.pingleserver.exception.CustomException;
9+
import org.pingle.pingleserver.service.LockManager;
10+
import org.springframework.beans.factory.annotation.Qualifier;
11+
import org.springframework.stereotype.Component;
12+
13+
@Component
14+
public class DatabaseLockManager implements LockManager {
15+
16+
private final DataSource lockDataSource;
17+
18+
public DatabaseLockManager(@Qualifier("lockDataSource") DataSource lockDataSource) {
19+
this.lockDataSource = lockDataSource;
20+
}
21+
22+
public void executeWithLock(String key, Runnable runnable) {
23+
try (Connection connection = lockDataSource.getConnection()) {
24+
try {
25+
getLock(connection, key);
26+
runnable.run();
27+
} finally {
28+
releaseLock(connection, key);
29+
}
30+
} catch (SQLException e) {
31+
throw new CustomException(ErrorMessage.INTERNAL_SERVER_ERROR);
32+
}
33+
}
34+
35+
private void getLock(final Connection connection, final String key) {
36+
String sql = "SELECT get_lock(?, ?)";
37+
38+
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
39+
stmt.setString(1, key);
40+
stmt.setInt(2, 3000);
41+
stmt.executeQuery();
42+
} catch (SQLException e) {
43+
throw new CustomException(ErrorMessage.INTERNAL_SERVER_ERROR);
44+
}
45+
}
46+
47+
private void releaseLock(final Connection connection, final String key) {
48+
String sql = "SELECT RELEASE_LOCK(?)";
49+
50+
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
51+
stmt.setString(1, key);
52+
stmt.executeQuery();
53+
} catch (SQLException e) {
54+
throw new CustomException(ErrorMessage.INTERNAL_SERVER_ERROR);
55+
}
56+
}
57+
}

src/test/java/org/pingle/pingleserver/PingleserverApplicationTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.junit.jupiter.api.Test;
44
import org.springframework.boot.test.context.SpringBootTest;
5+
import org.springframework.test.context.ActiveProfiles;
56

67
@SpringBootTest
8+
@ActiveProfiles("test")
79
class PingleserverApplicationTests {
810

911
@Test
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.pingle.pingleserver;
2+
3+
import org.junit.jupiter.api.extension.ExtendWith;
4+
import org.pingle.pingleserver.util.DatabaseCleanerExtension;
5+
import org.springframework.boot.test.context.SpringBootTest;
6+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
7+
import org.springframework.test.context.ActiveProfiles;
8+
9+
@ExtendWith(DatabaseCleanerExtension.class)
10+
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
11+
@ActiveProfiles("test")
12+
public abstract class ServiceSliceTest {
13+
}
14+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.pingle.pingleserver.fixture;
2+
3+
import java.time.LocalDateTime;
4+
import org.pingle.pingleserver.domain.Meeting;
5+
import org.pingle.pingleserver.domain.Pin;
6+
import org.pingle.pingleserver.domain.enums.MCategory;
7+
8+
public abstract class MeetingFixture {
9+
private MeetingFixture() {
10+
}
11+
12+
public static Meeting create(Pin pin, int maxParticipants , LocalDateTime start, LocalDateTime end) {
13+
return new Meeting(pin, MCategory.MULTI, "name", maxParticipants, "link", start, end);
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.pingle.pingleserver.fixture;
2+
3+
import org.pingle.pingleserver.domain.Address;
4+
import org.pingle.pingleserver.domain.Pin;
5+
import org.pingle.pingleserver.domain.Point;
6+
import org.pingle.pingleserver.domain.Team;
7+
8+
public abstract class PinFixture {
9+
private PinFixture() {
10+
}
11+
12+
public static Pin create(Team team) {
13+
return new Pin(team, new Point(1.0, 1.0), new Address("add1", "add2"), "name");
14+
}
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.pingle.pingleserver.fixture;
2+
3+
import org.pingle.pingleserver.domain.Team;
4+
import org.pingle.pingleserver.domain.enums.TKeyword;
5+
6+
public abstract class TeamFixture {
7+
private TeamFixture() {
8+
}
9+
10+
public static Team create() {
11+
return new Team("team1", "team1", "code", TKeyword.ETC);
12+
}
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.pingle.pingleserver.fixture;
2+
3+
import org.pingle.pingleserver.domain.User;
4+
import org.pingle.pingleserver.domain.enums.Provider;
5+
import org.pingle.pingleserver.domain.enums.URole;
6+
7+
public abstract class UserFixture {
8+
private UserFixture() {
9+
}
10+
11+
public static User create() {
12+
return new User("serial", "name", "email", Provider.APPLE, URole.USER, "refreshToken");
13+
}
14+
}

0 commit comments

Comments
 (0)