Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feature/143_june-777-…
Browse files Browse the repository at this point in the history
…kimhyun5u_음식-상품-재고-캐싱

# Conflicts:
#	build.gradle
#	src/main/java/camp/woowak/lab/infra/config/RedissonConfiguration.java
#	src/main/resources/application.yaml
#	src/test/resources/application.yml
  • Loading branch information
june-777 committed Aug 28, 2024
2 parents c450761 + 6f2b4fe commit 239d4ee
Show file tree
Hide file tree
Showing 13 changed files with 432 additions and 42 deletions.
61 changes: 30 additions & 31 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
}

group = 'camp.woowak'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.3.3'
Expand All @@ -43,34 +43,33 @@ dependencies {
// https://mvnrepository.com/artifact/com.github.codemonstur/embedded-redis
implementation 'com.github.codemonstur:embedded-redis:1.4.3'

//QueryDsl
// QueryDsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
// QueryDsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform()
}

// Querydsl 설정부
def generated = 'src/main/generated'

// querydsl QClass 생성 위치
tasks.withType(JavaCompile) {
options.generatedSourceOutputDirectory = file(generated)
options.generatedSourceOutputDirectory = file(generated)
}

task cleanGenerated(type: Delete) {
delete generated
delete generated
}

clean.dependsOn cleanGenerated
compileJava.dependsOn clean

// java source set 에 querydsl QClass 위치 추가
sourceSets {
main.java.srcDirs += "$projectDir/build/generated"
main.java.srcDirs += "$projectDir/build/generated"
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ public interface CouponRepository extends JpaRepository<Coupon, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM Coupon c WHERE c.id = :id")
Optional<Coupon> findByIdWithPessimisticLock(Long id);

@Query("SELECT c FROM Coupon c WHERE c.id = :id")
Optional<Coupon> findById(Long id);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package camp.woowak.lab.coupon.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import camp.woowak.lab.coupon.domain.Coupon;
import camp.woowak.lab.coupon.domain.CouponIssuance;
Expand All @@ -12,7 +13,7 @@
import camp.woowak.lab.coupon.service.command.IssueCouponCommand;
import camp.woowak.lab.customer.domain.Customer;
import camp.woowak.lab.customer.repository.CustomerRepository;
import jakarta.transaction.Transactional;
import camp.woowak.lab.infra.aop.DistributedLock;

@Service
public class IssueCouponService {
Expand Down Expand Up @@ -54,4 +55,26 @@ public Long issueCoupon(IssueCouponCommand cmd) {
// coupon issuance 저장
return couponIssuanceRepository.save(newCouponIssuance).getId();
}

@DistributedLock(key = "#cmd.couponId()", leaseTime = 30L)
public Long issueCouponWithDistributionLock(IssueCouponCommand cmd) {
// customer 조회
Customer targetCustomer = customerRepository.findById(cmd.customerId())
.orElseThrow(() -> new InvalidICreationIssuanceException("customer not found"));

// coupon 조회
Coupon targetCoupon = couponRepository.findById(cmd.couponId())
.orElseThrow(() -> new InvalidICreationIssuanceException("coupon not found"));

// coupon 수량 확인
if (!targetCoupon.hasAvailableQuantity()) {
throw new InsufficientCouponQuantityException("quantity of coupon is insufficient");
}

// coupon issuance 생성
CouponIssuance newCouponIssuance = new CouponIssuance(targetCoupon, targetCustomer);

// coupon issuance 저장
return couponIssuanceRepository.save(newCouponIssuance).getId();
}
}
15 changes: 15 additions & 0 deletions src/main/java/camp/woowak/lab/infra/aop/AopForTransaction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package camp.woowak.lab.infra.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class AopForTransaction {

@Transactional(propagation = Propagation.REQUIRES_NEW)
public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
}
20 changes: 20 additions & 0 deletions src/main/java/camp/woowak/lab/infra/aop/CustomSpringELParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package camp.woowak.lab.infra.aop;

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

public class CustomSpringELParser {
private CustomSpringELParser() {
}

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

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

return spelExpressionParser.parseExpression(key).getValue(context, Object.class);
}
}
33 changes: 33 additions & 0 deletions src/main/java/camp/woowak/lab/infra/aop/DistributedLock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package camp.woowak.lab.infra.aop;

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 {

/**
* @return 분산락의 키 이름
*/
String key();

/**
* @return 분산락 단위 시간
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;

/**
* @return 분산락 대기 시간
*/
long waitTime() default 5L;

/**
* @return 분산락을 점유한 스레드가 정해진 시간이 지나면 락을 해제
*/
long leaseTime() default 5L;

}
65 changes: 65 additions & 0 deletions src/main/java/camp/woowak/lab/infra/aop/DistributedLockAop.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package camp.woowak.lab.infra.aop;

import java.lang.reflect.Method;

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.stereotype.Component;

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

@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class DistributedLockAop {

private static final String REDISSON_LOCK_PREFIX = "LOCK:";
private final RedissonClient redissonClient;
private final AopForTransaction aopForTransaction;

@Around("@annotation(camp.woowak.lab.infra.aop.DistributedLock)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);

String key = REDISSON_LOCK_PREFIX + CustomSpringELParser.getDynamicValue(signature.getParameterNames(),
joinPoint.getArgs(),
distributedLock.key());
RLock rLock = redissonClient.getLock(key);

try {
boolean locked = rLock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(),
distributedLock.timeUnit());
if (!locked) {
log.warn("Failed to acquire lock for method {} with key {}", method.getName(), key);
throw new IllegalStateException("Unable to acquire lock");
}
log.info("Acquired lock for method {} with key {}", method.getName(), key);
return aopForTransaction.proceed(joinPoint);
} catch (InterruptedException e) { // rLock.tryLock
log.warn("Interrupted while trying to acquire lock for method {} with key {}", method.getName(), key);
throw e;
} finally {
releaseLock(rLock, method, key);
}
}

private void releaseLock(RLock rLock, Method method, String key) {
try {
if (rLock.isHeldByCurrentThread()) {
rLock.unlock();
log.info("Released lock for method {} with key {}", method.getName(), key);
}
} catch (IllegalMonitorStateException e) {
log.warn("Failed to release lock for method {} with key {}", method.getName(), key, e);
throw e;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class RedissonConfiguration {
private static final String REDISSON_URL_PREFIX = "redis://";
@Value("${spring.data.redis.host}")
private String redisHost;

@Value("${spring.data.redis.port}")
private String redisPort;

Expand All @@ -22,4 +23,5 @@ public RedissonClient redissonClient() {
.setAddress(REDISSON_URL_PREFIX + redisHost + ":" + redisPort);
return Redisson.create(config);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public IssueCouponResponse issueCoupon(@AuthenticationPrincipal LoginCustomer lo
@PathVariable Long couponId) {
IssueCouponCommand cmd = new IssueCouponCommand(loginCustomer.getId(), couponId);

issueCouponService.issueCoupon(cmd);
issueCouponService.issueCouponWithDistributionLock(cmd);

return new IssueCouponResponse();
}
Expand Down
11 changes: 6 additions & 5 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ spring:
enabled: true
path: /h2-console

data:
redis:
host: localhost
port: 6379

jpa:
hibernate:
ddl-auto: create
Expand All @@ -26,3 +21,9 @@ spring:
hibernate:
format_sql: true
show_sql: true

data:
redis:
host: localhost
port: 6379
password:
Loading

0 comments on commit 239d4ee

Please sign in to comment.