JPA 오류 정리

yumyeonghan·2023년 3월 21일
0

오류

목록 보기
1/1

🍃이 글은 개발하다가 만난 오류 중, JPA와 관련된 오류들을 정리한 글입니다.🍃

1. LazyInitializationException

LazyInitializationException은 Hibernate와 같은 개체 관계형 매핑(ORM) 프레임워크에서 아직 초기화되지 않은 느리게 로드된 엔터티 또는 컬렉션에 액세스하려고 시도할 때 발생하는 예외이다.

발생 원인

  1. 데이터 조회 시, 데이터베이스 리소스를 절약하기 위해 다음과 같이 FetchType을 LAZY로 설정하여 실제 데이터를 사용할 때까지 초기화가 되지 않는다.

  2. 이런 상황에 트랜잭션을 지원하지 않는 Controller 계층에서 초기화되지 않은 컬렉션에 접근하려 해서 생긴 문제였다.

//엔티티 클래스의 코드
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "noticeBoard_id", foreignKey = @ForeignKey(name = "fk_notice_board_upload_file_to_notice_board"))
private NoticeBoard noticeBoard;

해결 방법: 프록시 객체(컬렉션) 초기화는 트랜잭션 범위 안에서 동작하는 영속성 컨텍스트를 통해 진행되기 때문에 트랜잭션을 사용하는 Service 계층에서 컬렉션 초기화를 진행한다.

//서비스 계층 클래스의 코드
@Transactional
public NoticeBoard findDetailNoticeBoard(Long id) {
        NoticeBoard noticeBoard = noticeBoardRepository.findById(id).get();
        noticeBoard.updateViews();
        initNoticeBoard(noticeBoard);
        return noticeBoard;
    }

private static void initNoticeBoard(NoticeBoard noticeBoard) {
        List<NoticeBoardUploadFile> uploadFiles = noticeBoard.getUploadFiles();
        if (uploadFiles.size() != 0) {
            noticeBoard.getUploadFiles().get(0);
        }
    }
  • fetch join으로 초기화를 할 수 있지만, 일대다 관계로 인한 조회 데이터 중복의 경우를 고려해 batch size를 설정해 초기화하는 방법을 선택했다.

2. HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance

해당 오류는 컬렉션이 엔티티에 의해 참조되지 않는다. 즉, 하이버네이트가 컬렉션을 관리하지 않아서 발생한 오류이다.

발생원인

  1. 우선 부모 엔티티와 자식 엔티티의 생명주기를 맞추기 위해서 @OneToMany(mappedBy = "noticeBoard", cascade = CascadeType.ALL, orphanRemoval = true) 설정해주었다.

  2. 이 상황에, 클라이언트에서 부모 엔티티의 수정 요청이 들어왔을 때 기존 컬렉션을 새롭게 만들어진 컬렉션으로 set()를 통해 변경한다.

  3. 이렇게 하이버네이트에 의해 관리되던 기존 컬렉션을 새로운 컬렉션으로 변경하려고 해서 생긴 문제였다.

//엔티티 클래스의 코드
@OneToMany(mappedBy = "noticeBoard", cascade = CascadeType.ALL, orphanRemoval = true)
private List<NoticeBoardUploadFile> uploadFiles = new ArrayList<>();

//기존 서비스 계층 클래스의 코드
@Transactional
public void updateNoticeBoard(Long id, NoticeBoardRequestDto noticeBoardRequestDto, List<NoticeBoardUploadFile> storeFiles) {
        NoticeBoard noticeBoard = noticeBoardRepository.findById(id).get();
        noticeBoard.updateTitle(noticeBoardRequestDto.getTitle());
        noticeBoard.updateText(noticeBoardRequestDto.getText());
        noticeBoard.setUploadFiles(storeFIles);
    }

해결방법: 컬렉션의 데이터들을 변경할 때 참조하고 있는 컬렉션 자체를 바꾸지 말고, 그 안의 데이터들만 바꾸는 형식으로 개발해야 해당 오류가 발생하지 않는다.

