[Spring] identifier of an instance of was altered from to

Oayenn·2023년 7월 6일
0

Spring

목록 보기
1/3
post-thumbnail

💡개요

org.springframework.orm.jpa.JpaSystemException: identifier of an instance of com.example.scenchive.domain.filter.repository.PerfumeTag was altered from com.example.scenchive.domain.filter.repository.PerfumeTag$PerfumeTagId@776a779d to com.example.scenchive.domain.filter.repository.PerfumeTag$PerfumeTagId@4b8b343

키워드 필터링 검색 결과 총 개수를 반환하는 메소드를 추가한 후 GET 요청을 보냈을 때 발생한 오류다. 검색 결과를 반환하는 메소드의 일부를 그대로 사용한 것이기 때문에 오류의 원인을 파악하기 더욱 어려웠다.

getPerfumesByKeyword()

향수 리스트를 조회하는 메소드이다.

// PerfumeService.java

// 키워드 필터링 결과로 나온 향수 리스트 조회
    public List<PerfumeDto> getPerfumesByKeyword(List<PTag> keywordIds, Pageable pageable) {
        // 주어진 키워드 id들로 PerfumeTag 리스트 조회
        List<PerfumeTag> perfumeTags = perfumeTagRepository.findByPtagIn(keywordIds);
        // perfume_id 오름차순으로 조회
        Set<Perfume> uniquePerfumes = new TreeSet<>((p1, p2) -> p1.getId().compareTo(p2.getId())); // Set: 다중 키워드로 인해 중복된 향수가 있는 경우 제거

        for (PerfumeTag perfumeTag : perfumeTags) {
            Perfume perfume = perfumeTag.getPerfume();
            uniquePerfumes.add(perfume);
        }

        List<PerfumeDto> perfumes = new ArrayList<>();

        int startIndex = (int) pageable.getOffset();
        int endIndex = Math.min(startIndex + pageable.getPageSize(), uniquePerfumes.size());

        List<Perfume> paginatedPerfumes = new ArrayList<>(uniquePerfumes).subList(startIndex, endIndex);

        for (Perfume perfume : paginatedPerfumes) {
            List<Long> perfumeKeywordIds = new ArrayList<>();
            for (PerfumeTag perfumeTag : perfumeTags) {
                if (perfumeTag.getPerfume().equals(perfume)) {
                    perfumeKeywordIds.add(perfumeTag.getPtag().getId());
                }
            }

            Brand brand = brandRepository.findById(perfume.getBrandId()).orElse(null);
            String brandName = (brand != null) ? brand.getBrandName() : null;
            PerfumeDto perfumeDto = new PerfumeDto(perfume.getId(), perfume.getPerfumeName(), brandName, perfumeKeywordIds);
            perfumes.add(perfumeDto);
        }

        // 향수 DTO 리스트 반환
        return perfumes;
    }

getTotalPerfumeCount()

향수 리스트 조회 결과 총 개수를 구하는 메소드이다.

// PerfumeService.java
// 키워드 필터링 결과로 나온 전체 향수 개수 구하기
    public long getTotalPerfumeCount(List<PTag> keywordIds) {
        List<PerfumeTag> perfumeTags = perfumeTagRepository.findByPtagIn(keywordIds);
        Set<Perfume> uniquePerfumes = new HashSet<>();

        for (PerfumeTag perfumeTag : perfumeTags) {
            Perfume perfume = perfumeTag.getPerfume();
            uniquePerfumes.add(perfume);
        }
        return uniquePerfumes.size();
    }

💡원인 생각해보기

1. HashSet<>

getPerfumesByKeyword 메소드에서 달라진 점이라고는 TreeSet을 HashSet으로 변경했다는 것 하나였다. 배열 정렬 단계를 제외했을 뿐이기 때문에 이건 아닐 거라고 생각했다.

2. DB 복합키 문제

관련 문서들에 의하면 hibernate가 기존 엔티티의 ID 변경을 제한하기 때문에 발생하는 오류라고 했다. 오류가 발생한 클래스는 다음과 같다.

// PerfumeTag.java

@Getter
@Setter
@NoArgsConstructor
@Entity
@IdClass(PerfumeTag.PerfumeTagId.class)
@Table(name = "perfumetag")
public class PerfumeTag {
    @Id
    @ManyToOne
    @JoinColumn(name = "perfume_id")
    private Perfume perfume;

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ptag_id")
    private PTag ptag;

    public static class PerfumeTagId implements Serializable {
        private Perfume perfume;
        private PTag ptag;

    }
    public Long getPerfumeId() {
        return perfume != null ? perfume.getId() : null;
    }

    public void setPerfumeId(Long perfumeId) {
        this.perfume = new Perfume();
        this.perfume.setId(perfumeId);
    }

    public Long getPtagId() {
        return ptag != null ? ptag.getId() : null;
    }

    public void setPtagId(Long ptagId) {
        this.ptag = new PTag();
        this.ptag.setId(ptagId);
    }

}

