Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BSVR-217] 리뷰 공감에 분산락 추가 #166

Merged
merged 27 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0d82f11
build: embedded redis dependency 추가
EunjiShin Aug 25, 2024
7ecb17d
feat: embedded redis 설정 파일 작성
EunjiShin Aug 25, 2024
9f94e75
refactor: OS 종속적이지 않게 수정
EunjiShin Aug 25, 2024
835679e
feat: 분산락 어노테이션 추가
EunjiShin Aug 25, 2024
b016ff1
build: common 모듈에 aop dependency 추가
EunjiShin Aug 25, 2024
7b993bb
feat: 분산락 AOP 구현
EunjiShin Aug 25, 2024
69900fc
feat: 리뷰 공감 메서드에 분산락 적용
EunjiShin Aug 25, 2024
0f6170a
test: FakeReviewLikeRepository 생성
EunjiShin Aug 25, 2024
e855e3f
test: reviewLikeService 생성
EunjiShin Aug 25, 2024
5150605
test: 동시성 테스트 유틸 추가
EunjiShin Aug 25, 2024
7cb393a
refactor: sout -> log로 변경
EunjiShin Aug 25, 2024
16b1e46
test: 리뷰 공감 테스트코드 작성
EunjiShin Aug 25, 2024
5b2d6f6
test: 리뷰 공감에 필요한 repository 구현
EunjiShin Aug 25, 2024
65ec2ab
build: testContainer 의존성 설정
EunjiShin Aug 26, 2024
dc07697
refactor: @Value를 Properties로 대체
EunjiShin Aug 26, 2024
ae2808e
feat: test용 프로필 생성
EunjiShin Aug 26, 2024
1c64294
test: 리뷰 공감 테스트에 필요한 데이터
EunjiShin Aug 26, 2024
76f89f4
fix: ColumnDefault 수정
EunjiShin Aug 26, 2024
d6e9a13
test: 리뷰 공감 테스트 추가
EunjiShin Aug 26, 2024
82b7b06
fix: resolve merge conflict
EunjiShin Aug 26, 2024
db92ca9
feat: 불필요한 코드 삭제
EunjiShin Aug 26, 2024
f73237b
feat: 불필요한 코드 삭제
EunjiShin Aug 26, 2024
2efb503
test: 통합 테스트로 바꾸면서 불필요해진 fake 삭제
EunjiShin Aug 26, 2024
179c6eb
test: reviewLikeRepository fake 보완
EunjiShin Aug 26, 2024
e3f5039
fix: resolve merge conflict
EunjiShin Aug 26, 2024
0eb7ef5
feat: oauth property 추가
EunjiShin Aug 26, 2024
0683484
refactor: 불필요한 fake 삭제
EunjiShin Aug 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ dependencies {
// aop
implementation("org.springframework.boot:spring-boot-starter-aop")

// test container
testImplementation("org.testcontainers:testcontainers:_")
testImplementation("org.testcontainers:junit-jupiter:_")
testImplementation("org.testcontainers:mysql:_")
testImplementation("org.testcontainers:jdbc:_")

}

