본문 바로가기

Kotlin

코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (3)

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에서 추가된 코드지요.

https://github.com/spring-projects/spring-data-commons/blob/3c887f631940a0492e607ac6f885aff7be6b4fa4/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java#L96-L174

 

 

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 객체가 생성이 되지 않음을 깨닫고,

 

기존의 코드에서 변경해야 하는 상황들이 나왔었습니다.

 

이를 정리해 보도록 할 예정입니다.