티스토리 뷰

예제 생성기를 살펴보다 발견한 것

Apache ShardingSphere의 examples 쪽을 살펴보던 중이었습니다. ShardingSphere에는 shardingsphere-jdbc-example-generator라는 흥미로운 모듈이 있습니다. 이름 그대로, 원하는 조합(샤딩·암호화 같은 기능, 저장 모드, 트랜잭션 방식 등)을 골라 설정하면 그에 맞는 예제 프로젝트를 통째로 만들어 주는 도구입니다.

 

이 생성기는 FreeMarker 템플릿을 사용합니다. config.yaml에서 어떤 조합을 만들지 정하면, .ftl 확장자를 가진 템플릿들이 그 값을 받아 실제 pom.xml, YAML 설정, 자바 소스로 렌더링됩니다. 클러스터 모드의 저장소 선택지를 따라가 보니, ZooKeeper 대신 etcd를 고르면 생성된 pom.xml에 저장소 프로바이더 의존성이 들어가지 않았습니다. 이대로라면 생성된 예제는 실행 시점에 ServiceProviderNotFoundException을 던지며 멈추게 됩니다. 이 글은 그 작은 결함과, 그것을 고쳐 올린 PR(apache/shardingsphere#39151)을 정리한 기록입니다.

ServiceProviderNotFoundException이 말해 준 것

이 예외가 나는 이유는 ShardingSphere의 확장 구조에 있습니다. ShardingSphere는 기능을 하드코딩으로 엮지 않고 SPI(Service Provider Interface) 방식으로 느슨하게 연결합니다. 클러스터 모드에서 메타데이터를 어디에 저장할지 — ZooKeeper인지 etcd인지 — 도 이 SPI로 결정됩니다. 실행 시점에 ShardingSphere는 클래스패스를 훑어 요청된 타입("etcd")에 맞는 저장소 프로바이더 구현을 찾습니다. 그런데 그 구현이 클래스패스에 아예 없으면, 등록된 후보 중 맞는 것을 못 찾았다는 뜻으로 ServiceProviderNotFoundException이 납니다.

 

즉 이 예외는 "etcd 설정이 틀렸다"가 아니라 "etcd를 처리할 코드 자체가 이 프로젝트에 들어오지 않았다"는 신호입니다. etcd용 저장소 프로바이더는 ShardingSphere 안에 이미 존재합니다. mode/type/cluster/repository/provider/etcd 경로에 있는 shardingsphere-cluster-mode-repository-etcd 모듈이 그것입니다. 문제는 생성된 예제의 pom.xml에 이 의존성이 빠져 있어, 런타임 클래스패스에 프로바이더가 올라오지 않는다는 데 있었습니다.

템플릿을 열어 보니 형제 블록만 있었습니다

원인을 짚기 위해 렌더링의 원본인 template/pom.ftl을 열었습니다. FreeMarker 템플릿은 평범한 텍스트에 <#if ...> 같은 지시문을 섞어 두는 구조라, 어떤 조건에서 어떤 의존성이 들어가는지 눈으로 따라갈 수 있습니다. 거기에는 클러스터-ZooKeeper 모드일 때 ZooKeeper 저장소 의존성을 넣는 블록은 있었지만, 클러스터-etcd 모드에 대응하는 블록은 없었습니다.

 

이 대비가 결정적이었습니다. etcd가 지원되지 않는 모드였다면 그건 기능의 부재이지 결함이 아닙니다. 그런데 확인해 보니 etcd는 분명히 지원 목록에 있었습니다. 모드 선택지를 정의하는 YamlExampleConfigurationSupportedValue.MODES에도, 기본 config.yaml에도 cluster-etcd가 들어 있었고, etcd 모드의 YAML 설정을 렌더링하는 template/resources/yaml/mode/cluster-etcd.ftl 템플릿까지 이미 존재했습니다. 다시 말해 설정과 YAML은 etcd를 온전히 지원하는데, pom.ftl만 그 흐름에서 빠져 있었습니다.

수정은 형제 블록을 그대로 따라가는 것으로

고칠 방향은 분명했습니다. 이미 잘 동작하는 ZooKeeper 블록이 옆에 있으니, etcd 블록도 같은 모양으로 하나 더 두면 됩니다. 저는 <#if mode=="cluster-etcd"> 조건 블록을 추가하고, 그 안에 etcd 저장소 프로바이더 의존성을 넣었습니다.

<#if mode=="cluster-etcd">
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-cluster-mode-repository-etcd</artifactId>
        <version>${r'${project.version}'}</version>
    </dependency>
</#if>

${r'${project.version}'} 부분이 처음엔 낯설게 보일 수 있습니다. FreeMarker에서 ${...}는 자기 문법으로 값을 치환하는 표현이라, 렌더링 결과물인 pom.xml에 Maven용 ${project.version} 문자열을 그대로 남기려면 이렇게 감싸 줘야 합니다. 옆의 ZooKeeper 블록도 똑같은 방식을 쓰고 있어서, 저는 그 관례를 그대로 따랐습니다.

 

여기서 신경 쓴 부분은 변경을 최소한으로 두는 것이었습니다. 추가한 shardingsphere-cluster-mode-repository-etcd는 ShardingSphere 안에 이미 있는 내부 모듈입니다. 바깥에서 새로운 서드파티 라이브러리를 끌어오는 것이 아니라, 프로젝트가 이미 가지고 있는 구현을 예제의 클래스패스에 얹어 주는 것뿐입니다. 그래서 의존성 트리가 새로 복잡해질 걱정은 없었습니다. 형제 블록을 미러링한다는 원칙을 지키면, 리뷰하는 쪽에서도 "왜 이 의존성을 이런 형식으로 넣었는가"를 따로 설명 없이 이해할 수 있으리라 생각했습니다.

테스트로 렌더링 결과를 붙잡아 두기

ShardingSphere는 동작이 바뀌는 변경에 테스트를 함께 두는 것을 규약으로 삼습니다. 이 모듈에는 이미 PomTemplateTest가 있어서, 데이터 모델을 넣고 템플릿을 렌더링한 뒤 결과 문자열에 특정 의존성이 담겼는지 확인하는 방식으로 검증하고 있었습니다. 저는 그 기존 패턴을 그대로 이어받아 두 개의 테스트를 더했습니다. 하나는 제가 새로 넣은 etcd 블록을 확인하는 것이고, 다른 하나는 형제인 ZooKeeper 블록을 확인하는 것입니다. etcd만 검증하기보다 짝이 되는 ZooKeeper까지 함께 걸어 두면, 두 클러스터 모드가 모두 자기 저장소 프로바이더를 챙기는지를 나란히 지킬 수 있다고 봤습니다.

@Test
public void assertClusterEtcdRepositoryDependency() throws IOException, TemplateException {
    Map<String, Object> dataModel = createBaseDataModel();
    dataModel.put("mode", "cluster-etcd");
    String content = renderPomTemplate(dataModel);
    assertThat(content, containsString("shardingsphere-cluster-mode-repository-etcd"));
}

테스트는 프로젝트 규약대로 JUnit5와 Hamcrest로 작성했습니다. 메서드 이름은 assert로 시작하고, 단언은 assertThat(content, containsString(...)) 형태로, 렌더링된 pom 안에 해당 저장소 프로바이더 문자열이 들어 있는지 확인합니다. 값이 아니라 "포함 여부"만 보는 이유는, 이 변경의 본질이 "etcd 모드일 때 그 의존성이 결과물에 나타나느냐"이기 때문입니다.

이 모듈의 검증은 조금 달랐습니다

ShardingSphere 본체는 마감 게이트가 촘촘합니다. 포맷을 보는 spotless:check, 스타일을 보는 checkstyle:check, 새 파일의 라이선스 헤더를 보는 apache-rat:check가 -Pcheck로 함께 걸립니다. 그런데 예제 생성기 모듈은 본체와 함께 빌드되는 대신 독립(standalone) 빌드로 구성돼 있습니다. 그래서 이 모듈에서는 spotless나 checkstyle을 돌리지 않습니다. 저는 이 사실에 맞춰 로컬 검증을 컴파일과 apache-rat, 그리고 PomTemplateTest 실행으로 한정했고, 그 범위를 PR 본문에도 그대로 적어 두었습니다. 프로젝트마다, 때로는 모듈마다 검증 방식이 다를 수 있다는 점을 확인할 수 있었던 대목입니다.

 

한 가지 분명히 해 둘 것은, 이 변경으로 RELEASE-NOTES.md를 건드리지는 않았다는 점입니다. 변경 범위를 예제 생성기 템플릿과 그 테스트에 한정했고, 릴리스 노트 항목은 이 PR에 포함하지 않았습니다. 작은 수정일수록 손대는 곳을 좁게 유지하는 편이 리뷰하는 쪽에도, 나중에 이력을 되짚는 쪽에도 낫다고 생각했습니다.

돌아보며 남는 것

이번 기여는 코드 몇 줄과 테스트 두 개가 전부인, 크지 않은 변경입니다. 그럼에도 기억에 남는 이유는, 문제를 좇는 과정이 제법 또렷했기 때문입니다. 설정은 etcd를 지원하는데 의존성만 빠졌다는 어긋남을 짚고, 그것이 실행 시점에 ServiceProviderNotFoundException이라는 SPI 로딩 실패로 이어진다는 점을 확인하고, 이미 잘 동작하는 형제 블록을 근거 삼아 같은 모양으로 채워 넣는 흐름이었습니다. 새로운 것을 발명하기보다, 프로젝트가 이미 정해 둔 방식과 나란히 맞추는 일에 가까웠습니다.

 

오픈소스에 기여할 때 대단한 변경만 의미가 있는 것은 아니라는 점도 다시 느꼈습니다. 선택지로 열어 둔 모드가 정작 실행되지 않는 상태는, 그 모드를 골라 본 사람에게는 당황스러운 경험입니다. 그런 작은 구멍 하나를 메우는 것도 프로젝트를 쓰는 다음 사람에게는 도움이 될 수 있습니다. 저로서는 낯선 대형 코드베이스에서 문제의 실마리를 잡고, 그 저장소가 이미 세워 둔 규약과 형제 코드의 결을 존중하며 변경을 다듬는 연습이었습니다. 앞으로 이어 갈 기여의 한 걸음으로 이 경험을 기록해 둡니다.