Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
wellbeing-dough committed Aug 1, 2024
2 parents 771e18e + ae3c68c commit 5716d9c
Show file tree
Hide file tree
Showing 17 changed files with 371 additions and 19 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-aop'


// swagger
Expand All @@ -56,6 +57,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.17.4'

//s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import kr.co.studyhubinu.studyhubserver.study.repository.StudyRepository;
import kr.co.studyhubinu.studyhubserver.studypost.domain.StudyPostEntity;
import kr.co.studyhubinu.studyhubserver.studypost.domain.implementations.StudyPostApplyEventPublisher;
import kr.co.studyhubinu.studyhubserver.studypost.domain.implementations.StudyPostReader;
import kr.co.studyhubinu.studyhubserver.studypost.repository.StudyPostRepository;
import kr.co.studyhubinu.studyhubserver.user.domain.UserEntity;
import kr.co.studyhubinu.studyhubserver.user.dto.data.UserId;
Expand All @@ -43,7 +44,6 @@

@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class ApplyService {

private final UserRepository userRepository;
Expand All @@ -53,6 +53,7 @@ public class ApplyService {
private final StudyPostRepository studyPostRepository;
private final StudyPostApplyEventPublisher studyPostApplyEventPublisher;
private final ApplyWriter applyWriter;
private final StudyPostReader studyPostReader;

@Transactional
public void enroll(UserId userId, EnrollApplyRequest request) {
Expand Down Expand Up @@ -110,13 +111,13 @@ public void rejectApply(final RejectApplyRequest rejectApplyRequest, final UserI
rejectRepository.save(rejectApplyRequest.toRejectEntity());
}

@Transactional
public void acceptApply(AcceptApplyRequest acceptApplyRequest, UserId userId) {
userRepository.findById(userId.getId()).orElseThrow(UserNotFoundException::new);
StudyEntity study = studyRepository.findById(acceptApplyRequest.getStudyId()).orElseThrow(StudyNotFoundException::new);
ApplyEntity applyEntity = applyRepository.findByUserIdAndStudyId(acceptApplyRequest.getRejectedUserId(), study.getId()).orElseThrow(ApplyNotFoundException::new);
StudyPostEntity studyPost = studyPostReader.readByStudyId(study.getId());
applyWriter.applyAccept(applyEntity);
studyPostApplyEventPublisher.acceptApplyEventPublish(study.getId());
studyPostApplyEventPublisher.acceptApplyEventPublish(studyPost.getId());
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kr.co.studyhubinu.studyhubserver.common.redisson;

import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

@Slf4j
@NoArgsConstructor
public class CustomSpringELParser {
public static String 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]);
}
Object value = parser.parseExpression(key).getValue(context, Object.class);
if (value == null) {
log.warn("CustomSpringELParser evaluated value is null for key={}", key);
return null;
}
return value.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package kr.co.studyhubinu.studyhubserver.common.redisson;

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

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

String hashKey();
String field();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package kr.co.studyhubinu.studyhubserver.common.redisson;

import kr.co.studyhubinu.studyhubserver.exception.apply.LockAcquisitionException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

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

private final RedissonClient redissonClient;
private static final int LOCK_WAIT_TIME = 10;
private static final int LOCK_LEASE_TIME = 3;

@Around("@annotation(kr.co.studyhubinu.studyhubserver.common.redisson.RedissonDistributedLock)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedissonDistributedLock redissonDistributedLock = method.getAnnotation(RedissonDistributedLock.class);
String hashKey = getDynamicValue(signature, joinPoint, redissonDistributedLock.hashKey());
String field = getDynamicValue(signature, joinPoint, redissonDistributedLock.field());

RLock lock = redissonClient.getLock(hashKey + ":" + field);

Object result;
boolean available = false;
try {
available = lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS);
if (!available) {
log.warn("Redisson GetLock Timeout {}", field);
throw new LockAcquisitionException();
}

result = joinPoint.proceed();
} catch (InterruptedException e) {
throw new LockAcquisitionException();
} finally {
if (available) {
lock.unlock();
}
}
return result;
}

// 메서드 파라미터(field와 hashkey)를 기반으로 동적으로 값을 지정
public String getDynamicValue(MethodSignature signature, ProceedingJoinPoint joinPoint, String distributedLock) {
return CustomSpringELParser.getDynamicValue(
signature.getParameterNames(),
joinPoint.getArgs(),
distributedLock);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kr.co.studyhubinu.studyhubserver.config;

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

@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String redisHost;

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

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

@Bean
public RedissonClient redissonClient() {
RedissonClient redisson = null;
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
redisson = Redisson.create(config);
return redisson;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public enum StatusType {
SORT_TYPE_NOT_FOUND(404, "SORT_TYPE_NOT_FOUND"),
MAJOR_TYPE_NOT_FOUND(404, "MAJOR_TYPE_NOT_FOUND"),
NO_REMAINING_SEAT(409, "NO_REMAINING_SEAT"),
STUDY_POST_CLOSED(409, "STUDY_POST_CLOSED");
STUDY_APPLY_LOCK_ACQUISITION(423, "STUDY_APPLY_LOCK_ACQUISITION"),
STUDY_POST_CLOSED(409, "STUDY_POST_CLOSED"),
REMAINING_SEATS_OVER_STUDY_PERSON(409, "REMAINING_SEATS_OVER_STUDY_PERSON");

private final int statusCode;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.co.studyhubinu.studyhubserver.exception.apply;

import kr.co.studyhubinu.studyhubserver.exception.StatusType;
import kr.co.studyhubinu.studyhubserver.exception.common.CustomException;

public class LockAcquisitionException extends CustomException {

private final StatusType status;
private static final String message = "스터디 지원에 대한 락 획득에 실패했습니다.";

public LockAcquisitionException() {
super(message);
this.status = StatusType.STUDY_APPLY_LOCK_ACQUISITION;
}

@Override
public StatusType getStatus() {
return status;
}

@Override
public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.co.studyhubinu.studyhubserver.exception.study;

import kr.co.studyhubinu.studyhubserver.exception.StatusType;
import kr.co.studyhubinu.studyhubserver.exception.common.CustomException;

public class PostRemainingSeatOverStudyPerson extends CustomException {

private final StatusType status;
private static final String message = "잔여석은 0개가 될 수 없습니다";

public PostRemainingSeatOverStudyPerson() {
super(message);
this.status = StatusType.REMAINING_SEATS_OVER_STUDY_PERSON;
}

@Override
public StatusType getStatus() {
return status;
}

@Override
public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public StudyPostEntity(Long id, String title, String content, String chatUrl, Ma
this.studyId = studyId;
}

public void update(UpdateStudyPostInfo info) {
public void update(UpdateStudyPostInfo info, int currentJoinCount) {
this.title = info.getTitle();
this.content = info.getContent();
this.chatUrl = info.getChatUrl();
Expand All @@ -103,6 +103,7 @@ public void update(UpdateStudyPostInfo info) {
this.close = info.isClose();
this.studyStartDate = info.getStudyStartDate();
this.studyEndDate = info.getStudyEndDate();
this.remainingSeat = info.getStudyPerson() - currentJoinCount;
}

public boolean isPostOfUser(Long userId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
package kr.co.studyhubinu.studyhubserver.studypost.domain.implementations;

import kr.co.studyhubinu.studyhubserver.exception.study.PostNotFoundException;
import kr.co.studyhubinu.studyhubserver.studypost.domain.StudyPostEntity;
import kr.co.studyhubinu.studyhubserver.studypost.repository.StudyPostRepository;
import kr.co.studyhubinu.studyhubserver.common.redisson.RedissonDistributedLock;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
@Slf4j
public class StudyPostApplyEventPublisher {

private final StudyPostRepository studyPostRepository;
private final StudyPostWriter studyPostWriter;

@Transactional
public void acceptApplyEventPublish(Long studyId) {
StudyPostEntity studyPost = studyPostRepository.findByIdWithPessimisticLock(studyId).orElseThrow(PostNotFoundException::new);
studyPost.decreaseRemainingSeat();
studyPost.closeStudyPostIfRemainingSeatIsZero();
studyPostRepository.save(studyPost);
@RedissonDistributedLock(hashKey = "'apply'", field = "#studyPostId")
public void acceptApplyEventPublish(Long studyPostId) {
studyPostWriter.updateStudyPostApply(studyPostId);
}

// RLock lock = redissonClient.getLock(studyPostId.toString());
// boolean available = false;
// try {
// available = lock.tryLock(10, 1, TimeUnit.SECONDS);
// if (!available) {
// throw new StudyApplyLockAcquisitionException();
// }
// studyPostWriter.updateStudyPostApply(studyPostId);
// } catch (InterruptedException e) {
// throw new StudyApplyLockAcquisitionException();
// } finally {
// if (available) {
// lock.unlock();
// }
// }
// @Transactional
// public void acceptApplyEventPublish(Long studyId) {
// StudyPostEntity studyPost = studyPostRepository.findByIdWithPessimisticLock(studyId).orElseThrow(PostNotFoundException::new);
// studyPost.decreaseRemainingSeat();
// studyPost.closeStudyPostIfRemainingSeatIsZero();
// studyPostRepository.save(studyPost);
// }


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.co.studyhubinu.studyhubserver.studypost.domain.implementations;

import kr.co.studyhubinu.studyhubserver.exception.study.PostNotFoundException;
import kr.co.studyhubinu.studyhubserver.studypost.domain.StudyPostEntity;
import kr.co.studyhubinu.studyhubserver.studypost.repository.StudyPostRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class StudyPostReader {

private final StudyPostRepository studyPostRepository;

public StudyPostEntity readByStudyId(Long studyId) {
return studyPostRepository.findByStudyId(studyId).orElseThrow(PostNotFoundException::new);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kr.co.studyhubinu.studyhubserver.studypost.domain.implementations;

import kr.co.studyhubinu.studyhubserver.exception.study.PostNotFoundException;
import kr.co.studyhubinu.studyhubserver.studypost.domain.StudyPostEntity;
import kr.co.studyhubinu.studyhubserver.studypost.repository.StudyPostRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;


@Component
@RequiredArgsConstructor
@Slf4j
public class StudyPostWriter {

private final StudyPostRepository studyPostRepository;
private final StudyPostReader studyPostReader;

@Transactional
public void updateStudyPostApply(Long studyPostId) {
StudyPostEntity studyPost = studyPostRepository.findById(studyPostId).orElseThrow(PostNotFoundException::new);
studyPost.decreaseRemainingSeat();
studyPost.closeStudyPostIfRemainingSeatIsZero();
studyPostRepository.saveAndFlush(studyPost);
}

}
Loading

0 comments on commit 5716d9c

Please sign in to comment.