본문 바로가기

Programing/Framework

[스프링] 생성자가 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;
}