//엔티티 클래스의 코드
public void updateUploadFiles(List<NoticeBoardUploadFile> uploadFiles) {

        this.uploadFiles.clear(); //기존 컬렉션 안에 있던 데이터만 삭제
        this.uploadFiles.addAll(uploadFiles);  //기존 컬렉션에 파라미터로 받은 컬렉션 안의 요소들을 추가함
        uploadFiles.stream().forEach(e -> e.designateNoticeBoard(this));
 }
 
 //변경된 서비스 계층 클래스의 코드
 @Transactional
 public void updateNoticeBoard(Long id, NoticeBoardRequestDto noticeBoardRequestDto, List<NoticeBoardUploadFile> storeFiles) {
        NoticeBoard noticeBoard = noticeBoardRepository.findById(id).get();
        noticeBoard.updateTitle(noticeBoardRequestDto.getTitle());
        noticeBoard.updateText(noticeBoardRequestDto.getText());

        if (noticeBoardRequestDto.getFiles() != null) {
            noticeBoard.updateUploadFiles(storeFiles);
        }
  }
  • 나는 엔티티 클래스에서 다음과 같은 양방향 연관관계 편의 메서드를 만들어서 사용했다.

3. 데이터 수정 시 변경 감지가 안 되는 현상

발생원인

@Component
@EnableScheduling
@RequiredArgsConstructor
public class ScheduledTask {

    private final ScheduleBoardService scheduleBoardService;

    @Scheduled(cron = "0 0 0 * * ?") // logic to be executed 'every day at 00:00'
    public void updateSchedulesState() {
    	List<Schedules> schedules = sheduleBoardService.findScheduleList();
        scheduleBoardService.autoUpdateSchedulesState(schedules);
   }
}

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ScheduleBoardService {

	@Transactional
    public void autoUpdateSchedulesState(List<Schedules> findScheduleList) {
        findScheduleList.stream().forEach(e->e.updateScheduleState());
    }
}
  1. 스프링 부트의 @Scheduled 기능을 이용해 매일 00:00에 작성한 로직(데이터 변경)이 실행되게 하려 했다.
  2. 테스트 과정에서 로직(데이터 변경)은 실행되나 실제 데이터 베이스의 값이 변경되지 않는 문제가 생겼다.
  3. 조회 시, 트랜잭션 범위 밖의 ScheduledTask 클래스에서 엔티티를 조회하기 때문에 엔티티를 영속성 컨텍스트가 관리하지 않아 생긴 문제였다.
  4. 엔티티 자체가 영속성 컨텍스트에 의해 관리되지 않으므로, 위 코드처럼 트랜잭션 범위인 서비스 계층에서 데이터를 변경해도 변경 감지가 일어나지 않은 것이다.

해결방법

@Component
@EnableScheduling
@RequiredArgsConstructor
public class ScheduledTask {

    private final ScheduleBoardService scheduleBoardService;

    @Scheduled(cron = "0 0 0 * * ?") // logic to be executed 'every day at 00:00'
    public void updateSchedulesState() {
        scheduleBoardService.autoUpdateSchedulesState();
    }
}

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ScheduleBoardService {

    @Transactional
    public void autoUpdateSchedulesState() {
        List<Schedules> findScheduleList = scheduleRepository.findAll();
        findScheduleList.stream().forEach(e->e.updateScheduleState());
    }
 }
  1. 엔티티 조회를 트랜잭션 범위 안인 서비스 계층에서 함으로써 조회된 엔티티가 영속성 컨텍스트에 의해 관리되게 했다.
  2. 이제 엔티티가 영속성 컨텍스트에 의해 관리되기 때문에, 데이터 변경이 일어나면 트랜잭션 커밋 시점에 UPDATE 쿼리가 나가면서 문제를 해결했다.
profile
웹 개발에 관심 있습니다.

0개의 댓글