Chef.Yeon
Code Cook
Chef.Yeon
전체 방문자
오늘
어제
  • 분류 전체보기 (230)
    • 게임 개발 (1)
      • Unity (1)
    • Android (27)
      • Kotlin (19)
      • 우아한테크코스 5기 (4)
    • Language (11)
      • 파이썬 (3)
      • Java (7)
    • DB (2)
      • SQL (16)
    • Spring (25)
    • 코딩테스트 (56)
    • Git (1)
    • TIL (85)
    • DevOps (6)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 다이나믹 프로그래밍
  • webflux
  • kibana
  • 우아한테크코스
  • 코틀린
  • SQL
  • 파이썬
  • spring
  • 에라토스테네스의 체
  • 내림차순
  • 코딩테스트
  • 프로그래머스
  • til
  • 프리코스
  • MariaDB
  • 코틀린 인 액션
  • java
  • ec2
  • 레포지토리
  • Wil
  • Android
  • 문자열
  • elasticsearch
  • 백준
  • 안드로이드
  • grafana
  • rsocket
  • enum
  • kotlin
  • Docker

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Chef.Yeon

Code Cook

[TIL-20231222] Illegal pop() with non-matching JdbcValuesSourceProcessingState
TIL

[TIL-20231222] Illegal pop() with non-matching JdbcValuesSourceProcessingState

2023. 12. 22. 02:06

 

💻문제점1

