|
| 1 | +--- |
| 2 | +layout: "post" |
| 3 | +title: "컨트리뷰트로 배운 Spring AI" |
| 4 | +description: |
| 5 | + "KSUG Spring AI Meetup 에서 발표한 '컨트리뷰트로 배운 Spring AI' 내용을 정리한 글입니다.\ |
| 6 | + \ Spring AI에 기여하게 된 계기부터 Ollama, Elasticsearch Vector Store 기여 사례,\ |
| 7 | + \ 그리고 기여하면서 느낀 AI 업계와 커리어에 대한 인사이트를 공유합니다." |
| 8 | +categories: |
| 9 | + - "오픈소스" |
| 10 | +tags: |
| 11 | + - "Spring AI" |
| 12 | + - "오픈소스" |
| 13 | + - "기여" |
| 14 | + - "Ollama" |
| 15 | + - "Elasticsearch" |
| 16 | + - "Vector Store" |
| 17 | + - "RAG" |
| 18 | +date: "2025-10-26 00:00:00 +0900" |
| 19 | +toc: true |
| 20 | +--- |
| 21 | + |
| 22 | +발표 했던 내용을 정리해보면 좋겠다 싶어 AI로 내용을 정리하여 기록으로 남깁니다. AI로 정리하여 일부 내용은 발표했던 내용과 다를 수 있고, 오류가 있을 수 있습니다. |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +이 글은 [KSUG Spring AI Meetup](https://event-us.kr/ksug/event/114010) 에서 발표한 "컨트리뷰트로 배운 Spring AI" 발표 내용을 AI로 정리한 글입니다. |
| 27 | + |
| 28 | +발표 당시 Spring AI 상위 기여자이자 한국인 중 1위였습니다. |
| 29 | + |
| 30 | +왜 Spring AI에 기여를 하게 되었고, 어떻게 기여하였는지 사례를 소개합니다. |
| 31 | +이 글을 읽고 '나도 기여해볼 수 있겠다'라는 생각을 가지실 수 있기를 기대합니다. |
| 32 | + |
| 33 | +# 왜 Spring AI에 기여를 하게 되었나 |
| 34 | + |
| 35 | +## AI 시대를 맞이한 개발자의 고민 |
| 36 | + |
| 37 | +AI 시대를 맞이하면서 백엔드 개발자로서 고민이 많았다. "나... AI 시대에 살아남을 수 있을까?" 라는 생각도 했고, 지금이라도 AI를 공부해야 하는 건 아닌지 고민했다. |
| 38 | + |
| 39 | +백엔드 개발자로서, 어떻게 AI를 서비스에 통합할 수 있을지 이해하고 있으면 좋을 것이라고 생각했다. 그런데 현재 회사에서 하는 업무는 AI와 직접적인 관련이 없었다. |
| 40 | + |
| 41 | +멘토님께서 이런 조언을 해주셨다. |
| 42 | + |
| 43 | +> 회사에서 하지 않는다면, 직접 구축해서 학습해보면 되지 않겠나. |
| 44 | +
|
| 45 | +그래서 관련 프로젝트에 직접 기여를 해보기로 했다. 이슈를 해결해가면서 간접적으로 느껴보자는 마음으로 Spring AI에 기여를 시작했다. |
| 46 | + |
| 47 | +# Spring AI 소개 |
| 48 | + |
| 49 | +Spring AI는 다양한 AI Provider를 쉽게 통합할 수 있도록 돕는 프로젝트이다. 다양한 프로바이더를 통해 AI Model을 제공받을 수 있는데, Spring AI는 이들을 하나의 일관된 인터페이스로 묶어준다. |
| 50 | + |
| 51 | +# 오픈소스에 기여하는 법 |
| 52 | + |
| 53 | +기여는 생각보다 어렵지 않다. 다음 5단계로 이루어진다. |
| 54 | + |
| 55 | +1. **이슈 생성 또는 선정** - 해결할 이슈를 찾거나 직접 생성한다. |
| 56 | +2. **구현 방향 논의** - 메인테이너와 어떻게 해결할지 논의한다. |
| 57 | +3. **구현 (코드 작성)** - 실제 코드를 작성한다. |
| 58 | +4. **PR 생성 및 코드 리뷰** - Pull Request를 올리고 리뷰를 받는다. |
| 59 | +5. **반영 (Merge)** - 코드가 머지된다. |
| 60 | + |
| 61 | +# 기여 현황 |
| 62 | + |
| 63 | +M6부터 RC를 거쳐 정식 릴리즈(1.0.x)까지의 여정에 함께하였다. 약 40개의 PR을 남겼고, 기여 분류는 다음과 같다. |
| 64 | + |
| 65 | +| 분류 | 비율 | |
| 66 | +| ------------ | ----- | |
| 67 | +| Model | 36.6% | |
| 68 | +| Vector Store | 26.8% | |
| 69 | +| Doc | 24.4% | |
| 70 | +| Testing | 9.8% | |
| 71 | +| MCP | 2.4% | |
| 72 | + |
| 73 | +이 글에서는 Model과 Vector Store 기여 사례를 소개한다. |
| 74 | + |
| 75 | +# Model 기여 사례 |
| 76 | + |
| 77 | +## AI Model 이란 |
| 78 | + |
| 79 | +AI Model은 방대한 정보 데이터 세트로 학습된 컴퓨터 프로그램 또는 알고리즘이다. 다양한 종류의 AI Model들이 있으며, 다양한 프로바이더를 통해 제공받을 수 있다. |
| 80 | + |
| 81 | +## Ollama |
| 82 | + |
| 83 | +Ollama는 로컬에 배포되는 AI 모델 실행기로, 사용자가 직접 대규모 언어 모델(LLM)을 다운로드하고 실행할 수 있도록 설계되었다. |
| 84 | + |
| 85 | +### Ollama #1: keep-alive 음수값 버그 수정 |
| 86 | + |
| 87 | +**"왜 Keep-Alive를 -1로 넣으면 죽어요?"** |
| 88 | + |
| 89 | +keep-alive 옵션은 모델이 메모리에 올라온 뒤, 얼마나 오랫동안 계속 유지될지를 정하는 설정이다. -1을 넣으면 모델이 계속 메모리에 유지되어야 하는데, 오류가 발생하고 있었다. |
| 90 | + |
| 91 | +원인은 정규식에 있었다. 기존 정규식은 `\\d+`로 되어 있어 0 이상의 숫자만 입력 가능했다. 음수 값을 허용하지 않았던 것이다. |
| 92 | + |
| 93 | +수정은 간단했다. `-?\\d+`로 변경하여 음수도 허용되도록 수정했다. |
| 94 | + |
| 95 | +### Ollama #2: Builder 패턴 적용 |
| 96 | + |
| 97 | +Spring AI의 Model은 일정한 구조로 구성되어 있다. 어느 날 OpenAI 쪽에 Builder 패턴을 적용하는 PR이 올라왔다. |
| 98 | + |
| 99 | +"이거 다른 곳에도 적용하면 좋을 것 같은데?" 라는 생각이 들었다. |
| 100 | + |
| 101 | +빠르게 작업하여 OllamaApi에도 Builder 패턴을 적용하는 PR을 등록했다. |
| 102 | + |
| 103 | +**AS-IS**: 각 상황별로 생성자를 직접 구현하는 방식이었다. |
| 104 | + |
| 105 | +**TO-BE**: Builder 패턴을 사용하여 유연하게 설정할 수 있도록 변경했다. |
| 106 | + |
| 107 | +### Ollama #3: 오래된 PR 머지로 인한 파일 경로 문제 |
| 108 | + |
| 109 | +"이 파일은 왜 혼자 덩그라니 있지...?" |
| 110 | + |
| 111 | +6개월 동안 묵혀진 오래된 PR이 머지되면서 파일의 이전 경로에서 업데이트가 된 것이었다. 프로젝트 구조가 변경되면서 파일 경로가 바뀌었는데, 오래된 PR은 이전 경로 기준으로 작성되어 있었던 것이다. |
| 112 | + |
| 113 | +| 이전 경로 | 새 경로 | |
| 114 | +| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | |
| 115 | +| `spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/ollama/` | `auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/` | |
| 116 | + |
| 117 | +약 10일 동안 PR 작성자도, 메인테이너도 이 문제를 인지하지 못하고 있었다. CI 테스트에서는 코드에 영향이 없기 때문에 그냥 넘어간 것이다. |
| 118 | + |
| 119 | +이동된 경로에서 변경 사항을 반영하는 PR을 생성했다. 코드 자체는 짧았으나, 업데이트한 내용으로 인해 문제가 없는지 테스트를 수행하는 데 많은 시간이 들었다. |
| 120 | + |
| 121 | +#### Testcontainer 이야기 |
| 122 | + |
| 123 | +Spring AI에서는 Testcontainer를 사용한 통합 테스트를 적극적으로 활용한다. 일반적인 경우에는 크게 문제 없겠지만, Ollama의 경우 테스트할 때마다 새로운 Container가 띄워지면 매번 모델을 다시 다운로드해야 한다. 테스트는 얼마 안 걸리는데 모델을 다운로드하는 데 한세월 걸린다. |
| 124 | + |
| 125 | +이를 방지하기 위해 CI 시에는 Testcontainer가 아닌 별도의 Ollama 컨테이너를 활용한다. |
| 126 | + |
| 127 | +# Vector Store 기여 사례 |
| 128 | + |
| 129 | +## Vector Store 란 |
| 130 | + |
| 131 | +"우리 회사에서 정산과 관련된 파트의 담당자는 누구인가요?" 라는 질문을 AI에게 한다고 가정해보자. |
| 132 | + |
| 133 | +이런 내부 정보는 AI 모델이 학습하지 않은 데이터이다. 이때 필요한 것이 바로 **검색 증강 생성(RAG, Retrieval-Augmented Generation)** 이다. RAG는 대규모 언어 모델의 출력을 최적화하여, 응답을 생성하기 전에 외부의 신뢰할 수 있는 기술 자료를 참조하도록 하는 프로세스이다. |
| 134 | + |
| 135 | +예를 들어 사내 문서에 아래와 같은 정보가 있다고 하자. |
| 136 | + |
| 137 | +> [업무 담당자 정리] |
| 138 | +> 개발 담당자 : 박종훈 |
| 139 | +> 정산 담당자 : 김OO |
| 140 | +
|
| 141 | +이 문서를 벡터화하여 Vector Store에 저장해두면, "정산 담당자가 누구인가요?"라는 질문이 들어왔을 때 관련 문서를 검색하여 "정산과 관련된 파트의 담당자는 김OO입니다."라고 답변할 수 있게 된다. |
| 142 | + |
| 143 | +Vector Store는 이러한 RAG에서 사용되는 벡터를 저장하는 역할을 한다. Spring AI는 다양한 Vector Store를 지원하고 있다. |
| 144 | + |
| 145 | +## Elasticsearch: embedding field 하드코딩 문제 |
| 146 | + |
| 147 | +**"왜 embedding field가 하드코딩 되어있는 거에요?"** |
| 148 | + |
| 149 | +Elasticsearch Vector Store의 코드를 살펴보니, 읽어올 때(doSimilaritySearch)와 기록할 때(doAdd) 모두 embedding 필드명이 하드코딩되어 있었다. 이 필드명을 dynamic하게 처리할 필요가 있었다. |
| 150 | + |
| 151 | +### 메인테이너와의 논의 |
| 152 | + |
| 153 | +"record 말고 Map을 이용해서 key를 dynamic하게 처리하는 건 어떨까요?" 라고 제안했다. |
| 154 | + |
| 155 | +메인테이너의 반응: |
| 156 | + |
| 157 | +> 우리는 Map을 사용하는 것보다 구체적인 타입을 사용하는 방향을 선호해요. Record에서 Map으로 변경하려는 이유가 있을까요? |
| 158 | +
|
| 159 | +embedding 필드의 이름을 동적으로 받아야 한다고 설명하고, 더 좋은 방법이 있는지 물었다. 결국 메인테이너도 Map을 쓰면 쉽게 해결할 수 있다는 것에 동의했고, PR이 머지되었다. |
| 160 | + |
| 161 | +### 이후 발생한 이슈: Document 구조 변경 |
| 162 | + |
| 163 | +PR이 머지된 후 어느 날 메일이 한 통 온다. |
| 164 | + |
| 165 | +> ElasticSearch doSimilaritySearch broken after recent changes in Document |
| 166 | +
|
| 167 | +"너가 수정한 내용 때문에 toDocument 기능이 더 복잡해졌어 :-(" |
| 168 | + |
| 169 | +내 코드 때문에 문제가 발생한다면 어떻게 해야 할까? 우선 record로 처리하는 방식으로 PR을 새로 생성하고 물었다. "그럼 만족해?" 이슈 제기자는 "응, 도움이 될 것 같아"라고 했지만, 이어서 "그런데 문제는 끝나지 않았어"라고 했다. |
| 170 | + |
| 171 | +"...? 그럼 이 분은 뭘 원하고 계신거지?" |
| 172 | + |
| 173 | +### 문제 다시 파악하기 |
| 174 | + |
| 175 | +이슈 내용을 다시 정리해보니, serialize/deserialize 과정에서 에러가 발생 중이었다. 이슈 제기자에게 확인차 물어보았다. |
| 176 | + |
| 177 | +> 옛날 버전에서 저장한 데이터를 새 버전에서 불러오려는 거야? 아니면 새 버전에서 저장한 데이터를 새 버전에서 불러왔을 때도 문제가 있다는 거야? |
| 178 | +
|
| 179 | +> 두 케이스 모두 에러가 발생되고 있어. 저장할 때는 "content" 프로퍼티로 저장되어 있는데 불러올 때는 "text" 프로퍼티로 불러와지고 있어서 그래. |
| 180 | +
|
| 181 | +기존에는 `content`라는 필드명을 사용하고 있었는데, 미디어에 대한 임베딩 처리가 요구되면서 `text` 필드와 `media` 필드로 나눠지게 된 것이 원인이었다. 기존 코드와의 호환성을 위해 데이터 매핑 시에는 `content`라는 필드를 유지하고 있었지만, 실제로는 필드 불일치가 발생하고 있었던 것이다. |
| 182 | + |
| 183 | +확인해보니 이슈 제기자는 과거 버전(M5 이전)을 사용 중이셨던 것으로 보였고, 최신 버전으로 마이그레이션 시 에러가 발생하고 있었다. |
| 184 | + |
| 185 | +두 케이스 모두 확인한 후 답변했다. |
| 186 | + |
| 187 | +> 첫 번째 케이스는 필요하다면 데이터 마이그레이션을 하면 될 것 같고, 두 번째 케이스는 정상적으로 데이터가 저장되고 불러오도록 처리되어 있어. 혹시 문제가 되는 코드를 제공해줄 수 있어? |
| 188 | +
|
| 189 | +최종적으로 이슈 제기자도 "너의 말이 맞아, 데이터 마이그레이션이 필요하겠다"라며 동의하면서 해결되었다. |
| 190 | + |
| 191 | +# 기여하면서 느낀 것 |
| 192 | + |
| 193 | +## AI 업계에 대해서 |
| 194 | + |
| 195 | +- **빠른 변화**: 많은 기술들이 나오지만 살아남는 게 쉽지 않다. 얼마 지나지 않아 또 새로운 기능이 나온다. |
| 196 | +- **대세의 중요성**: 하나의 확실한 대세가 생기고 나면 그 후에 다른 것들은 쉽지 않다. 대부분의 Model API에서 OpenAI 호환 API를 제공하고 있으며, 작은 규모의 프로젝트에서는 이 전략이 맞을지도 모른다. Gemini도 결국 OpenAI 호환 API를 제공하게 되었다. MCP 출시 후 A2A도 나왔지만 여전히 주도권에서는 다소 뒤처진 모습이다. |
| 197 | +- **돈이 많으면 간단하게 갈 수 있다**: 리소스가 충분하다면 선택지가 넓어진다. |
| 198 | +- **RAG는 생각보다 잘 되지 않는다**: 다양한 방법을 연구해야 한다. |
| 199 | +- **핫한 것 vs 실제 사용**: 핫한 것과 실제 사용되는 것은 다른 이야기이다. "MCP는 쓰는 사람보다 만드는 사람이 많은 유일한 기술일 것이다." |
| 200 | + |
| 201 | +## 커리어 관점에서 |
| 202 | + |
| 203 | +- **글로벌 협업 경험**: 다른 회사는 어떻게 일할까? 빅테크, 글로벌 IT 기업은 어떻게 일할까? 오픈소스 기여를 통해 실제로 경험해볼 수 있는 기회가 된다. "이 사람들도 사람이구나"라고 느낄 수도 있다. |
| 204 | +- **프로젝트 방향성 체감**: 해당 프로젝트의 방향성을 직접 느껴볼 수 있다. 기여를 하다 보면 또 다른 기여와 연결되고, 프로젝트에 참여하면서 나의 지식도 확장된다. 프로젝트의 깊은 부분까지 알아볼 수 있으며, 내 의견을 직접 어필할 수 있는 기회가 된다. |
| 205 | +- **도움을 주는 입장으로 전환**: 도움만 받는 입장에서 도움을 주는 입장으로 전환된다. 기여한 코드로 다른 개발자들의 편의성을 올려줄 수 있고, 리뷰어로 다른 기여자들에게 도움을 드리기도 하며, 직접 이슈를 남기기도 한다. |
| 206 | + |
| 207 | +# 마무리 |
| 208 | + |
| 209 | +왜 Spring AI에 기여를 하게 되었고, 어떻게 기여하였는지 사례를 소개하였다. |
| 210 | + |
| 211 | +'나도 기여해볼 수 있겠다'라고 생각이 드셨다면, 여러분들도 Spring AI에 기여해보시길 기대한다. [오픈소스 기여모임](https://litt.ly/opensource)에 참여해보시는 것도 좋은 방법이다. 다양한 프로젝트에 함께 기여하고 있다. |
0 commit comments