0) TL;DR ๐งพ
- ๋ฌธ์ : ์กฐ๊ฑด ์กฐํฉ ํญ๋ฐ, ๊ฑฐ๋ ๋ฉ์๋, JPA -> MyBatis ์ ํ ๋ฑ ๋ฌธ์
- ํด๋ฒ:
Predicate(์กฐ๊ฑด), Sort(์ ๋ ฌ), Score(์ธ๊ธฐ ์ ์) ๋ฑ์ ํ์ผ๋ก ๋ถ๋ฆฌํ๊ณ ์ด๋ํฐ์์ ์ ์ธ์ ์ผ๋ก ์กฐ๋ฆฝํ๋ค.
- ํจ๊ณผ: โจ ๋ณ๊ฒฝ ์ฉ์ด ยท ๐งช ํ
์คํธ์ฑ ยท ๐งฉ ์ฌ์ฌ์ฉ์ฑ ยท ๐งญ ์ํคํ
์ฒ ์ผ๊ด์ฑ.
๐ฏ 1) ์ ์ด ๊ตฌ์กฐ์ธ๊ฐ?
๐ต ํ์ฅ์์ ๊ฒช๋ ๋ฌธ์
- ํํฐ ์กฐ๊ฑด์ด ๋์ด๋ ์๋ก ๋ณต์กํด์ง
- ํค์๋, ์ฃผ์ , ์์ค, ํ์
, ์ฐ๋โฆ ์กฐ๊ฑด์ด ๋ง์์ง๋ ์ฝ๋๊ฐ if/else๋ก ๋ค์ํจ๋ค.
- ํ ๋ฉ์๋๊ฐ ๋๋ฌด ์ปค์ง
- ์ ๋ ฌ ์ต์
(์ต์ ์, ๋ค์ด๋ก๋์, ํ์ฉ์ ๋ฑ), ์ธ๊ธฐ ์ ์ ๊ณ์ฐ, ์กฐ์ธ, ๊ทธ๋ฃน, ํ์ด์ง๊น์ง ํ ๋ฉ์๋ ์์ ๋ค ๋ชฐ๋ ค์๋ค.
- ์์ ๋ณ๊ฒฝ๋ ํฐ ์ํฅ
- ํํฐ ํ๋๋ง ๋ฐ๊ฟจ๋๋ฐ ์ ์ฒด ์ฟผ๋ฆฌ๊ฐ ํ๋ค๋ฆฌ๊ณ , ์ฌ์ด๋์ดํํธ๊ฐ ์์ฃผ ๋ฐ์ํ๋ค.
- ๋ก์ง์ด ์ง์ ๋ถํด์ง
๐ฏ ์ฐ๋ฆฌ๊ฐ ์ธ์ด ๋ชฉํ
- ์กฐ๊ฑดยท์ ๋ ฌยท์ง๊ณ๋ ๋ฐ๋ก๋ฐ๋ก
- ํ ๊ฐ์ง ์ฑ
์๋ง ๋งก๋๋ก ์ชผ๊ฐ๊ณ , ์ด๋ํฐ์์ ํผ์ฆ ๋ง์ถ๋ฏ ์กฐ๋ฆฝํ๋ค.
- ์
๋ ฅ์ด ์์ผ๋ฉด ์๋ ๋ฌด์
- null๋ก ๊ฐ์ด ์์ผ๋ฉด where ์ ์์ ๊ทธ๋ฅ ๋น ์ ธ์, ๊น๋ํ๊ฒ ํํฐ ์กฐํฉ์ด ๊ฐ๋ฅํ๋ค.
- ์ฟผ๋ฆฌ ๋ํ
์ผ์ ์ด๋ํฐ์ ๋ชฐ์๋ฃ๋๋ค.
- ์ฝ๊ธฐ ์ฟผ๋ฆฌ๋ ์์ ๋กญ๊ฒ ์ต์ ํ
- ํ์ํ ๋ ์กฐ์ธยทํ๋ก์ ์
ยทํ์ด์ง์ ๋ง์๊ป ์จ๋ ์ฝ์ด ๋ก์ง์ ํ๋ค๋ฆฌ์ง ์๋๋ค.
๐ 2) ํ๋์ ๋ณด๋ ํ๋ฆ
[Web Controller]
โ [UseCase (Port In)]
โ [Service]
โ [Port Out (์ฟผ๋ฆฌ ์ธํฐํ์ด์ค)]
โ [Query Adapter (QueryDSL)]
โโ Predicate ์กฐ๋ฆฝ(ํค์๋/ํ ํฝ/์์ค/ํ์
/์ฐ๋ ๋ฑ)
โโ Sort ์กฐ๋ฆฝ(์ต์ /์ค๋๋/๋ค์ด๋ก๋/ํ์ฉ์)
โโ Score ๊ณ์ฐ(๋ค์ด๋ก๋ยทํ๋ก์ ํธ์ ๊ฐ์ค์น ๊ณ์ฐ)
โโ ์กฐ์ธ/๊ทธ๋ฃน/ํ์ด์ง
โโ ์ํฐํฐ <-> ๋๋ฉ์ธ/DTO ๋งคํ
์ด์ ๊ฐ์์ฑ: LoggerFactory.query().logQueryStart/End(...)๋ก ์ฟผ๋ฆฌ ์์/์ข
๋ฃ๋ฅผ ํ์ค ๋ก๊น
ํ๋ค.
๐๏ธ 3) ํด๋ ๊ตฌ์ฑ(ํต์ฌ)
modules/dataset
โโ adapter
โ โโ jpa
โ โ โโ entity/ QDataEntity ...
โ โ โโ mapper/ DataEntityMapper
โ โโ query
โ โโ predicates/
โ โ โโ DataFilterPredicate.java // id/keyword/topicId/sourceId/typeId
โ โ โโ DataDatePredicate.java // yearBetween
โ โโ sort/
โ โ โโ DataSortBuilder.java // ์ ๋ ฌ ์ต์
โ OrderSpecifier[]
โ โ โโ DataPopularOrderBuilder.java // ์ธ๊ธฐ ์ ์ ๊ณ์ฐ์
โ โโ ReadDataQueryDslAdapter.java // ๋จ๊ฑด/์ฐ๊ฒฐ/์ต๊ทผ/์ธ๊ธฐ/๊ทธ๋ฃน ์นด์ดํธ
โ โโ SearchDataQueryDslAdapter.java // ๋ณตํฉ ํํฐ + ์ ๋ ฌ + ํ์ด์ง
โโ application
โ โโ port/out/query/read/*, /search/*
โ โโ dto/request/search/FilteringDataRequest
โ /response/support/DataWithProjectCountDto
โโ domain
โโ model/Data
โโ enums/DataSortType (LATEST/OLDEST/DOWNLOAD/UTILIZE)
๐งฉ 4) ํต์ฌ ๋น๋ฉ ๋ธ๋ก(์ฝ๋ ๋ฐ์ท)
4.1 Predicate โ ์กฐ๊ฑด์ ๋ฉ์๋๋ก ์ชผ๊ฐ๊ณ null-๋ฌด์
public static BooleanExpression keywordContains(String keyword) {
if (!StringUtils.hasText(keyword)) return null;
return dataEntity.title.containsIgnoreCase(keyword)
.or(dataEntity.description.containsIgnoreCase(keyword));
}
public static BooleanExpression yearBetween(Integer year) {
if (year == null) return null;
NumberTemplate<Integer> s = Expressions.numberTemplate(Integer.class, "year({0})", data.startDate);
NumberTemplate<Integer> e = Expressions.numberTemplate(Integer.class, "year({0})", data.endDate);
return s.loe(year).and(e.goe(year));
}
๋ฌด์์ ํ๋์?
- keywordContains๋ ์ ๋ชฉ/์ค๋ช
์ปฌ๋ผ์ ๋์๋ฌธ์ ๋ฌด์ ๋ถ๋ถ ๊ฒ์์ ๊ฑด๋ค.
- yearBetween์ ์ฃผ์ด์ง ์ฐ๋๊ฐ startDate ~ endDate ์ฌ์ด์ ํฌํจ๋๋์ง๋ฅผ ํ๋จ
์ ์ด๋ ๊ฒ ์ชผ๊ฐ๋์?
- SRP(๋จ์ผ ์ฑ
์): ์กฐ๊ฑด ํ๋๋น ๋ฉ์๋ ํ๋๋ฉด, ๋ณ๊ฒฝ/์ถ๊ฐ/ํ
์คํธ๊ฐ ์ฝ๋ค.
- null-๋ฌด์ ํจํด: ์
๋ ฅ์ด ์์ผ๋ฉด null์ ๋ฐํํด์ where(a, b, null, d)์ฒ๋ผ ์๋์ผ๋ก ์คํต๋๋ค( QueryDSL์ด null์ ๋ฌด์ ).
์ด๋ป๊ฒ ๋์ํ๋์?
- keywordContains: ๊ณต๋ฐฑ/๋น ๋ฌธ์์ด์ ๋ฐ๋ก null ๋ฐํ โ where ์ ์์ ์ ์ธ.
- ์ค์ SQL์ LOWER(title) like %keyword% OR LOWER(description) like %keyword% ํํ๊ฐ ๋๋ค.
- yearBetween: DB ํจ์ year(date)๋ก startDate โค year โค endDate๋ฅผ ๊ฒ์ฌ.
- ํ ์ค๋ก ํฉ์ณ ๊ฐ๋
์ฑ์ ์ฑ๊ธฐ๊ณ , ๋ค๋ฅธ ์กฐ๊ฑด๋ค๊ณผ ์กฐ๋ฆฝ๋๋๋ก BooleanExpression์ ๋ฐํํ๋ค.
๐งช ํ
์คํธ ํฌ์ธํธ:
- keyword = null / "" / " "์ผ ๋ null ๋ฐํ๋๋์ง
- year = null, ๋ฒ์ ๊ฒฝ๊ณ(์: ๊ตฌ๊ฐ ์์/๋ ์ฐ๋) ํฌํจ ์ฌ๋ถ
- ๋ค๊ตญ์ด/๋์๋ฌธ์ ์ผ์ด์ค
4.2 Sort & Score โ ์ ์ธ์ ์ ๋ ฌ / ๊ฐ์ค์น ์ ์
public static OrderSpecifier<?>[] fromSortOption(DataSortType sort, NumberPath<Long> projectCountPath) {
QDataEntity data = QDataEntity.dataEntity;
if (sort == null) return new OrderSpecifier[]{data.createdAt.desc()};
return switch (sort) {
case LATEST -> new OrderSpecifier[]{data.createdAt.desc()};
case OLDEST -> new OrderSpecifier[]{data.createdAt.asc()};
case DOWNLOAD -> new OrderSpecifier[]{data.downloadCount.desc()};
case UTILIZE -> new OrderSpecifier[]{projectCountPath.desc()};
};
}
public static NumberExpression<Double> popularScore(QDataEntity data, NumberExpression<Long> projectCountExpr) {
NumberExpression<Double> projectScore = projectCountExpr.castToNum(Double.class).multiply(1.5);
NumberExpression<Double> downloadScore = data.downloadCount.castToNum(Double.class).multiply(2.0);
return downloadScore.add(projectScore);
}
๋ฌด์์ ํ๋์?
- fromSortOption์ DataSortType์ ๋ฐ๋ผ ์ ๋ ฌ ๊ธฐ์ค์ OrderSpecifier[]๋ก ๋๋ ค์ค๋ค.
(์ต์ /์ค๋๋/๋ค์ด๋ก๋/ํ์ฉ์)
- popularScore๋ ๋ค์ด๋ก๋ร2.0 + ํ๋ก์ ํธ์ร1.5๋ก ์ธ๊ธฐ ์ ์๋ฅผ ๊ณ์ฐํ์ฌ ์ธ๊ธฐ์๋ ํ๋ก์ ํธ ๋ชฉ๋ก์ ์กฐํํ๋ค.
์ ์ด๋ ๊ฒ ์ชผ๊ฐ๋์?
- ์ ์ธ์ ์ ์ด: ์ ๋ ฌ ์ ์ฑ
์ enum์ ๋งตํํ๋ฉด, โ์ด๋ค ๊ธฐ์ค์ผ๋ก ์ ๋ ฌํ๋๊ฐโ๊ฐ ํ๋์ ๋ค์ด์ฌ ์ ์๋ค.
- ์ ๊ธฐ์ค์ด ์๊ฒจ๋ ์ค์์น ํ ์นธ ์ถ๊ฐ๋ก ๋.
- ๋น์ฆ๋์ค ๋ถ๋ฆฌ: ์ธ๊ธฐ ์ ์ ๊ณต์์ ํ ํ์ผ์ ๋ชจ์๋๋ฉด, ๊ฐ์ค์น๋ง ๋ฐ๊ฟ๋ ๋ค๋ฅธ ์ฟผ๋ฆฌ๋ฅผ ์ ๊ฑด๋๋ ค๋ ๊ฐ๋ฅํ๋ค.
4.3 Search ์ด๋ํฐ โ ๋ณตํฉ ํํฐ + ์ ๋ ฌ + ํ์ด์ง
@Override
public Page<DataWithProjectCountDto> searchByFilters(FilteringDataRequest request,
Pageable pageable, DataSortType sortType) {
NumberPath<Long> projectCountPath = Expressions.numberPath(Long.class, "projectCount");
List<Tuple> tuples = queryFactory
.select(data, projectData.id.count().as(projectCountPath))
.from(data)
.leftJoin(projectData).on(projectData.dataId.eq(data.id))
.leftJoin(data.metadata).fetchJoin()
.where(
DataFilterPredicate.keywordContains(request.keyword()),
DataFilterPredicate.topicIdEq(request.topicId()),
DataFilterPredicate.dataSourceIdEq(request.dataSourceId()),
DataFilterPredicate.dataTypeIdEq(request.dataTypeId()),
DataDatePredicate.yearBetween(request.year())
)
.groupBy(data.id)
.orderBy(DataSortBuilder.fromSortOption(sortType, projectCountPath))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<DataWithProjectCountDto> contents = tuples.stream()
.map(t -> new DataWithProjectCountDto(DataEntityMapper.toDomain(t.get(data)),
t.get(projectCountPath)))
.toList();
long total = Optional.ofNullable(
queryFactory.select(data.id.countDistinct())
.from(data)
.leftJoin(projectData).on(projectData.dataId.eq(data.id))
.where(
DataFilterPredicate.keywordContains(request.keyword()),
DataFilterPredicate.topicIdEq(request.topicId()),
DataFilterPredicate.dataSourceIdEq(request.dataSourceId()),
DataFilterPredicate.dataTypeIdEq(request.dataTypeId()),
DataDatePredicate.yearBetween(request.year())
)
.fetchOne()
).orElse(0L);
return new PageImpl<>(contents, pageable, total);
}
๋ฌด์์ ํ๋์?
- ์ฌ๋ฌ ํํฐ(ํค์๋/ํ ํฝ/์์ค/ํ์
/์ฐ๋)๋ฅผ ์กฐ๋ฆฝํ๊ณ ,
ํ๋ก์ ํธ ์ฐ๊ฒฐ์(projectCount)๋ฅผ ์ง๊ณํ ๋ค,
์ ๋ ฌ + ํ์ด์ง์ ์ ์ฉํด ํ์ด์ง ์๋ต์ ๋ง๋ค์ด์ค๋ค.
์ ์ด๋ ๊ฒ ๊ตฌ์ฑํ๋์?
- ์ฝ๊ธฐ ์ ์ฉ ์ต์ ํ: ์กฐ์ธ/๊ทธ๋ฃน/ํ๋ก์ ์
์ ์ด๋ํฐ ์์ค์์ ์์ ๋กญ๊ฒ ์ฐ๊ณ ,
๋๋ฉ์ธ/์ ์ค์ผ์ด์ค๋ ๋จ์ํ ๊ณ์ฝ๋ง ์ ์งํ๋ค.
- ์นด์ดํธ ๋ถ๋ฆฌ: contents ์กฐํ์ total ์นด์ดํธ๋ฅผ ๋ถ๋ฆฌํ๋ฉด,
๋ฐ์ดํฐ ์์ด ์ปค์ ธ๋ ํ์ฅ์ฑ์ด ์ข๋ค(ํ ๋ฒ์ fetchResults()๋ ๋น์ถ์ฒ/Deprecated).
์ด๋ป๊ฒ ๋์ํ๋์?
- projectCountPath๋ผ๋ ํ์ ์ปฌ๋ผ(NumberPath)์ ์ ์ธํด ํ๋ก์ ํธ ํ์ฉ ์count() ๊ฒฐ๊ณผ๋ฅผ ๋ด์ ๋ณ์นญ์ ์ค๋น.
- select(data, projectData.id.count().as(projectCountPath))๋ก
์ํฐํฐ + ํด๋น ๋ฐ์ดํฐ์
์ ์ฐ๊ฒฐ๋ ํ๋ก์ ํธ ์๋ฅผ ํ ๋ฒ์ ์กฐํ.
- leftJoin(projectData)๋ก ์ฐ๊ฒฐ ์ฌ๋ถ๊ฐ ์์ด๋ ๋ฐ์ดํฐ๊ฐ ๋์ค๊ฒ ํ๊ณ ,
leftJoin(data.metadata).fetchJoin()์ผ๋ก ๋ฉํ๋ฐ์ดํฐ N+1 ๋ฐฉ์ง(1:1 ๋งคํ ๊ด๊ณ).
- where(...)์ Predicate ๋ฉ์๋๋ค์ ๋์ด(์
๋ ฅ ์์ผ๋ฉด null โ ์๋ ์คํต).
- groupBy(data.id)๋ก ๋ฐ์ดํฐ์
๋ณ๋ก ์ฐ๊ฒฐ์ ์ง๊ณ.
- orderBy(DataSortBuilder.fromSortOption(sortType, projectCountPath))๋ก
์ ์ธ์ ์ ๋ ฌ์ ์ ์ฉ.
- offset/limit์ผ๋ก ํ์ด์ง.
- ๊ฒฐ๊ณผ Tuple์ DataEntityMapper๋ก ๋๋ฉ์ธ ๋ณํํด DTO์ ๋ด์.
- total์ ๋ณ๋ ์ฟผ๋ฆฌ๋ก countDistinct()๋ง ์ํ(๋์ผ where, ์กฐ์ธ์ ์ต์ํ ๊ฐ๋ฅ).
ํ์
์์์ ํ/์ฃผ์์ (์ถํ ํ์ฅ ๊ณ ๋ ค)
- โ๏ธ fetch-join + ํ์ด์ง: ์ปฌ๋ ์
(@OneToMany)์ fetch-joinํ๋ฉด ํ์ด์ง์ด ๊ผฌ์ผ ์ ์๋ค.
- ์ปฌ๋ ์
์ด๋ฉด ๋ฐฐ์น/2-step ์กฐํ๋ก ์ ๋ต์ ์ถํ ๊ณ ๋ คํด๋ณธ๋ค.
- ๐งฎ countDistinct ๋น์ฉ: ํฐ ํ
์ด๋ธ์์๋ ๋น์ธ๋ค.
- ์บ์/ํ๋ฆฌ์นด์ดํธ/๊ทผ์ค์๊ฐ ์ง๊ณ ํ
์ด๋ธ๋ก ๋ณด์ํ๊ฑฐ๋, ํํฐ๊ฐ ๋จ์ํ๋ฉด ์นด์ดํธ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ํํ๋ค(๋ถํ์ํ ์กฐ์ธ ์ ๊ฑฐ).
- ๐ ๋๋ ํ์ด์ง ํ์: ๊น์ ํ์ด์ง๋ก ๊ฐ์๋ก offset ๋น์ฉ์ด ํฌ๋ค.
์ถํ ํค์
ํ์ด์ง(seek)์ผ๋ก ๋ฐ๊ฟ ์ ์๋๋ก ์ ๋ ฌ ์ปฌ๋ผ(์: createdAt, id)์ ์ค๋นํด๋๋ฉด ์ข๋ค.
๐งช ํ
์คํธ ํฌ์ธํธ:
- ๊ฐ ํํฐ ์กฐํฉ(์
๋ ฅ/๋ฏธ์
๋ ฅ)์์ ๊ฒฐ๊ณผ ๊ฑด์/ํ์ด์ง ์๊ฐ ๊ธฐ๋๋๋ก ๋์ค๋์ง
- ์ ๋ ฌ ์ต์
๋ณ ์์ ๋ช ๊ฑด์ ์ ๋ ฌ ์ผ์น ์ฌ๋ถ
- ์ฐ๊ฒฐ์๊ฐ ์๋ ๋ฐ์ดํฐ๋ ๋น ์ง์ง ์๋์ง(left join ํ์ธ)
โจ 5) ํจ๊ณผ
- ๋ณ๊ฒฝ ๋ด์ฑ: โ์กฐ๊ฑด ํ๋ ๋ฐ๋๋ฉด ๊ทธ ํ์ผ๋ง ๊ณ ์น๋ค.โ
- ์ ์ง๋ณด์์ฑ: ๊ฑฐ๋ ์ฟผ๋ฆฌ ๋์ ์์ ๋ชจ๋์ ๋ฆฌ๋ทฐ/๊ต์ฒด.
- ํ
์คํธ์ฑ: Predicate/Sort๋ ์์ ๋จ์ ํ
์คํธ๋ก ๊ฒ์ฆ ๊ฐ๋ฅ.
- ์ ํ ์ ์ฐ์ฑ: JPAโMyBatis ์ ํ ์์๋ Port ๊ณ์ฝ/DTO/๋๋ฉ์ธ ๋ชจ๋ธ์ ๊ทธ๋๋ก ์ฌ์ฉ.
๐งฏ 6) ์ฃผ์์ฌํญ - ์ถํ ๊ณ ๋ ค์ฌํญ
- ๐ ํจ์ ๊ธฐ๋ฐ ํํ์์ ์ธ๋ฑ์ค ํ์ฉ์ ๋ฐฉํด โ ๋ฒ์ ๋น๊ต/์์ฑ ์ปฌ๋ผ ์ธ๋ฑ์ค ํ์ฉ.
- โ๏ธ fetch join + paging: ์ปฌ๋ ์
fetch-join์ ํ์ด์ง ์ ์ฝ โ ๋ฐฐ์น ์ฌ์ด์ฆ/2-step ์กฐํ/์ ์ฉ ์ฝ๊ธฐ ๋ชจ๋ธ ๋ถ๋ฆฌ.
- ๐งฎ countDistinct ๋น์ฉ: ๋ํ ๋ฐ์ดํฐ์
์์๋ ๋น์ โ ์บ์/ํ๋ฆฌ์นด์ดํธ/๊ทผ์ค์๊ฐ ์ง๊ณ ๊ณ ๋ ค.
- ๐งฑ ๊ฐ์ค์น ๋ณ๊ฒฝ: ์ธ๊ธฐ ์ ์ ๊ฐ์ค์น๋ ์ค์ ๊ฐ/์ ๋ต ํจํด ๋ถ๋ฆฌ๋ก ์ด์ ๋ณ๊ฒฝ ์ฉ์ด.
๐งโ๐ซ 7) ๊ฒฐ๋ก
์ฟผ๋ฆฌ๋ฅผ ์ชผ๊ฐ์ ์กฐ๋ฆฝํ๋ฉด, ๊ธฐ๋ฅ์ด ๋์ด๋๋ ๋ณต์ก๋๋ฅผ ํต์ ํ ์ ์๋ค.
ํต์ฌ์ ์๊ฒ ์ถ๊ฐ ยท ์์ ํ๊ฒ ๋ณ๊ฒฝ ยท ์ฝ๊ฒ ํ
์คํธ๋ค.
ํ ์ปจ๋ฒค์
์ผ๋ก ์ ์ฐฉ์ํค๋ฉด โํํฐ ํ๋ ์ถ๊ฐํ๋๋ฐ ์ ์ ์ฒด๊ฐ ๊นจ์ง์ฃ ?โ๋ผ๋ ์ง๋ฌธ์ด ์ฌ๋ผ์ง ์ ์์ด ์ค๊ณ ์ ์ ๋ฆฌํ ์ ์์ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ๋ ๋ค. ๐
์ถํ ๋ ๊ณ ๋ คํด์ผ ํ ์์๋ค์ด ์๊ธฐ์ ๊ณ์ ๋ฆฌํฉํ ๋งํ์ฌ ๊ฐ์ ํด ๋๊ฐ ๊ณํ์ ํด์ผ๊ฒ ๋ค.