Hibernate에서 객체 할당 시, 기본 생성자 필요한 이유

대영·2024년 11월 9일

🙏내용에 대한 피드백은 언제나 환영입니다!!🙏

이전 글에서 Reflection에 대해서 알아보았다.

이번에는 Hibernate에서 Reflection이 사용되는데, 왜 기본생성자가 있어야 작동이 하는지 알아보겠다.

☘️ 왜 기본생성자가 필요할까?

특정 생성자를 이용하여 인스턴스를 생성하고자 한다면 매개변수에 대한 타입과 순서(주입하고자 하는 순서) 모두 일치해야 한다.

하지만, Hibernate에서 엔티티 객체 할당하는 과정에서는 생성자의 정확한 매개변수의 위치나 타입 정보를 알 수 없다.
(Jackson 역직렬화에서도 해당 - @JsonCreator 설정을 안했다고 가정.)

그래서, 디버깅을 통해 동작과정을 한번 살펴 보았다.

🧐 Hibernate에서의 리플렉션과 기본 생성자 과정

과정이 복잡하기에 리플렉션을 통해 기본 생성자를 할당 받고, 필드에 값을 주입하는 과정을 살펴 보았다.

👉 1. 엔티티 인스턴스 생성 호출

org.hibernate.persister.entity.AbstractEntityPersister; 위치에 아래의 메서드가 있다.

	@Override
	public Object instantiate(Object id, SharedSessionContractImplementor session) {
		final Object instance = getRepresentationStrategy().getInstantiator().instantiate( session.getFactory() );
		linkToSession( instance, session );
		if ( id != null ) {
			setIdentifier( instance, id, session );
		}
		return instance;
	}

이 곳에서,

final Object instance = getRepresentationStrategy().getInstantiator().instantiate( session.getFactory() );
이부분이 바로 엔티티의 인스턴스 생성을 위해 호출하는 부분이다.

getRepresentationStrategy()는 엔티티 표현을 위한 전략 패턴이기 때문에 해당 메서드에 들어가 확인하면

	@Override
	public EntityRepresentationStrategy getRepresentationStrategy() {
		return representationStrategy;
	}

인 것을 볼 수 있다.

여기서, representationStrategy 이거는 EntityRepresentationStrategyPojoStandard 를 가리킨다.

👉 2. 엔티티 표현 전략의 인스턴스 생성기 찾기

1번에서 인스턴스 생성을 위해 EntityRepresentationStrategyPojoStandard을 가리키는 것을 알 수 있었다.

그래서, org.hibernate.metamodel.internal.EntityRepresentationStrategyPojoStandard 해당 위치로 이동하여 아래의 메서드를 확인 하였다.

	@Override
	public EntityInstantiator getInstantiator() {
		return instantiator;
	}

여기서 인스턴스 생성기를 찾기 위해 instantiator을 반환하는데, 이것이 가리키는 것이 EntityInstantiatorPojostandard 이다.
즉, 해당 구간에서 실제로 인스턴스를 생성하는 것이다.

👉 3. 기본 생성자 확인 및 호출

이제 실제로 인스턴스를 호출하는 org.hibernate.metamodel.internal.EntityInstantiatorPojoStandard 보려고 한다.

해당 클래서에서 생성자를 호출할 떄, 아래의 메서드를 호출한다.

	protected static Constructor<?> resolveConstructor(Class<?> mappedPojoClass) {
		try {
			return ReflectHelper.getDefaultConstructor( mappedPojoClass);
		}
		catch ( PropertyNotFoundException e ) {
			LOG.noDefaultConstructor( mappedPojoClass.getName() );
			return null;
		}
	}

이곳에서 볼 점은, return ReflectHelper.getDefaultConstructor(mappedPojoClass); 이 부분이다.

이 부분이 바로, 기본 생성자를 할당 받기 위한 과정이다.

해당 클래스에 들어가면 아래와 같이 기본 생성자를 받고, private에도 접근하기 위해 접근 설정을 하는 것을 볼 수 있다.

하지만, Entity에서 기본 생성자를 private를 하지 않는 것을 권장한다.
지연 로딩프록시 객체의 동작 방식과 관련이 있다. 프록시는 Entity를 상속해서 만들기 때문에, 지연 로딩으로 인해 프록시 객체를 넣어줘야 한다면 Entity를 상속하여 만들어야 한다. 그리고, 실제 필요한 시점에 Entity를 조회하여 프록시 객체가 원본 Entity를 참조하는 것이다.
그렇기 때문에, Entity를 private하면 지연로딩을 위한 프록시 객체를 생성할 수 없게 되는 것이다.

	public static <T> Constructor<T> getDefaultConstructor(Class<T> clazz) throws PropertyNotFoundException {
		if ( isAbstractClass( clazz ) ) {
			return null;
		}

		try {
			Constructor<T> constructor = clazz.getDeclaredConstructor( NO_PARAM_SIGNATURE );
			ensureAccessibility( constructor );
			return constructor;
		}
		catch ( NoSuchMethodException nme ) {
			throw new PropertyNotFoundException(
					"Object class [" + clazz.getName() + "] must declare a default (no-argument) constructor"
			);
		}
	}
    
    // private 접근 설정
    public static void ensureAccessibility(AccessibleObject accessibleObject) {
		if ( !accessibleObject.isAccessible() ) {
			accessibleObject.setAccessible( true );
		}
	}

