티스토리 뷰

JPA 를 사용할때 N+1 로 인한 성능 저하는 경계해야할 대표적인 문제중에 하나입니다.

N+1 이 발생하는 경우는 여러가지가 있지만 그 중에서 referncedColmnName 과 관련된 내용을 코드 분석과 함께 알아보고자 합니다.

 

N+1 재현 환경

Repo : https://github.com/SonUngBea/hibernate-project/tree/N%2B1/referencedName

ERD

DATA

Student School

Code

Student Entity

 - NOTE : @ManyToOne 으로 Join 을 걸때 school 의 PK 인 id 가 아닌 name 으로 Join 을 걸었다.

@Entity
public class Student implements Serializable {
	@Id
	private Long id;
	private String name;
	private Long schoolId;
	private String schoolName;

	@ManyToOne
	@JoinColumn(name = "schoolName", referencedColumnName = "name", insertable = false ,updatable = false)
	private School school;
}

School Entity

@Entity
public class School implements Serializable {
	@Id
	private Long id;
	private String name;
}

Load all student

List<Student> students = em.createQuery("select m from Student as m", Student.class).getResultList();

Result

 불러온 Student 들의 School 을 채우기 위해서 Student 갯수만큼 School 을 조회하기 위한 Query 가 날라간 것을 확인할 수 있다.

Hibernate: 
    /* select
        m 
    from
        Student as m */ select
            student0_.id as id1_1_,
            student0_.name as name2_1_,
            student0_.schoolName as schoolNa4_1_,
            student0_.schoolId as schoolId3_1_ 
        from
            Student student0_
Hibernate: 
    /* load School */ select
        school0_.id as id1_0_0_,
        school0_.name as name2_0_0_ 
    from
        School school0_ 
    where
        school0_.name=?
Hibernate: 
    /* load School */ select
        school0_.id as id1_0_0_,
        school0_.name as name2_0_0_ 
    from
        School school0_ 
    where
        school0_.name=?

 

N+1 해결해보기

일반적으로 @ManyToOne N+1 문제를 해결하기 위하여 FetchType 을 LAZY 로 설정하여 School 을 참조할때 불러오도록한다.

@Entity
public class Student implements Serializable {
	@Id
	private Long id;
	private String name;
	private Long schoolId;
	private String schoolName;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "schoolName", referencedColumnName = "name", insertable = false ,updatable = false)
	private School school;
}

위와 같이 ManyToOne 의 fetchType 을 LAZY 로 설정하여 실행해보았다.

 

Hibernate: 
    /* select
        m 
    from
        Student as m */ select
            student0_.id as id1_1_,
            student0_.name as name2_1_,
            student0_.schoolName as schoolNa4_1_,
            student0_.schoolId as schoolId3_1_ 
        from
            Student student0_
Hibernate: 
    /* load School */ select
        school0_.id as id1_0_0_,
        school0_.name as name2_0_0_ 
    from
        School school0_ 
    where
        school0_.name=?
Hibernate: 
    /* load School */ select
        school0_.id as id1_0_0_,
        school0_.name as name2_0_0_ 
    from
        School school0_ 
    where
        school0_.name=?

하지만 여전히 Student 갯수만큼 School 을 조회하는 쿼리가 날라가는 것을 확인할 수 있다.

fetchType 이 LAZY 임에도 불구하고 Student 의 멤버 변수인 school 에 접근하지 않았음에도 fetch 하는 이유가 무엇일까

 

LAZY 동작하지 않는 원인 파악

 Hibernate 를 이용하여 Entity 를 조회하게되면 우리가 작성한 Entity 를 Parsing 하여 resolve 하는 단계를 거치게 된다. 이때 Parsing 된 Type 별로 각기 다른 resolve() 함수를 타게 되며 @ManyToOne 을 사용하여 Join 을 걸어 놓은 멤버변수는 EntityType 이 된다.

 

따라서 EntityType 클래스의 resolve() 함수에서 해당 멤버변수를 초기화 하게 된다.

