티스토리 뷰
[Open source contribution] Apache Iceberg 오픈소스 기여 경험기 - 1줄 수정으로 되짚어본 Java 접근 제어자의 원칙
ebson 2026. 4. 7. 07:56Apache Iceberg와 Arrow 모듈
Apache Iceberg는 대규모 분석 워크로드를 위한 고성능 오픈 테이블 포맷입니다. Netflix에서 시작되어 현재는 Apache 재단의 Top-Level 프로젝트로 성장했으며, Spark, Flink, Trino 등 주요 데이터 처리 엔진에서 폭넓게 사용되고 있습니다.
Iceberg의 Java 레퍼런스 구현체는 여러 모듈로 구성되어 있습니다. 그중 Arrow 모듈은 Apache Arrow의 컬럼나 메모리 포맷을 활용하여 데이터를 벡터화된 방식으로 읽어 들이는 역할을 담당합니다. 이 글에서 다루는 기여는 Arrow 모듈의 VectorHolder 클래스에서 생성자의 접근 제어자를 package-private에서 private으로 변경한 단 1줄의 수정입니다.
PR: apache/iceberg#15804 — Arrow: Tighten VectorHolder constructor visibility to private
규모가 크지 않은 변경이었지만, 이 과정에서 Java 접근 제어자의 의미를 다시 한번 정리하게 되었고, 오픈소스 프로젝트에서 작은 코드 품질 개선이 어떤 맥락으로 이루어지는지를 경험할 수 있었습니다.
@VisibleForTesting에서 private으로 - 방향을 수정하기까지
이 기여의 출발점은 Arrow 모듈의 코드를 분석하면서 VectorHolder 클래스의 3-arg 생성자가 접근 제어자 없이 선언되어 있다는 점을 발견한 것이었습니다. Java에서 접근 제어자를 명시하지 않으면 package-private이 됩니다. 처음에는 이 생성자가 테스트를 위해 가시성이 넓어진 것은 아닌지 의심하여 @VisibleForTesting 어노테이션을 추가하는 방향을 검토했습니다.
그런데 실제로 호출자를 추적해보니, 이 생성자를 호출하는 코드는 테스트가 아니었습니다. arrow/src/test/ 디렉토리에서 이 생성자를 참조하는 파일은 없었고, 호출자는 모두 VectorHolder.java 파일 안에 있었습니다. 구체적으로는 두 곳이었습니다.
첫째, PositionVectorHolder라는 static inner class가 super(vector, icebergField, nulls)로 부모 생성자를 호출하고 있었습니다.
public static class PositionVectorHolder extends VectorHolder {
public PositionVectorHolder(
FieldVector vector, Types.NestedField icebergField, NullabilityHolder nulls) {
super(vector, icebergField, nulls);
}
}
둘째, vectorHolder()라는 정적 팩토리 메서드가 new VectorHolder(vector, icebergField, nulls)로 직접 인스턴스를 생성하고 있었습니다.
public static VectorHolder vectorHolder(
FieldVector vector, Types.NestedField icebergField, NullabilityHolder nulls) {
return new VectorHolder(vector, icebergField, nulls);
}
두 호출자 모두 같은 최상위 클래스(VectorHolder) 안에 위치합니다. @VisibleForTesting의 의미는 "테스트 코드에서만 사용하기 위해 본래보다 넓은 가시성을 부여했다"는 것인데, 이 생성자는 테스트에서 사용되지 않으므로 해당 어노테이션을 추가하는 것은 오히려 잘못된 신호를 줄 수 있었습니다. 대신, 모든 호출자가 같은 클래스 내부에 있으므로 가시성을 private으로 축소하는 것이 정확한 조치였습니다.
Java의 접근 제어자와 static nested class
이 변경이 안전한 이유를 이해하려면, Java에서 static nested class가 enclosing class의 private 멤버에 접근할 수 있다는 규칙을 확인할 필요가 있습니다.
Java Language Specification(JLS) §6.6.1은 접근 가능성(accessibility)에 대해 다음과 같이 규정합니다. private으로 선언된 멤버는 해당 멤버가 선언된 최상위 클래스(top-level class)의 본문 내에서만 접근할 수 있습니다. 여기서 중요한 점은 "해당 클래스의 인스턴스에서만"이 아니라 "최상위 클래스의 본문 내에서"라는 범위 지정입니다. static nested class도 최상위 클래스의 본문에 포함되므로, enclosing class의 private 생성자나 필드에 자연스럽게 접근할 수 있습니다.
PositionVectorHolder는 VectorHolder 안에 선언된 static inner class이고, vectorHolder() 팩토리 메서드 역시 VectorHolder 클래스의 본문에 속합니다. 따라서 해당 생성자를 private으로 변경하더라도, 이 두 호출자의 접근에는 아무런 영향이 없습니다.
반면 package-private은 같은 패키지에 속하는 모든 클래스에서 접근할 수 있습니다.
VectorHolder가 속한 org.apache.iceberg.arrow.vectorized 패키지에는 ArrowReader, VectorizedArrowReader 등 여러 클래스가 있는데, 이 생성자를 package-private으로 두면 이 클래스들도 이론적으로 해당 생성자를 호출할 수 있는 상태가 됩니다. 실제로 호출하는 곳이 없다면, 불필요하게 넓은 접근 범위를 부여하고 있는 셈입니다.
실제 변경 내용
변경은 VectorHolder.java의 76행, 단 한 줄이었습니다.
- VectorHolder(FieldVector vec, Types.NestedField field, NullabilityHolder nulls) {
+ private VectorHolder(FieldVector vec, Types.NestedField field, NullabilityHolder nulls) {
private 키워드 하나를 추가한 것입니다. 로직 변경은 전혀 없으며, 생성자 본문도 그대로입니다.
private VectorHolder(FieldVector vec, Types.NestedField field, NullabilityHolder nulls) {
columnDescriptor = null;
vector = vec;
isDictionaryEncoded = false;
dictionary = null;
nullabilityHolder = nulls;
icebergField = field;
}
이 생성자는 ColumnDescriptor와 Dictionary 없이 간략하게 VectorHolder를 생성하는 용도입니다. 6-arg public 생성자가 전체 필드를 받는 것과 달리, 이 생성자는 내부적으로 position vector나 간단한 벡터 홀더를 만들 때 사용됩니다. 용도가 내부에 한정되어 있으므로, 가시성도 그에 맞추는 것이 자연스럽습니다.
빌드 검증과 PR 제출
PR을 제출하기 전에 세 단계의 검증을 수행했습니다.
먼저, ./gradlew spotlessCheck로 코드 스타일을 확인했습니다. Iceberg 프로젝트는 Google Java Style을 기반으로 spotless 플러그인을 통해 포매팅을 강제하고 있어, 스타일 위반이 있으면 CI에서 빌드가 실패합니다. 접근 제어자만 추가한 변경이므로 스타일 검증은 문제없이 통과했습니다.
다음으로, ./gradlew :iceberg-arrow:build를 실행하여 Arrow 모듈의 전체 빌드와 테스트를 수행했습니다. 컴파일 오류나 테스트 실패가 없는지 확인하는 과정이었습니다. 모든 테스트가 통과했고, 이는 package-private에서 private으로의 변경이 기존 동작에 영향을 주지 않았음을 보여줍니다.
마지막으로, Arrow 모듈에 의존하는 Spark 4.1 모듈의 컴파일도 확인했습니다. ./gradlew :iceberg-spark:iceberg-spark-4.1_2.13:compileJava를 실행하여, 의존 모듈 측에서 이 생성자를 참조하고 있지 않다는 것을 한번 더 검증했습니다.
PR을 제출한 뒤, Iceberg 커미터인 huaxingao 님이 리뷰를 진행해주셨습니다. 별도의 수정 요청 없이 승인이 이루어졌고, 2026년 3월 28일에 머지되었습니다.
작은 변경의 의미
단 1줄의 수정이지만, 이 변경에는 몇 가지 원칙이 반영되어 있습니다.
소프트웨어 설계에서 최소 권한 원칙(Principle of Least Privilege)은 "필요한 최소한의 권한만 부여한다"는 개념입니다. 접근 제어자에 적용하면, 외부에서 호출할 필요가 없는 멤버는 가능한 한 좁은 범위로 제한하는 것이 바람직합니다. package-private도 충분히 좁은 범위이지만, 호출자가 모두 같은 클래스 안에 있다면 private이 더 정확한 선택입니다. 이렇게 하면 향후 같은 패키지에 새로운 클래스가 추가되더라도 이 생성자에 의존하게 될 가능성을 원천적으로 차단할 수 있습니다.
바이너리 호환성 관점에서도 이 변경은 안전합니다. package-private 멤버는 공개 API가 아니며, 실제로 같은 패키지 내 다른 클래스에서 호출하는 곳이 없었으므로 어떤 외부 코드에도 영향을 주지 않습니다.
Apache Iceberg 프로젝트에서는 이런 종류의 가시성 정리(visibility cleanup)가 꾸준히 이루어지고 있습니다. 예를 들어, PR #15756에서는 Core 모듈의 SchemaUpdate 생성자에 @VisibleForTesting 어노테이션을 추가하는 작업이 진행되었습니다. 프로젝트 컨벤션 문서에서도 "package-private by default - only make public with demonstrated need"라는 원칙을 명시하고 있어, 가시성을 필요 이상으로 넓히지 않는 것이 프로젝트 전반의 방향성과 일치합니다.
VectorHolder 클래스의 구조
VectorHolder는 Arrow 벡터 배치 읽기에 필요한 상태를 담는 컨테이너 클래스입니다. 이 클래스 안에는 다양한 용도의 내부 클래스와 팩토리 메서드가 정의되어 있습니다.
6-arg public 생성자는 ColumnDescriptor, FieldVector, isDictionaryEncoded, Dictionary, NullabilityHolder, NestedField를 모두 받으며, Parquet 컬럼 디스크립터 기반의 완전한 벡터 홀더를 만들 때 사용됩니다. 이 생성자는 외부 클래스에서도 호출할 수 있어야 하므로 public입니다.
반면 이번에 private으로 변경한 3-arg 생성자는 컬럼 디스크립터와 딕셔너리 없이 간략한 벡터 홀더를 만드는 용도입니다. PositionVectorHolder는 position delete 처리 시 위치 정보를 담는 벡터를 감싸고, vectorHolder() 팩토리 메서드는 일반적인 비-딕셔너리 벡터를 간편하게 생성합니다. 두 경우 모두 VectorHolder 내부에서만 사용되는 패턴이므로, 생성자의 가시성이 private으로 한정되는 것이 클래스 설계 의도를 더 명확히 드러냅니다.
이 외에도 VectorHolder에는 인자 없는 private 생성자(dummy holder용)와 1-arg private 생성자(typed constant holder용)가 이미 private으로 선언되어 있었습니다. 3-arg 생성자만 package-private으로 남아 있던 것은 의도적인 설계라기보다는 초기 작성 시 누락된 것으로 보입니다. 이번 변경으로 내부 전용 생성자들의 가시성이 일관되게 private으로 통일되었습니다.
마무리
이번 기여를 통해 얻은 인사이트를 정리합니다.
코드 분석에서 가장 먼저 세워야 할 가설은 "이 코드의 호출자가 누구인가"였습니다. @VisibleForTesting 추가가 적절해 보였던 첫 판단은, 실제 호출자를 추적하면서 뒤집어졌습니다. 이 경험은 코드를 수정하기 전에 정적 분석의 습관을 갖는 것이 얼마나 중요한지를 다시 확인시켜 주었습니다.
또한, 작은 변경이라 하더라도 오픈소스 프로젝트에서는 일정한 검증 절차를 거치는 것이 기본이라는 점을 체감했습니다. 코드 스타일 검증, 모듈 빌드, 의존 모듈 컴파일 확인까지 단계적으로 수행한 뒤 PR을 제출하는 과정이 단순해 보이지만, 이 절차가 리뷰어의 신뢰를 얻는 기반이 됩니다.
Java의 접근 제어자는 문법적으로 간단하지만, 실제 코드베이스에서 적절한 수준을 판단하려면 호출자 분석, 클래스 구조 이해, 프로젝트 컨벤션 파악이 필요합니다. 1줄의 수정이었지만, 그 1줄에 도달하기까지의 분석 과정에서 배운 것이 더 많았습니다. 앞으로도 이런 작은 코드 품질 개선 기여를 통해 프로젝트의 코드베이스를 더 깊이 이해하고, 점진적으로 기여의 범위를 넓혀가고자 합니다.
'OPEN SOURCE' 카테고리의 다른 글
- Total
- Today
- Yesterday
- TTL 설계
- Initialization-on-Demand Holder Idiom
- 백엔드 성능
- Eager Initialization
- Cache Aside
- 백엔드 아키텍처
- 백엔드 성능 설계
- DB 인덱스 성능
- Cache Avalanche
- DB 트랜잭션
- Spring Batch
- Double-Checked Locking
- spring batch 5
- 트래픽 처리
- 스레드 생명주기
- Hot Key 문제
- Java Performance
- Enum 기반 싱글톤
- Redis 성능 개선
- 캐시 성능 비교
- 동시성처리
- 캐시 장애
- Redis 캐시 전략
- InterruptedException
- mybatis
- 백엔드 성능 튜닝
- Redis vs DB
- 트랜잭션 관리
- Cache Penetration
- 캐시와 인덱스
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
