OPEN SOURCE

[Open source contribution] OpenTelemetry-Java 오픈소스 기여 경험기 - 공개 클래스의 한 줄 요약은 왜 중요한가

ebson 2026. 7. 17. 15:09

OpenTelemetry Java(open-telemetry/opentelemetry-java)에 기여를 이어 가면서, 같은 모듈 안에서 비슷한 역할을 하는 클래스들을 나란히 놓고 비교하는 습관이 생겼습니다. 이번 기여(#8490)도 그렇게 시작했습니다. 세 형제 exporter를 차례로 열어 보다가, 그중 하나만 클래스 설명이 통째로 비어 있는 것을 발견했습니다.


같은 모듈, 같은 역할, 다른 문서

문제의 자리는 :exporters:logging 모듈이었습니다. 이 모듈은 텔레메트리를 로그나 표준 출력으로 찍어 주는 exporter를 모아 둡니다. 수집기로 보내기 전에 콘솔이나 로그에서 무엇이 나가는지 확인하는, 거친 디버깅용 도구입니다. 여기에는 신호별로 세 exporter가 한 벌로 들어 있습니다. LoggingSpanExporter, LoggingMetricExporter, 그리고 SystemOutLogRecordExporter입니다.

 

세 클래스를 위에서부터 읽어 보니 두 개에는 클래스 바로 위에 한 줄 설명이 있었습니다. LoggingSpanExporter는 이렇게 적고 있었습니다.

/** A Span Exporter that logs every span at INFO level using java.util.logging. */
public final class LoggingSpanExporter implements SpanExporter {

SystemOutLogRecordExporter에도 클래스 설명이 달려 있었습니다. 그런데 LoggingMetricExporter만은 import 다음 줄에 곧장 클래스 선언이 나왔습니다.

 

import java.util.logging.Logger;

public final class LoggingMetricExporter implements MetricExporter {

같은 모듈에서 같은 일을 하는 형제 셋 중, metric exporter만 자기 소개가 없었던 셈입니다. 이 클래스는 -alpha가 붙지 않은 stable 아티팩트(opentelemetry-exporter-logging)로 공개되는 public 클래스라, IDE의 자동완성이나 Javadoc 사이트에서 누군가 이 타입을 처음 만났을 때 보여 줄 한 줄 요약이 비어 있는 상태였습니다.

 

클래스의 한 줄 요약은 생성된 Javadoc 문서의 목록에서 클래스 이름 옆에 그대로 노출되고, 편집기에서 타입 위에 마우스를 올렸을 때 가장 먼저 뜨는 안내문이기도 합니다. 형제 둘은 그 자리에서 "이건 무엇을 어디로 내보내는 exporter"라고 한 줄로 알려 주는데, metric만 그 자리가 비어 있으니 같은 모듈을 훑어보는 사람 입장에서는 설명이 빠진 것이 눈에 띌 수밖에 없었습니다.


다르다고 해서 다 버그는 아니다

여기서 한 가지는 조심해야 했습니다. 형제끼리 모양이 다르다고 곧장 "버그"라고 단정하면, 의도된 차이를 잘못 건드릴 수 있습니다. 그래서 클래스 설명 말고 다른 부분도 함께 비교해 봤습니다. 특히 자원을 비워 내는 flush()가 신호마다 다르게 동작하지는 않는지 살폈습니다.

// LoggingMetricExporter.flush()
public CompletableResultCode flush() {
  CompletableResultCode resultCode = new CompletableResultCode();
  for (Handler handler : logger.getHandlers()) {
    try {
      handler.flush();
    } catch (Throwable t) {
      return resultCode.fail();
    }
  }
  ...
}

LoggingSpanExporter.flush()도 줄 단위로 같은 모양이었습니다. 핸들러를 돌며 flush하다가 예외가 나면 실패로 반환하는 구조가 두 곳에서 일치했습니다. 종료 처리인 shutdown()도 마찬가지여서, 두 exporter 모두 이미 종료된 상태에서 다시 불리면 "Calling shutdown() multiple times."를 INFO로 남기고 같은 결과를 돌려주고 있었습니다. 즉 동작에는 형제 사이의 어긋남이 없었고, 정작 비어 있는 것은 클래스 설명 한 줄뿐이었습니다. 다른 차이를 찾지 못했다는 사실을 확인하고 나서야, 이 기여의 범위를 "빠진 문서 한 줄을 채우는 것"으로 좁힐 수 있었습니다.

 

처음에는 이렇게 작은 차이까지 일일이 비교하는 일이 과하게 느껴지기도 했습니다. 하지만 형제 사이의 진짜 결함과 그저 비어 있을 뿐인 자리를 구분하려면, 동작이 정말 같은지 직접 읽어 확인하는 수밖에 없었습니다. 이 확인을 건너뛰고 문서만 보고 손을 댔다면, 자칫 동작이 다른 부분을 같은 것으로 착각하거나 의도된 차이를 지웠을지도 모릅니다.


채워 넣을 문장은 멀리서 찾지 않았다

새 설명을 새로 지어내기보다, 형제 LoggingSpanExporter가 이미 쓰고 있던 문장을 가져와 신호 이름만 바꾸는 쪽을 택했습니다. 다만 그 문장이 metric exporter의 실제 동작과 맞는지는 따로 확인해야 했습니다. export()를 열어 보니, 받은 metric을 logger.info(...)와 logger.log(Level.INFO, ...)로 INFO 레벨에 기록하고 있었습니다.

logger.info("Received a collection of " + metrics.size() + " metrics for export.");
...
logger.log(Level.INFO, "metric: {0}", metricData);

그러니 "logs every metric at INFO level using java.util.logging"이라는 설명은 추측이 아니라 코드가 실제로 하는 일과 일치합니다. 실제 머지된 변경은 클래스 선언 위에 이 한 줄을 더한 것이 전부입니다.

 

/** A Metric Exporter that logs every metric at INFO level using java.util.logging. */
public final class LoggingMetricExporter implements MetricExporter {

이 변경이 동작을 바꾸지 않는다는 점은 분명합니다. 시그니처도, 출력 내용도, 로깅 레벨도 그대로입니다. 바뀐 것은 클래스를 소개하는 한 문장뿐입니다. 그래서 이 기여는 버그 수정이 아니라, 형제들 사이에서 metric만 빠져 있던 문서를 같은 기준으로 맞춘 정정에 가깝습니다.


모듈의 성격이 정한 절차

opentelemetry-exporter-logging은 stable 아티팩트입니다. stable 표면이 깨지면 japicmp가 빌드를 멈추기 때문에 평소라면 신중해야 합니다. 다만 클래스 Javadoc은 시그니처의 일부가 아니라, 추가해도 공개 API 비교에는 잡히지 않습니다. 실제로 빌드의 japicmp 검사도 변경 없음으로 지나갔고, 그래서 docs/apidiffs diff를 새로 커밋할 일도 없었습니다. 사용자에게 보이는 동작이나 기본값이 바뀌지 않으니 CHANGELOG 항목도 두지 않았습니다.

 

검증은 모듈 단위로 좁혀 돌렸습니다.

./gradlew :exporters:logging:check

이 검사에는 포맷 검사와 Javadoc 게이트, 그리고 기존 테스트가 함께 들어 있어, 문서 한 줄을 더한 변경이 기존 동작을 건드리지 않았는지 확인하기에 충분했습니다. 모듈 단위로 좁혀 돌리면 결과를 빨리 받을 수 있어, 작은 변경을 확인하는 데에는 이렇게 범위를 좁히는 편이 편했습니다. 동작이 바뀌지 않는 Javadoc 보강에는 단위 테스트를 새로 붙이지 않는 것이 이 저장소의 관행이라, 테스트는 추가하지 않았습니다. 대신 PR 본문에 Javadoc만 바꿨고 동작 변경이 없으며 새 테스트도 없다는 점을 그대로 적었습니다. squash merge라 PR 제목이 곧 커밋 메시지가 되는 점을 떠올려, 제목도 "Add class Javadoc to LoggingMetricExporter"로 무엇을 했는지 한 줄에 담기게 정했습니다.


한 줄을 더하며 남긴 생각

이번 기여로 한 가지를 더 의식하게 됐습니다. 클래스의 한 줄 요약은 막상 그 클래스를 만든 사람에게는 너무 당연해서 빠뜨리기 쉽지만, 처음 그 타입을 마주하는 사람에게는 가장 먼저 읽는 안내문이라는 점입니다. 특히 형제가 여럿인 모듈에서는 하나만 설명이 비어 있으면 그 비대칭이 더 도드라집니다. 그래서 형제들을 나란히 놓고 보는 일이 결함을 찾는 좋은 출발점이 됐습니다.

 

또 하나는, 차이를 발견했을 때 곧장 고치기보다 그 차이가 의도된 것인지 먼저 따져 보는 습관입니다. 이번에는 flush()처럼 달라 보일 수 있는 곳까지 비교해 보고 나서야, 손댈 곳이 클래스 설명 한 줄뿐임을 확신할 수 있었습니다. 이런 한 줄은 한 번 채워 두면 그 클래스가 쓰이는 동안 계속 같은 자리에서 첫 안내를 합니다. 그래서 작아 보여도 형제들과 같은 기준으로 맞춰 둘 가치가 있다고 보았습니다. 거창한 기능을 더하지 않더라도, 공개된 클래스가 자기를 한 줄로 설명하도록 맞춰 두는 일에도 나름의 쓸모가 있다고, 이번에도 다시 느꼈습니다. 다음에도 또 다른 모듈에서 형제들을 나란히 펼쳐 놓고 천천히 읽어 볼 생각입니다.