๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
SQL/๊ฐœ๋ฐœ ๊ธฐ๋ก

QueryDSL์˜ transform์„ ์‚ฌ์šฉํ•ด ๋ฆฌ๋ทฐ ํ†ต๊ณ„ ์ •๋ณด ๋ฐ˜ํ™˜ํ•˜๊ธฐ

by oliviarla 2024. 1. 14.

์š”๊ตฌ์‚ฌํ•ญ

ํ–ฅ์ˆ˜ ๋ฆฌ๋ทฐ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ, ํ–ฅ์ˆ˜ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ํ†ต๊ณ„๋ฅผ percentage๋กœ ๋‚ด์–ด ๋ฐ˜ํ™˜ํ•˜๋Š” ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ์—ˆ๋‹ค. ์•„๋ž˜ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์ด ํŠน์ • ํ–ฅ์ˆ˜์— ๋Œ€ํ•ด ์‚ฌ๋žŒ๋“ค์ด ๋ฆฌ๋ทฐํ•œ ๋ฌด๊ฒŒ๊ฐ ์ •๋„, ์ง€์†๋ ฅ, ๊ณ„์ ˆ, ํ™•์‚ฐ๋ ฅ ์ •๋„, ์‚ฌ์šฉ์ž์˜ ์„ฑ๋ณ„์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์˜ค๊ฐํ˜•์„ ๋‚˜ํƒ€๋‚ด์–ด์•ผ ํ•œ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ํŠน์ • ํ–ฅ์ˆ˜์— ๋Œ€ํ•œ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด ๊ฐ๊ฐ์˜ ๋ฆฌ๋ทฐ ์š”์†Œ๋งˆ๋‹ค ๋น„์œจ์„ ๊ณ„์‚ฐํ•ด ์กฐํšŒํ•˜๊ณ ์ž ํ•œ๋‹ค. ๋‹น์—ฐํ•œ ๋ง์ด์ง€๋งŒ ๊ฐ๊ฐ์˜ Percentage๋ฅผ ๋ชจ๋‘ ํ•ฉํ•˜๋ฉด 100์ด ๋˜์–ด์•ผ ํ•œ๋‹ค.

ํ”„๋กœ์ ํŠธ์˜ ๋ฆฌ๋ทฐ ํ…Œ์ด๋ธ”๊ณผ ์œ ์ € ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‚ดํŽด๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค. (์ผ๋ถ€ ํ•„์š”์—†๋Š” ๋ฐ์ดํ„ฐ๋Š” ์ƒ๋žตํ–ˆ๋‹ค.)

 

ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ๋Š” ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ์™€ ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ•œ ์‚ฌ์šฉ์ž์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฆฌ๋ทฐ ์š”์†Œ๋งˆ๋‹ค percentage๋ฅผ ๊ณ„์‚ฐํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ด ์ค„ ๊ฒƒ์ด๋‹ค.

{
    "strength": {
        "LIGHT": 24,
        "HEAVY": 36,
        "MODERATE":40
    },
    "duration": {
        "MEDIUM": 0,
        "LONG": 0,
        "TOO_SHORT": 98,
        "SHORT": 2
    },
    "season": {
        "FALL": 0,
        "WINTER": 1,
        "SPRING": 99,
        "SUMMER": 0
    },
    "dayType": {
        "TRAVEL": 30,
        "SPECIAL": 13,
        "REST": 22,
        "DAILY": 35
    },
    "sex": {
        "FEMALE": 5,
        "OTHER": 70,
        "MALE": 25
    }
}

ํ•ด๊ฒฐํ•˜๊ธฐ

๋จผ์ € ๋ฆฌ๋ทฐ ์š”์†Œ ๊ธฐ์ค€์œผ๋กœ ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๋ฅผ DB์—์„œ ์กฐํšŒํ•œ ํ›„ ํ”„๋กœ๊ทธ๋žจ ๋‹จ์—์„œ ๋น„์œจ๋กœ ๋ณ€ํ™˜ํ•ด์ค„ ๊ฒƒ์ด๋‹ค. ๋ณธ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์กฐํšŒ ์‹œ์—๋Š” QueryDSL์„ ์ ๊ทน ํ™œ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ QueryDSL์„ ์‚ฌ์šฉํ•ด DB ์งˆ์˜๋ฅผ ๋ณด๋‚ผ ๊ฒƒ์ด๋‹ค. ๐Ÿพ

๋ฆฌ๋ทฐ ์š”์†Œ ๊ธฐ์ค€์œผ๋กœ ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜ ๊ตฌํ•˜๊ธฐ

