문제 상황
MapReduce 보조 정렬(secondary sort) 기능을 구현하는 과정에서 맵 태스크 종료 시점에 다음과 같은 NullPointerException이 발생했습니다.
java.lang.NullPointerException
at org.apache.hadoop.io.WritableComparator.compare(WritableComparator.java:157)
at org.apache.hadoop.mapred.MapTask$MapOutputBuffer.compare(MapTask.java:1269)
at org.apache.hadoop.util.QuickSort.sortInternal(QuickSort.java:74)
...
at org.apache.hadoop.mapred.MapTask$NewOutputCollector.close(MapTask.java:723)
원인 분석
스택 추적을 따라가면 예외는 WritableComparator.compare() 메서드 내의 this.buffer.reset(b1, s1, l1) 코드에서 발생했습니다. 이는 buffer 객체가 null 상태임을 나타냅니다.
WritableComparator 클래스의 구조를 살펴보면 다음과 같은 멤버 변수들이 있습니다:
private final WritableComparable key1;
private final WritableComparable key2;
private final DataInputBuffer buffer;
이 변수들은 생성자를 통해서만 초기화됩니다. 사용자 정의 Comparator 클래스에 기본 생성자를 정의하지 않으면, 이들 멤버 변수가 null로 남아 있어 정렬 과정에서 예외가 발생합니다.
문제 코드 예시
다음은 생성자가 누락되어 오류를 일으킨 그룹화 비교기입니다:
public static class StockGroupingComparator extends WritableComparator {
// 오류: 기본 생성자 없음
@Override
public int compare(WritableComparable a, WritableComparable b) {
CompositeKey one = (CompositeKey) a;
CompositeKey two = (CompositeKey) b;
return one.getYear() - two.getYear(); // 연도별 그룹화
}
}
해결 방안
반드시 기본 생성자를 추가하고 부모 클래스의 생성자를 올바르게 호출해야 합니다.
1. 그룹화 비교기 수정
public static class YearGroupingComparator extends WritableComparator {
// 올바른 생성자 구현
public YearGroupingComparator() {
super(CompositeKey.class, true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
CompositeKey k1 = (CompositeKey) a;
CompositeKey k2 = (CompositeKey) b;
// 기본 키(연도) 기준으로 그룹화
return Integer.compare(k1.getPrimaryKey(), k2.getPrimaryKey());
}
}
2. 복합 키 정렬 비교기 수정
public static class CompositeSortComparator extends WritableComparator {
public CompositeSortComparator() {
super(CompositeKey.class, true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
CompositeKey k1 = (CompositeKey) a;
CompositeKey k2 = (CompositeKey) b;
// 1차 정렬: 기본 키(연도) 오름차순
int primaryCmp = Integer.compare(k1.getPrimaryKey(), k2.getPrimaryKey());
if (primaryCmp != 0) {
return primaryCmp;
}
// 2차 정렬: 보조 키(값) 내림차순
return Double.compare(k2.getSecondaryKey(), k1.getSecondaryKey());
}
}
생성자 동작 원리
super(CompositeKey.class, true) 호출은 다음과 같은 초기화 과정을 수행합니다:
protected WritableComparator(Class<? extends WritableComparable> keyClass,
boolean createInstances) {
this.keyClass = keyClass;
if (createInstances) {
// 내부 비교에 사용할 객체들 생성
this.key1 = newKey(); // 첫 번째 키 객체
this.key2 = newKey(); // 두 번째 키 객체
this.buffer = new DataInputBuffer(); // 직렬화 데이터 버퍼
} else {
this.key1 = null;
this.key2 = null;
this.buffer = null; // 이 경우 NullPointerException 발생
}
}
두 번째 인자 true는 createInstances 플래그로, 이 값이 false이거나 생성자가 생략되면 모든 내부 멤버가 null로 유지됩니다. 이 상태에서 compare() 메서드가 호출되면 buffer.reset() 시점에 NullPointerException이 발생하게 됩니다.
결론적으로, MapReduce 보조 정렬을 구현할 때 WritableComparator를 상속받는 모든 클래스는 반드시 기본 생성자를 정의하고 super(YourKeyClass.class, true)를 호출하여 내부 멤버들을 올바르게 초기화해야 합니다.