Skip to content
Open
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
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ dependencies {
implementation 'io.awspring.cloud:spring-cloud-starter-aws-secrets-manager-config:2.4.4'
// export file
implementation 'org.apache.poi:poi-ooxml:5.2.3'

}

def querydslDir = "$buildDir/generated/querydsl"
Expand Down
21 changes: 18 additions & 3 deletions src/main/java/com/formssafe/domain/form/entity/Form.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Version;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -82,6 +83,9 @@ public class Form extends BaseTimeEntity {

private boolean isDeleted;

@Version
private int version;

@BatchSize(size = FormConstraints.PAGE_SIZE)
@OneToMany(mappedBy = "form")
private List<FormTag> formTagList = new ArrayList<>();
Expand All @@ -105,9 +109,20 @@ public class Form extends BaseTimeEntity {
private List<RewardRecipient> rewardRecipientList = new ArrayList<>();

@Builder
private Form(Long id, User user, String title, String detail, List<String> imageUrl,
LocalDateTime startDate, LocalDateTime endDate, int expectTime, boolean isEmailVisible,
LocalDateTime privacyDisposalDate, FormStatus status, int questionCnt, int responseCnt, boolean isTemp,
private Form(Long id,
User user,
String title,
String detail,
List<String> imageUrl,
LocalDateTime startDate,
LocalDateTime endDate,
int expectTime,
boolean isEmailVisible,
LocalDateTime privacyDisposalDate,
FormStatus status,
int questionCnt,
int responseCnt,
boolean isTemp,
boolean isDeleted) {
this.id = id;
this.user = user;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
import com.formssafe.domain.user.dto.UserRequest.LoginUserDto;
import com.formssafe.domain.user.entity.User;
import com.formssafe.domain.user.service.UserService;
import com.formssafe.global.aop.Retry;
import com.formssafe.global.error.ErrorCode;
import com.formssafe.global.error.type.BadRequestException;
import com.formssafe.global.util.DateTimeUtil;
import jakarta.persistence.EntityManager;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -50,29 +52,30 @@ public class SubmissionService {
private final DescriptiveSubmissionRepository descriptiveSubmissionRepository;
private final ObjectiveSubmissionRepository objectiveSubmissionRepository;
private final ApplicationEventPublisher applicationEventPublisher;
private final EntityManager em;

@Transactional
@Retry
public void create(long formId, SubmissionCreateDto request, LoginUserDto loginUser) {
User user = userService.getUserById(loginUser.id());

Form form = formService.getForm(formId);

if (getSubmissionByUserAndForm(user, form) != null) {
throw new BadRequestException(ErrorCode.ONLY_ONE_SUBMISSION_ALLOWED,
"한 사용자가 하나의 설문에 대하여 두 개 이상의 응답을 작성할 수 없습니다.");
}

validate(user, form);

Submission submission = createSubmission(request, user, form);

createDetailSubmission(request.submissions(), submission, form);

if (!request.isTemp()) {
form.increaseResponseCount();
applicationEventPublisher.publishEvent(
new FormParticipantsNotificationEvent(new FormParticipantsNotificationEventDto(form, user), this));
}

em.flush();

Submission submission = createSubmission(request, user, form);
createDetailSubmission(request.submissions(), submission, form);
}

@Transactional
Expand Down Expand Up @@ -106,8 +109,6 @@ public void modify(long formId, SubmissionCreateDto request, LoginUserDto loginU
}

public SubmissionResponseDto getSubmission(long formId, LoginUserDto loginUser) {
User user = userService.getUserById(loginUser.id());

Submission submission = submissionRepository.findSubmissionByFormIDAndUserId(formId, loginUser.id())
.orElse(null);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.formssafe.global.aop;

import jakarta.persistence.OptimisticLockException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.hibernate.StaleObjectStateException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Component;

@Order(Ordered.LOWEST_PRECEDENCE - 1)
@Aspect
@Component
public class OptimisticLockRetryAspect {
private static final int MAX_RETRIES = 1000;
private static final int RETRY_DELAY_MS = 100;

@Pointcut("@annotation(Retry)")
public void retry() {
}

@Around("retry()")
public Object retryOptimisticLock(ProceedingJoinPoint joinPoint) throws Throwable {
Exception exceptionHolder = null;
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return joinPoint.proceed();
} catch (OptimisticLockException | ObjectOptimisticLockingFailureException | StaleObjectStateException e) {
exceptionHolder = e;
Thread.sleep(RETRY_DELAY_MS);
}
}
throw exceptionHolder;
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/formssafe/global/aop/Retry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.formssafe.global.aop;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class 설문_상세_조회 {
}

@Test
void 작성자가아닌_사용자가_설문_상세_조회시_예외가_발생한다() {
void 작성자가_아닌_사용자가_설문_상세_조회_시_예외가_발생한다() {
//given
Form form = formRepository.save(createForm(testUser, "설문1", "설문설명1"));
LoginUserDto loginUserDto = new LoginUserDto(testUser.getId() + 1);
Expand All @@ -108,7 +108,7 @@ class 설문_상세_조회 {
}

@Test
void 작성자가_임시설문이_아닌_설문_상세_조회시_예외가_발생한다() {
void 작성자가_임시설문이_아닌_설문_상세_조회_시_예외가_발생한다() {
//given
Form form = formRepository.save(createForm(testUser, "설문1", "설문설명1"));
LoginUserDto loginUserDto = new LoginUserDto(testUser.getId());
Expand All @@ -133,7 +133,7 @@ class 수동_설문_종료 {
}

@Test
void 로그인유저와_설문작성자가_다르다면_예외가_발생한다() {
void 로그인_유저와_설문_작성자가_다르다면_예외가_발생한다() {
//given
Form form = formRepository.save(createForm(testUser, "설문1", "설문설명1"));
LoginUserDto loginUser = new LoginUserDto(testUser.getId() + 1);
Expand All @@ -154,7 +154,7 @@ class 수동_설문_종료 {

@EnumSource(value = FormStatus.class, names = {"NOT_STARTED", "DONE", "REWARDED"})
@ParameterizedTest
void 설문이_진행중이_아니라면_예외가_발생한다(FormStatus status) {
void 설문이_진행_중이_아니라면_예외가_발생한다(FormStatus status) {
//given
LocalDateTime endDate = LocalDateTime.of(2024, 1, 1, 0, 0, 0);
Form form = formRepository.save(
Expand All @@ -181,7 +181,7 @@ class 설문_삭제 {
}

@Test
void 이미_삭제된_설문_삭제시_예외가_발생한다() {
void 이미_삭제된_설문_삭제_시_예외가_발생한다() {
//given
Form form = formRepository.save(createDeletedForm(testUser, "설문1", "설문설명1"));
LoginUserDto loginUserDto = new LoginUserDto(testUser.getId());
Expand All @@ -191,7 +191,7 @@ class 설문_삭제 {
}

@Test
void 설문작성자와_로그인유저가_다르다면_예외가_발생한다() {
void 설문_작성자와_로그인_유저가_다르다면_예외가_발생한다() {
//given
Form form = formRepository.save(createForm(testUser, "설문1", "설문설명1"));
LoginUserDto loginUserDto = new LoginUserDto(testUser.getId() + 1);
Expand All @@ -205,7 +205,7 @@ class 설문_삭제 {
class 설문_마감 {

@Test
void 경품없는_설문을_마감한다() {
void 경품_없는_설문을_마감한다() {
//given
Form form = formRepository.save(createForm(testUser, "설문1", "설문설명1"));
LoginUserDto loginUserDto = new LoginUserDto(testUser.getId());
Expand All @@ -217,7 +217,7 @@ class 설문_마감 {
}

@Test
void 경품있는_설문을_마감한다() {
void 경품_있는_설문을_마감한다() {
//given
List<User> users = createUsers(5);
List<User> deletedUsers = List.of(
Expand Down Expand Up @@ -260,7 +260,7 @@ class 설문_마감 {
}

@Test
void 설문작성자와_로그인유저가_다르다면_예외가_발생한다() {
void 설문_작성자와_로그인_유저가_다르다면_예외가_발생한다() {
//given
Form form = formRepository.save(createForm(testUser, "설문1", "설문설명1"));
LoginUserDto loginUserDto = new LoginUserDto(testUser.getId() + 1);
Expand All @@ -271,7 +271,7 @@ class 설문_마감 {

@EnumSource(value = FormStatus.class, names = {"NOT_STARTED", "DONE", "REWARDED"})
@ParameterizedTest
void 진행중인_설문이_아니라면_예외가_발생한다(FormStatus status) {
void 진행_중인_설문이_아니라면_예외가_발생한다(FormStatus status) {
//given
Form form = formRepository.save(createFormWithStatus(testUser, "설문1", "설문설명1", status));
LoginUserDto loginUserDto = new LoginUserDto(testUser.getId());
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/com/formssafe/util/Fixture.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,27 @@ public static Form createForm(User author,
.build();
}

public static Form createForm(User author,
String title,
String detail,
int questionCnt) {
return Form.builder()
.user(author)
.title(title)
.imageUrl(null)
.detail(detail)
.startDate(LocalDateTime.now())
.endDate(LocalDateTime.now().plusDays(2))
.expectTime(10)
.isEmailVisible(false)
.privacyDisposalDate(null)
.status(FormStatus.PROGRESS)
.isTemp(false)
.isDeleted(false)
.questionCnt(questionCnt)
.build();
}

/**
* 삭제된 설문 엔티티를 생성한다.
*
Expand Down
2 changes: 1 addition & 1 deletion src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ spring:
open-in-view: false
database: mysql
hibernate:
ddl-auto: create-drop
ddl-auto: create
defer-datasource-initialization: true
show-sql: true
properties:
Expand Down