project list 조회 기능 2차 refactoring

권희·2025년 9월 20일

졸업작품

목록 보기
3/7

목차

  1. 현재 project list api와 관련된 db 설명
  2. query dsl 도입 (생략됨)
  3. refactoring
  4. 마무리

1. 현재 project list api와 관련된 db 설명

project list에 들어가는 정보

  1. project 정보 (이름, 만료일자, 만료 여부)
  2. project tag (tag table, M대N 관계)
  3. project 대표 이미지
  4. project like count(좋아요 갯수) (like table)
  5. is Like (나의 좋아요 여부) (like table)
  6. funding count (funding한 총 갯수) (Funding관련 table)
  7. completionRate (완성도 : 펀딩 금액 / 목표 금액 * 100) (Funding관련 tabel)
  8. view count (조회수) (project_view table)
  9. paging 기능 (tatalCount, PageCount, page)

관련 db table들

  1. project table
  2. project img table
  3. project tag table
  4. funding 관련 table (FreeFunding, OptionFunding)
  5. like table
  6. project view table

2. query dsl 도입

1차 refactoring query dsl 도입

query dsl 도입

public List<ResponseProjectListDto> selectProjectDsl(SelectProjectListRequestDto dto) {
        return queryFactory
            .selectDistinct(Projections.constructor(ResponseProjectListDto.class,
                projectEntity.기본 정보들,
                // completeRate
                completeRate(projectEntity),
                // like count
                likeCount(projectEntity),
                // user_count
                fundingUserCount(projectEntity),
                // is_like
                isLike(projectEntity, dto.getUserId()),
                // view_count
                viewCount(projectEntity),
                // total _ select count :: totalCount
                totalCount(projectEntity, dto)
            ))
            .from(projectEntity)
            .leftJoin(생략)
            .where(
                where(projectEntity, projectTagEntity, dto.getUserId(), dto.getTagIds(), dto.getSearch(), dto.getMyPageWhere())
            )
            .orderBy(
                orderByType(projectEntity, dto.getUserId(), ProjectOrderType.getType(dto.getOrder()), dto.getDesc()),
                projectEntity.createdAt.desc()
            )
            .limit(pageCount)
            .offset((dto.getPage() - 1) * pageCount)
            .fetch();
    }

1. where 절

조건에 따른 where 절 작성

tag(List tadIds)를 통한 tag 조건
search(String)를 통한 제목 검색

2. order by 절

order Type에 따른 order 절 작성

COMPLETION, FUNDING_COUNT, CREATED, EXPIRED, VIEW_COUNT

3. refactoring

문제점

project list 조회 api를 사용하는 service가 더 필요해졌음 이에 따른 기능 확장 필요

main page : tag, search 별 project list 조회
my page : 내가 펀딩한, 내가 좋아요한, 내가 최근 조회한 project list 조회

2개 이상의 service에서 조건에 맞는 Project List api를 만들어야 할 필요가 생겼다.
간단하게 구현한다면 그냥 한개의 함수에서 모든 기능에 필요한 paramter(Type)을 받고 엄청나게 많은 분기점을 잡으면 된다.

  //모든 기능에서 사용되는 모든 Type값들을 Parameter로 받고
  public List<ResponseProjectListDto> selectProjectDsl(
  	Long userId, List<Long> tagIds, String search, 
    MyPageType type, OrderByType orderType, Long limit, Long pageCount
  );
  // where절, order 절에 모든 type들의 분기점을 잡으면 된다
  // (order by는 where절도 필요하니 where절의 약 2배 정도 된다)
 	BooleanExpression where(List<Long>tagIds, String search, MyPageType type) {
      if(...)
      if(...)
  	  if(...)
      ...
  }

결론 : 유지보수, 확장 불가능 / 다른 table(project view, like table)과도 추가 확장이 필요한 만큼 위의 방법만큼은 용납할 수 없다.

목표 설정 : where, order by 부분을 Service에서 구현한다

query dsl을 사용방법을 보며 한 가지의 방식을 떠올렸다

  queryFactory
  	.select(...)
  	.where(...)
  	.fetch()

query dsl처럼 where절과 order by절을 람다로 구현 할 수 있다면?
service부분에서 필요한 project들의 where절과 oreder 절을 구현하고 ProjectSelectRepo로 넘겨줄 수 있다면?

  구현 목표 (다른 Service별 사용 방법 예시)
    List<ProjectListDto> list = ProjectSelectListRepos
        .select()
        .where((QEntity)->{}) // QProjectEntity 외 다른 QEntity까지
        .oderBy((QEntity)->{}) // default project.createdAt desc
        .limit()
        .offset()
        .commit()

여러개의 service로 확장한다 하더라도 각 service에 맞는 where, order by절만 구현하면 됨으로 중복 코드가 발생하지 않는다.

구현 방법 시도

그렇다면 위의 목표대로 where, order by절을 외부에서 주입 받는게 가능한까?
일단 구현에 필요한 FunctionalInterface부터 만든다

@FunctionalInterface
public interface FunctionInterface<RT, T extends EntityPathBase> {
    RT function(T entity);
}
  // RT는 return type의 약자, T는 QEntity.class들 중 한개이다
  // where, order by절에 필요한 QEntity(QProjectEntity, QProjectTagEntity, ...) 중 한개이다

이런 FunctionalInterface를 Service에서 구현하고 넘겨 받으면 목표를 구현할 수 있을 것이다

    // 받는 부분
  public void where(Class<T> entityClass, FunctionalInterface whereFunction) {
  	BooleanExpression where = whereFunction.function(projectTagEntity);
  }
    // service(주는 부분), where절 구현
  .where(QProjectTagEntity.class, new FunctionalInterfacr<BooleanExpression,  QProjectTagEntity.class> {
  		@Override
  		where(QProjectTagEntity tag) {return tag.id.in(tagIds);}
  	})

