코딩하는 털보

21.11.01 TIL 본문

Diary/Today I Learned

21.11.01 TIL

이정인 2021. 11. 2. 00:23

오늘의 삽질

org.hibernate.LazyInitializationException

User 엔티티는 Habit 엔티티와 다대일 양방향 매핑되어있다.

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Habit> habit;

그런데 아래 비즈니스 로직에서 for문을 돌때 org.hibernate.LazyInitializationException 예외가 발생한다.

@Override
public List<HabitSummaryVo> getHabitSummaryList(User user) {
    List<HabitSummaryVo> habitSummaryList = new ArrayList<>();
    List<Habit> habits = user.getHabit();
    for (Habit habit : habits) {
        habitSummaryList.add(HabitSummaryVo.of((HabitWithCounter) habit));
    }
    return habitSummaryList;
}

예외 이름만 봐도 @OneToMany는 fetchmode Lazy 가 디폴트이므로 발생하는 문제라는 것은 알 수 있었다.
Lazy는 처음에는 프록시 객체를 생성하여 넣어놓지만 실제로 그 객체를 사용하려고 할 때 조회쿼리가 자동으로 나간다고 알 고 있었는데..
여기서는 왜 문제가 발생하는 것일까?

결과부터 말하자면 트랜잭션 밖에서 조회했기 때문이다.

User 엔티티는 UserDetails 구현 서비스에서 아래와 같이 DB에서 꺼내온다.

public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
    User user = userRepository.findBySocialId(userId)
            .orElseThrow(() -> new UsernameNotFoundException(userId + "를 찾을 수 없습니다."));

    return new UserDetailsImpl(user);

그 이후에 HandlerMapping과 컨트롤러를 거쳐서 위의 서비스 로직까지 도달하게 된다.
예상에는 그 사이에 트랜잭션이 계속 유지되지 않는 것으로 생각된다.

user 엔티티의 ID를 조건으로하여 habit 테이블에서 찾아오는 쿼리를 한번 더 날리면 문제는 해결된다.

@Override
public List<HabitSummaryVo> getHabitSummaryList(User user) {
    List<HabitSummaryVo> habitSummaryList = new ArrayList<>();
    List<Habit> habits = habitWithCounterRepository.findByUser(user);
    for (Habit habit : habits) {
        habitSummaryList.add(HabitSummaryVo.of((HabitWithCounter) habit));
    }
    return habitSummaryList;
}

org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing

분명히 요청에는 Body를 Json 타입으로 넣어서 보내주는데, 바디가 없다는 에러가 발생한다.

@ApiOperation(value = "몬스터 변경", notes = "변경된 몬스터 데이터 반환")
@PatchMapping("/user/monster")
public MonsterResponseDto updateMonster(
        @ApiIgnore @AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestBody MonsterSelectRequestDto requestDto) {
    return monsterService.updateMonster(userDetails.getUser(), requestDto);
}

Http Method도 바꿔보고 Content Type 도 바꿔보고 이것저것 해보다가
검색으로 원인을 파악할 수 있었다.

원인은 filter에 있었던 getInputStream() 때문이었다.
정확히는 HttpServletRequest의 InputStream은 한 번 읽으면 다시 읽을 수 없기 때문에
@RequestBody 어노테이션이 제대로 request를 읽을 수 없었던 것.

ServletInputStream inputStream = request.getInputStream();

filter에 있던 getInputStream() 메서드는 토큰 오류시 요청 바디를 다시 넘겨주기 위해서 사용하고 있었으므로
오류가 발생할 때만 getInputStream() 메서드를 사용하도록 분기했다.

Comments