[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개의 댓글

관련 채용 정보