Skip to content

Commit

Permalink
refactor: ✏️ 매일 정기 푸시 알림 배치 성능 개선 (#137)
Browse files Browse the repository at this point in the history
* chore: application-domain.yml jdbc url query parameter 수정

* style: step config 파일 삭제 -> job config에 통합

* style: dto 패키지 경로 common 하위로 수정

* fix: notificaion batch insert ; 제거 && batch size 1000으로 수정

* feat: where 함수형 인터페이스 정의

* feat: where expression 정의

* feat: order expression 상수 정의

* feat: expression 상수 정의

* feat: querydsl_no_offset_options 추상 클래스 정의

* feat: no offset의 타입이 number인 경우를 위한 구현체 정의

* rename: 정적 팩토리 메서드 주석에 주의 사항 추가

* feat: no offset의 타입이 string인 경우를 위한 구현체 정의

* feat: querydsl_paging_item_reader 추가

* feat: querydsl_no_offset_paging_item_reader 정의

* fix: repository_item_reader -> querydsl_no_offset_paging_item_reader 변경

* fix: @job_scope 및 @step_scope 추가 && step reader 수정

* fix: device_token_custom_repository 제거

* test: device_token_cutome_repository 테스트 제거

* style: device_token_owner 경로 domain -> batch로 수정

* test: redisson 테스트 ignore 처리

* chore: batch application db connection pool 2개로 수정
  • Loading branch information
psychology50 authored Jul 24, 2024
1 parent 4f51e7b commit 381f3ea
Show file tree
Hide file tree
Showing 27 changed files with 745 additions and 328 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package kr.co.pennyway.batch.dto;
package kr.co.pennyway.batch.common.dto;

import kr.co.pennyway.domain.domains.device.dto.DeviceTokenOwner;
import kr.co.pennyway.domain.domains.notification.type.Announcement;
import lombok.Builder;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package kr.co.pennyway.domain.domains.device.dto;
package kr.co.pennyway.batch.common.dto;

/**
* 디바이스 토큰과 유저 아이디를 담은 DTO
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package kr.co.pennyway.batch.common.reader;

import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import kr.co.pennyway.batch.common.reader.options.QuerydslNoOffsetOptions;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;

import java.util.function.Function;

public class QuerydslNoOffsetPagingItemReader<T> extends QuerydslPagingItemReader<T> {
private QuerydslNoOffsetOptions<T> options;

private QuerydslNoOffsetPagingItemReader() {
super();
setName(ClassUtils.getShortName(QuerydslNoOffsetPagingItemReader.class));
}

public QuerydslNoOffsetPagingItemReader(EntityManagerFactory entityManagerFactory,
int pageSize,
QuerydslNoOffsetOptions<T> options,
Function<JPAQueryFactory, JPAQuery<T>> queryFunction) {
super(entityManagerFactory, pageSize, queryFunction);
setName(ClassUtils.getShortName(QuerydslNoOffsetPagingItemReader.class));
this.options = options;
}

@Override
@SuppressWarnings("unchecked")
protected void doReadPage() {

EntityTransaction tx = getTxOrNull();

JPQLQuery<T> query = createQuery().limit(getPageSize());

initResults();

fetchQuery(query, tx);

resetCurrentIdIfNotLastPage();
}

@Override
protected JPAQuery<T> createQuery() {
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
JPAQuery<T> query = queryFunction.apply(queryFactory);
options.initKeys(query, getPage()); // 제일 첫번째 페이징시 시작해야할 ID 찾기

return options.createQuery(query, getPage());
}

private void resetCurrentIdIfNotLastPage() {
if (isNotEmptyResults()) {
options.resetCurrentId(getLastItem());
}
}

// 조회결과가 Empty이면 results에 null이 담긴다
private boolean isNotEmptyResults() {
return !CollectionUtils.isEmpty(results) && results.get(0) != null;
}

private T getLastItem() {
return results.get(results.size() - 1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package kr.co.pennyway.batch.common.reader;

import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import org.springframework.batch.item.database.AbstractPagingItemReader;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;

/**
* Querydsl을 이용한 커스텀 PagingItemReader
* <p>
* <a href="https://github.com/jojoldu/spring-batch-querydsl">이동욱님 깃헙 </a> 참고
*/
public class QuerydslPagingItemReader<T> extends AbstractPagingItemReader<T> {

protected final Map<String, Object> jpaPropertyMap = new HashMap<>();
protected EntityManagerFactory entityManagerFactory;
protected EntityManager entityManager;
protected Function<JPAQueryFactory, JPAQuery<T>> queryFunction;
protected boolean transacted = true; // default value

protected QuerydslPagingItemReader() {
setName(ClassUtils.getShortName(QuerydslPagingItemReader.class));
}

public QuerydslPagingItemReader(EntityManagerFactory entityManagerFactory,
int pageSize,
Function<JPAQueryFactory, JPAQuery<T>> queryFunction) {
this(entityManagerFactory, pageSize, true, queryFunction);
}

public QuerydslPagingItemReader(EntityManagerFactory entityManagerFactory,
int pageSize,
boolean transacted,
Function<JPAQueryFactory, JPAQuery<T>> queryFunction) {
this();
this.entityManagerFactory = entityManagerFactory;
this.queryFunction = queryFunction;
setPageSize(pageSize);
setTransacted(transacted);
}

/**
* Reader의 트랜잭션격리 옵션 <br/>
* - false: 격리 시키지 않고, Chunk 트랜잭션에 의존한다 <br/>
* (hibernate.default_batch_fetch_size 옵션 사용가능) <br/>
* - true: 격리 시킨다 <br/>
* (Reader 조회 결과를 삭제하고 다시 조회했을때 삭제된게 반영되고 조회되길 원할때 사용한다.)
*/
public void setTransacted(boolean transacted) {
this.transacted = transacted;
}

@Override
protected void doOpen() throws Exception {
super.doOpen();

entityManager = entityManagerFactory.createEntityManager(jpaPropertyMap);
if (entityManager == null) {
throw new DataAccessResourceFailureException("Unable to obtain an EntityManager");
}
}

@Override
@SuppressWarnings("unchecked")
protected void doReadPage() {
EntityTransaction tx = getTxOrNull();

JPQLQuery<T> query = createQuery()
.offset(getPage() * getPageSize())
.limit(getPageSize());

initResults();

fetchQuery(query, tx);
}

protected EntityTransaction getTxOrNull() {
if (transacted) {
EntityTransaction tx = entityManager.getTransaction();
tx.begin();

entityManager.flush();
entityManager.clear();
return tx;
}

return null;
}

protected JPAQuery<T> createQuery() {
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
return queryFunction.apply(queryFactory);
}

protected void initResults() {
if (CollectionUtils.isEmpty(results)) {
results = new CopyOnWriteArrayList<>();
} else {
results.clear();
}
}

/**
* where 의 조건은 id max/min 을 이용한 제한된 범위를 가지게 한다
*
* @param query
* @param tx
*/
protected void fetchQuery(JPQLQuery<T> query, EntityTransaction tx) {
if (transacted) {
results.addAll(query.fetch());
if (tx != null) {
tx.commit();
}
} else {
List<T> queryResult = query.fetch();
for (T entity : queryResult) {
entityManager.detach(entity);
results.add(entity);
}
}
}

@Override
protected void doClose() throws Exception {
entityManager.close();
super.doClose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package kr.co.pennyway.batch.common.reader.expression;

import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.StringPath;

public enum Expression {
ASC(WhereExpression.GT, OrderExpression.ASC),
DESC(WhereExpression.LT, OrderExpression.DESC);

private final WhereExpression where;
private final OrderExpression order;

Expression(WhereExpression where, OrderExpression order) {
this.where = where;
this.order = order;
}

public boolean isAsc() {
return this == ASC;
}

public BooleanExpression where(StringPath id, int page, String currentId) {
return where.expression(id, page, currentId);
}

public <N extends Number & Comparable<?>> BooleanExpression where(NumberPath<N> id, int page, N currentId) {
return where.expression(id, page, currentId);
}

public OrderSpecifier<String> order(StringPath id) {
return isAsc() ? id.asc() : id.desc();
}

public <N extends Number & Comparable<?>> OrderSpecifier<N> order(NumberPath<N> id) {
return isAsc() ? id.asc() : id.desc();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kr.co.pennyway.batch.common.reader.expression;

public enum OrderExpression {
ASC, DESC
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package kr.co.pennyway.batch.common.reader.expression;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.StringPath;

/**
* 첫페이지 조회시에는 >=, <=
* 두번째 페이지부터는 >, <
*/
public enum WhereExpression {
GT(
(id, page, currentId) -> page == 0 ? id.goe(currentId) : id.gt(currentId),
(id, page, currentId) -> page == 0 ? id.goe(currentId) : id.gt(currentId)),
LT(
(id, page, currentId) -> page == 0 ? id.loe(currentId) : id.lt(currentId),
(id, page, currentId) -> page == 0 ? id.loe(currentId) : id.lt(currentId)
);

private final WhereStringFunction string;
private final WhereNumberFunction number;

WhereExpression(WhereStringFunction string, WhereNumberFunction number) {
this.string = string;
this.number = number;
}

public BooleanExpression expression(StringPath id, int page, String currentId) {
return this.string.apply(id, page, currentId);
}

public <N extends Number & Comparable<?>> BooleanExpression expression(NumberPath<N> id, int page, N currentId) {
return this.number.apply(id, page, currentId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.co.pennyway.batch.common.reader.expression;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.NumberPath;

@FunctionalInterface
public interface WhereNumberFunction<N extends Number & Comparable<?>> {
BooleanExpression apply(NumberPath<N> id, int page, N currentId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.co.pennyway.batch.common.reader.expression;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.StringPath;

@FunctionalInterface
public interface WhereStringFunction {
BooleanExpression apply(StringPath id, int page, String currentId);
}
Loading

0 comments on commit 381f3ea

Please sign in to comment.