하지만 QEntity값은 첫번 째 파라미터 Class entityClass로 충분히 추정 가능하기 때문에 Service에서 FunctionInterface를 직접 선언할 필요가 없다

    // 받는 부분
  public void where(Class<T> entityClass, FunctionalInterface<BooleanExpression, T>whereFunction) {
  	BooleanExpression where = whereFunction.function(projectTagEntity);
  }
    // service(주는 부분), where절 구현
  .where(QProjectTagEntity.class, (projectTag)->tag.id.in(tagIds));

Builder class 추가

where, order 절을 query dsl 함수에서 바로 받으면 한 번만 받을 수 있다
여러개의 where, order by절을 받기 위해 builder class를 추가한다

  public class Builder {
        private BooleanExpression where = Expressions.TRUE;
        private List<OrderSpecifier> orderBy = new ArrayList<>();
        private long limit;
        private long page;
  
  		private <T extends EntityPathBase> T getEntity(Class<T> entityClass) {
            if(entityClass.equals(QProjectTagEntity.class))
                return (T) projectTagEntity;
            if(entityClass.equals(QProjectViewEntity.class))
                return (T) projectViewEntity;
            if(entityClass.equals(QLikeEntity.class))
                return (T) likeEntity;
            else
                return (T) projectEntity;
        }
  }

이제 Builder class에 위에 구현한 where, order by함수를 추가한다

  public class Builder {
  	public <T extends EntityPathBase> Builder where(Class<T> entityClass, FunctionInterface<BooleanExpression, T> whereFunction) {
      where = where.and(whereFunction.function(getEntity(entityClass))); return this;
    }
    public <T extends EntityPathBase> Builder orderBy(Class<T> entityClass, FunctionInterface<OrderSpecifier, T> whereFunction) {
      orderBy.add(whereFunction.function(getEntity(entityClass))); return this;
    }
  }

목표 구현 / 사용 예시

이제 builder class를 통해서 where, order 절을 받고 각각 where, orderBy 변수에 추가하면 된다
builder class와 service부분 사용한 실제 사용 예시이다

  public class Builder {
        private BooleanExpression where = Expressions.TRUE;
        private List<OrderSpecifier> orderBy = new ArrayList<>();
        private long limit;
        private long page;

        public <T extends EntityPathBase> Builder where(Class<T> entityClass, FunctionInterface<BooleanExpression, T> whereFunction) {
            where = where.and(whereFunction.function(getEntity(entityClass))); return this;
        }
        public <T extends EntityPathBase> Builder orderBy(Class<T> entityClass, FunctionInterface<OrderSpecifier, T> whereFunction) {
            orderBy.add(whereFunction.function(getEntity(entityClass))); return this;
        }
        public Builder limit(long limit, long page) {
            this.limit = limit; this.page = page; return this;
        }
        public ResponseProjectListDto commit(Long userId){
            orderBy.add(projectEntity.createdAt.desc());
            List<ResponseProjectListDetailDto> projectList = selectProjectsWithSingle(userId, where, orderBy.toArray(OrderSpecifier[]::new), limit, page * limit);
            projectList = setTag(projectList);
            return new ResponseProjectListDto(projectList, totalCount(where), limit, page);
        }

        private <T extends EntityPathBase> T getEntity(Class<T> entityClass) {
            if(entityClass.equals(QProjectTagEntity.class))
                return (T) projectTagEntity;
            if(entityClass.equals(QProjectViewEntity.class))
                return (T) projectViewEntity;
            if(entityClass.equals(QLikeEntity.class))
                return (T) likeEntity;
            else
                return (T) projectEntity;
        }
    }
  
  실 사용 예시
  public List<ResponseProjectDto> selectProjectDsl(RequestMainPageProjectListDto dto) {
        List<ResponseProjectDto> projectList = selectProjectRepository
                .builder()
                .where(QProjectEntity.class, (project)->whereSearch(project, dto.getSearch()))
                .where(QProjectEntity.class, (project)->whereTag(project, dto.getTagIds()))
                .orderBy(QProjectEntity.class, (project)->orderByType(project, ProjectOrderType.getType(dto.getOrder()), dto.getDesc()))
                .limit(dto.getLimit(), dto.getPage())
                .commit(dto.getUserId());

        return projectList;
    }

이제 다른 service에서 확장을 해도, 이전 기능들에 다른 where조건을 추가할 때도 언제나 Service부분에서 충분히 처리가 가능해졌다. (목표 달성!)

4. 마무리

각각의 service에서 충분히 사용가능할 정도까지 refactoring이 진행되었다. 하지만 그만큼 project dsl repository의 중요성이 올라가게되었다. main query가 최적화 되지 않는다면 모든 service에서의 query또한 느려진다.

이후 추가 refactoring 필요한 부분

  1. null값 체크의 부족
    userId값이 null값인 경우에 대한 null 값 체크가 부족하다
  2. left join 또한 외부 설정
    현재 방법은 모든 service의 query 실행을 위해 필요한 모든 table을 left join하는 형식이다. 이는 필요 없는 db연산을 초래한다.

언제나 다른 사람들의 의견을 기다리고 있습니다!

profile
나의 개발 기록

2개의 댓글

comment-user-thumbnail
2025년 9월 20일

언제나 다른 사람들의 의견을 기다리고 있습니다!

답글 달기
comment-user-thumbnail
2025년 9월 23일

트레이드오프에 대한 고민이 잘 보이는 글이네요.
잘봤습니다.

답글 달기