public Object resolve(Object value, SessionImplementor session, Object owner) throws HibernateException {
	if ( isNotEmbedded( session ) ) {
		return value;
	}
	if ( value != null && !isNull( owner, session ) ) {
		if ( isReferenceToPrimaryKey() ) {
			return resolveIdentifier( (Serializable) value, session );
		}
		else if ( uniqueKeyPropertyName != null ) {
			return loadByUniqueKey( getAssociatedEntityName(), uniqueKeyPropertyName, value, session );
		}
	}	
	return null;
}

 해당 함수에 isRefernceToPrimaryKey() 에 따라서 resolveIdentifier() 를 호출할지 loadByUniqueKey() 를 호출할지 결정하는 조건문을 확인할 수 있다.

 

Student 엔티티에서 볼 수 있듯이 refernceColumnName 을 사용하여 School 테이블의 PK 가 아닌 Column 를 참조하도록 설정하였다. 

 

그로 인해 isRefernceToPrimaryKey() 는 false 가 리턴되어 loadbyUniqueKey() 함수를 사용하여 School Entity 를 채우게 된다. 

	public Object loadByUniqueKey(
			String entityName, 
			String uniqueKeyPropertyName, 
			Object key, 
			SessionImplementor session) throws HibernateException {
		final SessionFactoryImplementor factory = session.getFactory();
		UniqueKeyLoadable persister = ( UniqueKeyLoadable ) factory.getEntityPersister( entityName );

		//TODO: implement caching?! proxies?!

		EntityUniqueKey euk = new EntityUniqueKey(
				entityName, 
				uniqueKeyPropertyName, 
				key, 
				getIdentifierOrUniqueKeyType( factory ),
				persister.getEntityMode(),
				session.getFactory()
		);

		final PersistenceContext persistenceContext = session.getPersistenceContext();
		Object result = persistenceContext.getEntity( euk );
		if ( result == null ) {
			result = persister.loadByUniqueKey( uniqueKeyPropertyName, key, session );
		}
		return result == null ? null : persistenceContext.proxyFor( result );
	}

loadbyUniqueKey() 에서는 EntityUniqueKey 클래스를 사용하여 영속성컨텍스트에서 해당 엔티티를 조회해보고 리턴값이 없다면 DB 조회를 하게 된다.

 

즉, loadbyUniqueKey() 함수 내에는 LAZY 처리를 위한 기능이 없기 때문에 LAZY 옵션을 주더라도 EAGER 로 동작하게 되는 것이다. 

그렇다면 resolveIdentifier() 는 어떨까?

	protected final Object resolveIdentifier(Serializable id, SessionImplementor session) throws HibernateException {
		boolean isProxyUnwrapEnabled = unwrapProxy &&
				getAssociatedEntityPersister( session.getFactory() )
						.isInstrumented();

		Object proxyOrEntity = session.internalLoad(
				getAssociatedEntityName(),
				id,
				eager,
				isNullable() && !isProxyUnwrapEnabled
		);

		if ( proxyOrEntity instanceof HibernateProxy ) {
			( ( HibernateProxy ) proxyOrEntity ).getHibernateLazyInitializer()
					.setUnwrap( isProxyUnwrapEnabled );
		}

		return proxyOrEntity;
	}

resolveIdentifier() 내에는 internalLoad() 함수를 호출할때 eager 라는 파라미터를 받고 있으며 Lazy 로 동작해야할 경우 Proxy 가 리턴될 것을 암시하는 변수명을 확인할 수 있다.

 

resolveIdentifier() 를 호출할 경우에 LAZY 로딩이 동작하는 것을 확인해보기 위하여 Student Entity 의 School 조인을 schoolName 이 아닌 schoolId 로 수정하여 실행해보았다.

@Entity
public class Student implements Serializable {
	@Id
	private Long id;
	private String name;
	private Long schoolId;
	private String schoolName;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "schoolId", insertable = false ,updatable = false)
	private School school;
}

예상한대로 resolveIdentifier() 함수를 타게 되었고 아래와 같이 LAZY 로딩이 정상적으로 동작하였다.

