Skip to content

Commit

Permalink
Merge pull request #6 from youKeon/BE/#5
Browse files Browse the repository at this point in the history
Be/#5 : Spring Batch를 적용한 문제 완전 삭제 기능 구현
  • Loading branch information
youKeon authored Aug 31, 2023
2 parents 661796b + d0a8fda commit 70b208b
Show file tree
Hide file tree
Showing 18 changed files with 264 additions and 102 deletions.
6 changes: 4 additions & 2 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ jacocoTestReport {
"**/Q*",
'**/dto/**',
'**/exception/**',
'**/batch/**',
'**/BaseEntity*',
// '**/batch/**',
"**/*Application*",
"**/global/**"
])
Expand Down Expand Up @@ -139,7 +140,8 @@ jacocoTestCoverageVerification {
'*.Q*',
'*.*Exception',
'*.dto.*',
'*.batch.*',
// '*.batch.*',
'**.*BaseEntity*',
'*.global.*',
'*.BaseEntity',
]
Expand Down
2 changes: 2 additions & 0 deletions backend/lombok.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.psq.backend.batch;

import com.psq.backend.problem.domain.Problem;
import com.psq.backend.problem.persistence.ProblemRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
@EnableBatchProcessing
@RequiredArgsConstructor
public class BatchConfiguration {

private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;

private final ProblemRepository problemRepository;

@Bean
public Job removeDeletedProblemJob() {
log.info("[문제 완전 삭제] job start");
Job job = jobBuilderFactory.get("job")
.start(removeDeletedProblemStep())
.build();
return job;
}

@Bean
public Step removeDeletedProblemStep() {
log.info("[문제 완전 삭제] step start");
return stepBuilderFactory.get("step")
.tasklet((contribution, chunkContext) -> {
long deletedCount = problemRepository.deleteSoftDeletedProblem();
log.info("[문제 완전 삭제] 삭제된 문제 : {} 문제", deletedCount);
return RepeatStatus.FINISHED;
})
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.persistence.Column;

@Component
@RequiredArgsConstructor
Expand All @@ -22,7 +20,9 @@ public class BatchScheduler {
private final Job problemDeleteJob;

@Scheduled(cron = "0 0 0 * * ?") // 자정에 실행
void launchProblemDeletionJob() throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException {
void deleteProblemJob() throws JobExecutionAlreadyRunningException, JobRestartException,
JobInstanceAlreadyCompleteException, JobParametersInvalidException {

JobParameters jobParameters = new JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.toJobParameters();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.psq.backend.problem.domain.Problem;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import javax.persistence.*;

Expand All @@ -17,11 +19,14 @@ public class Bookmark extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "problem_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private Problem problem;

public Bookmark(Member member, Problem problem) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.psq.backend.bookmark.exception.DuplicatedBookmarkException;
import com.psq.backend.bookmark.exception.NoSuchBookmarkException;
import com.psq.backend.member.exception.DuplicatedEmailException;
import com.psq.backend.member.exception.InvalidMemberException;
import com.psq.backend.member.exception.NoSuchMemberException;
import com.psq.backend.problem.exception.InvalidProblemException;
Expand Down Expand Up @@ -60,6 +61,7 @@ public ResponseEntity<ErrorResponse> handleNoSuchData(final RuntimeException e)
InvalidProblemException.class,
InvalidMemberException.class,
DuplicatedBookmarkException.class,
DuplicatedEmailException.class,
NotDeletedProblemException.class,
})
public ResponseEntity<ErrorResponse> handleInvalidData(final RuntimeException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.Optional;

public interface ProblemCustomRepository {
Expand All @@ -15,4 +16,6 @@ Page<Problem> findAllProblem(Long memberId,
Pageable pageable);

Optional<Problem> pollProblem(Long memberId);

long deleteSoftDeletedProblem();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -60,4 +61,15 @@ public Optional<Problem> pollProblem(Long memberId) {
.fetchOne()
);
}

@Override
public long deleteSoftDeletedProblem() {
return jpaQueryFactory
.delete(problem)
.where(
problem.isDeleted.isTrue(),
problem.updatedAt.before(LocalDateTime.now().minusDays(3))
)
.execute();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface ProblemRepository extends JpaRepository<Problem, Long>, ProblemCustomRepository {
@Query(value = "SELECT * " +
"FROM Problem " +
"WHERE is_deleted = true " +
"AND " +
"DATE(updated_at) < DATE_SUB(NOW(), INTERVAL 3 DAY)"
, nativeQuery = true)
Page<Problem> findDeletedProblems(Pageable pageable);
}
1 change: 1 addition & 0 deletions backend/src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ spring:
show-sql: true
hibernate:
ddl-auto: create

2 changes: 1 addition & 1 deletion backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ spring:
server:
servlet:
session:
timeout: 315360000
timeout: 315350000
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.psq.backend.batch;

import com.psq.backend.member.domain.Member;
import com.psq.backend.member.persistence.MemberRepository;
import com.psq.backend.problem.domain.Category;
import com.psq.backend.problem.domain.Problem;
import com.psq.backend.problem.persistence.ProblemRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.*;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.test.context.SpringBatchTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.util.ReflectionTestUtils;

import java.time.LocalDateTime;
import java.time.Month;
import java.util.List;

import static org.assertj.core.api.Assertions.*;

@SpringBatchTest
@ActiveProfiles("test")
@SpringBootTest
public class RemoveDeletedProblemTest {

@Autowired
private BatchScheduler batchScheduler;

@Autowired
private JobLauncher jobLauncher;

@Autowired
private ProblemRepository problemRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private Job problemDeleteJob;

@AfterEach
public void cleanup() {
problemRepository.deleteAll();
memberRepository.deleteAll();
}

@BeforeEach
public void setup() {
Member member = new Member("[email protected]", "password", "salt");
memberRepository.save(member);

LocalDateTime dateTime = LocalDateTime.of(2023, Month.AUGUST, 14, 0, 0);

// 삭제 대상 Problem 데이터 7개 생성
for (int i = 0; i < 7; i++) {
Problem problem = new Problem(member, "title" + i, "url" + i, i % 5 + 1, Category.DFS, false);
problemRepository.save(problem);
problem.softDelete();
ReflectionTestUtils.setField(problem, "updatedAt", dateTime);
problemRepository.save(problem);
}

// 일반 Problem 데이터 5개 생성
for (int i = 0; i < 5; i++) {
problemRepository.save(new Problem(member, "title" + (i + 7), "url" + (i + 7), i % 5 + 1, Category.DFS, false));
}
}

@Test
@DisplayName("BatchScheduler를 통해 problemDeleteJob을 실행시킨다")
public void removeDeletedProblemJobTest() throws Exception {
// when
batchScheduler.deleteProblemJob();
List<Problem> actual = problemRepository.findAll();

// then
assertThat(actual).hasSize(5);
}

@Test
@DisplayName("삭제된지 3일이 지난 문제는 완전 삭제된다")
public void removeDeletedProblemTest() throws Exception {
// when
JobParameters jobParameters = new JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.toJobParameters();

JobExecution execution = jobLauncher.run(problemDeleteJob, jobParameters);

List<Problem> actual = problemRepository.findAll();

// then 전체 문제(12) - 삭제된 문제(7) = 5
assertThat(execution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
assertThat(actual).hasSize(5);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ public abstract class ServiceTest {
@Mock
protected ProblemRepository problemRepository;
@Mock
protected PasswordUtil passwordUtil;
@Mock
protected HttpServletRequest request;

protected static Pageable pageable = PageRequest.of(0, 3);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.psq.backend.common.annotation;

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
@EnableBatchProcessing
public class TestBatchConfig {}
Loading

0 comments on commit 70b208b

Please sign in to comment.