MemberContorller.java

    @GetMapping("/nutrients/week")
    public ResponseEntity<CalorieIntakeResponse> getWeeklyIntakeCalorie(
            @AuthenticationPrincipal UserDetailsImpl userDetails,
            @RequestParam("startDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
            @RequestParam("endDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {
        List<MealCalories> mealCalories =
                nutrientsQueryService.searchWeeklyIntakeCalories(
                        userDetails.id(), startDate, endDate);
        int totalCalorie = nutrientsQueryService.calcTotalCalorie(mealCalories);
        return ResponseEntity.ok(
                nutrientsMapper.toCalorieIntakeResponse(totalCalorie, mealCalories));
    }

 

NutrientsQueryService.java

    public List<MealCalories> searchWeeklyIntakeCalories(
            Long memberId, LocalDate startDate, LocalDate endDate) {
        memberQueryService.search(memberId);
        List<MealCalories> mealCalories = new ArrayList<>();
        LocalDate currentDate = startDate;
        while (!currentDate.isAfter(endDate)) {
            LocalDateTime startDateTime = startDate.atStartOfDay();
            LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX);
            List<MealLog> mealLogs = findMealLog(memberId, startDateTime, endDateTime);
            mealCalories.add(sumCalorieByMealType(mealLogs));
            currentDate.plusDays(1);
        }
        return mealCalories;
    }
    
    public int calcTotalCalorie(List<MealCalories> mealCalories) {
        return mealCalories.parallelStream()
                .reduce(
                        0,
                        (accumulator, mealCalorie) ->
                                accumulator
                                        + mealCalorie.breakfast()
                                        + mealCalorie.lunch()
                                        + mealCalorie.dinner()
                                        + mealCalorie.snack(),
                        Integer::sum);
    }
    
    private List<MealLog> findMealLog(
            Long memberId, LocalDateTime startDate, LocalDateTime endDate) {
        return mealLogRepository.findAllByMemberIdAndModifiedAtBetween(
                memberId, startDate, endDate);
    }
    
        private MealCalories sumCalorieByMealType(List<MealLog> mealLogs) {
        Map<MealType, Integer> caloriesByType = calcCaloriesByMealTypeGroup(mealLogs);
        int snackTotalCalorie = sumSnackTotalCalorie(caloriesByType);
        return new MealCalories(
                caloriesByType.getOrDefault(MealType.BREAKFAST, 0),
                caloriesByType.getOrDefault(MealType.LUNCH, 0),
                caloriesByType.getOrDefault(MealType.DINNER, 0),
                snackTotalCalorie);
    }

    private Map<MealType, Integer> calcCaloriesByMealTypeGroup(List<MealLog> mealLogs) {
        return mealLogs.parallelStream()
                .flatMap(
                        mealLog ->
                                mealLog.getMeals().stream()
                                        .map(
                                                meal ->
                                                        new AbstractMap.SimpleEntry<>(
                                                                mealLog.getMealType(),
                                                                meal.getCalorie())))
                .collect(
                        Collectors.groupingByConcurrent(
                                Map.Entry::getKey, Collectors.summingInt(Map.Entry::getValue)));
    }

    private int sumSnackTotalCalorie(Map<MealType, Integer> caloriesByType) {
        return Arrays.stream(MealType.values())
                .filter(MealType::isSnack)
                .mapToInt(type -> caloriesByType.getOrDefault(type, 0))
                .sum();
    }

 

주간 섭취량 조회 API를 테스트 하려고 PostMan으로 요청을 보냈다.

그런데 오류 발생.. 이게 대체 무슨 오류지?


📃문제점1-시도

우선 로그를 살펴보자.

 

searchWeeklyIntakeCalories 메서드에서 오류가 발생했다.

 

mealLogRepository.findAllByMemberIdAndModifiedAtBetween 까지는 정상적으로 수행되었으니, 그 이후가 문제이다.

 

로그를 찍어보니 calcCaloriesByMealTypeGroup()가 문제인 듯 하다.

 

병렬 처리를 위해 parallelStream을 사용했다. 일단 stream()과, groupingBy()로 변경하고 수행했다.


💻문제점2

수행은 되는데,,, 무한루프 문제가 발생했다.

일주일 중에 각 날짜에 대한 MealLog를 가져와야 하기 때문에 findAllByMemberIdAndModifiedAtBetween는 총 7번 수행된다. 각 인자는 startDate: 2023-12-17T00:00, endDate: 2023-12-17T23:59:59.9999 (각 날짜의 처음과 끝, 17일,18일...23일)으로 들어가야 하는데, endDate가 변하지 않고, startDate도 .plueDay(1)이 되지 않는 것을 확인 할 수 있다.


🔍문제점1,2-해결

하,, 아래 코드에서 currentDate를 사용하지 않고 startDate와 endDate를 사용했기 때문이다(이마짚..)

plusDays가 안됐던 이유는 currentDate에 다시 할당해주는걸 잊었다..(바보

    public List<MealCalories> searchWeeklyIntakeCalories(
            Long memberId, LocalDate startDate, LocalDate endDate) {
        memberQueryService.search(memberId);
        List<MealCalories> mealCalories = new ArrayList<>();
        LocalDate currentDate = startDate;
        while (!currentDate.isAfter(endDate)) {
            LocalDateTime startDateTime = startDate.atStartOfDay(); //여기
            LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); //여기
            List<MealLog> mealLogs = findMealLog(memberId, startDateTime, endDateTime);
            mealCalories.add(sumCalorieByMealType(mealLogs));
            currentDate = currentDate.plusDays(1);
        }
        return mealCalories;
    }

 

문제점2를 해결하고 나니 무사히 수행되었다.

 

결론은 전부 내 잘못이었다... 코드는 거짓말을 하지 않지.


🤦‍♀️의문점

하지만 아직 나의 의문을 풀리지 않았다.

왜 parallelStream()을 사용하면 오류가 날까?

 

Hibernate 세션은 thread-safe 하지 않다. 

The Hibernate session is not thread-safe (nor "stream-safe"), so using it across different threads (or reactive streams) may cause bugs that are 
extremely hard to detect. Don’t say we didn’t warn you!
ref. https://hibernate.org/reactive/documentation/2.0/reference/html_single/#_sessions_and_vert_x_contexts

 

calcCaloriesByMealTypeGroup()에서는 meal.getMeals()를 호출하여 처리한다.

이 객체에는 LAZY 로딩 옵션이 주어져 있다. getMeals()로 호출하게 될 때 프록시 객체가 초기화 되는데 Hibernate는  thread-safe하지 않기 때문에 병렬 처리 시에 오류가 발생하는 것 같다.

 

GPT에게도 해당 문제에 대해 물어봤다.

 

병렬 스트림과 데이터베이스 엔티티 처리의 문제

병렬 스트림은 여러 쓰레드를 이용해 연산을 병렬로 수행합니다. 이 때 각 쓰레드는 독립적으로 작업을 처리합니다. 하지만 데이터베이스와 연동되는 ORM (Object-Relational Mapping) 프레임워크, 예를 들어 Hibernate,는 기본적으로 '스레드 안전하지 않습니다(thread-unsafe)'. 즉, 하나의 데이터베이스 세션 또는 엔티티 매니저는 한 번에 하나의 쓰레드만이 안전하게 사용할 수 있습니다.

 

발생 가능한 문제

  1. 스레드 안전성 문제: 병렬 스트림에서 여러 쓰레드가 동시에 하나의 세션에 접근하려고 할 때, 데이터의 일관성이나 세션의 상태가 예측 불가능하게 될 수 있습니다.
  2. Lazy Loading 문제: Hibernate에서는 Lazy Loading 방식을 사용하여 연관된 엔티티를 필요할 때만 불러옵니다. 병렬 스트림을 사용할 경우, 이 Lazy Loading이 예상치 못한 시점에 발생하며, 이는 세션 관리 문제로 이어질 수 있습니다.
  3. 트랜잭션 관리 문제: 일반적으로 트랜잭션은 스레드에 종속됩니다. 병렬 처리 시, 트랜잭션 관리가 복잡해지고, 예상치 못한 롤백이나 커밋이 발생할 수 있습니다.
728x90

'TIL' 카테고리의 다른 글

[TIL - 20231222] AuthenticationEntryPoint를 사용한 JWT 예외 핸들링  (1) 2023.12.22
[TIL - 20231222] searchWeeklyIntakeCalories 로직 개선  (0) 2023.12.22
[TIL-20231220] 리플렉션을 사용한 private 필드 접근 및 값 설정  (0) 2023.12.20
[TIL - 20230914~15] Github Actions+Docker CI/CD 트러블슈팅  (0) 2023.09.15
[커뮤 프로젝트] 영상 파일 삭제 로직 (동기, 비동기 처리)  (0) 2023.07.04
    'TIL' 카테고리의 다른 글
    • [TIL - 20231222] AuthenticationEntryPoint를 사용한 JWT 예외 핸들링
    • [TIL - 20231222] searchWeeklyIntakeCalories 로직 개선
    • [TIL-20231220] 리플렉션을 사용한 private 필드 접근 및 값 설정
    • [TIL - 20230914~15] Github Actions+Docker CI/CD 트러블슈팅
    Chef.Yeon
    Chef.Yeon
    보기 좋고 깔끔한 코드를 요리하기 위해 노력하고 있습니다.

    티스토리툴바