OpenSource

오픈소스 Hibernate 버그 PR 의견 제시에서 Jira 이슈 등록까지

지화자_ 2025. 3. 1. 01:01

 

오픈소스 문제 해결에 참여해보고자 하이버네이트 지라에 업로드 된 버그 관련 이슈를 살펴봤습니다. 

그러던 중 꽤 흥미로워 보이는 이슈를 발견했습니다.

관련 지라 이슈글 (https://hibernate.atlassian.net/browse/HHH-19206)

 

 

이슈 내용 번역:

Bytecode-enhanced dirty checking ineffective if entity's embedded ID set manually (to same value)

특정 상황에서 Hibernate의 바이트코드 향상(Bytecode Enhancement) 기능을 사용할 경우, Embedded Identifier를 설정하면 다른 변경 사항이 업데이트되지 않는 버그가 발생할 수 있다.

 

예를 들어, 다음 코드는 data 필드 값을 "updated"로 변경하려고 하지만, 더티체킹이 수행되지 않아 업데이트가 이루어지지 않는다.

 

테스트 코드

scope.inTransaction(session -> {
    var entity = new MyEntity();
    entity.setAnId(new MyEntityId(1L));
    entity.setData("initial");
    session.persist(entity);

    // 이 코드는 불필요하지만, 해가 되지는 않아야 함...
    // 그러나 dirty checking이 비정상적으로 동작하게 만든다.
    // 아래 줄을 주석 처리하면 정상적으로 업데이트가 이루어진다.
    entity.setAnId(new MyEntityId(1L));

    entity.setData("updated");
});

 

참고: 여기서 ID를 설정하는 것이 다소 이상해 보일 수 있지만, ID를 명시적으로 설정하는 것이 합리적인 경우도 있다.

 

내부 코드 분석

// 속성 이름을 정렬하여 매핑을 효율적으로 탐색할 수 있도록 함
Arrays.sort(attributeNames);

int index = 0;
for (int i = 0; i < attributeMappings.size(); i++) {
    final AttributeMapping attributeMapping = attributeMappings.get(i);
    if (isPrefix(attributeMapping, attributeNames[index])) {
        fields.add(attributeMapping.getStateArrayPosition());
        index++;
        if (index < attributeNames.length) {
            // 중복된 속성 이름을 건너뜀
            do {
                if (attributeNames[index].equals(attributeMapping.getAttributeName())) {
                    index++;
                } else {
                    break;
                }
            } while (index < attributeNames.length);
        } else {
            break;
        }
    }
}

위 코드에서, attributeNames 배열의 첫 번째 요소에 예상치 못한 값이 들어가면, 그 이후의 속성들은 변경 사항이 있는지 확인되지 않는다. 즉, 위 예제에서 setAnId를 다시 호출하는 경우, anId가 attributeNames 배열의 첫 번째 요소로 들어가게 되고, 알파벳 순서상 그 뒤에 위치한 data 속성의 변경 사항이 무시되는 문제가 발생한다.

 

핵심 질문:
"왜 ID 속성이 attributeNames에 포함되는 것일까? 이는 명백히 예상치 못한 동작이다."

 

코드 살펴보기

 

문제를 해결하기 위해 하이버네이트 테스트 코드 (Reproducer)를 받아 직접 내부 과정을 탐색했다.

그리고 resolveDirtAttributeIndexes 메서드가 문제의 메서드임을 알 수 있었다.

 

이해를 위해 간단히 설명하면, 해당 메서드는 dirty checking을 수행하는 역할을 수행하는데 그 중 변경된 속성(dirty fileds)를 찾아서 해당 속성의 인덱스를 반환하는 기능을 한다. 즉 fields에 추가되는 Integer가 변경된 컬럼의 idx라는 의미다.

 

그럼 이제 차근차근 코드를 살펴보자.

entityMetamodel은 엔티티의 메타데이터를 관리하는 객체로 이해하면 쉽다. 해당 엔티티에서 변경 가능한 속성을 가져오며, 예상되는 변경 속성 개수를 파악하기 위한 용도로 사용된다. 하지만 객체의 Id가 만약 @EmbededId (ComponentType)으로 선언될 경우, mutablePropertiesIndexes는 null을 반환한다.

 

이후 Issue에서 언급된 코드가 나온다. 간단히 설명하면, 위 코드에서 가져온 attributeNames(dirty checking에 대상이 되는 코드) 에서 업데이트를 수행하는데 여기서 테스트 코드에 새로운 Id를 할당했으므로 값 객체이지만 다른 주소로 인식되고 attributeNames에 들어감 그러나, attributeMapping에는 들어가지 않아서 fields에 추가되는 컬럼이 사라진다.

 

결국 AttributeNames에 새로운 Id가 들어가는 문제를 해결하면 되는 것이었다. 하지만 나는 처음엔 이를 제대로 이해하지 못해서 @EmbededId로 설정된 엔티티에 대해서 mutable한 컬럼이 들어가지 않아 dirty checking이 실패하는 것이라 댓글을 작성했다.

(이후 디버깅과 지라 이슈 등록을 통해 의도된 설계였다는 것을 알게 되었다.)

 

피드백과 검증 받기

https://github.com/hibernate/hibernate-test-case-templates/pull/479

그리고, 하이버네이트 개발자분께서 답글을 달아주셨다.

 

이후 해당 내용을 지라에 올리고자 글을 작성하던 중, EntityMetamodel 객체가 하이버네이트의 ComponentType에 대해 불변의 특성 고려하여 설계됐다는 것을 알게 됐고, 새로운 버그 발견이 아닌 설계 의도를 확인하기 위한 목적으로 변경되어 지라 이슈를 등록했다.(https://hibernate.atlassian.net/browse/HHH-19212) 그리고 다음과 같은 답을 받을 수 있었다.

 

 

마무리

이번 기회를 통해 하이버네이트의 실제 동작을 더 많이 뜯어볼 수 있어서 재밌었다. 앞으로도 시간 날때 오픈소스를 탐방하며, 이슈 해결에 기여하고자 하는 욕심이 생겼다.

 

추가로, 처음 제기된 이슈에 대한 해결은 (https://github.com/hibernate/hibernate-orm/pull/9815/commits/31c62f754987de4b5a2c959d3382c218ac6f5541) dirty checking handler에서 EmbededId에 대한 조건을 추가해서 AttributeNames에 Id가 할당되지 않도록 한 것으로 확인할 수 있었다.