QueryDSL์˜ where ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด ๋จผ์ € ์—ฌ๋Ÿฌ ๋ฆฌ๋ทฐ ์ค‘ ํŠน์ • perfumeId์— ํ•ด๋‹นํ•˜๋Š” ๋ฆฌ๋ทฐ์ด๋ฉด์„œ ์‚ญ์ œ๋˜์ง€ ์•Š์€ ๋ฆฌ๋ทฐ๋งŒ ์„ ๋ณ„ํ•œ๋‹ค.

์ฐธ๊ณ ๋กœ ๋ณธ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” soft delete ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฆฌ๋ทฐ๋ฅผ ์‚ญ์ œํ•˜๋”๋ผ๋„ ๋ฐ์ดํ„ฐ ์ž์ฒด๋ฅผ ์ง€์šฐ๋Š” ๊ฒƒ์ด ์•„๋‹Œ deleted_at์„true๋กœ ๋งˆํ‚นํ•˜๋„๋ก ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— deletedAt์ด null์ธ, ์ฆ‰ ์‚ญ์ œ๋˜์ง€ ์•Š์€ ๋ฆฌ๋ทฐ๋งŒ ์กฐํšŒํ•˜๋„๋ก ํ–ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ํ–ฅ์ˆ˜ ๋ฌด๊ฒŒ๊ฐ์„ ๊ธฐ์ค€์œผ๋กœ ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•ด groupBy ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ๊ทธ๋ฃนํ•‘ ํ•˜์˜€๋‹ค.

์ดํ›„ QueryDSL์—์„œ ์ œ๊ณตํ•˜๋Š” transform ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ํ–ฅ์ˆ˜ ๋ฌด๊ฒŒ๊ฐ์„ key๋กœ ํ•˜๊ณ  ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๋ฅผ value๋กœ ํ•˜๋Š” Map ์ž๋ฃŒ๊ตฌ์กฐ์— ๋‹ด๋„๋ก ํ•œ๋‹ค.

Map<Strength, Long> strengthMap =
    jpaQueryFactory
        .from(reviewEntity)
        .where(reviewEntity.perfumeId.eq(perfumeId), reviewEntity.deletedAt.isNull())
        .groupBy(reviewEntity.strength)
        .transform(groupBy(reviewEntity.strength).as(reviewEntity.id.count()));

 

์œ„์™€ ๋™์ผํ•˜๊ฒŒ ํ–ฅ์ˆ˜ ์ง€์†๋ ฅ, ์–ด์šธ๋ฆฌ๋Š” ๊ณ„์ ˆ, ๋ฆฌ๋ทฐ์–ด ์„ฑ๋ณ„ ๋“ฑ ๋‹ค๋ฅธ ๋ฆฌ๋ทฐ ์š”์†Œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ๋„ ๊ฐ๊ฐ ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ–ˆ๋‹ค. 

Map<Duration, Long> durationMap =
    jpaQueryFactory
        .from(reviewEntity)
        .where(reviewEntity.perfumeId.eq(perfumeId), reviewEntity.deletedAt.isNull())
        .groupBy(reviewEntity.duration)
        .transform(groupBy(reviewEntity.duration).as(reviewEntity.id.count()));

Map<Season, Long> seasonMap =
    jpaQueryFactory
        .from(reviewEntity)
        .where(reviewEntity.perfumeId.eq(perfumeId), reviewEntity.deletedAt.isNull())
        .groupBy(reviewEntity.season)
        .transform(groupBy(reviewEntity.season).as(reviewEntity.id.count()));

Map<DayType, Long> dayTypeMap =
    jpaQueryFactory
        .from(reviewEntity)
        .where(reviewEntity.perfumeId.eq(perfumeId), reviewEntity.deletedAt.isNull())
        .groupBy(reviewEntity.dayType)
        .transform(groupBy(reviewEntity.dayType).as(reviewEntity.id.count()));

Map<Sex, Long> sexMap =
    jpaQueryFactory
        .from(reviewEntity)
        .leftJoin(userEntity)
        .on(reviewEntity.userId.eq(userEntity.id), userEntity.deletedAt.isNull())
        .where(reviewEntity.perfumeId.eq(perfumeId), reviewEntity.deletedAt.isNull())
        .groupBy(userEntity.sex)
        .transform(groupBy(userEntity.sex).as(reviewEntity.id.count()));

 

์ด๋ ‡๊ฒŒ ์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ๋“ค์„ ๋ชจ์•„ ReviewFeatureCount๋ผ๋Š” ํด๋ž˜์Šค์— ๋‹ด์„ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.

