[주라벨] Map을 활용한 Sensory 정보 그룹화 및 성능 개선

개요 

주라벨 프로젝트를 진행하며 시음노트 관련 엔티티 설계 및 기능 개발을 진행했습니다. 
Map을 활용한 데이터 그룹화를 통해 조회 기능을 개선한 경험을 공유하고자 합니다.

 

Sensory (촉각 정보) 

테이블 설명
sensory 술의 촉각 정보를 정의하는 테이블
sensory_level 술의 촉각 정보를 점수화하여 정량적으로 평가한 테이블
alcohol_type 술의 종류(주종)를 정의하는 테이블
alcohol_type_sensory 특정 주종과 촉각 정보를 연결하는 테이블
tasting_note 술의 알코올 도수, 양조장, 내용, 평점, 색상, 주관 평가 등을 저장하는 시음 노트 테이블
tasting_note_sensory_level 시음 노트에 저장된 촉각 정보와 그에 대한 점수 및 설명을 기록하는 테이블

 

[트러블 슈팅] 조회 로직 개선하기

문제 상황 : 반복적인 조회를 진행하게 되어 성능 저하가 발생한다.

시음노트 작성, 촉각 정보 조회

시음 노트 작성 시, 사용자는 각 주종별로 다른 촉각 정보를 입력하게 됩니다. 주종에 따라 조회되는 촉각 정보의 종류가 다르고, 각 촉각 타입에 따라 점수(score)와 설명(description)도 다르게 나타납니다. 여기서 주종별로 촉각 정보를 조회할 때 각 sensoryId(촉각 정보 고유 번호)별로 여러 개의 SensoryLevel을 조회할 경우 sensoryId별로 반복적인 조회를 진행하게 되어 성능 저하를 유발하게 됩니다.

 

해결 방안 : Map을 활용하여 데이터를 그룹화한다.

Map을 활용하여 Sensory 정보를 한 번에 조회하고, 각 sensoryId를 기준으로 Level 데이터를 그룹화하여 성능을 최적화했습니다.

@Reader
@RequiredArgsConstructor
public class AlcoholTypeSensoryReader {

    private final AlcoholTypeSensoryJpaRepository alcoholTypeSensoryJpaRepository;
    private final AlcoholTypeSensoryQueryRepository alcoholTypeSensoryQueryRepository;

    // ...
    
    public List<SensoryLevelInfo> getAllSensoryLevelInfoByAlcoholTypeId(Long alcoholTypeId) {
        return alcoholTypeSensoryQueryRepository.findAllInfoByAlcoholTypeId(alcoholTypeId);
    }

}
@Repository
@RequiredArgsConstructor
public class AlcoholTypeSensoryQueryRepository {

    private final JPAQueryFactory jpaQueryFactory;

    QAlcoholTypeSensory alcoholTypeSensory = QAlcoholTypeSensory.alcoholTypeSensory;
    QSensory sensory = QSensory.sensory;
    QSensoryLevel sensoryLevel = QSensoryLevel.sensoryLevel;

    public List<SensoryLevelInfo> findAllInfoByAlcoholTypeId(Long alcoholTypeId) {
        Map<Long, SensoryLevelInfo> sensoryMap = new HashMap<>();

        List<Tuple> sensoryTuples = jpaQueryFactory
                .select(
                        sensory.id,
                        sensory.name,
                        sensoryLevel.id,
                        sensoryLevel.score,
                        sensoryLevel.description
                )
                .from(alcoholTypeSensory)
                .innerJoin(sensory).on(alcoholTypeSensory.sensory.eq(sensory))
                .innerJoin(sensoryLevel).on(sensory.eq(sensoryLevel.sensory))
                .where(
                        eqAlcoholTypeId(alcoholTypeSensory.alcoholType, alcoholTypeId),
                        isUsed(alcoholTypeSensory)
                )
                .orderBy(
                        sensory.id.asc(),
                        sensoryLevel.score.asc()
                )
                .fetch();

        sensoryTuples.forEach(s -> {
            Long sensoryId = s.get(this.sensory.id);

            SensoryLevelInfo sensoryLevelInfo = sensoryMap.computeIfAbsent(sensoryId,
                    id -> new SensoryLevelInfo(
                            new SensoryInfo(
                                    sensoryId,
                                    s.get(this.sensory.name)
                            ),
                            new ArrayList<>()
                    ));

            Level level = new Level(
                    s.get(this.sensoryLevel.id),
                    s.get(this.sensoryLevel.score),
                    s.get(this.sensoryLevel.description)
            );

            sensoryLevelInfo.levels().add(level);
        });

        return new ArrayList<>(sensoryMap.values());
    }

    // ...
}

 

 


 

1. Sensory 정보와 그에 해당하는 Level 정보를 한 번의 쿼리로 모두 가져옵니다.

 

 

2. 가져온 데이터 sensoryTuples를 sensoryId별로 그룹화하여 Map에 저장한 후, 이를 통해 sensoryId에 해당하는 Level 정보들을 리스트에 추가합니다.

("computeIfAbsent()" 메소드는 Java의 Map 인터페이스에 포함된 메소드 중 하나로, 특정 키에 대해 값이 없을 경우 계산된 값을 맵에 저장하고, 그 값을 반환하는 역할을 합니다.)

 

 

3. 이후 Map의 "values()" 메소드를 통해 List<SensoryLevelInfo>를 가져옵니다.

 

 

이를 통해 D별로 반복 조회하는 로직을 제거하고 성능을 최적화했습니다. 또한 데이터를 그룹화하여 가독성을 높였습니다.

감사합니다. : )