UserProfile 엔티티 : 회원의 프로필 (나이, 대학, 직업 등 다양한 속성이 포함되어 있음)
University 엔티티 : 대학 (UserProfile에서 ManyToOne 연관관계로 가지고 있음)
UniversityCategory 엔티티 : 대학의 카테고리 (University에서 ManyToOne 연관관계로 가지고 있음)
문제 발생 시점 : University와 UniversityCategory 매핑 관계를 간접매핑(University가 UniversityCategory의 id값만 가지고 있는 방식)에서 직접매핑(ManyToOne 어노테이션을 사용한 방식)으로 변경하는 과정에서 나온 에러
문제 설명
에러 메시지에서 java.lang.NoClassDefFoundError: Could not initialize class com.swyp.glint.user.domain.QUserProfile가 발생했습니다. 이는 QUserProfile 클래스를 초기화할 때 문제가 발생했음을 나타냅니다. 이 문제의 근본 원인은 QUniversity 클래스의 초기화 과정에서 java.lang.NoSuchMethodError가 발생했기 때문입니다.
원인 분석
java.lang.NoSuchMethodError: 'void com.swyp.glint.keyword.domain.QUniversityCategory.<init>(com.querydsl.core.types.PathMetadata, com.querydsl.core.types.dsl.PathInits)' 에러는 QUniversityCategory 클래스에 생성자가 잘못 정의되었거나 QueryDSL 코드 생성 과정에서 문제가 발생했음을 나타냅니다.
정말 하라는 모든 걸 다 해봤지만(gradle을 clean&build, build.gradle 파일 설정 바꾸기, 문제 될 만한 코드 전부 수정하기) 해결이 안됐음
해결 방법
UniversityCategory 엔티티(domain) 코드에서 생성자 접근 제어자를 Private이 아닌 Public으로 변경해주고 다시 gradle을 clean&build 하는 방법도 있지만 더 좋은건

@PersistenceCreator 사용 (Spring Data JPA 3.0 이후) 하는 것이다!!!!! 이러면 생성자의 접근제어자를 객체지향적이지 않은 public으로 변경해줄 필요 없이 QueryDSL과 같은 도구가 특정 생성자를 사용할 수 있도록 지정할 수 있다!
실제 원인
QueryDSL은 엔티티 클래스를 기반으로 Q 클래스를 생성함. 이 과정에서 리플렉션을 사용하여 엔티티 클래스의 메타데이터를 읽음.
리플렉션은 클래스의 생성자, 필드, 메서드 등을 동적으로 접근할 수 있게 해줌
정리
원인
- QueryDSL의 동작 방식: QueryDSL은 엔티티 클래스를 기반으로 Q 클래스를 생성합니다. 이 과정에서 리플렉션을 사용하여 엔티티 클래스의 메타데이터를 읽습니다. 리플렉션은 클래스의 생성자, 필드, 메서드 등을 동적으로 접근할 수 있게 해줍니다.
- 접근 제어자 제한: 엔티티 클래스의 생성자에 protected 또는 private 접근 제어자가 설정되어 있으면, 리플렉션을 사용하는 QueryDSL이 해당 생성자에 접근하지 못할 수 있습니다. 이는 Q 클래스가 생성될 때 필요한 생성자에 접근할 수 없게 되어 오류가 발생하게 됩니다.
- NoClassDefFoundError와 NoSuchMethodError: 이 오류들은 QueryDSL이 Q 클래스를 초기화하는 동안 필요한 생성자나 메서드를 찾지 못할 때 발생합니다. NoSuchMethodError는 특정 메서드를 찾지 못했을 때 발생하는 오류로, 이 경우 QUniversityCategory 클래스의 초기화 과정에서 문제가 발생한 것입니다.
해결 방법
- 생성자 접근 제어자 변경: UniversityCategory 엔티티의 생성자의 접근 제어자를 public으로 변경했습니다. 이는 QueryDSL이 리플렉션을 통해 엔티티의 생성자에 접근할 수 있도록 해줍니다.
- Q 클래스 재생성: ./gradlew clean 및 ./gradlew build 명령어를 실행하여 QueryDSL이 Q 클래스를 다시 생성하도록 했습니다. 생성자 접근 제어자를 public으로 변경한 후 Q 클래스를 재생성하면, QueryDSL이 올바르게 엔티티 클래스를 처리할 수 있습니다.
더 자세한 설명
- 리플렉션과 접근 제어자: 자바 리플렉션은 런타임에 클래스의 구조를 분석하고 접근할 수 있는 강력한 도구입니다. 그러나, 접근 제어자에 의해 제한될 수 있습니다. 기본적으로 public 접근 제어자는 어디서나 접근 가능하지만, protected와 private 접근 제어자는 클래스 내부 또는 특정 패키지 내에서만 접근할 수 있습니다. QueryDSL이 엔티티 메타데이터를 생성할 때, protected 또는 private 접근 제어자가 설정된 생성자에 접근하지 못하면 문제가 발생합니다.
- QueryDSL과 Q 클래스: QueryDSL은 쿼리 타입을 생성하기 위해 엔티티 클래스를 분석합니다. 이 과정에서 엔티티 클래스의 필드와 메서드를 참조하고, 이를 기반으로 Q 클래스를 생성합니다. 생성자에 접근할 수 없으면 필요한 정보를 추출하지 못하게 되므로 Q 클래스 생성에 실패하게 됩니다.
요약
- 문제 원인: QueryDSL이 리플렉션을 사용하여 엔티티의 메타데이터를 읽는 과정에서, protected 또는 private 생성자로 인해 필요한 생성자에 접근하지 못했기 때문에 발생. (에러 메시지에서 볼 수 있듯이, QUniversityCategory 클래스의 초기화 과정에서 NoSuchMethodError가 발생함. 이는 QueryDSL이 QUniversityCategory 클래스를 생성하는 과정에서 적절한 생성자를 찾지 못했기 때문이다!!!.)
- 해결 방법:
생성자의 접근 제어자를 public으로 변경하여 QueryDSL이 리플렉션을 통해 생성자에 접근할 수 있도록 함.- 위와 같이 @Builder(access = AccessLevel.PRIVATE)와 @PersistenceCreator를 사용하여 생성자를 정의하면, 객체지향적인 관점에서도 문제없고 QueryDSL과 Spring Data JPA도 올바르게 동작할 것입니다.
- 효과: QueryDSL이 엔티티 클래스를 올바르게 처리하고, Q 클래스를 성공적으로 생성하여 문제를 해결함.
@PersistenceCreator에 대한 자세한 document
Spring Data automatically tries to detect a persistent entity’s constructor to be used to materialize objects of that type. The resolution algorithm works as follows:
- If there is a single static factory method annotated with @PersistenceCreator then it is used.
- If there is a single constructor, it is used.
- If there are multiple constructors and exactly one is annotated with @PersistenceCreator, it is used.
- If the type is a Java Record the canonical constructor is used.
- If there’s a no-argument constructor, it is used. Other constructors will be ignored.