๊ทธ๋ฆฌ๊ณ  totalReviews ๊ฐœ์ˆ˜๋ฅผ ์ €์žฅํ•ด๋‘” ์ด์œ ๋Š” Percentage๋ฅผ ๊ตฌํ•˜๊ธฐ ์œ„ํ•ด ํŠน์ • ํ–ฅ์ˆ˜์— ํ•ด๋‹นํ•˜๋Š” ๋ชจ๋“  ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

public record ReviewFeatureCount(
    Map<Strength, Long> strengthMap,
    Map<Duration, Long> durationMap,
    Map<Season, Long> seasonMap,
    Map<DayType, Long> dayTypeMap,
    Map<Sex, Long> sexMap,
    Long totalReviews) {};

 

Percentage ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ

์ด์ œ ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜ ๊ธฐ๋ฐ˜์œผ๋กœ ๋œ Map์„ calculatePercentage ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋น„์œจ ๊ธฐ๋ฐ˜์œผ๋กœ ๋œ Map์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋„๋ก ํ•œ๋‹ค.

calculatePercentage์˜ ์ธ์ž๋ฅผ ์‚ดํŽด๋ณด๋ฉด, Enum ํƒ€์ž…์„ ์ž…๋ ฅ๋ฐ›์•„ ์žฌ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์œผ๋ฉฐ, ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜ ๊ธฐ๋ฐ˜์œผ๋กœ ๋œ Map๊ณผ ํŠน์ • ํ–ฅ์ˆ˜์˜ ์ด ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๋ฅผ ์ž…๋ ฅ๋ฐ›๋„๋ก ๋˜์–ด ์žˆ๋‹ค.

Map<Strength, Long> strengthMap = calculatePercentage(
	Strength.class, reviewFeatureCount.strengthMap(), totalReviewCount);

 

์ž์„ธํ•œ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

๋จผ์ € ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๊ฐ€ ์ €์žฅ๋œ Map์„ ์ƒˆ๋กœ์šด mutableMap์ด๋ผ๋Š” ๋ณ€์ˆ˜์— ์ €์žฅํ•œ๋‹ค. ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๊ฐ€ 0์ธ ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ๊ฐ์˜ Enum value๋ฅผ ๋Œ๋ฉด์„œ Map์— ๋นˆ ๊ฐ’์„ ์ €์žฅํ•œ๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค์–ด ์ž…๋ ฅ๋ฐ›์€ map์ด {LIGHT: 49, MODERATE: 51} ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ ์˜€๋‹ค๋ฉด, ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜๊ฐ€ 0์ธ Enum value๋„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด {LIGHT: 49, MODERATE: 51, HEAVY: 0} ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.

 

์ด๋ ‡๊ฒŒ ์ƒ์„ฑํ•œ Map์„ stream()์œผ๋กœ ์ˆœํšŒํ•˜๋ฉด์„œ (๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜) / (์ด ๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜) * 100 ์ˆ˜์‹์„ ์ ์šฉํ•ด percentage๋กœ ๋ณ€ํ™˜ํ•˜๋ฉด ๋œ๋‹ค.

private <V extends Enum<V>> Map<V, Long> calculatePercentage(
  final Class<V> enumClazz, final Map<V, Long> map, final long totalReviews) {
    Map<V, Long> mutableMap = new HashMap<>(map);
    EnumSet.allOf(enumClazz).forEach(enumValue -> mutableMap.putIfAbsent(enumValue, 0L));
    return mutableMap.entrySet().stream()
        .collect(
            Collectors.toMap(
                Map.Entry::getKey, e -> Math.round((double) e.getValue() / totalReviews * 100)));
}

 

๋งˆ๋ฌด๋ฆฌ

ํ‰์†Œ DB ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•  ์ผ์ด ์—†๋Š” ๋‚˜์—๊ฒŒ๋Š” ๋ณต์žกํ•œ DB ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ๊ธฐํšŒ์˜€๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ์š”์ฒญ ๋ณด๋‚ผ ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด ๋น„์œจ์„ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์€ ์‚ฌ์‹ค์ƒ ์‹ค๋ฌด์—์„œ ์‚ฌ์šฉ๋˜๊ธฐ ์–ด๋ ค์šธ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค. ์‹ค๋ฌด์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹ฑํ•ด๋‘๊ณ  ํ•˜๋ฃจ์— ํ•œ ๋ฒˆ์”ฉ ๋งŒ๋ฃŒ์‹œ์ผœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐฑ์‹ ์‹œ์ผœ์ฃผ๋Š” ๋“ฑ์˜ ์บ์‹ฑ ์ „๋žต์ด ์‚ฌ์šฉ๋  ๊ฒƒ ๊ฐ™๋‹ค.

๋Œ“๊ธ€