1. 코틀린의 생성자
코틀린을 사용하면 보통 primary constructor 를 사용합니다.
그리고 자바를 배웠다면, 다른 생성자가 정의되어 있으면 기본 생성자가 자동으로 생성되지 않음을 알고 있을 것입니다.
그러면, 기본 생성자 없이 entity 코드가 잘 돌아가는가?
가 이번 글의 주제입니다.
2. 기본 생성자가 필요할 것 같은데?
제가 의심한 상황은 두가지 였습니다.
- java + hibernate 사용할 때 entity 객체에 기본 생성자는 필수였다.
- proxy 객체를 만들 때 기본 생성자가 필요하지 않을까 ?
두번째 의심은 첫번째 글에서 봤듯이, CGLIB 에서 objenesis 라이브러리를 사용함으로서 해결.
그러면 첫번째 의심인 java + hibernate 처럼 kotlin + hibernate 도 기본 생성자가 필수일까 ? 가 남았습니다.
3. 근데 기본 생성자 없이 잘 됬잖아.
말 그대로입니다. 전 글에서 보시면 아시겠지만, 따로 기본생성자를 생성하는 어떠한 코드도 넣지 않았는데 잘 생성되었습니다.
이에 대해서는, 회사에서 최근에 공유받은 내용이 많은 도움이 되었습니다.
3-1. 이 문제의 시발점.
이 문제는 아래의 pr 로 해결됬습니다.
https://github.com/spring-projects/spring-data-commons/pull/233
우리가 살펴볼 코드는 아래입니다. 위의 pr에서 추가된 코드지요.
enum Discoverers {
/**
* Discovers a {@link PreferredConstructor} for Java types.
*/
DEFAULT {
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.PreferredConstructorDiscoverers#discover(org.springframework.data.util.TypeInformation, org.springframework.data.mapping.PersistentEntity)
*/
@Nullable
@Override
<T, P extends PersistentProperty<P>> PreferredConstructor<T, P> discover(TypeInformation<T> type,
@Nullable PersistentEntity<T, P> entity) {
Class<?> rawOwningType = type.getType();
List<Constructor<?>> candidates = new ArrayList<>();
Constructor<?> noArg = null;
for (Constructor<?> candidate : rawOwningType.getDeclaredConstructors()) {
// Synthetic constructors should not be considered
if (candidate.isSynthetic()) {
continue;
}
if (candidate.isAnnotationPresent(PersistenceConstructor.class)) {
return buildPreferredConstructor(candidate, type, entity);
}
if (candidate.getParameterCount() == 0) {
noArg = candidate;
} else {
candidates.add(candidate);
}
}
if (noArg != null) {
return buildPreferredConstructor(noArg, type, entity);
}
return candidates.size() > 1 || candidates.isEmpty() ? null
: buildPreferredConstructor(candidates.iterator().next(), type, entity);
}
},
/**
* Discovers a {@link PreferredConstructor} for Kotlin types.
*/
KOTLIN {
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.PreferredConstructorDiscoverers#discover(org.springframework.data.util.TypeInformation, org.springframework.data.mapping.PersistentEntity)
*/
@Nullable
@Override
<T, P extends PersistentProperty<P>> PreferredConstructor<T, P> discover(TypeInformation<T> type,
@Nullable PersistentEntity<T, P> entity) {
Class<?> rawOwningType = type.getType();
return Arrays.stream(rawOwningType.getDeclaredConstructors()) //
.filter(it -> !it.isSynthetic()) // Synthetic constructors should not be considered
.filter(it -> it.isAnnotationPresent(PersistenceConstructor.class)) // Explicitly defined constructor trumps
// all
.map(it -> buildPreferredConstructor(it, type, entity)) //
.findFirst() //
.orElseGet(() -> {
KFunction<T> primaryConstructor = KClasses
.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(type.getType()));
if (primaryConstructor == null) {
return DEFAULT.discover(type, entity);
}
Constructor<T> javaConstructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor);
return javaConstructor != null ? buildPreferredConstructor(javaConstructor, type, entity) : null;
});
}
};
javadoc 에도 적혀있듯 DEFAULT 는 자바용, KOTLIN 은 코틀린용 으로 나눠져 있습니다.
3-2 . 해석
DEFAULT 의 내용을 요약하자면 아래와 같습니다.
- 선언된 생성자를 싸그리 가져온다.
- isSynthetic() 인 것은 패스.
- @PersistenceConstructor 가 붙은 생성자라면 해당 생성자로 entity 생성.
- 선언된 생성자를 싸그리 가져와서 for문을 돌리는데, @PersistenceConstructor 가 여러개 붙어있다면 어떤 생성자가 먼저 이 조건을 만족하냐에 따라 매번 다른 생성자가 사용될 수 있다.
- 따라서 되도록이면 @PersistenceConstructor 은 하나만 붙이자.
- 생성자의 파라미터가 0개라면 즉, 빈 생성자라면 noArg 라는 변수에 저장.
- 위의 조건에 아무것도 만족하지 않으면 candidates 라는 리스트에 저장.
위의 루프문을 돌린 후,
- noArg 변수가 비어있지 않다면 즉, 빈 생성자가 있다면 빈 생성자로 entity 생성
- noArg 도 비어있고, candidates 의 사이즈가 0 이상이라면 null 반환.
잘 읽어보면, 우리가 기존에 알던 java + hibernate 의 기본 생성자를 넣어야 하는 조건에 맞죠.
또는 @PersistenceConstructor 를 붙이던가.
KOTLIN 의 내용을 요약하면 아래와 같습니다.
- 마찬가지로 isSyntheic() 인 것은 패스.
- @PersistenceConstructor 붙은것을 찾음.
- 위의 조건에 맞는 것이 있다면, 해당 생성자로 생성.
여기까지는 자바와 다르지 않습니다.
하지만 그 아래의 내용은 다르죠.
핵심 코드는 이것입니다.
KFunction<T> primaryConstructor
= KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(type.getType()));
뭔가 primaryConstructor 를 찾고 있네요.
코틀린에서는 primaryConstructor 가 있다면, entity 를 만드는데 사용한다는 것입니다.
자바와는 다르죠.
이후에 primaryConstructor 도 없다면, DEFAULT 의 로직을 따라가게 됩니다.
빈 생성자를 찾는 것이죠.
따라서, 두가지의 의심이 모두 해결이 됬고,
앞의 글에서 기본생성자가 없이도 코드가 잘 돌아간 이유가 설명이 됩니다.
4. 결론
kotlin 으로 프로그래밍하다 보면 primary constructor 는 프로퍼티의 초기화와 생성자를 한꺼번에 처리할 수 있는 이점 때문에 많이 사용합니다.
물론, primary constructor 를 사용하지 않는다면 빈 생성자를 만들어야겠습니다.
코틀린에서 백퍼센트 no-args 플러그인을 사용하여 빈 생성자를 만들지 않아도 된다 ! 라고 말하긴 힘들지만,
우리가 보통 사용하던, 인터넷에서 보이던것만 믿고 알지 못해서 no-args 를 붙이는 일은 없어야 하겠습니다.
5. 다음 주제
코틀린에서 data class 또는 class 를 사용하면 Proxy 객체가 생성이 되지 않음을 깨닫고,
기존의 코드에서 변경해야 하는 상황들이 나왔었습니다.
이를 정리해 보도록 할 예정입니다.
'Kotlin' 카테고리의 다른 글
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (6) (0) | 2021.09.30 |
---|---|
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (5) (1) | 2021.09.07 |
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (4) (0) | 2021.09.06 |
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (2) (0) | 2021.09.06 |
코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (1) (0) | 2021.09.06 |