티스토리 뷰
[Open source contribution] langchain4j 오픈소스 기여 경험기 - BOM에서 빠진 docling 문서 파서 모듈을 채워 넣은 과정
ebson 2026. 6. 23. 15:00지금까지 제가 고친 것들은 대부분 코드의 한 줄이 잘못 동작하는 종류였습니다. 값이 좁혀지거나, 인자가 자리를 바꾸거나, 참조가 끊기는 식이었습니다. 그런데 이번에 다룰 LangChain4j 기여는 조금 결이 다릅니다. 잘못 쓰인 코드가 있어서가 아니라, 있어야 할 한 줄이 아예 빠져 있어서 생긴 문제였습니다. 그리고 그 빠진 자리는 자바 소스가 아니라 빌드 설정 파일, 그중에서도 BOM이라고 부르는 곳이었습니다.
이 글은 그 한 건의 기여를, 무엇이 빠져 있었고 그것을 어떻게 확인했으며 소스가 없는 파일을 어떻게 검증했는지 처음부터 끝까지 따라가며 정리한 기록입니다. 최종 변경은 의존성 항목 하나를 더한 작은 수정이지만, 그 한 항목이 정말 빠진 게 맞는지, 그리고 어떤 버전으로 채워야 하는지를 확신하기까지 거친 확인 과정이 저에게는 더 오래 남았습니다.
여러 모듈의 버전을 한곳에서 맞춰 주는 BOM이라는 파일
먼저 BOM이 무엇인지부터 짚어 두겠습니다. LangChain4j처럼 모듈이 수십 개로 나뉜 프로젝트를 쓸 때, 사용자는 보통 그중 몇 개만 골라서 의존성에 추가합니다. 그런데 모듈마다 버전을 일일이 적다 보면, 서로 호환되지 않는 버전을 섞어 쓰는 실수가 생기기 쉽습니다. 이런 번거로움을 덜어 주는 것이 BOM(Bill of Materials)입니다. BOM은 "이 프로젝트의 모듈들은 서로 이런 버전으로 맞춰 쓰세요"라는 목록을 한곳에 모아 둔 특별한 pom 파일입니다. 사용자가 이 BOM 하나만 가져오면, 개별 모듈의 버전을 직접 적지 않아도 BOM이 정해 둔 호환 버전을 받게 됩니다.
LangChain4j에서 이 역할을 하는 것이 langchain4j-bom 모듈입니다. 이 모듈에는 자바 코드가 한 줄도 없습니다. 오직 dependencyManagement 블록 안에 프로젝트의 모듈들을 죽 나열해 두고, 각각 어떤 버전으로 관리할지를 적어 둔 것이 전부입니다. 말하자면 코드가 아니라 목록인 셈입니다.
제가 들여다본 것은 바로 이 목록이었습니다. 코드의 동작을 따라가는 대신, "발행되는 모듈들이 이 목록에 빠짐없이 들어 있는가"를 확인하는 일이었습니다.
이 목록이 왜 중요한지는 BOM이 빠졌을 때 사용자가 겪는 일을 떠올려 보면 분명해집니다. BOM에 모듈이 등록되어 있으면, 사용자는 의존성에 그 모듈을 적을 때 버전을 생략할 수 있습니다. BOM이 버전을 대신 정해 주기 때문입니다. 그런데 어떤 모듈이 BOM에서 빠져 있으면, 그 모듈만큼은 사용자가 버전을 직접 찾아 적어야 합니다. 다른 모듈들은 버전 없이 깔끔하게 쓰는데 유독 한 모듈만 버전을 손수 적어야 한다면, 사용자는 "이건 왜 다르지" 하고 멈칫하게 됩니다. 게다가 그렇게 직접 적은 버전이 나머지 모듈들과 어긋나기라도 하면, BOM이 막아 주려던 바로 그 호환성 문제가 다시 고개를 듭니다. 빠진 한 줄은 단순히 불편한 정도가 아니라, BOM이 존재하는 이유 자체를 그 모듈에 한해 무력화하는 셈입니다.
발행되는 모듈과 목록을 나란히 놓고 비교했습니다
목록에 빠진 항목을 찾으려면 비교할 기준이 필요했습니다. 기준은 프로젝트의 루트 pom.xml에 있는 모듈 목록이었습니다. 여기에는 이 프로젝트가 빌드하는 모든 모듈이 등록되어 있습니다. 그중에서 실제로 사용자에게 배포되는, 즉 jar로 발행되는 모듈들을 추려 보고, 그 목록을 BOM의 항목들과 하나씩 맞춰 보는 방식이었습니다.
발행되는 모듈인지 아닌지는 각 모듈의 pom 파일을 보면 알 수 있습니다. 패키징이 jar이고(별도 선언이 없으면 기본값이 jar입니다), 부모로부터 dev.langchain4j라는 그룹을 물려받으며, 배포를 건너뛰라는 설정이 없으면 발행 대상입니다. 반대로 BOM 자신이나 부모 pom처럼 패키징이 pom인 것, 내부 빌드 도구처럼 사용자에게 배포되지 않는 것은 BOM 목록에 들어갈 대상이 아닙니다.
이렇게 추려 BOM과 대조하다 보니 한 모듈이 눈에 걸렸습니다. langchain4j-document-parser-docling이라는 문서 파서 모듈이었습니다. 루트 모듈 목록에는 분명히 등록되어 있고, 자체 pom을 보니 패키징도 jar이고 배포를 막는 설정도 없는 정상적인 발행 모듈이었습니다. 그런데 BOM의 dependencyManagement 어디에도 이 모듈이 없었습니다.
특히 마음에 걸린 것은 주변과의 비대칭이었습니다. BOM에는 문서 파서들을 모아 둔 구역이 있는데, 거기에는 pdfbox, poi, tika, markdown, yaml까지 다섯 개의 문서 파서가 나란히 등록되어 있었습니다. 같은 부류의 형제 다섯은 모두 있는데 docling 하나만 빠져 있었던 것입니다. 한두 개가 모두 없다면 의도된 설계일 수도 있겠지만, 같은 그룹에서 다섯은 있고 하나만 없다는 것은 설계가 아니라 누락을 강하게 의심하게 하는 신호였습니다.
일부러 뺀 것이 아니라 빠뜨린 것임을 확인했습니다
여기서 멈추지 않고 한 가지를 더 확인했습니다. 형제와의 비대칭만으로는 "원래 없던 것을 누군가 일부러 뺀 것"인지 "넣었어야 하는데 빠뜨린 것"인지가 분명하지 않았기 때문입니다. 둘은 다릅니다. 일부러 제외한 것이라면 거기에는 이유가 있을 테고, 함부로 되돌리면 안 됩니다.
그래서 이 모듈 이름이 BOM 파일의 변경 이력에 한 번이라도 등장한 적이 있는지를 살펴봤습니다. 만약 과거에 등록되었다가 어느 시점에 의도적으로 제거된 것이라면, 이력에 그 흔적이 남아 있어야 합니다. 그런데 이력을 뒤져 보니 이 모듈 이름은 BOM 파일에서 단 한 번도 나타난 적이 없었습니다. 추가된 적이 없으니 제거된 적도 없는, 처음부터 들어온 적 없는 항목이었습니다.
마지막으로 이 모듈이 프로젝트에 처음 도입된 시점을 확인했습니다. docling 파서를 추가한 변경을 따라가 보니, 그 변경은 모듈을 새로 만들고 루트 모듈 목록에는 등록했지만 BOM 파일은 아예 건드리지 않았습니다. 새 모듈을 추가하면서 BOM에 등재하는 단계를 놓친, 전형적인 동기화 누락이었습니다. 이로써 의심이 확신으로 바뀌었습니다. 이것은 누군가의 설계 판단이 아니라 그냥 빠뜨린 자리였습니다.
사실 비슷하게 빠져 보이는 다른 모듈도 한둘 더 눈에 띄긴 했습니다. 다만 그중 하나는 도입 당시의 변경이 BOM 파일을 직접 손대면서도 그 모듈만 일부러 넣지 않은 흔적이 있어서, 누락이 아니라 의도된 제외일 가능성이 남아 있었습니다. 그래서 이번에는 누락이라고 확실히 말할 수 있는 docling 하나만 다루기로 했습니다. 확신이 서지 않는 항목까지 한 번에 묶어 올리면, 깔끔하게 끝낼 수 있는 변경에 불필요한 논쟁거리를 더하는 셈이라고 생각했기 때문입니다.
형제와 똑같은 형식으로 한 항목만 더했습니다
고치는 방법은 간단했습니다. 형제 다섯이 이미 따르고 있는 형식 그대로, docling 항목 하나를 문서 파서 구역에 더하면 됐습니다.
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-docling</artifactId>
<version>${langchain4j.beta.version}</version>
</dependency>
여기서 가장 신경 쓴 부분은 버전을 어떻게 적느냐였습니다. 버전을 잘못 적으면 BOM이 엉뚱한 버전을 가리키게 되어, 빠진 것을 채운다면서 오히려 새로운 문제를 만들 수 있습니다. docling 모듈은 자체 버전을 따로 선언하지 않고 부모로부터 베타 버전을 물려받고 있었습니다. 그리고 형제인 markdown, yaml도 모두 ${langchain4j.beta.version}이라는 같은 속성을 써서 버전을 지정하고 있었습니다. 이 속성이 가리키는 값과 docling이 실제로 발행되는 버전이 정확히 같다는 것을 확인한 뒤에야, 형제와 동일한 이 속성을 쓰기로 정했습니다. 숫자를 직접 박아 넣는 대신 형제와 같은 속성을 따른 것은, 나중에 버전이 올라갈 때 형제들과 함께 자연스럽게 따라 올라가도록 하기 위해서이기도 했습니다.
항목을 넣는 위치도 아무 데나가 아니라 문서 파서 구역의 끝, 그러니까 yaml 바로 뒤에 두었습니다. 같은 부류끼리 모여 있는 정렬을 흐트러뜨리지 않는 것이, 이 파일을 나중에 읽을 사람에게도 자연스럽기 때문입니다. 결과적으로 바뀐 것은 기존 항목을 하나도 건드리지 않은, 순수한 추가뿐이었습니다.
소스가 없는 모듈을 검증하는 법
여기서 한 가지 고민이 생겼습니다. 이 저장소는 테스트가 없는 변경은 검토하지 않는다는 규칙을 분명히 두고 있습니다. 그런데 BOM은 자바 소스가 한 줄도 없는 목록 파일입니다. 단위 테스트를 붙일 코드 자체가 없습니다. 그렇다면 이 변경이 올바른지는 어떻게 보여 줄 수 있을까요.
답은 빌드 도구 자체의 검증 기능을 쓰는 것이었습니다. 먼저 BOM 모듈에 대해 Maven의 검증 단계를 돌려 보았습니다. 이 단계에서는 같은 의존성이 중복으로 들어가지는 않았는지 같은 규칙들을 점검하는데, 새로 더한 항목이 이 검사를 무리 없이 통과했습니다. 그다음에는 BOM이 실제로 계산해 내는 최종 결과를 들여다봤습니다. BOM이 적용된 뒤 docling 모듈이 어떤 버전으로 풀리는지를 확인하는 명령을 돌렸더니, 의도한 베타 버전으로 정확히 해석되었습니다. 목록에 한 줄을 넣은 것이 실제로 사용자가 받게 될 버전으로 이어진다는 것을, 빌드 도구의 출력으로 직접 확인한 셈입니다.
PR 본문에는 이 검증 과정을 그대로 적어 두었습니다. 테스트 항목에는 단위 테스트를 붙일 수 없는 이유를 솔직히 밝히고, 대신 어떤 방법으로 올바름을 확인했는지를 함께 남겼습니다. 검토하는 사람이 "왜 테스트가 없느냐"고 묻기 전에, BOM이라는 파일의 성격과 그에 맞는 검증 방식을 미리 설명해 두고 싶었습니다.
민감한 파일일수록 변경을 작게 두었습니다
이 변경을 올리면서 한 가지 더 의식한 점이 있습니다. BOM은 프로젝트의 모든 소비자가 의존하는 파일이라, 조심해서 다뤄야 하는 자리입니다. 여기서 기존 항목의 버전을 잘못 바꾸거나 무언가를 지우면, 그 영향이 BOM을 쓰는 모든 사용자에게 번질 수 있습니다.
그래서 이번 변경은 철저히 순수한 추가에만 머물도록 했습니다. 기존 항목은 단 하나도 손대지 않았고, 형제와 같은 형식과 같은 버전 속성을 그대로 따랐으며, 빠진 한 줄을 채우는 것 외에 다른 정리는 일절 하지 않았습니다. 민감한 파일에서 변경을 최소로 유지하는 것이, 검토하는 사람이 이 변경의 안전성을 한눈에 판단할 수 있게 하는 길이라고 생각했습니다.
기여 절차도 차례로 밟았습니다. 이 저장소의 기여 안내에는 새 모듈을 추가할 때 그 모듈을 BOM의 알맞은 구역에 등록하라는 내용이 분명히 적혀 있습니다. 이번 변경은 그 규칙을 뒤늦게 따르는 셈이었습니다. 또 PR 본문에는 같은 종류의 누락을 과거에 메운 선례, 즉 다른 모듈이 BOM에서 빠져 있던 것을 채운 변경이 이미 머지된 적이 있다는 점을 함께 적었습니다. 이런 종류의 수정이 처음이 아니라 이미 받아들여진 적 있는 패턴임을 보여 주면, 검토하는 사람이 변경의 정당성을 더 빨리 납득할 수 있다고 보았기 때문입니다. squash merge 정책에 맞춰 PR 제목도 영어 명령형으로, 무엇을 어디에 더하는지가 한 줄에 드러나도록 다듬었습니다.
빠진 한 줄에서 배운 것
이 기여를 마치고 돌아보면, 더해진 것은 의존성 항목 하나입니다. 코드의 동작을 바꾼 것도, 새로운 무언가를 설계한 것도 아닙니다. 그런데 그 한 줄에 도달하기까지의 과정에서 얻은 것은 줄 수와 비례하지 않았습니다.
가장 크게 남은 것은, 잘못된 코드만 버그가 아니라 빠진 항목도 결함이 될 수 있다는 감각입니다. 동작하는 코드를 한 줄씩 읽는 것만으로는 이런 종류의 문제를 찾기 어렵습니다. 무엇이 있어야 하는지를 알고, 실제로 있는 것과 나란히 놓고 빈자리를 찾아야 비로소 보입니다. 발행되는 모듈의 목록과 BOM의 목록을 맞춰 보는 단순한 대조가, 코드 어디에도 드러나지 않던 누락을 드러내 주었습니다.
또 하나는, 누락을 발견했을 때 그것이 정말 실수인지 의도인지를 가려내는 일의 중요함입니다. 비어 있다고 해서 모두 채워야 하는 것은 아닙니다. 이번에도 변경 이력과 도입 시점을 확인해 "일부러 뺀 것이 아니라 빠뜨린 것"임을 분명히 한 뒤에야 손을 댔고, 확신이 서지 않는 다른 항목은 일부러 남겨 두었습니다. 빈자리를 채우는 일에도 그 자리가 왜 비어 있는지를 먼저 묻는 신중함이 필요하다는 것을 배웠습니다.
마지막으로, 테스트를 붙일 수 없는 변경에도 그 나름의 검증 방법이 있다는 것을 알게 되었습니다. 소스가 없는 BOM에는 단위 테스트 대신 빌드 도구의 검사와 최종 결과 확인이 그 역할을 대신했습니다. 변경의 종류가 달라지면 그것을 증명하는 방법도 달라져야 한다는 점을, 이 작은 수정이 알려 주었습니다.
여전히 저는 오픈소스 기여를 막 익혀 가는 사람이고, 한 줄짜리 추가로 무언가를 다 안다고 말할 수는 없습니다. 다만 무엇이 있어야 하는지를 기준 삼아 빈자리를 찾고, 그 자리가 비어 있는 이유를 확인하고, 민감한 파일일수록 변경을 작게 두며, 코드가 아닌 변경에도 알맞은 검증을 붙이는 이 과정 자체가, 기능 하나를 새로 만드는 것보다 저를 더 단단하게 만들어 준다는 것은 분명히 느꼈습니다. 다음에 또 어떤 목록에서 빠진 한 줄을 발견하게 될지는 모르지만, 그때도 같은 방식으로 차분히 짚어 볼 생각입니다.
이 글에서 다룬 기여: langchain4j/langchain4j#5505
'OPEN SOURCE' 카테고리의 다른 글
- Total
- Today
- Yesterday
- spring batch 5
- Enum 기반 싱글톤
- DB 트랜잭션
- 캐시와 인덱스
- Redis 캐시 전략
- 트랜잭션 관리
- mybatis
- Redis vs DB
- Java Performance
- DB 인덱스 성능
- Double-Checked Locking
- 백엔드 아키텍처
- 캐시 장애
- Cache Aside
- 백엔드 성능
- TTL 설계
- Redis 성능 개선
- 백엔드 성능 튜닝
- 동시성처리
- 백엔드 성능 설계
- 캐시 성능 비교
- Hot Key 문제
- Cache Penetration
- Eager Initialization
- 트래픽 처리
- Initialization-on-Demand Holder Idiom
- 스레드 생명주기
- Cache Avalanche
- Spring Batch
- InterruptedException
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