Hibernate: 
    /* select
        m 
    from
        Student as m */ select
            student0_.id as id1_1_,
            student0_.name as name2_1_,
            student0_.schoolId as schoolId3_1_,
            student0_.schoolName as schoolNa4_1_ 
        from
            Student student0_

 

PK 가 아닌 컬럼으로 조인한 경우에 Lazy fetch 가 동작하지 않는 이유

PK 가 아닌 컬럼으로 조인하였을때 LAZY fetch 가 동작하지 않는 이유가 궁금하여 찾아보았다.

Lazy Initialize 기능을 사용하려면 Hibernate Proxy 로 감싸진 객체를 Return 해야하는데 Proxy 는 Null 을 감쌀 수 없기 때문에 PK 가 아닌 컬럼으로 Join 이 걸려있으면 Null 이 아닐 가능성이 있으므로 LAZY 로 동작하지 않도록 하였다는 내용을 찾을 수 있다.

하지만 이와 관련된 코드를 찾지는 못하였다.

 

결론

N+1 문제를 해결하기 위하여 Lazy Fetch 를 적용한다고 하더라도 일부 케이스에서는 해결책이 될 수 없다.


추가정보1) Hiberate4 에서는 Student 의 School 이 동일하더라도 매번 School 을 찾기 위해 쿼리가 날라간 반면 Hibernate5 에서는 School 이 동일하다면 한번의 쿼리만 날라갔다.

Hibernate4

 같은 School 이더라도 매번 조회 함

Hibernate: 
    /* select
        m 
    from
        Student as m */ select
            student0_.id as id1_1_,
            student0_.name as name2_1_,
            student0_.schoolName as schoolNa4_1_,
            student0_.schoolId as schoolId3_1_ 
        from
            Student student0_
Hibernate: 
    /* load School */ select
        school0_.id as id1_0_0_,
        school0_.name as name2_0_0_ 
    from
        School school0_ 
    where
        school0_.name=?
Hibernate: 
    /* load School */ select
        school0_.id as id1_0_0_,
        school0_.name as name2_0_0_ 
    from
        School school0_ 
    where
        school0_.name=?

Hibernate5

 같은 School 은 한번 조회하고 다시 쿼리가 안날라감

Hibernate: 
    /* select
        m 
    from
        Student as m */ select
            student0_.id as id1_1_,
            student0_.name as name2_1_,
            student0_.schoolName as schoolNa4_1_,
            student0_.schoolId as schoolId3_1_ 
        from
            Student student0_
Hibernate: 
    /* load School */ select
        school0_.id as id1_0_0_,
        school0_.name as name2_0_0_ 
    from
        School school0_ 
    where
        school0_.name=?

 

이 차이는 앞서 살펴보았던 loadbyUniqueKey() 내부에서 영속성 Context 에 School 이 저장되느냐 안되느냐의 차이에서 발생한다.

final PersistenceContext persistenceContext = session.getPersistenceContext();
Object result = persistenceContext.getEntity( euk );

 

Hibernate4 의 경우에는 persistenceContext.getEntity( euk ); 했을때 이전에 조회한 Entity 가 Return 되지 않아 DB 에서 다시 조회하였고 Hiberante5 의 경우에는 이전에 조회한 Entity 가 Return 되어 영속성 컨텍스트에 저장되어있는 Entity 가 사용되어 쿼리가 다시 날라가지 않았다.

// If the entity was not in the Persistence Context, but was found now,
// add it to the Persistence Context
if (result != null) {
	persistenceContext.addEntity(euk, result);
}

 

Hibernate5 부터 loadbyUniqueKey() 내부에서 불러온 Entity 를 영속성 컨텍스트에 저장하고 있기 때문에 발생하는 차이이다.

 

즉, 불러온 Student 의 School 이 모두 같을 경우 Hibernate4 의 경우에는 Student 갯수 만큼 School 조회 쿼리가 날라가고 Hibernate5 의 경우에는 School 조회 쿼리가 딱 한번만 날라간다. 하지만 School 의 종류가 다르다면 Hibernate5 에서도 School 의 종류 만큼 쿼리가 날라간다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함