perfumetag 테이블에는 perfume과 ptag가 복합 기본키로 묶여 있다.
테이블에 접근하면서 둘 중 한 컬럼의 ID 값이 변경되는 건가 싶어 auto-increment가 되는 id 컬럼을 추가한 뒤 단일 기본키로 설정하고, 기존 두 컬럼의 PK 설정을 해제하였다.

이쯤에서 해결되기를 바랐으나 여전히 동일한 오류가 발생했다.

3. Service 클래스 메소드 구현 오류

getTotalPerfumeCount 메소드 자체를 잘못 구현해서 size 값이 제대로 안 나오는 건가 싶어서 하나하나 콘솔에 찍어가며 값을 확인해보았다. 디버깅 하는 게 정석일 텐데 아직 디버깅 방법을 모른다.. (삽질 오브 삽질)

모두 잘 구해진다. uniquePerfumes는 HashSet이라 반복문으로 출력해야 하기 때문에 안 찍히는 게 맞고, size는 잘 나오니 문제 없다고 판단했다.

4. 그러면 Controller 쪽의 문제인가?

@GetMapping("/perfumes/recommend")
    public PerfumeResponseDto recommendPerfumes(@RequestParam("keywordId") List<PTag> keywordIds,
                                              @PageableDefault(size = 10) Pageable pageable) { // 향수 10개씩 반환
        // 유저가 선택한 키워드를 받아와 해당 키워드에 대한 향수 목록 조회
        List<PerfumeDto> recommendedPerfumes = perfumeService.getPerfumesByKeyword(keywordIds, pageable);
        long totalPerfumeCount = perfumeService.getTotalPerfumeCount(keywordIds);

        PerfumeResponseDto responseDto = new PerfumeResponseDto(recommendedPerfumes, totalPerfumeCount);
        return responseDto;
    }

컨트롤러 클래스에는 문제가 될 만한 여지가 없었다. 혹시 자료형 문제인가 싶어 long으로 설정된 totalPerfumeCount의 자료형을 int로도, Long으로도 바꿔보았지만 해결되지 않았다.


💡지원 요청 (썸바디헲미....)

최후의 수단으로 함께 프로젝트를 진행 중인 백엔드 팀원에게 도움을 요청했다.
줌 화면 공유를 통해 함께 코드를 처음부터 뜯어보았다.
머리 두 개가 모였으나 안타깝게도 원인을 알 수 없었고 결국 깃헙에 푸시하면 팀원분이 풀 받아서 천천히 다시 확인해보기로 했다.

💡해결

팀원분이 해결해주셨다.......그저 빛..
과정을 설명해보자면 다음과 같다.

1. ID 클래스 생성

// PerfumeTagId.java

@AllArgsConstructor
@NoArgsContructor
@EqualsAndHashCode
public class PerfumeTagId implements Serializable {
	private Perfume perfume;
    private PTag ptag;
}

PerfumeTag 엔티티에 있던 PerfumeTagId 클래스를 따로 만들어보았으나 미해결.

2. @Transactional(readOnly=true)

'그러면 Service 클래스의 메소드가 데이터를 변경하나?' 싶어서 getPerfumesByKeyword 메소드와 getTotalPerfumeCount 메소드에 @Transactional(readOnly=true) 어노테이션을 달고 재실행

📌 코드 수정하면서 내가 다시 확인했을 때는 getTotalPerfumeCount 메소드에만 추가해줘도 정상 작동했다.

HttpMediaTypeNotAcceptableException 에러 발생 (<- new!)

PerfumeResponseDto를 확인해보니 게터가 없어 응답 내용이 DTO에 포함되지 않기 때문에 발생하는 문제였고, @Getter 어노테이션을 추가해주자 해결됨!

예쁘게 잘 나온다.👏🏻

🪺회고

스프링을 단계별로 공부하지 않고 냅다 실전 프로젝트부터 진행하니 여러모로 막히는 순간이 많다. 이번 에러 역시 @Transactional 어노테이션에 대해 알고 있었다면 스스로 해결할 수도 있었을 거라는 생각이 든다.

프로젝트를 진행하는 동안 이런 오류가 발생하면 이틀은 붙들고 끙끙대다가 도저히 풀 수 없을 것 같을 때 팀원분에게 도움을 요청했다. Spring 커리를 제대로 한 번 타신 분이라서 그런지, 한 두 시간 만에 문제를 해결하시는 걸 보면서 현타를 느끼기도 한다.

그렇지만 노력 없이 얻을 수 있는 건 없기 때문에 당연한 일이다!
이번 방학에는 스프링부트를 차근차근 공부해보려 한다.

일단은 @Transactional에 대해 알아봐야겠다. 데이터 조회 메소드에는 전부 이 어노테이션을 추가하는 게 맞는 것인지, 그리고 getPerfumesByKeyword 메소드만 있을 때는 readOnly 설정을 하지 않아도 정상 작동했는데 어째서 getTotalPerfumeCount 메소드를 추가했더니 이런 오류가 생기는 것인지도 궁금하다.

오케이 공부 킵고잉!!!

참고 자료

profile
차근차근 쌓아올리기

0개의 댓글