// spring boot main application이므로 실행 가능한 jar를 생성한다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import org.depromeet.spot.infrastructure.InfrastructureConfig;
import org.depromeet.spot.usecase.config.UsecaseConfig;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@ComponentScan(basePackages = {"org.depromeet.spot.application"})
@Configuration
@EnableConfigurationProperties
@ComponentScan(basePackages = {"org.depromeet.spot.application"})
@ConfigurationPropertiesScan(basePackages = {"org.depromeet.spot.application"})
@Import(value = {UsecaseConfig.class, SwaggerConfig.class, InfrastructureConfig.class})
public class SpotApplicationConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.depromeet.spot.application.common.jwt;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "spring.jwt")
public record JwtProperties(String secret) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Properties 변환 굿이에용

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import org.depromeet.spot.application.common.exception.JwtErrorCode;
import org.depromeet.spot.domain.member.Member;
import org.depromeet.spot.domain.member.enums.MemberRole;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
Expand All @@ -37,8 +36,7 @@ public class JwtTokenUtil {
// JWT를 생성하고 관리하는 클래스

// 토큰에 사용되는 시크릿 키
@Value("${spring.jwt.secret}")
private String SECRETKEY;
private final JwtProperties properties;

public String getJWTToken(Member member) {
// TODO 토큰 구현하기.
Expand All @@ -61,21 +59,21 @@ public String generateToken(Long memberId, MemberRole memberRole) {
.setClaims(createClaims(memberId, memberRole))
.setIssuedAt(current)
.setExpiration(expiredAt)
.signWith(SignatureAlgorithm.HS256, SECRETKEY.getBytes())
.signWith(SignatureAlgorithm.HS256, properties.secret().getBytes())
.compact();
}

public Long getIdFromJWT(String token) {
return Jwts.parser()
.setSigningKey(SECRETKEY.getBytes())
.setSigningKey(properties.secret().getBytes())
.parseClaimsJws(token)
.getBody()
.get("memberId", Long.class);
}

public String getRoleFromJWT(String token) {
return Jwts.parser()
.setSigningKey(SECRETKEY.getBytes())
.setSigningKey(properties.secret().getBytes())
.parseClaimsJws(token)
.getBody()
.get("role", String.class);
Expand Down Expand Up @@ -124,7 +122,7 @@ private Map<String, Object> createClaims(Long memberId, MemberRole role) {
}

private Key createSignature() {
byte[] apiKeySecretBytes = SECRETKEY.getBytes();
byte[] apiKeySecretBytes = properties.secret().getBytes();
return new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package org.depromeet.spot.application;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;

import org.depromeet.spot.domain.member.Level;
import org.depromeet.spot.domain.member.Member;
import org.depromeet.spot.domain.member.enums.MemberRole;
import org.depromeet.spot.domain.member.enums.SnsProvider;
import org.depromeet.spot.domain.review.Review;
import org.depromeet.spot.usecase.port.in.review.ReadReviewUsecase;
import org.depromeet.spot.usecase.port.out.member.LevelRepository;
import org.depromeet.spot.usecase.port.out.member.MemberRepository;
import org.depromeet.spot.usecase.service.review.like.ReviewLikeService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
import org.springframework.test.context.jdbc.SqlGroup;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.junit.jupiter.Testcontainers;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
@TestPropertySource("classpath:application-test.yml")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@SqlGroup({
@Sql(
value = "/sql/delete-data-after-review-like.sql",
executionPhase = ExecutionPhase.AFTER_TEST_METHOD),
@Sql(
value = "/sql/review-like-service-data.sql",
executionPhase = ExecutionPhase.BEFORE_TEST_METHOD),
})
class ReviewLikeServiceTest {

@Autowired private ReviewLikeService reviewLikeService;

@Autowired private ReadReviewUsecase readReviewUsecase;

@Autowired private MemberRepository memberRepository;

@Autowired private LevelRepository levelRepository;

private static final int NUMBER_OF_THREAD = 100;

@BeforeEach
@Transactional
void init() {
Level level = levelRepository.findByValue(0);
AtomicLong memberIdGenerator = new AtomicLong(1);

for (int i = 0; i < NUMBER_OF_THREAD; i++) {
long memberId = memberIdGenerator.getAndIncrement();
memberRepository.save(
Member.builder()
.id(memberId)
.snsProvider(SnsProvider.KAKAO)
.teamId(1L)
.role(MemberRole.ROLE_ADMIN)
.idToken("idToken" + memberId)
.nickname(String.valueOf(memberId))
.phoneNumber(String.valueOf(memberId))
.email("email" + memberId)
.build(),
level);
}
}

@Test
void 멀티_스레드_환경에서_리뷰_공감_수를_정상적으로_증가시킬_수_있다() throws InterruptedException {
// given
final long reviewId = 1L;
AtomicLong memberIdGenerator = new AtomicLong(1);
final ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_OF_THREAD);
final CountDownLatch latch = new CountDownLatch(NUMBER_OF_THREAD);

// when
for (int i = 0; i < NUMBER_OF_THREAD; i++) {
long memberId = memberIdGenerator.getAndIncrement();
executorService.execute(
() -> {
try {
reviewLikeService.toggleLike(memberId, reviewId);
System.out.println(
"Thread " + Thread.currentThread().getId() + " - 성공");
} catch (Throwable e) {
System.out.println(
"Thread "
+ Thread.currentThread().getId()
+ " - 실패"
+ e.getClass().getName());
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();

// then
Review review = readReviewUsecase.findById(reviewId);
assertEquals(100, review.getLikesCount());
}
}
45 changes: 45 additions & 0 deletions application/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
loki:
url: ${LOKI_URL}

aws:
s3:
accessKey: ${AWS_S3_ACCESS_KEY}
secretKey: ${AWS_S3_SECRET_KEY}
bucketName: ${AWS_S3_BUCKET_NAME}
redis:
host: localhost
port: 6379

oauth:
kakaoClientId: ${KAKAO_CLIENT_ID}
kakaoAuthTokenUrlHost: ${KAKAO_AUTH_TOKEN_URL_HOST}
kakaoAuthUserUrlHost: ${KAKAO_AUTH_USER_URL_HOST}
kakaoRedirectUrl: ${KAKAO_REDIRECT_URL}
googleClientId: ${GOOGLE_CLIENT_ID}
googleClientSecret: ${GOOGLE_CLIENT_SECRET}
googleRedirectUrl: ${GOOGLE_REDIRECT_URL}
googleAuthTokenUrlHost: ${GOOGLE_AUTH_TOKEN_URL_HOST}
googleUserUrlHost: ${GOOGLE_USER_URL_HOST}

spring:
datasource:
url: jdbc:tc:mysql:8.0.36:///testdb
username: testuser
password: testpassword
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
hikari:
connection-timeout: 100000
maximum-pool-size: 300
max-lifetime: 100000
jpa:
database: mysql
hibernate:
ddl-auto: create
database-platform: org.hibernate.dialect.MySQL8Dialect
defer-datasource-initialization: true
jwt:
secret: ${JWT_SECRETKEY}


server:
port: 8080
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
delete from members where id > 0;
delete from reviews where id > 0;
delete from review_likes where id > 0;
delete from levels where id > 0;
delete from stadiums where id > 0;
delete from baseball_teams where id > 0;
delete from sections where id > 0;
delete from blocks where id > 0;
delete from block_rows where id > 0;
delete from seats where id > 0;
41 changes: 41 additions & 0 deletions application/src/test/resources/sql/review-like-service-data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- levels
INSERT INTO levels (id, value, title, mascot_image_url, created_at, updated_at, deleted_at)
VALUES (1, 0, '직관 꿈나무', null, null, null, null),
(2, 1, '직관 첫 걸음', null, null, null, null),
(3, 2, '경기장 탐험가', null, null, null, null),
(4, 3, '직관의 여유', null, null, null, null),
(5, 4, '응원 단장', null, null, null, null),
(6, 5, '야구장 VIP', null, null, null, null),
(7, 6, '전설의 직관러', null, null, null, null);

-- Stadiums
INSERT INTO stadiums (id, name, main_image, seating_chart_image, labeled_seating_chart_image,
is_active)
VALUES (1, '잠실 야구 경기장', 'main_image_a.jpg', 'seating_chart_a.jpg', 'labeled_seating_chart_a.jpg',
1);

-- Baseball Teams
INSERT INTO baseball_teams (id, name, alias, logo, label_font_color)
VALUES (1, 'Team A', 'A', 'logo_a.png', '#FFFFFF');

-- Stadium Sections
INSERT INTO sections (id, stadium_id, name, alias)
VALUES (1, 1, '오렌지석', '응원석');

-- Block
INSERT INTO blocks (id, stadium_id, section_id, code, max_rows)
VALUES (1, 1, 1, "207", 3);

-- Row
INSERT INTO block_rows (id, block_id, number, max_seats)
VALUES (1, 1, 1, 3);

-- Seats
INSERT INTO seats (id, stadium_id, section_id, block_id, row_id, seat_number)
VALUES (1, 1, 1, 1, 1, 1);

-- reviews
INSERT INTO reviews (id, member_id, stadium_id, section_id, block_id, row_id, seat_id, date_time, content, likes_count, scraps_count, review_type)
VALUES
(1, 1, 1, 1, 1, 1, 1, '2023-06-01 19:00:00', '좋은 경기였습니다!', 0, 0, 'VIEW'),
(2, 1, 1, 1, 1, 1, 1, '2023-06-01 19:00:00', '좋은 경기였습니다!', 0, 0, 'VIEW');
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.depromeet.spot.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {

String key();

TimeUnit timeUnit() default TimeUnit.SECONDS;

long leaseTime() default 5L;

long waitTime() default 5L;
}
2 changes: 2 additions & 0 deletions infrastructure/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.springframework.boot:spring-boot-starter-data-jpa:_")



// mysql
runtimeOnly("com.mysql:mysql-connector-j")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import org.depromeet.spot.infrastructure.aws.property.ObjectStorageProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
Expand All @@ -14,7 +13,6 @@
import lombok.RequiredArgsConstructor;

@Configuration
@Profile("!test")
@RequiredArgsConstructor
public class ObjectStorageConfig {

Expand Down
Loading
Loading