다음은, 1번에서 보았던 instantiate 부분이다. 실제로 이것을 통해 기본 생성자를 할당 받는다.

	@Override
	public Object instantiate(SessionFactoryImplementor sessionFactory) {
		if ( isAbstract() ) {
			throw new InstantiationException( "Cannot instantiate abstract class or interface", getMappedPojoClass() );
		}
		else if ( constructor == null ) {
			throw new InstantiationException( "No default constructor for entity", getMappedPojoClass() );
		}
		else {
			try {
				return applyInterception( constructor.newInstance( (Object[]) null ) );
			}
			catch ( Exception e ) {
				throw new InstantiationException( "Could not instantiate entity", getMappedPojoClass(), e );
			}
		}
	}

그 결과, 아래 사진과 같이 모든 필드의 값이 null인 객체가 생기게 된다.

👉 4. 엔티티 인스턴스의 필드 값 할당

org.hibernate.property.access.spi.SetterFieldImpl에서 기본 생성자를 통해 만들어진 필드의 값을 주입하는 과정이 아래와 있다.

	@Override
	public void set(Object target, @Nullable Object value) {
		try {
			field.set( target, value );
		}
		catch (Exception e) {
			if (value == null && field.getType().isPrimitive()) {
				throw new PropertyAccessException(
						e,
						String.format(
								Locale.ROOT,
								"Null value was assigned to a property [%s.%s] of primitive type",
								containerClass,
								propertyName
         .
         .
         .
     }

이러한 과정을 통해서, DB에서 어떻게 리플렉션이 기본 생성자를 생성하고 필드에 값을 주입할 수 있는지 알 수 있다.

➕ Rest Api에서의 Jackson 역직렬화 시, 왜 기본생성자를 안써도 되냐

리플렉션에서는 기본 생성자가 필요하다. 이러한 개념 때문에, Rest Api에서는 JSON을 역직렬화할 DTO에 항상 기본 생성자를 생성했던 기억이 있다.

하지만, 다시 과정을 보니 기본 생성자가 없어도 제대로 동작하였다.

왜 그런 것일까?

우선, Jackson에서 생성자를 사용하는 방식은 여러 개가 있다.

기본 생성자를 사용하는 것과 특정 생성자를 할당하여(@JsonCreater) 사용하는 것이다.

하지만 나는 DTO에 @JsonCreater를 사용하지 않았지만, 정상적으로 작동하였다.

그 이유는 ParameterNamesModule라이브러리 덕분이다.

ParameterNamesModule란❓

자세한 것은 FasterXML/jackson-modules-java8에서 봐도 좋을 것 같다.

ParameterNamesModule은 Jackson의 모듈 중 하나이다.

클래스의 생성자 및 메서드 매개변수 이름에 런타임 접근을 가능하게 만들어 준다.

이 모듈을 사용하면 Jackson이 JSON 데이터를 객체로 역직렬화할 때 생성자 매개변수 이름을 기준으로 JSON 필드와 자동 매핑할 수 있어 @JsonProperty와 같은 어노테이션을 생략할 수 있다.

이것을 사용한다면 기본 생성자가 없이 모든 필드에 대한 생성자(@AllArgsConstructor)만 있어도 작동하는 것이다.

ParameterNamesModule 사용 방식

ObjectMapper에 모듈 등록

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ParameterNamesModule());

JsonCreator.Mode 설정

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES));

모드를 설정하여 여러 생성자를 처리하는 방식을 정의할 수 있다.

  • DEFAULT: 단일 인자 생성자는 JSON 전체 값을 매개변수로 해석.
  • PROPERTIES: JSON 필드를 여러 매개변수를 가진 생성자와 매핑.
  • DELEGATING: JSON 객체 전체를 단일 인자 생성자의 매개변수로 전달.
  • DISABLED: 생성자 기반 역직렬화 비활성화, 기본 생성자나 setter를 통해 역직렬화.

이렇게 모듈을 설정하면 아래와 같이 기본 생성자가 없더라도 역직렬화가 가능한 것이다.

class Person {
    private final String name;
    private final String surname;

    // 모든 필드를 받는 생성자
    public Person(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }

    public String getName() {
        return name;
    }

    public String getSurname() {
        return surname;
    }
}

👉 web에서의 ParameterNamesModule 설정

implementation 'org.springframework.boot:spring-boot-starter-web'를 등록하면 아래 사진과 같이 ParameterNamesModule 라이브러리가 등록된다.

그렇기 때문에, Rest Api에서 @RequestBody를 통해 JSON을 역직렬화하는 과정에서 기본 생성자가 없더라도 정상적으로 실행되는 것이였다.

💡 느낀점

리플렉션은 기본 생성자가 필요하다. 이정도의 개념만 알고 있었다.
디버깅을 통해 흐름을 따라가보니 그러한 과정에 대해서 정확히 이해할 수 있었다.

또한 Restful api에서 JSON을 역직렬화할 때, 기본 생성자가 반드시 필요로 하지 않는다는 것을 알 수 있었어서 나의 잘못된 지식에 대해서 고칠 수 있었다.

profile
Better than yesterday.

0개의 댓글