[스프링] 생성자가 private 일때 스프링은 객체는 어떻게 만들까?
어제 지니님의 요청한 코드리뷰를 하다 아래와 같은 코드를 발견했다. (이름은 적절히 각색하였습니다.)
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
class ReceiptProperties {
private String receiptUrl;
}
내가 하려는 이야기는 lombok을 썼다는 것이 아니고 왜 private 생성자로 결정 했을까가 포인트이다.
위의 코드를 일반 자바코드로 풀어쓰면 아래와 같다.
class ReceiptProperties {
private String receiptUrl;
private ReceiptProperties() {
}
public String getReceiptUrl() {
return receiptUrl;
}
public void setReceiptUrl(String receiptUrl) {
this.receiptUrl = receiptUrl;
}
}
일반 코드라면 생성자가 private이므로 아래와 같이 인스턴스를 생성할 수 없다.
ReceiptProperties properties = new ReceiptProperties(); // 컴파일 에러!
properties.setReceiptUrl(...);
그러면 어떻게 만들었지?
결론부터 설명하면 리플렉션을 이용하면 접근 제어를 풀 수 있다.
스프링에서는 빈 생성과 리플렉션을 자주하기에 편의를 위해 유틸리티로 만들어 놓았다.
BeanUtils
package org.springframework.beans;
public abstract class BeanUtils {
// ..
public static <T> T instantiateClass(Class<T> clazz) throws BeanInstantiationException {
// ..
try {
return instantiateClass(clazz.getDeclaredConstructor()); // 여기
}
catch (NoSuchMethodException ex) {
// ..
}
// ..
}
public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
try {
ReflectionUtils.makeAccessible(ctor); // 여기가 접근제어자를 바꾸는 부분
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) {
return KotlinDelegate.instantiateClass(ctor, args);
}
else {
// ..
return ctor.newInstance(argsWithDefaultValues); // 여기
}
}
catch (InstantiationException ex) {
// ..
}
// ..
}
위의 코드를 보면 ReflectionUtils.makeAccessible 에서 접근제어자를 해제하는 부분이다.
아래 ReflectionUtils에서 보면 알 수 있듯이 setAccessible 를 사용하고 있다.
ReflectionUtils
package org.springframework.util;
public abstract class ReflectionUtils {
// ..
@SuppressWarnings("deprecation") // on JDK 9
public static void makeAccessible(Constructor<?> ctor) {
if ((!Modifier.isPublic(ctor.getModifiers()) ||
!Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) &&
!ctor.isAccessible()) {
ctor.setAccessible(true);
}
}
따러서 사용자의 생성자가 private이 더라도 접근제어를 맘대로 해제하고 생성자 호출을 할 수 있는 것이다.
위의 주석에도 되어 있지만 deprecation 에 대해 경고 억제(SuppressWarnings)를 하고 있다.
이것은 JDK9 부터는 모듈 시스템이 도입되었기 때문이다.
따라서 Java 9 이상의 환경에서는 setAccessible 호출시 InaccessibleObjectException 예외가 발생할 수 있고 위의 방식이 잘 동작하지 않을 수 있다.
Java 11 에서 테스트
혹시나 싶어 Runtime을 JDK11로 올려서 테스트 해보았으나 다행히 잘 돌아갔다.
기본 모듈 등이 없어진 것이 있어서 아래와 같은 예외가 발생했다.
Caused by: java.lang.NoClassDefFoundError: javax/xml/soap/MessageFactory
at java.base/java.lang.Class.getDeclaredConstructors0(Native Method)
at java.base/java.lang.Class.privateGetDeclaredConstructors(Class.java:3138)
at java.base/java.lang.Class.getConstructor0(Class.java:3343)
at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2554)
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:138)
... 87 common frames omitted
Caused by: java.lang.ClassNotFoundException: javax.xml.soap.MessageFactory
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
... 92 common frames omitted
Jakarta SOAP 구현체를 의존성에 추가해주면 다행히 동작했다.
compile group: 'com.sun.xml.messaging.saaj', name: 'saaj-impl', version: '1.5.2'
결론
현재 운영중인 환경은 Java 8 이나 다음 LTS에서도 동작이 되는 것을 확인했기에 당장은 문제가 없을 것 같다.
프레임워크 역시 객체를 만들기 위해 생성자를 이용해야 한다.
private로 막았을 경우 얻을 수 있는게 무엇인가를 생각해 보았을 때는 이유는 딱히 모르겠다.
그냥 아래와 같이 단순하게 사용하면 테스트 코드 만들때도 문제없고 프레임워크도 setAccessible 할 필요도 없어진다.
또한 일단 보기에도 간결하니 일석삼조다.
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
class ReceiptProperties {
private String receiptUrl;
}