Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ dependencies {
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'

implementation 'org.redisson:redisson-spring-boot-starter:3.27.0' //Redisson

testImplementation 'com.h2database:h2'
testCompileOnly 'org.projectlombok:lombok' // 테스트 의존성 추가
testAnnotationProcessor 'org.projectlombok:lombok'
Expand Down
54 changes: 28 additions & 26 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ services:
depends_on:
- mysqldb
- redis
env_file:
- .env
environment:
- SPRING_PROFILES_ACTIVE=docker
networks:
Expand Down Expand Up @@ -61,32 +63,32 @@ services:
networks:
- app-tier

prometheus:
user: "root"
image: prom/prometheus
container_name: prometheus_container
volumes:
- /home/ubuntu/WEB5_7_3star_BE/prometheus-grafana/prometheus/config:/etc/prometheus
- /home/ubuntu/WEB5_7_3star_BE/prometheus-grafana/prometheus/volume:/prometheus/data
ports:
- 9090:9090
command:
- '--config.file=/etc/prometheus/prometheus.yml'
restart: always
networks:
- app-tier

grafana:
user: "root"
image: grafana/grafana
container_name: grafana_container
ports:
- 3000:3000
volumes:
- ./grafana/volume:/var/lib/grafana
restart: always
networks:
- app-tier
# prometheus:
# user: "root"
# image: prom/prometheus
# container_name: prometheus_container
# volumes:
# - /home/ubuntu/WEB5_7_3star_BE/prometheus-grafana/prometheus/config:/etc/prometheus
# - /home/ubuntu/WEB5_7_3star_BE/prometheus-grafana/prometheus/volume:/prometheus/data
# ports:
# - 9090:9090
# command:
# - '--config.file=/etc/prometheus/prometheus.yml'
# restart: always
# networks:
# - app-tier
#
# grafana:
# user: "root"
# image: grafana/grafana
# container_name: grafana_container
# ports:
# - 3000:3000
# volumes:
# - ./grafana/volume:/var/lib/grafana
# restart: always
# networks:
# - app-tier

volumes:
mysqldb-data:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.threestar.trainus.domain.coupon.user.repository.UserCouponRepository;
import com.threestar.trainus.domain.user.entity.User;
import com.threestar.trainus.domain.user.service.UserService;
import com.threestar.trainus.global.annotation.RedissonLock;
import com.threestar.trainus.global.exception.domain.ErrorCode;
import com.threestar.trainus.global.exception.handler.BusinessException;

Expand All @@ -34,12 +35,14 @@ public class CouponService {
private final UserService userService;

@Transactional
@RedissonLock(value = "#couponId")
public CreateUserCouponResponseDto createUserCoupon(Long userId, Long couponId) {
User user = userService.getUserById(userId);
Coupon coupon = couponRepository.findByIdWithPessimisticLock(couponId)
// Coupon coupon = couponRepository.findByIdWithPessimisticLock(couponId)
// .orElseThrow(() -> new BusinessException(ErrorCode.COUPON_NOT_FOUND));
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new BusinessException(ErrorCode.COUPON_NOT_FOUND));

//쿠폰 발급 종료시각이 지났으면 예외처리
// 쿠폰 발급 종료시각이 지났으면 예외처리
if (LocalDateTime.now().isAfter(coupon.getCloseAt())) {
throw new BusinessException(ErrorCode.COUPON_EXPIRED);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.threestar.trainus.global.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedissonLock {

String value(); //Lock 이름

long waitTime() default 5000L; //Lock을 획득을 시도하는 최대 시간 ms

long leaseTime() default 2000L; //락을 획득한 후, 점유하는 최대 시간 ms
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public void addCorsMappings(CorsRegistry registry) {
.allowedOrigins("http://localhost:3000",
"http://localhost:8080",
"http://localhost:8031",
"http://43.202.206.47:8080",
"http://43.202.206.47:3000",
"http://43.202.206.47:8031")
"http://15.165.184.145:8080",
"http://15.165.184.145:3000",
"http://15.165.184.145:8031")
.allowCredentials(true) // 쿠키 허용
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("*");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.threestar.trainus.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
Expand All @@ -10,8 +9,6 @@
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;



@Configuration
public class RedisConfig {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.threestar.trainus.global.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

private static final String REDISSON_HOST_PREFIX = "redis://";

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + "localhost:6379");
return Redisson.create(config);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.threestar.trainus.global.utils;

import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class CustomSpringELParser {

public static Object getDynamicValue(String[] parameterNames, Object[] args, String key) {
SpelExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}

return parser.parseExpression(key).getValue(context, Object.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.threestar.trainus.global.utils;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import com.threestar.trainus.global.annotation.RedissonLock;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Aspect
@Order(1)
@Component
@RequiredArgsConstructor
public class RedssionLockAspect {

private final RedissonClient redissonClient;

@Around("@annotation(com.threestar.trainus.global.annotation.RedissonLock)")
public Object redissonLock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
RedissonLock annotation = method.getAnnotation(RedissonLock.class);
String lockKey =
method.getName() + CustomSpringELParser.getDynamicValue(signature.getParameterNames(),
joinPoint.getArgs(), annotation.value());

RLock lock = redissonClient.getFairLock(lockKey);

try {
boolean lockable = lock.tryLock(annotation.waitTime(), annotation.leaseTime(), TimeUnit.MILLISECONDS);
if (!lockable) {
log.info("Lock 획득 실패={}", lockKey);
return null;
}
log.info("로직 수행");
return joinPoint.proceed();
} catch (InterruptedException e) {
log.info("에러 발생");
throw e;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("락 해제 완료={}", lockKey);
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ void tearDown() {
@Test
@DisplayName("동시 요청 시 - 비관적 락 적용: 발급된 쿠폰 수량만큼 발급")
void 쿠폰_동시_발급_테스트() throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(300);
ExecutorService executor = Executors.newFixedThreadPool(100);
CountDownLatch latch = new CountDownLatch(CONCURRENT_USERS);

AtomicInteger successCount = new AtomicInteger();
Expand Down
Loading