오늘 겪은 황당한 에러에 대해 설명해 보겠다. 먼저 에러가 발생하는 상황을 이해하기 위해 몇 가지 코드를 제시하겠다. 먼저 Question test data 생성을 담당하는 QuestionBuilder 클래스다.
package net.slipp.domain.qna;
[...]
public class QuestionBuilder {
private SocialUser writer;
private String title;
private String contents;
private Set<Tag> tags = Sets.newHashSet();
private List<Answer> answers = Lists.newArrayList();
public static QuestionBuilder aQuestion() {
return new QuestionBuilder();
}
public QuestionBuilder withWriter(SocialUser writer) {
this.writer = writer;
return this;
}
public QuestionBuilder withTitle(String title) {
this.title = title;
return this;
}
public QuestionBuilder withContents(String contents) {
this.contents = contents;
return this;
}
public QuestionBuilder withTag(Tag tag) {
tags.add(tag);
return this;
}
public QuestionBuilder withAnswer(Answer answer) {
answers.add(answer);
return this;
}
public Question build() {
Question question = new Question(writer, title, contents, tags) {
public List<Answer> getAnswers() {
return answers;
}
};
return question;
}
}
위 QuestionBuilder를 활용해 Question 객체를 생성하고 QuestionRepository를 활용해 다음과 같이 테스트 데이터를 생성했다.
package net.slipp.repository.notification;
[...]
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:/test-applicationContext.xml")
public class NotificationRepositoryIT {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private SocialUserRepository socialUserRepository;
@Test
@Transactional
public void notification_lifecycle() throws Exception {
// given
SocialUser notifiee = SocialUserBuilder.aSocialUser().createTestUser("javajigi");
notifiee = socialUserRepository.save(notifiee);
Question question = QuestionBuilder.aQuestion()
.withTitle("this is title")
.withContents("this is contents")
.withWriter(notifiee)
.build();
question = questionRepository.save(question);
}
}
위와 같이 테스트 데이터를 생성했는데 다음과 같은 에러가 발생했다.
org.springframework.dao.InvalidDataAccessApiUsageException: Unknown entity: net.slipp.domain.qna.QuestionBuilder$1; nested exception is java.lang.IllegalArgumentException: Unknown entity: net.slipp.domain.qna.QuestionBuilder$1
분명 QuestionBuilder를 생성해 Question을 생성했는데 QuestionBuilder를 Entity로 인지하고 Unknown enttiy 에러가 발생한다. 이런 황당한 일이... 그래서 QuestionBuilder를 활용하지 않고 직접 Question을 생성했다.
package net.slipp.repository.notification;
[...]
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:/test-applicationContext.xml")
public class NotificationRepositoryIT {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private SocialUserRepository socialUserRepository;
@Test
@Transactional
public void notification_lifecycle() throws Exception {
// given
SocialUser notifiee = SocialUserBuilder.aSocialUser().createTestUser("javajigi");
notifiee = socialUserRepository.save(notifiee);
Question question = new Question(notifiee, "this is title", "this is contents", new HashSet<Tag>());
question = questionRepository.save(question);
}
}
위와 같이 수정하고 테스트하니 잘 된다. 이런 황당한 일이 발생하다니.. 어디에서 문제가 있는 것인가?
JPA에 사용되는 어노테이션에 @Inherited 타입 어노테이션이 달리지 않았기 때문입니다. new ... {} 방식은 지정한 클래스를 상속해서 익명 클래스를 만드는 표현이므로 클래스에 달았던 JPA 관련 어노테이션이 (부모 클래스까지 점검하지 않는 한) 다 날아가게 됩니다.
8개의 의견 from SLiPP
가장먼저 해볼 수 있는게 패키지를 builder 로 빼서 해보시는 거?
net.slipp.builder.qna.QuestionBuilder
위 이슈에 대해서 다양한 시도를 진행해 봤다. 아무래도 QuestionBuilder의 build() 내부 구현에 이슈가 있는 것은 아닌지라는 생각이 들었다. 특히 Question 객체에 전달하는 Tag와 Answer는 QuestionBuilder가 Collection으로 가지고 있기 때문에 이에 대한 reference를 가지는 상황이 생기리라 생각했다. 그래서 다음과 같이 소스 코드를 수정해 봤다.
위와 같이 Question을 바로 생성하는 구조로 변경하면 정상적으로 동작한다. 그런데 다음과 같이 수정해 보자.
달라진 부분은 Question 객체를 생성할 때 {}를 사용해 재정의한 부분 밖에 없다. 위와 같이 생성했을 때는 에러 없이 동작하지만 아래와 같이 구현할 경우 위 질문에서 발생한 에러가 그대로 발생한다. {}를 사용할 때와 사용하지 않을 때의 값이 전달되는 방법이 다르다는 생각이 든다. 이 두가지 구현에 대해 무엇이 다른지 찾아봐야겠다. call by reference, call by value의 차이가 발생하는 부분이 있는 건가?
JPA에 사용되는 어노테이션에 @Inherited 타입 어노테이션이 달리지 않았기 때문입니다. new ... {} 방식은 지정한 클래스를 상속해서 익명 클래스를 만드는 표현이므로 클래스에 달았던 JPA 관련 어노테이션이 (부모 클래스까지 점검하지 않는 한) 다 날아가게 됩니다.
@fupfin 좋은 정보 감사합니다. @Inherited 어노테이션은 없고 @Inheritance 이 놈인 듯 합니다. 하지만 Inheritance를 하면 제가 의도하지 않은 상황이 발생해서 어떻게 구현하는 것이 좋을지 다시 고민해 봐야겠네요.
@Inherited 맞습니다. http://docs.oracle.com/javase/6/docs/api/java/lang/annotation/Inherited.html
상속 구조를 RDB에 어떻게 매핑할지 지정하는 JPA의 @Inheritance랑은 다릅니다.
제가 혼돈이 되도록 글을 썼네요. 회의 중에 써서... 다시 설명해보겠습니다.
JPA의 @Entity 어노테이션을 예를 들어 보면 http://docs.oracle.com/javaee/5/api/javax/persistence/Entity.html
@Entity 어노테이션은 @Target(value=TYPE) 와 @Retention(value=RUNTIME)이라는 메타 어노테이션이 달려 있습니다. @Inherited는 안 달려있죠. 그래서 인터페이스나 클래스 같은 타입에 지정 가능하고 런타임까지 유지되기는 하지만 @Entity로 지정된 인터페이스나 클래스를 상속한 서브 클래스에는 유산으로 넘어가지 않습니다.
그럼 @Inherited를 달았다면 해결되었느냐...면 그렇지도 않습니다. @Inherited 어노테이션은 타입에 지정되는 어노테이션에만 효과가 있으며 그 조차 interface를 여러 개 구현한 서브 클래스에는 먹히지 않습니다.
이는 원래 어노테이션을 설계할 때 어노테이션을 상속함으로서 생기는 복잡성과 오용을 막기 위해서 의도적으로 막았기 때문입니다. JSR-175의 FAQ에 보면 이런 항목이 있습니다.
It complicates the attribute type system, and makes it much more difficult to write "Specific Tools" (per the taxonomy in Section VI).
결국 단순하게 어노테이션은 서브클래스에 상속되지 않는다고 보면 맞을 것 같습니다.
PS1. 제가 잘 못 알고 있을 수도 있으니 확인 부탁드립니다. PS2. 마크업이 textile인 줄 알았는데 아니네요. 편집창 위에 있는 버튼 말고는 쓸 수 있는 마크업이 없나요?
@fupfin 답변 감사합니다. 어제 답변을 보고 몇 가지 상황을 테스트했는데 똑같은 에러가 발생하는 것을 확인했습니다. 답변에서 이야기했듯이 @Entity를 가지는 클래스를 상속하는 구조가 되는데 이 @Entity가 하위 클래스에도 영향을 미치지 못하기 때문으로 판단됩니다. @Inherited 어노테이션은 클래스 레벨에서는 사용할 수 없고 Field와 Method 레벨에서만 사용가능한데요. 이렇게 일일이 추가한 후 테스트는 해보지 못했습니다. 아무래도 @Inherited를 사용해도 해결책을 사용할 수 없을 것으로 판단됩니다. 이 이슈는 상속을 사용하지 않도록 수정하는 것이 원론적인 해결책이라 생각됩니다.
PS. 이 에디터는 confluence 위키 구문을 따르도록 파서가 구현되어 있습니다. 그런데 최근의 흐름이 markdown으로 넘어가고 있어서 markdown으로 변경하려고 준비하고 있습니다.
Annotation @Entity의 성격을 확인해 보았습니다. Annotation @Entity는 적용할 수 있는 유형이 Type(클래스 또는 인터페이스) 이며 Runtime까지 유지되지만 Inherited 성격이 없어 SubClass로 Entity 성격을 전파할 수 없다. 또한 Entity을 구분할 수 있는 요소는 name 을 정의함으로써 가능하다. 명시적으로 name을 기술하지 않으면 Entity name은 @Entity를 적용한 클래스의 unqualified name을 갖는다 라고 되어 있습니다.
위 내용을 토대로 보면,
anonymous class는 호환(?) 가능한 Entity가 될 수 없습니다.
-- 덧붙이는 글 -- 제가 JPA에 대해서 아는것이 없어서 추론만 했습니다. 그리고 혹시나 해서 subclass 에서도 @Entity(name="Question")을 superclass에서도 @Entity(name="Question")을 같이 사용하면 어떻게 될지 궁금하군요. Entity란 용어의 성격으로 보면 안될것 같고, oo개념으로 보면 될것도 같고 이상하군요 -_-;
의견을 남기기 위해서는 SLiPP 계정이 필요합니다.
안심하세요! 회원가입/로그인 후에도 작성하시던 내용은 안전하게 보존됩니다.
SLiPP 계정으로 로그인하세요.
또는, SNS 계정으로 로그인하세요.