기술 블로그 모음

국내 IT 기업들의 기술 블로그 글을 한 곳에서 모아보세요

전체 프론트엔드 백엔드 데브옵스 AI 아키텍처 DB 네트워크 보안 기타
LLM을 활용한 스마트폰 시세 조회 서비스 구축
당근마켓
LLM을 활용한 스마트폰 시세 조회 서비스 구축

스마트폰을 바꾼 후 이전에 썼던 기기를 중고로 팔아보신 적 있으세요? ‘이 정도 상태의 기기면 어느 정도 가격대가 적당한 거지?’ 고민하며, 수많은 중고 매물 게시글을 일일이 확인하지는 않으셨나요? 이제는 LLM(대형 언어 모델) 덕분에 이렇게 번거롭고 어려웠던 작업이 훨씬 쉽고 빠르게 해결되고 있어요.이 글에서는 LLM을 활용해 중고거래 게시글에서 스마트폰 정보를 추출하고, 이를 통해 시세를 산출한 방법을 소개하려고 해요. 먼저 스마트폰 시세조회 서비스를 왜 만들게 됐는지 배경을 간단히 살펴본 후, LLM으로 게시글을 분류·정제하는 과정, BigQuery를 이용해 정보를 후처리하고 시세를 집계하는 과정, 마지막으로 벡터 DB 기반으로 유사 게시글을 추천하는 과정을 단계별로 소개해 드릴게요. LLM으로 사용자 경험을 효과적으로 개선할 방법을 고민 중인 분들에게 이 사례가 큰 도움이 되면 좋겠어요.스마트폰 시세조회 서비스의 모습스마트폰 시세조회는 왜 필요할까요?많은 중고거래 판매자들이 물품의 적절한 가격을 결정하는 걸 어려워해요. 개인 간 거래는 워낙 다양한 상품이 혼재되어 있기 때문인데요. 종류도 워낙 다양한데 상태도 가지각색이라, 물품의 정확한 시세를 한눈에 파악하기 어려운 거죠. 스마트폰을 예로 들면 단순히 같은 기종만 검색해서 끝날 일이 아니라, 사용 기간, 배터리 효율, 스크래치 여부 등 상태가 비슷한 기기가 얼마에 팔리는지 일일이 확인해야 하는 거예요.중고거래팀은 사용자가 물품의 시세를 한눈에 확인하고 더 쉽게 가격을 결정할 수 있도록, 아이폰, 갤럭시 기종을 대상으로 한 스마트폰 시세 조회 서비스를 테스트하기로 했어요. 다양한 물품 중 스마트폰을 베타 테스트 대상으로 선정한 이유는 다음과 같아요. 스마트폰은 제품 모델이 명확하고 게시글 수가 많아 데이터 기반 시세 계산에 유리해요. 또 판매 단가가 높아 가격 결정이 중요한 상품이기도 하고요.결과적으로 모델, 용량, 새 상품 여부, 스크래치 및 파손, 배터리 효율 등 구체적인 물품 상태에 따라 시세가 어느 정도인지 파악할 수 있는 서비스를 만들었어요. 예를 들어 사용자가 ‘아이폰 16 Pro 128GB’를 선택하고 필터에서 구체적인 ‘사용 상태’나 ‘배터리 성능’을 설정하면, 곧바로 그에 따른 시세 정보를 ‘OOO만원-OOO만원’과 같은 가격 범위의 형태로 확인할 수 있어요. 이번 프로젝트는 머신러닝을 활용해 당근 중고거래 데이터를 기반으로 정확한 시세를 제공한 첫 번째 시도로, 팀 내에서도 의미가 큰 프로젝트이기도 했는데요. 그럼 본격적으로 기능을 구현해 나간 과정을 단계별로 소개해 드릴게요.Step 1. 상품 정보 추출가장 큰 문제는 게시글에서 상품 정보를 추출하는 것이에요. 당근은 판매자의 글쓰기 허들을 낮추기 위해 중고거래 게시글에 구체적인 기종이나 물품 상태를 입력하도록 요구하지 않아요. 하지만 구체적인 물품 상태별로 스마트폰 시세를 제공하려면 모델, 용량, 새 상품 여부, 스크래치 및 파손 여부, 배터리 효율 등 여러 가지 다양한 조건을 알아내야 했어요.기존에는 이런 데이터를 추출하려면 복잡한 정규식을 만들거나 이에 특화된 ML 모델을 만들어야 했어요. 하지만 LLM을 도입하여 ‘모델명’, ‘용량’, ‘스크래치 여부’ 등을 추출할 수 있게 되었어요. 정규식이나 별도 모델을 구축할 때와 달리, 프롬프트 수정만으로도 추출 정확도를 높일 수 있어 공수가 매우 줄었어요.게시글에서 정보를 추출하는 과정 예시우선 타겟 게시글들을 정한 후 프롬프트 엔지니어링을 통해 결과물을 뽑았어요. 그 결과물을 채점하고 프롬프트를 수정하여 추출의 정확도를 높였어요. 만족할만한 성능이 나온 후에는 게시글이 생성될 때마다 LLM을 적용하여 사내 데이터 웨어하우스인 BigQuery에 적재하는 파이프라인을 구축했어요.BigQuery에 적재한 이후에는 데이터의 후처리를 거쳤어요. LLM 특성상 잘못된 분류를 하거나 허구의 정보를 생성하는 환각 문제를 완전히 피할 수는 없었어요. 특히 모델명을 추출하는 과정에서 나열한 기종 이외에 다른 이름으로 추출한다거나, 복잡한 기종 명의 경우 잘못된 이름으로 추출하는 경우도 있었어요.예를 들어, 1세대 갤럭시 폴드의 경우 “Galaxy Fold”지만 2세대부터는 “Galaxy Z Fold2”라는 이름을 가져요. 하지만 LLM은 “Galaxy Z Fold”나 “Galaxy Fold1”, “Galaxy Fold 1”처럼 사용자의 입력한 잘못된 모델명을 그대로 추출하는 경우가 있었어요.결국 프롬프트에서 모든 예외 케이스를 처리하기보다는, BigQuery View Table을 통해 2차 가공을 하기로 했어요. 아래 코드는 특정 스마트폰 시리즈(예: Galaxy Fold, Galaxy Flip 등)의 여러 가지 잘못된 표기를 정규화하는 SQL 예시예요. 이 로직을 만들 때도 GPT를 활용해 삽질 과정을 크게 줄였어요.-- Galaxy Fold 패턴 처리WHEN REGEXP_CONTAINS( REGEXP_REPLACE(REGEXP_REPLACE(item_name, r'\\s5G$', ''), r'(?i)(\\+|plus)', '+'), r'^Galaxy\\sFold($|\\s?\\d+)' ) THEN CASEWHEN REGEXP_CONTAINS( REGEXP_REPLACE(REGEXP_REPLACE(item_name, r'\\s5G$', ''), r'(?i)(\\+|plus)', '+'), r'^Galaxy\\sFold($|\\s?1)$' ) THEN 'Galaxy Fold'ELSE REGEXP_REPLACE( REGEXP_REPLACE( REGEXP_REPLACE(item_name, r'\\s5G$', ''), r'(?i)(\\+|plus)', '+' ), r'Galaxy\\sFold\\s?(\\d+)', 'Galaxy Z Fold\\\\1' )-- ...Step 2. 데이터 기반 시세 집계위 과정을 통해 정제된 모델명과 흠집, 배터리 용량 등에 대한 원시 데이터를 얻었어요. 이제 이 데이터들을 집계해서 시세 정보를 만들어낼 수 있어요. 데이터를 집계하고, 사용자분들에게 제공하는 데에는 BigQuery와 MySQL, 두 개의 저장소를 사용했어요. 각 저장소의 장단점이 다르다 보니 각각의 장점을 활용해 더 좋은 서비스를 만들어내기 위해서였어요. 두 저장소의 특징을 비교해 보면 다음과 같아요.MySQL주요 용도: 트랜잭션 처리(OLTP), CRUD 작업, 실시간 데이터 제공 및 웹 백엔드성능 특성: 낮은 지연 시간과 빠른 트랜잭션 처리로 실시간 응답에 유리데이터 이동 및 적재 전략: 사용자에게 빠른 응답을 위한 최종 집계 결과나 가공된 데이터 저장에 적합사용 사례: 웹 애플리케이션 백엔드, 실시간 거래 처리BigQuery주요 용도: 대규모 데이터 분석(OLAP), 데이터 웨어하우징, 로그/이벤트 분석성능 특성: 대규모 집계 및 복잡한 분석 쿼리에 최적화데이터 이동 및 적재 전략: 원본 대용량 데이터 분석에 집중, 불필요한 데이터 이동 최소화사용 사례: 데이터 사이언스, 머신러닝, 대규모 로그 분석, 배치 분석두 저장소의 장점을 얻기 위해 팀에서 사용한 방법은 다음과 같아요. 우선 빅쿼리에서 주간 시세조회 처리 같은 대용량 작업을 마친 후, 집계 결과만을 MySQL로 옮겨 저장했어요. 그 후 사용자가 화면에 진입할 때는 BigQuery 접근 없이 MySQL을 활용해서 시세조회 결과를 내려줬어요.BigQuery에서 MySQL로 모든 데이터를 덤프했다면, 비효율이 발생했거나 응답시간이 느려졌을 텐데요. 이 과정을 통해 그런 문제들을 방지할 수 있었어요. 또한 집계 결과를 BigQuery에서 MySQL로 옮겨오는 작업을 멱등하게 설계하여서 운영의 편의성을 높였어요.Step 3. 유사 게시글 제공이 과정을 통해 사용자가 원하는 조건의 상품 시세를 구체적인 가격 범위로 제공하게 됐어요. 그런데 당근에서 물건을 팔기 전 비슷한 물건을 하나하나 확인해 보는 것처럼, 일부 사용자의 경우 좀 더 정확한 가격 책정을 위해 다른 게시글을 직접 확인하고 싶어 할 수도 있겠다고 판단했어요. 이 과정을 편리하게 만들기 위해 시세 조회 화면에서 시세 통계 데이터뿐만 아니라 유사 게시글도 제공하려 했어요. 이 기능은 통계 데이터와는 다르게 게시글의 임베딩을 활용해 구현했어요.임베딩은 텍스트, 이미지 등의 개체를 수학적인 형태로 바꾸어 표현한 것이에요. 좋은 임베딩 모델은 텍스트의 의미를 수학적으로 잘 변환하기 때문에, 의미상으로 유사한 게시글을 빠르게 찾아낼 수 있어요. 예를 들어 영어로 작성한 “iPhone”과 한글로 적은 “아이폰”이 같은 의미라는 것은 단순히 문자열의 유사도로는 알아낼 수 없어요. 하지만 좋은 임베딩 모델을 사용한다면 이 두 단어는 비슷한 벡터로 변환이 되고, 따라서 사용자가 “아이폰”으로 검색하든 “iPhone”으로 검색하든 동일한 결과를 제공할 수 있게 돼요. 또 팀에서는 벡터 저장과 검색에 최적화된 데이터베이스인 벡터 DB, 그중 Pinecone을 도입해서 벡터 서빙을 최적화했어요.그 과정이 순탄하지만은 않았는데요. 쿼리와 문서의 불일치 때문에 어려움을 겪었어요. 당근의 게시글은 제목이나 본문이 모두 길고 상세하게 설명하는 형태예요. 하지만 스마트폰 시세조회의 경우 “아이폰 16 프로 흠집 있음”처럼 아주 짧은 단어로 이루어진 형태인데요. 이러다 보니 생각보다 유사하지 않은 게시글들이 검색되는 경우가 잦았어요.문제 해결을 위해 여러 가지 임베딩 모델을 테스트해 보다가 구글의 임베딩 모델은 작업 유형을 선택할 수 있다는 걸 알게 되었어요. 임베딩 모델을 호출할 때 문서의 경우 task_type: RETRIEVAL_DOCUMENT, 쿼리의 경우 task_type: RETRIEVAL_QUERY과 같은 형태로 옵션을 넘겨 해당 작업에 최적화된 형태로 임베딩을 만들어냈어요.위 옵션을 지정하고 다른 임베딩 모델들과 비교하자 훨씬 좋은 결과를 얻었어요. 임베딩의 평가는 해당 임베딩 모델을 통해 얻어낸 게시글이 추출 모델, 메타데이터 (흠집 유무, 배터리 사이클 등)에 맞을 때마다 더 높은 점수를 부여하는 방식으로 설계했어요. 이 채점 과정 또한 LLM을 통해 자동화하여 공수를 많이 줄였어요.유사한 게시글들을 잘 찾아내지만, 순서가 생각과 잘 맞지 않는 문제도 있었어요. “갤럭시 S24”를 검색했는데 15개의 게시글 중 갤럭시 S24가 10개, S24+가 3개, S23이 2개 있다고 생각해 보세요. 그러면 우리가 기대하는 결과는 S24, S24+, S23 순으로 게시글이 나열되는 거예요. 하지만 모두 높은 유사도를 보이다 보니 순서가 뒤죽박죽이었어요.RAG나 추천 등에 익숙하신 분이라면 ReRanker를 도입해서 문제를 풀면 될 거 같다는 생각이 드실 거예요. 저희도 ReRanker를 테스트해 보았는데, 파인튜닝 같이 도메인에 특화하지 않은 상태로 일반 모델을 적용했을 때는 딱히 더 나은 결과를 얻지 못했어요. 게다가 팀에는 이 과정을 도와줄 수 있는 ML 엔지니어도 없는 상황이어서 저희는 다른 방법을 택하기로 했어요.이미 유사한 게시글을 들고 온 이후기 때문에, 특정 규칙을 기반으로 어떤 문서들은 배제하고 사용했어요. 예를 들어 “탭”, “패드” 같은 단어 등장한 게시글은 사용하지 않는 식이죠. 같은 맥락으로 내 아이템과 일치하는 단어가 많을수록 더 상위에 위치시키고, 일치하지 않는 단어가 있을 경우 순위를 좀 더 아래로 조정했어요. 이 과정에서 기본적인 동의어 처리도 진행했고요. 예를 들어, 갤럭시 S23의 시세를 조회한다면 갤럭시의 동의어인 Galaxy S23이 있는 게시글은 상위에 위치시키고, S23 울트라는 울트라로 인해 감점되어서 더 아래로 내려가는 식이죠.마치며여태까지 LLM과 임베딩 모델 등 새로운 기술을 활용하여 당근의 자체 데이터 기반으로 시세 조회 기능을 만들어간 과정을 소개해 드렸어요. 그동안은 없었던 새로운 도구를 활용하여 사용자의 문제를 풀어나가 기술적으로도, 한 사람의 메이커로서도 즐거운 경험이었어요.이 과정에서 얻은 교훈은 다음과 같아요.LLM이 똑똑하고 좋은 도구는 맞지만, 모든 과정을 프롬프트 엔지니어링으로 해결하려고 하기보다는 후처리 과정을 따로 작성하는 게 더 효율적일 때도 있다는 것내가 필요한 장점을 가진 저장소를 선택하면 효율적으로 일할 수 있다는 것내 작업 유형에 잘 맞는 임베딩 모델을 사용하면 문제를 쉽게 풀어낼 수 있다는 것중고거래실은 이처럼 새로운 도구를 활용하여 사용자들의 문제를 풀고 더 좋은 경험을 제공하는 것에 진심인 팀이에요. 팀에 흥미가 생기셨다면 아래 공고를 통해 지원하실 수 있어요.Software Engineer, Backend — 중고거래LLM을 활용한 스마트폰 시세 조회 서비스 구축 was originally published in 당근 테크 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.

Kotlin Multiplatform 도구 – 방향 전환
JetBrain Korea
Kotlin Multiplatform 도구 – 방향 전환

몇 년 전 JetBrains는 Kotlin Multiplatform IDE를 만들어 KMP 애플리케이션의 개발을 지원하자는 구상에 착수했습니다. Fleet 플랫폼 기반으로 제작하며 독립적인 IDE를 출시할 의도로 이 모험을 시작했습니다. 그런데 이 기간 동안 특히 KMP를 사용하는 고객으로부터 IntelliJ Platform의 기능과 지원이 KMP용으...

AWS 주간 소식 모음: Cloud Club Captain 참가 신청, Formula 1®, Amazon Nova 프롬프트 엔지니어링 등
AWS KOREA
AWS 주간 소식 모음: Cloud Club Captain 참가 신청, Formula 1®, Amazon Nova 프롬프트 엔지니어링 등

지난 2월 20일에 열린 AWS Developer Day 2025 현장에서는 책임 있는 생성형 AI를 개발 워크플로에 통합하는 방법을 선보였습니다. 이 이벤트에서는 Generative AI Applications and Developer Experience의 Director Srini Iragavarapu, AWS Evangelism의 VP Jeff ...

2025년 국내기업 경영 환경 및 IT 투자 전망 (금융산업 편)
삼성 SDS
2025년 국내기업 경영 환경 및 IT 투자 전망 (금융산업 편)

이 전망은 삼성SDS 마케팅팀 MI그룹에서 2024년 말에 400여 명의 국내 IT 의사결정 관계자를 대상으로 실시한 설문 결과로서, 2025년도에 직면할 국내기업의 경영 환경과 IT 투자 전망 중 금융산업을 집중 분석하였습니다.

리디, 오리지널 소설 ‘식물, 상점’ 해외 9개국 판권 수출
리디
리디, 오리지널 소설 ‘식물, 상점’ 해외 9개국 판권 수출

글로벌 출판사와 총 10억 원대 판권 계약을 맺으며, 작품성과 글로벌 흥행성을 입증했다. The post 리디, 오리지널 소설 ‘식물, 상점’ 해외 9개국 판권 수출 appeared first on 리디주식회사 RIDI Corporation.

[행사스케치] 사우디 LEAP 2025, 팀네이버 기술을 널리 알렸습니다.
네이버 클라우드
[행사스케치] 사우디 LEAP 2025, 팀네이버 기술을 널리 알렸습니다.

안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. 얼마 전 팀네이버가 사우디에서 열리는 글로벌 기술 전시 행사 LEAP 2025에 2년 연속 참석한다는 소식을 전해드렸죠. 네이버 기술 전반을 소개한 작년에 이어 올해는 사우디의 디지털 문화 유산을 지키는 AI 기술 개발 협력 방향을 제안했습니다. 더불어 사우디 자치행...

[술술 읽히는 업무 해설집-근태편] 특별 휴가로 직원 만족도 끌어 올리기
네이버 클라우드
[술술 읽히는 업무 해설집-근태편] 특별 휴가로 직원 만족도 끌어 올리기

안녕하세요, 협업과 소통을 위한 필수 기능으로 글로벌 53만 기업의 든든한 협업툴 역할을 해온 네이버웍스(NAVER WORKS)입니다! "업무와 관련된 것이라면 뭐든지 쉽게 풀어드립니다!" 술술 읽히는 업무 해설집 회사의 성장에 있어서 구성원의 사기는 매우 중요한 요소인데요. 구성원의 사기 진작에 기여할 수 있는 방법이 바로 ‘휴가’입니다. 그러나 휴...

2025년 국내기업 경영 환경 및 IT 투자 전망
삼성 SDS
2025년 국내기업 경영 환경 및 IT 투자 전망

이 전망은 삼성SDS 마케팅팀 MI그룹에서 2024년 말에 400여 명의 국내 IT 의사결정 관계자를 대상으로 실시한 설문 결과로서, 2025년도에 직면할 국내기업의 경영 환경과 IT 투자 전망에 대해 분석하였습니다.

[캘린더️] 3월 무료 교육 웨비나 일정 모음
네이버 클라우드
[캘린더️] 3월 무료 교육 웨비나 일정 모음

안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다.

[웹툰파헤치기] 치명적 로맨스…’괴물 아가씨와 성기사’
리디
[웹툰파헤치기] 치명적 로맨스…’괴물 아가씨와 성기사’

The post [웹툰파헤치기] 치명적 로맨스…’괴물 아가씨와 성기사’ appeared first on 리디주식회사 RIDI Corporation.

YouTrack의 새로운 디자인
JetBrain Korea
YouTrack의 새로운 디자인

YouTrack 2025.1 릴리스에는 프로젝트와 작업을 더욱 빠르게 탐색하는 데 도움을 주는 대담한 새 디자인이 도입되었습니다. 이 새로운 모양과 느낌은 팀이 프로젝트를 수행하는 방식에서 얻은 통찰력을 바탕으로 합니다. 새로운 디자인에는 다음과 같은 부분이 도입되었습니다. 매일 수십만에 달하는 팀이 YouTrack을 통해 비즈니스를 이끌어가고 있습니...

귀하께 음식물분리수거위반 과징금고지서가 도착하였습니다. 보기:hxxp://g.fg***.cyou
이스트 시큐리티
귀하께 음식물분리수거위반 과징금고지서가 도착하였습니다. 보기:hxxp://g.fg***.cyou

      [2월 셋째주] 알약 스미싱 알림 본 포스트는 알약M 사용자 분들이 '신고하기' 기능을 통해 알약으로 신고해 주신 스미싱 내역 중 '특이 문자'를&nbsp...

검색 형태소 분석 사전 배포 과정 개선하기
당근마켓
검색 형태소 분석 사전 배포 과정 개선하기

안녕하세요! 검색플랫폼팀 테디예요. 당근 검색플랫폼팀은 사용자에게 보다 나은 검색 경험을 제공할 수 있도록 튼튼한 플랫폼을 만드는 팀이에요. 검색 서비스와 인프라를 운영 및 관리하면서 검색 트래픽을 안정적으로 소화할 수 있도록 만들죠.이를 위해 팀에서 다양한 노력을 하고 있는데요. 최근에는 검색 형태소 분석 사전 배포 과정을 개선하는 프로젝트를 진행했어요. 그래서 오늘은 프로젝트를 진행하게 된 계기와 과정, 그리고 결과를 간략하게 공유드려 볼게요.📚 형태소 분석을 위한 기본 사전검색에서 형태소 분석 기능은 필수예요. 이 기능은 주로 문서를 검색하거나 색인할 때, 검색어 또는 문서의 검색 대상 필드(제목, 본문 등)의 형태소를 분석하기 위해 사용돼요.저희 팀은 Elasticsearch를 검색 엔진으로 사용하면서 Nori를 기반으로 자체 구축한 Analysis-Karrot 플러그인을 통해 형태소를 분석하고 있어요. 이 플러그인은 내부에 가지고 있는 기본 사전 (또는 시스템 사전)을 기반으로 형태소를 분석해요. 해당 사전은 한국어 단어들의 품사, 형태, 가중치 등 형태소 분석에 사용되는 다양한 정보를 가지고 있어요.내부 사전 데이터 일부형태소를 잘 분석하고 나아가 검색 품질을 향상하기 위해서는 이 기본 사전을 잘 관리하는 것이 매우 중요해요. 왜냐하면 줄임말, 신조어 등이 계속해서 생겨나고, 어떤 단어라도 검색어로 들어올 수 있기 때문이에요. 이러한 변화에 잘 대응하기 위해 기본 사전 내용을 주기적으로 업데이트하며 고도화해야 해요.⚙️ 기본 사전 업데이트 프로세스AS-IS: 기본 사전 업데이트 프로세스현재 기본 사전 내용을 업데이트하는 과정은 아래와 같아요.검색 어드민을 통해서 새로운 사전 데이터를 사전 DB에 추가한다.사전 DB를 읽어서 새로운 사전 데이터 셋을 생성한다.사전 데이터 셋을 주입하여 Analysis-Karrot 플러그인을 생성한다.Analysis-Karrot 플러그인을 Elasticsearch 검색 클러스터에 재배포한다.여기서 문제는 ES 검색 클러스터 배포는 매우 신중해야 하는 무거운 작업이라는 점이에요. 만약 검색 클러스터 배포 과정에서 문제가 발생한다면 검색 기능 자체를 사용하지 못할 수도 있어요. (SPOF) 그래서 설정 변경 또는 버전 업데이트 등과 같이 검색 클러스터 운영에 필수적인 상황을 제외하고는 배포를 최소화하는 것이 운영 측면에서 지향하는 방향이에요. (DB 운영과 비슷하다고 볼 수 있죠.)🤔 검색 클러스터 배포 없이 사전 업데이트 가능할까?팀에서는 검색 클러스터 배포를 하지 않고 기본 사전을 업데이트할 수 있는 방법이 없을까 고민하면서, 기본 사전을 사용하는 플러그인 내부 코드를 분석했어요.Nori를 기반으로 자체 개발한 Analysis-Karrot ES 플러그인은 내부적으로 Lucene의 BinaryDictionary를 사용해요. 이를 통해 플러그인 내부에 들어있는 불변하는 성격의 사전 데이터를 싱글톤 객체를 사용해서 메모리에 로드하고 있어요. 여기서 저희는 Dictionary 객체를 생성할 때 플러그인 내부에 있는 사전 데이터가 아니라 외부에 저장한 사전 데이터를 참조할 수 있도록 변경하면 어떨까 생각했어요.BinaryDictionary 를 구현해서 만든 TokenInfoDictionary 클래스 명세 예시. 싱글톤으로 구현되어 있다. (코드 보기)💡 아이디어를 구현해 보자! (a.k.a PoC)TO-BE: 기본 사전 업데이트 프로세스위에는 아이디어를 조금 더 구체화하기 위해 그림으로 표현해 본 모습이에요. 기존에는 플러그인을 새로 만들어서 Elasticsearch 도커 이미지에 적용 및 배포를 진행했다면, 새로운 방식에서는 아래와 같은 단계로 새로운 사전을 배포하게 돼요.사전 데이터(*.dat)를 빌드 후 S3에 업로드한다.업로드된 사전 데이터를 검색 엔진 노드의 로컬 파일 시스템에 저장한다.인덱스를 생성할 때 설정에서 사전 데이터가 저장되어 있는 경로를 지정한다.형태소 분석 시 지정된 경로에 있는 사전으로 토크나이저 객체를 생성 및 사용한다.이 아이디어를 구현하기 위해 많은 요소들을 고려해야 했는데요. 그중 가장 핵심적인 부분은 기존에 싱글톤 패턴으로 오직 플러그인 내부에 저장된 사전만 로드해서 사용하던 사전 관련 로직을, 로컬 파일 시스템에 저장된 여러 개의 사전을 경로 기준으로 각각 로드해서 사용할 수 있도록 수정하는 부분이었어요. 아래는 싱글톤 인터페이스의 변경 예시예요.싱글톤 패턴으로 사용하던 사전 로직을 경로 기준으로 여러 개의 사전을 읽을 수 있게 수정한 코드 인터페이스 일부🫠 실패 그리고 원인 분석그러나 안타깝게도 저희 팀은 두 달 넘게 구현 및 단위 테스트를 진행한 이후 통합 테스트를 진행하는 과정에서 문제를 발견했어요. 크고 작은 문제들 중에서 가장 중요했던 건 힙 메모리 사용량이 30% 이상 증가했다는 점이었어요.Elasticsearch에서는 힙 메모리 사용량이 75% 이상이면 매우 주의를 요하며 적절한 최적화를 권유해요. 85% 이상이면 클러스터 퍼포먼스가 저하되고 circuit breaker 에러가 발생할 수 있다고 말하죠. 특히 운영 클러스터에서 circuit breaker 에러가 발생하면, 해당 노드가 일시적으로 중단 또는 재시작되면서 클러스터에 이상 현상을 발생시킬 수 있어요. 매우 조심해야 하는 거죠.PoC 전후 비교 결과 (왼쪽: 전, 오른쪽: 후)결국 PoC를 중단하고 힙 덤프를 통해 힙 사용량 증가에 대한 원인 파악에 나섰어요. 가장 눈에 띄는 문제는 Byte Array 크기 증가였어요. (2.26GB to 7.16GB) 이건 곧 사전 데이터 크기가 증가했다는 뜻이에요. 기존에는 싱글톤 패턴을 사용해서 단일 인스턴스로 사전을 생성하여 서빙했지만, 여러 버전의 사전을 서빙하기 위해 싱글톤 패턴을 제거했는데요. 이로 인해 힙 메모리가 증가할 거라고 어느 정도 예상하긴 했어도, 이렇게까지 많이 증가할 줄은 미처 몰랐어요.Heap Dump 결과 분석왜 Byte Array 크기가 증가했는지 파악하기 위해 계속해서 디버깅을 진행했어요. 그러다 Elasticsearch 코드에서 몰랐던 사실 하나를 발견했어요. 그것은 인덱스 스키마 설정에 Tokenizer를 선언하면 Tokenizer 객체를 생성하면서 내부적으로 기본 사전을 호출한다는 점이었어요. 저희는 인덱스 스키마에는 두 개의 Tokenizer를 정의하고 있었기 때문에, 인덱스를 생성할 때 기존 대비 최소 두 배 이상의 힙 메모리를 사용하게 된 거였어요.PUT my-index-000001{ "settings": { "analysis": { "analyzer": { "my_custom_analyzer": { "type": "custom", "tokenizer": "...", // Tokenizer 선언 예시 ... }...💪 재시도!실패에서 배운 점들을 교훈 삼아 두 번째 PoC를 진행하게 됐어요. 이번에는 레슨런을 바탕으로 두 가지 목표를 세우고 작업을 시작했어요.1) 사용하지 않는 사전 데이터를 제거하여 힙 사용량을 최적화하자저희가 사용하고 있는 플러그인은 Nori를 기반으로 만들어졌기 때문에 직접 구축한 사전 외에도 기본적으로 Nori가 가지고 있는 다양한 종류의 사전을 가지고 있어요. 또한 사전 스키마에도 12가지의 정보가 들어있고요. 여기서 사용하지 않는 데이터를 최대한 제거함으로써 플러그인을 경량화시키고 나아가 힙 메모리 사용량을 줄이고자 했어요. 결과적으로 이 과정을 통해 플러그인 크기를 33MB에서 7MB로 줄이게 됐어요.플러그인의 크기를 과거 33MB에서 7MB까지 감소2) 최대 2개의 최신 사전 데이터만 힙 메모리에 로드하자예시) 시간 순서(왼쪽에서 오른쪽)에 따라 달라지는 사전 업로드 상태앞선 실패로 얻은 교훈 중 하나는 “사전을 버전별로 유일하게 관리해서 같은 버전을 사용할 때 객체 참조가 가능하게 하자”라는 것이었어요. 워낙 빈번하게 객체 참조가 일어나기 때문에 참조할 때마다 새로운 객체를 만들게 되면 힙 메모리 사용량이 금방 100%에 도달하거든요. 그래서 이번에는 버전별 사전 관리를 위해 Map<Version, Dictionary>를 만들고 이 Map을 관리하는 매니저 객체를 싱글톤 패턴으로 구현했어요.싱글톤 패턴으로 사용하던 사전 로직을 매니저 인스턴스를 호출하도록 수정한 코드 인터페이스 일부🚀 배포 및 모니터링여러 번의 크고 작은 테스트를 거치면서 로직을 검증할 시간을 충분히 가졌고, 그 결과 프로덕션 환경에 배포해도 되겠다는 결론을 내렸어요. 그래서 중고거래 클러스터에 해당 기능을 배포했어요. 현재는 사전을 직접 운영 및 관리하고 있는 검색 운영팀에서 기본 사전 업데이트가 필요할 때마다 사전을 배포해 주고 계세요. 저희 팀은 함께 모니터링을 하고 있고요. 이로써 한 달에 한 번씩 몰아서 반영했던 사전 업데이트를 하루에 한 번씩 할 수 있게 되면서, 보다 빠르게 사용자 검색 경험을 개선할 수 있는 기반을 마련했어요.기본 사전 배포 이후 검색실 내부 반응🤔 앞으로 더 해야 할 일은?물론 아직 몇 가지 추가로 해야 할 일들이 남아있어요. 대표적으로는 프로덕션 환경이 다른 클러스터에도 점진적으로 배포해 나가는 일이 남아있어요. 또한 사전 업데이트 알람 추가나 모니터링 대시보드 고도화, 그리고 사전 업데이트 실패에 대한 Fallback 구성 등 안정성 및 가용성 측면의 작업들도 계속해서 진행할 예정이에요.🥕 함께 해요!당근 검색 플랫폼팀은 당근 검색 사용자들의 검색 경험 개선을 위해 앞으로도 빠른 피드백 루프를 바탕으로 플랫폼을 끊임없이 고도화해 나갈 거예요. 또한 검색의 방대한 트래픽을 무리 없이 소화하고 보다 나은 검색 결과를 제공할 수 있는 튼튼한 플랫폼을 만들기 위해 꾸준히 노력할 거예요.이러한 저희의 눈부신 여정에 함께하실 분들을 찾고 있어요. 많은 관심 부탁드려요!https://about.daangn.com/jobs/5688517003/읽어주셔서 감사해요!검색 형태소 분석 사전 배포 과정 개선하기 was originally published in 당근 테크 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.

리디, 인기 웹툰 ‘상류 사회’ 시즌2 연재 실시
리디
리디, 인기 웹툰 ‘상류 사회’ 시즌2 연재 실시

한층 깊어진 스토리와 본격적인 로맨스가 전개될 예정이다. The post 리디, 인기 웹툰 ‘상류 사회’ 시즌2 연재 실시 appeared first on 리디주식회사 RIDI Corporation.

2025년 주목해 볼 AWS 주요 고객 사례 모음
AWS KOREA
2025년 주목해 볼 AWS 주요 고객 사례 모음

안녕하세요! 2025년에도 고객 여러분의 비즈니스가 성공하실 수 있도록 AWS가 늘 함께하도록 하겠습니다. 예년과 같이 작년 한 해에 AWS 클라우드를 통해 빠른 민첩성과 비용 절감으로 기존 서비스를 혁신한 다양한 고객 사례를 모아보았습니다. 대기업, 중소 기업 뿐만 아니라 소프트웨어 개발 기업과 스타트업에 이르기까지 다양한 고객들이 어떻게 AWS를 ...

[이스트소프트x이스트시큐리티] ‘파트너 킥오프 2025’ 성료 소식!
이스트 시큐리티
[이스트소프트x이스트시큐리티] ‘파트너 킥오프 2025’ 성료 소식!

안녕하세요, 이스트시큐리티입니다.   지난 2월 14일, ‘이스트 파트너 킥오프(Kick-Off) 2025’ 행사가 성황리에 마무리되었습니다. 이번 행사에서는 이스트소프트 및 이스트시큐리티의 주요 파트너사 고객 120여 명이 참석해 자리를 빛내 주셨습니다.     파트너 킥오프 2025, 어떤 행사였을까...

데이터카탈로그에서 DataHub를 이용하는 방법
우아한형제들
데이터카탈로그에서 DataHub를 이용하는 방법

사내 데이터 디스커버리 도구인 데이터카탈로그는 DataHub를 기반으로 구축되었습니다. DataHub는 다양한 플랫폼과 연동되는 활발한 오픈소스 프로젝트로, 필요한 기능들을 새로 개발하지 않고도 활용할 수 있다는 장점이 있습니다. 그러나 DataHub를 처음 도입했을 때, 사내 구성원들로부터 UI/UX가 불편하다는 피드백을 받았습니다. 특히 데이터에 익숙하지 않은 사용자들에게는 DataHub의 다양한 기능들이 오히려 진입 장벽이 되었습니다. 저희는 사용자들이 원하는 데이터를 쉽게 찾고 활용하는 […] The post 데이터카탈로그에서 DataHub를 이용하는 방법 first appeared on 우아한형제들 기술블로그.

Flink SQL 도입기
하이퍼커넥트
Flink SQL 도입기

안녕하세요. Azar Matching Dev Team 의 Zeze 입니다. Flink 는 대다수의 백엔드 엔지니어들에게 친숙한 기술은 아니지만, 이벤트 스트리밍 처리를 위한 대표적인 기술 중 하나입니다. 대규모 실시간 데이터 스트리밍 처리를 위해 분산 환경에서 빠르고 유연하게 동작하는 오픈소스 데이터 처리 엔진이며, 팀 내에서는 Azar 의 핵심 로직...

[네이버클라우드 아카데미] 고려대 세종캠퍼스 SW중심대학과 함께한 <네이버클라우드 아카데미> 수료식
네이버 클라우드
[네이버클라우드 아카데미] 고려대 세종캠퍼스 SW중심대학과 함께한 <네이버클라우드 아카데미> 수료식

안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com입니다. #네이버클라우드 #네이버클라우드아카데미 #고려대세종캠SW중심대학 #NCA클라우드자격증 교육 고려대학교 세종캠퍼스 SW중심대학, 수료식 지난 1월 10일 금요일 진행된 이번 행사는 고려대학교 세종캠퍼스 SW 중심대학과 함께한 네이버클라우드 아카데미의 첫 번째 수료식인 만...

(~3/21) 스마트 팩토리 솔루션 찾고, 무료로 체험하기
네이버 클라우드
(~3/21) 스마트 팩토리 솔루션 찾고, 무료로 체험하기

안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. #스마트공장 #스마트팩토리 #공급기업찾기 2023년, 중소벤처기업부가 스마트팩토리 질적 확대에 집중한 新 디지털 제조혁신 MIDAS 2027 전략을 발표했습니다. 이미 22년까지 스마트팩토리 구축 사업을 통해 기초 단계 포함, 3만 개의 스마트 공장이 세워졌는데요. ...

리멤버에서 UT(사용자 테스트)는 어떻게 진행하나요?
리멤버
리멤버에서 UT(사용자 테스트)는 어떻게 진행하나요?

안녕하세요. 리멤버 프로덕트 디자이너 김희경입니다.명함관리 서비스로 시작해 구인구직 서비스와 직장인 커뮤니티 서비스, 데일리 뉴스 콘텐츠까지 직장인을 위한 다양한 서비스를 리멤버에서 제공해왔는데요. 이 모든 서비스를 한 화면에서 경험할 수 있는 새로운 화면을 신설하기에 앞서 사용성 테스트(Usability Test)를 진행했었어요. 새로운 피쳐가 배포...

토스 피플: 방황은 내게 방향을 제시해주었다
토스
토스 피플: 방황은 내게 방향을 제시해주었다

여러 분야의 방황을 통해 지금은 접근성 디자인을 맡고 계신 유라님. 지표를 중요하게 생각했던 토스에서 ‘정성적 경험’의 가치를 끌어올린 디자이너 유라님의 이야기를 들려드려요.

Polars와 pandas 비교: 어떻게 다를까요?
JetBrain Korea
Polars와 pandas 비교: 어떻게 다를까요?

지난 해에 Python DataFrame의 발전을 지켜보신 분이라면 대규모 데이터세트 작업용으로 설계된 강력한 DataFrame 라이브러리인 Polars를 들어보셨을 겁니다. DataSpell에서 Polars를 사용해 보세요 Spark, Dask나 Ray와 같은 다른 대용량 데이터세트 라이브러리와는 달리 Polars는 한 대의 시스템에서 사용되도록 설...

Introducing Impressions at Netflix
넷플릭스
Introducing Impressions at Netflix

Part 1: Creating the Source of Truth for ImpressionsBy: Tulika BhattImagine scrolling through Netflix, where each movie poster or promotional banner competes for your attention. Every image you hov...

올리브영 글로벌몰 주소 자동완성 및 검증 솔루션 도입기
올리브영
올리브영 글로벌몰 주소 자동완성 및 검증 솔루션 도입기

안녕하세요, 올리브영 글로벌몰 PO ZㅣZㅣ입니다. 🙋🏻‍♀️ 올리브영 글로벌몰을 잠깐만 소개할게요. "안녕하세요, 올리브영입니다. 필요하신 물건 있으면 말씀해주세요" 이는 한국에 있는 1,35…

데이터카탈로그 PM이 ‘데이터 디스커버리’라는 가치를 풀어내는 방법
우아한형제들
데이터카탈로그 PM이 ‘데이터 디스커버리’라는 가치를 풀어내는 방법

데이터 디스커버리란 조직 내에 존재하는 다양한 데이터를 쉽게 찾고 이해하여 분석에 활용할 수 있도록 돕는 과정입니다. 사용자 입장에서 이는 마치 거대한 서점에서 필요한 책을 찾는 것과 비슷합니다. 서점에는 수많은 책들이 있지만 이름만 알고 정확한 위치를 모르면 찾는 데 많은 시간이 걸립니다. 어떨 땐 정확한 이름도 모르는 상태에서 우선 둘러만 보려고 서점에 가기도 합니다. 데이터 디스커버리 [&#8230;] The post 데이터카탈로그 PM이 ‘데이터 디스커버리’라는 가치를 풀어내는 방법 first appeared on 우아한형제들 기술블로그.

[웍스 사용 설명서] 해외 지사와 쉽게 소통하는 법
네이버 클라우드
[웍스 사용 설명서] 해외 지사와 쉽게 소통하는 법

안녕하세요, 협업과 소통을 위한 필수 기능으로 글로벌 53만 기업의 든든한 협업툴 역할을 해온 네이버웍스(NAVER WORKS)입니다! 해외 지사의 현지 직원들과 메시지로, 메일로 대하는 데 언어의 어려움을 겪고 계시나요? 매번 시차를 계산해 가며 일정 잡지 않으신가요? 웍스 사용 설명서 3편에서는 해외 직원과 소통 장벽을 허무는 네이버웍스의 3가지 ...

당근 웹 플랫폼 외전 — 레거시 시스템 안전하게 제거하기
당근마켓
당근 웹 플랫폼 외전 — 레거시 시스템 안전하게 제거하기

당근 웹뷰 플랫폼 외전 — 레거시 시스템 안전하게 제거하기오래전에 소개했던 당근 웹뷰 배포 변천사에 이어지는 근황으로, 이번에는 조금 다른 주제를 다뤄보려고 해요 — 새로운 기술이 정착하면 이전에 사용하던 기술은 어떻게 되는 걸까요?당근마켓에 웹 프로젝트 배포하기 #1 — 파일 기반 웹뷰당근마켓에 웹 프로젝트 배포하기 #2 — 웹 서버로 돌아가기앞선 글에서 언급했던 것처럼 기존에 구축했던 로컬 파일 기반 웹뷰(이하 “로컬 웹뷰”)를 버리기로 결정하고 정말 오랜 시간이 지났어요.3년도 더 지난 로컬 웹뷰 deprecation 공지기존의 웹뷰와 새로 배포되는 웹뷰를 “로컬 웹뷰” / “리모트 웹뷰”라 구분하여 부르고 공지 이후 1년 간 기존 또는 새로운 웹뷰 배포들이 마이그레이션을 완료하면서 로컬 웹뷰는 사용하지 않는 기술이 되었어요.하지만 사용자가 이전에 사용된 로컬 웹뷰 기반의 딥링크로 진입하는 경우가 있었기 때문에 한 번이라도 로컬 웹뷰를 사용했던 서비스들은 하위 호환성을 위해 이중 배포를 계속 유지해야 했어요.로컬 웹뷰에서 리모트 웹뷰로 마이그레이션되는 방식로컬 웹뷰 배포는 사용자를 리모트 웹뷰로 이동시키는 코드만 포함해서 거의 변경이 없었어요. 시간이 지나면서 자연히 잊히길 원했지만, 맥락은 꾸준히 남아 프론트엔드 개발자들을 불편하게 했어요.라우팅 방식 변경, 진입 경로의 동작 변경 등 몇몇 큰 변경에서 로컬 웹뷰에도 대응하는 코드 추가가 필요했어요. 종종 필요한 대응을 빼먹고 배포를 진행하여 큰 버그가 생기고 뒤늦게 발견되기도 했어요.HTTP 리디렉션을 사용할 수 없어 JavaScript 코드에 의존했고, 로컬웹뷰 동작을 모두 기다린 후에 실행됐기 때문에 매우 느렸어요. 개발자들이 새로운 배포에서 아무리 초기 로딩을 개선하더라도 로컬웹뷰로 진입하는 경우 아무 소용이 없었어요.신규 입사한 개발자 분들이 당근의 웹뷰 구조를 파악하는데 큰 진입장벽이 되었어요. 더 이상 사용하지 않는 기술임에도 불구하고 여전히 로컬웹뷰가 무엇인지 가르치거나 파악하는데 시간을 써야 했어요.모바일 앱 개발자 분들에게도 “오래되어 손대기 어려운 코드”, 인프라 엔지니어 분들에게도 “자세히 모르지만 손대면 안 되는 리소스” 등 특별한 예외로 취급되었어요.문제점을 인지하고 그저 오래 기다리는 것 만으로는 기술을 퇴역시키는 데는 충분하지 않았어요. 로컬 웹뷰는 말 그대로 “기술 부채”로 남았어요.우리는 이제 이 부채를 청산하기 위해 적극적인 방식으로 상황을 개선해 보기로 했어요. 구체적으로는 위에서 언급한 문제들을 해결하기 위해 다음과 같은 목표를 가지고 프로젝트를 진행했어요.프론트엔드 개발자들이 더 이상 로컬 웹뷰 서빙을 하지 않는다.모바일 앱 코드에서 로컬 웹뷰를 처리하는 로직을 완전히 제거할 수 있다.로컬 웹뷰와 관련된 오래된 AWS 리소스를 완전히 정리한다.사용 패턴 분석하기“3년이요? 안 쓴 지 이만큼이나 지났다면 이제 그냥 제거해도 되는 것 아닐까요?”라는 의문이 자연스럽게 나왔지만 플랫폼은 여기에 대해 최대한 보수적으로 생각할 필요가 있어요.AWS CloudFront로 배포되는 웹 에셋들은 표준 로깅과 AWS Athena를 이용해 대략적인 유입량을 확인할 수 있어요. 먼저 표준 로깅을 활성화해서 일주일 정도 쌓인 CloudFront 로그를 대상으로 SQL로 약 일주일 간의 요청 수를 세보았어요.SELECT cs_uri_stem, count(cs_uri_stem) as countFROM &quot;cloudfront&quot;.&quot;cloudfront_logs_xxxxxx&quot;WHERE date &gt; CAST('YYYY-MM-DD' as DATE) -- 특정 일자 이후로 모든 로그 취합 AND cs_uri_stem LIKE '%meta.json' -- 로컬 웹뷰의 메타데이터 엔드포인트GROUP BY cs_uri_stemORDER BY count DESC/service-a/meta.json | 129,007,826/service-b/meta.json | 1,252,890/service-c/meta.json | 764,021...오래되었으니 거의 사용되지 않으리라는 희망사항과는 다르게, 실제로는 트래픽이 적지 않게 남아있다는 것을 확인했어요. 시스템을 일방적으로 제거해 버리면 앱 어딘가가 망가져서 여러 사용자들이 불편을 겪을 것이 분명했어요.문제는 로컬웹뷰가 표준 웹 요청과 다르게 HTTP 시맨틱을 온전히 따르지 않기 때문에 표준 로깅만으로 구체적인 사용패턴까지는 분석할 수 없다는 점이었어요.오래된 코드라 클라이언트 측 추적이 마련되어 있지 않았어요. 즉시 리디렉션되는 웹 환경이라 추적을 삽입하기 적절치 않고, 모바일 앱에 일시적으로 추적 코드를 추가하기에도 상황이 여의치 않았어요.결국 실행 가능한 방식으로 문제를 해결하기 위해 프로젝트에 더 도전적인 목표를 추가했어요.기존 로컬웹뷰 진입경로와 100% 호환되는 방식으로 동작한다.변경하지 않고도 리모트 웹뷰와 동등한 효과를 보장한다.망가뜨리지 않고 “업그레이드”하기앞서 설명했던 로컬 웹뷰 — 리모트 웹뷰 이 중 배포에서 사용자가 리모트 웹뷰까지 도달하는 길고 비효율적인 여정을 도식화하면 다음과 같아요.복잡한 기존 동작과 100% 호환되면서도 모든 목표를 달성하려면 전략적인 접근이 필요했어요.Q. “프론트엔드 개발자들이 더 이상 로컬 웹뷰 서빙을 하지 않으려면?”“로컬 웹뷰 서빙” 에는 리디렉션 룰, 라우팅, 추적 등을 위한 클라이언트 측 코드, 배포하기 위한 CI 워크플로우 코드, 관리하기 위한 각 서비스 팀의 개발 정책 등이 암묵적으로 포함되어 있어요.이 것들을 모두 취합하여 한 곳에서 정책화된 코드로 제공하면 문제를 해소할 수 있을 거라 생각했어요. 하지만 동시에 다음 질문에도 답해야 해요.Q. “모바일 앱 코드에서 로컬 웹뷰를 처리하는 로직을 완전히 제거하려면?”로컬웹뷰는 프론트엔드뿐 아니라 모바일 앱 개발자 분들에게도 부담인 부분으로, 단순히 관련 정책을 앱 코드로 이전하는 것으로는 궁극적으로 문제가 해결되지 않아요. 호환을 위해 코드가 들어간다고 하더라도 기존처럼 복잡한 것이 아니라 누가 언제 봐도 이해할 수 있는 수준으로 단순한 것이어야 해요.따라서 도식화된 “스킴 핸들러” 부분의 동작을 이식하면서 프론트엔드의 정책을 다루기 위해 서버 측으로 이전하기로 했어요.Q. “로컬 웹뷰와 관련된 오래된 AWS 리소스를 완전히 정리하려면?”그리고 이건 비교적 쉽게 해결할 수 있어요. AWS를 안쓰면 됩니다(??)하위 호환성을 완전히 보장하려면 스킴 핸들러의 동작을 일부 에뮬레이션 해야 하는데, AWS의 CloudFront Functions나 Lambda@Edge와 같은 서비스로는 다소 처리가 까다롭고 구성이 복잡해질 수 있어요. 기존 리소스를 정리하기는 커녕 더 많은 리소스를 추가하게 되겠죠.이전 글에서도 소개했던 Cloudflare Workers 를 사용하면 단일 위치, 단일 코드에서 ZIP 파일을 다루는 것과 같이 훨씬 더 복잡한 작업을 효과적으로 처리할 수 있어요. 기존의 CloudFront 동작을 Cloudflare Workers에서 처리하는 것으로 다음과 같이 정리할 수 있어요.중간의 CloudFront Distribution과 S3 Bucket, Athena 리소스를 모두 제거하고, 단일 Worker Script 코드로 정책을 관리하게 변경했어요. 동적인 생성 동작으로 변경되었지만, 템플릿 렌더링부터 ZIP 아카이빙까지 버퍼링 없는 단일 스트림으로 처리하여 기존 방식에 비교해도 추가적인 지연이 거의 없어요.(구체적으로는 HTMLRewriter라는 내장 유틸리티와 client-zip이라는 효율적인 ZIP 스트림 라이브러리를 사용했어요.)// 코드 형태의 단일 매니페스트로 모든 이전 로컬웹뷰 배포들의 정책을 통합export const manifest = { 'service-a': { id: 'service-a', title: '서비스 A', updatedAt: '20240920', // 모든 로컬 웹뷰 배포는 서버가 없기 때문에 HashRouter 방식을 사용했음 // 일부 SPA들은 내부 라우팅 방식을 변경하지 않고 그대로 이전 router: 'hash', remote: 'https://service-a', }, 'service-b': { id: 'service-b', title: '서비스 B', updatedAt: '20240621', // 리모트 웹뷰로 옮겨간 서비스들은 대부분 라우팅 방식을 변경했음 router: 'path', remote: 'https://service-b', },...이것만으로도 프론트엔드와 인프라 측의 관리 부담이 크게 개선되지만 모바일 코드도 함께 제거하기 위해 추가 작업이 필요해요.로컬 웹뷰 딥링크에는 서비스를 구분하기 위해 app 파라미터(deprecated)가 전달되는데, 이것을 Cloudflare Workers 기반 스킴 핸들러에 그대로 위임(Delegate)하는 것으로 동작을 변경했어요.워커가 HTTP 컨텍스트에 전달된 유저 에이전트 정보와 app 파라미터만 보고 나머지 동작을 결정할 수 있기 때문에 결과적으로 앱 코드에는 예전 파라미터를 위임하는 몇 줄의 코드만 남을 수 있어요.신규 버전의 앱 코드에서 로컬 웹뷰를 열면 이렇게 동작해요.Scheme Handler 워커는 직접 콘텐츠를 제공할 수 있는 일반 HTTP 기반 웹뷰이면서 동시에 여러 웹뷰들의 라우팅 정책을 취합하고 있는 진실의 원천이에요.더 이상 ZIP을 다운로드하고 압축을 해제하고 웹뷰를 띄우고 JavaScript 를 실행하는 긴 과정을 기다릴 필요가 없이 직접 원하는 웹뷰를 연 것과 동등하게 동작하여 사용성에도 큰 개선이 있어요.추가로 HTTP 기반이 아니어서 어려웠던 서버 기반 사용패턴 분석이 용이해졌어요. 진입점을 이전하기 위해 서비스 별 요청량을 실시간으로 모니터링하면서 숨은 진입점을 찾아 추가로 마이그레이션 할 수 있었어요. Workers Analytics Engine을 사용하면 워커에서 손쉽게 데이터포인트를 남기고 저렴한 비용으로 쿼리(via ClickHouse SQL)할 수 있어요.점차 감소하는 서비스 별 로컬 웹뷰 처리량이 중 실제 다운로드 요청이 의미상 0에 근접하게 된다면 추가적인 검증을 거쳐 “완전한 제거”까지 노려볼 수 있을 거예요.누구나 그럴싸한 계획은 있다…결과만 적어놓아 쉬운 여정으로 보일 수 있지만 꽤 우여곡절이 있었어요.예전 CloudFront Distribution에서 받던 meta.json 자체를 리디렉션하려고 보니 안드로이드 에이전트에 숨어있던 버그로 HTTP 리디렉션 응답을 받으면 크래시가 나서 버그를 먼저 수정해야 했어요.리디렉션 대신 DNS 수준에서 대상지를 변경하는 것으로 접근방식을 바꿨지만 인증서 피닝 업데이트가 누락되어 요청이 차단될 뻔하기도 했어요.AWS 리소스를 정리하고 보니 같은 버킷을 통해 배포되던 타 조직의 어드민 서비스 접근이 막혀 급하게 복구하는 상황도 있었어요. 최근 생성된 리소스는 모두 내부 플랫폼에 의해 관리되고 추적되지만 그전에 통합하여 사용하던 공통 버킷들은 아직도 암묵지에 가깝게 남아있어 어떤 변경이 어떻게 문제가 될지 (트래픽을 봤음에도) 미리 알기 어려웠어요.그러게요. 그게 왜 저기… (먼산)채팅 상단을 그리던 웹뷰는 완전히 커스텀한 로컬웹뷰 배포를 사용하고 있었어요. 구성을 동적으로 변경할 수 있었기 때문에 사전에 이전을 진행했지만 클라이언트에 주소지에 대한 캐시가 남아있어 이전 채팅방을 들어갈 때 오류 메시지가 나타나기도 했어요.캐시된 주소에 있던 리소스 제거로 404 응답이 노출됨(옛날에 열었던 채팅방에서만 나타나고 다시 들어가면 사라지기 때문에 CS 처리가 까다로웠어요.)100% 동작 호환을 목표로 했지만, 검증하고 적용하는 과정에서 여전히 구멍이 발견되고 사후에 수정해야 했어요. 처음 작업을 제안한 2024년 1월 이후 거의 1년 동안의 크고 작은 협력과 사후 이슈 대응을 거쳐, 마침내 당근 모바일 앱 스펙에서 “로컬 웹뷰”가 완전히 제거될 수 있었어요.개발자들은 오래된 맥락에서 자유로워졌고, 사용자는 로컬 웹뷰 리소스를 미리 패치하기 위해 발생하던 낭비되던 대역폭을 아낄 수 있게 되었어요.왜 도입됐고 왜 폐기되었나사실 로컬 웹뷰가 도입되던 그 시점에서 당근에서 배포된 최초의 웹뷰가 이미 비슷한 방식으로 구현되어 있었어요.웹뷰를 채택하는 서비스가 늘어나면서 이들에게 공통된 플랫폼을 제공하고 싶었고, 이전의 구현을 그대로 참고한 것이 시작이었어요. 오프라인 파일을 직접 동기화하면서, 알려져 있던 웹의 단점을 보완하고 오프라인 지원과 캐싱을 개선할 수 있지 않을까 하는 나름의 의도가 있었어요.하지만 거의 업데이트가 없던 기존에 웹뷰 화면과는 다르게 새로 추가되는 웹뷰 기반 서비스들은 매우 빈번한 배포 주기를 가지고 있었고, 결과적으로 로컬 웹뷰는 의도한 목표를 달성하지 못하면서 동시에 웹이 가지는 여러 장점을 희석시켰어요.이건 일종의 “성급한 최적화(Premature optimization)” 사례라고도 볼 수 있어요.We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%— Donald Knuth하지만 누구든지 모든 일에 대해 결과를 100% 알고 시작할 수는 없어요. 인용의 “3%의 기회”를 위해 시도하는 것들로 하여금 때로는 기술을 혁신하고 때로는 교훈을 통해 더 나은 시도의 밑거름 삼을 수 있어요.레거시가 주는 교훈그 이후 관련자가 모여 간단히 로컬 웹뷰 시스템에 대한 회고를 진행했어요. 로컬 웹뷰가 도입되고 폐기되는 여정에서 우리는 무엇을 배웠나요?최적화 의도는 도입하기에 앞서 사례 기반의 시뮬레이션과 측정, 그리고 최적화로 인해 포기하게 되는 것들에 대한 정량적인 평가가 수행되었으면 더 좋은 설계를 할 수 있었을 거예요.이미 앞서서 사용되고 있던 기술이라고 항상 타당하다고 전제하는 것 보단 바뀐 주변 환경과 요구되는 조건에 맞춰 새로운 평가 기준을 마련해야 해요.나중에 환경이 바뀌더라도 유연하게 대응할 수 있도록 충분한 “적응력”을 갖추도록 전체 시스템을 설계해야 해요.특히 캐시를 도입한다면 항상 제거(Purge)를 위한 장치를 함께 마련해야 해요. 그렇지 않으면 캐시가 효과적이더라도 이후 시스템 적응력을 크게 해칠 수 있어요.하위호환성을 제공하는 것은 종종 단순해 보여도 매우 어려운 일이 될 수 있어요.하위호환성을 고려하지 않고 기능을 삭제했다면 수많은 문제가 생겼을 거예요. 이런 문제들은 사용자 경험과 서비스 신뢰도에 악영향을 주고 서비스 개발자가 플랫폼에 의존하기 어렵게 만들어요. 그렇기 때문에 플랫폼은 상황이 어렵더라도 호환성을 쉽게 타협하면 안 돼요.오랜 시간이 지나면 맥락은 희미해져요. 사람의 기억에 의존할 수 없고, 문서조차 완전히 신뢰할 수 없으며, 결국 실행 중인 코드만이 유일하게 진실을 얘기할 때가 있어요.영향도 분석을 위해 코드를 읽는 것도 항상 유효한 수단이 아니에요. 손으로 배포되는 임시(라고 생각했던) 코드는 CI 워크플로우에 포함되지 않고, 독립적인 브랜치로 관리되는 경우 GitHub 코드 검색에 포함되지 않아요.이론적으로 보장되는 것이어도 실제 환경과의 미묘한 차이로 인지하지 못한 문제가 발생할 수 있어요. 잘못되었을 때 영향도가 크거나 돌이킬 수 없는 변경은 적용하기 전에 관련된 추적 코드를 먼저 배포하여 실제 영향도를 추가로 검증해야 해요.이런 교훈들은 “하이럼의 법칙”으로 유명한 Hyrum Wright의 “Deprecation” 에세이에서 읽을 수 있는 결론과도 일맥상통하지만, 책에서만 보던 교훈이 아니라 우리 사례에 맞게 직접 경험했기에 더 값지게 남았어요.점진적 개선을 향한 길당근 웹뷰는 이런 “폐기 작업”을 통해 무거운 짐을 떨쳐내고, 이전 과정을 교훈 삼아 다시 새로운 실험을 할 준비를 하고 있어요.나날이 발전 중인 브라우저 기술과 플랫폼 별 WebView API를 사용하여 또 다른 “3%의 가능성”을 탐구할 거예요. 그 과정에서 도입한 새로운 기술은 당근 웹 개발의 혁신이 될 수도 있지만, 어쩌면 오랜 시간이 지난 후 또 다른 “레거시 시스템”이 될지도 몰라요.플랫폼에서 중간마다 폐기 작업이 수반되지 않으면 결국 복잡한 맥락을 감당하지 못하고 경직되어 버릴 수밖에 없어요.저는 우리가 이런 과정을 통해 성장하여 점점 더 나은 판단과 설계를 하고, 두려움 없이 변화하는 조직이 되기를 희망해요.Deprecating in an organized and well-managed fashion is often overlooked as a source of benefit to an organization, but is essential for its long-term sustainability.— Deprecation by Hyrum Wright, “Software Engineering at Google”당근 웹 플랫폼 외전 — 레거시 시스템 안전하게 제거하기 was originally published in 당근 테크 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.

2025년 YouTrack 로드맵
JetBrain Korea
2025년 YouTrack 로드맵

고객을 향한 JetBrains의 약속 JetBrains는 지금까지 그래왔듯이 YouTrack을 고객의 요구 사항에 따라 발전하는 플랫폼으로 만들기 위해 최선을 다하고 있습니다. 이러한 노력의 일환으로 YouTrack Server와 Cloud 두 버전을 계속 제공하여 고객의 조직 상황과 데이터 거버넌스 정책에 가장 적합한 호스팅 옵션을 자유롭게 선택할 ...

Go GC를 너무 믿지 마세요 - 메모리 누수 탐지와 GC 주기 조절
네이버 D2
Go GC를 너무 믿지 마세요 - 메모리 누수 탐지와 GC 주기 조절

Noir는 Go로 작성한 개별 데이터 특화 검색 엔진으로, 메일처럼 사용자마다 데이터가 분리된 서비스에 효과적입니다. 대표적으로 네이버 메일, 메시지 검색 서비스 등이 Noir를 사용하고 있습니다. 수년간 Noir를 개발 및 운영하는 동안 Noir 서버 메모리 사용량이 시간이 갈수록 천천히 증가하는 현상이 자주 관찰되었고 이를 해결하기 위해 큰 노력을 기울였습니다. Go는 가비지 컬렉터(GC)가 있는 언어로, 개발자가 메모리 관리에 크게 신경 쓰지 않아도 된다는 장점이 있습니다. 하지만 개발자가 메모리 관리에 개입할 수 있는 여지가 적기에, Noir에서 발생한 메모리 사용량 증가 현상은 해결하기 까다로운 이슈였습니다. 다음 그래프는 실제 프로덕션에 배포된 Noir 서버 일부의 메모리 사용량 그래프입니다. 시간이 갈수록 애플리케이션의 메모리 사용량이 늘어납니다. 사용량이 줄어드는 부분은 운영자가 배포 등의 이유로 검색 서버를 재시작하여 메모리 할당이 해제된 경우입니다. 해당 문제의 원인을 파악하기 위해 여러 실험을 해본 결과 원인은 크게 두 가지로 나눌 수 있었습니다. cgo를 사용하는 경우 Go가 아닌 C 언어가 관리하는 메모리가 생기기 때문에 메모리 누수 발생 가능 애플리케이션이 메모리를 많이 사용한다면 GC에 의해 메모리가 할당 해제되는 속도보다 할당되는 속도가 빠를 수 있음 이번 글에서는 Noir의 메모리 사용량 증가 현상을 해결하기 위해 사용한 방법 중 일부를 소개합니다. RES와 heap 해결 방법을 살펴보기 전에 애플리케이션 프로세스의 RES와 heap에 대해서 알아보겠습니다. 출처: Memory - HPC @ QMUL RES(Resident Memory Size, Resident Set Size, RSS)는 프로세스가 실제로 사용하고 있는 물리 메모리의 크기입니다. OS는 프로세스가 요청한 메모리를 실제로 사용하기 전까지 물리 메모리를 할당하지 않습니다. 따라서 프로세스가 사용하는 메모리(virtual memory) 크기와 실제로 사용하는 물리 메모리 크기(RES)에는 차이가 있습니다. RES는 여러 프로세스와 함께 사용하는 공유 라이브러리(shared libraries)와 프로세스가 요청하여 사용하는 메모리(actual ram usage)로 나뉩니다. RES는 top 명령어로 확인할 수 있습니다. heap은 프로세스가 런타임에 데이터를 저장하기 위해 OS로부터 할당받는 메모리입니다. 프로세스는 런타임에 저장하는 데이터가 많아지면 OS로부터 메모리를 할당받고 할당받은 메모리가 필요 없어지면 할당 해제합니다. heap의 구현은 언어마다 다르며 Go의 경우 TCmalloc에 영향을 받은 런타임 메모리 할당 알고리즘을 사용하고 있습니다. Go 애플리케이션의 heap 크기는 runtime 패키지를 사용하거나 GODEBUG=gctrace=1 환경 변수를 설정해서 확인할 수 있습니다. 간단히 말해, RES는 OS가 관리하는 메모리와 관련된 개념이고 heap은 Go가 관리하는 메모리입니다. 그리고 RES는 heap뿐만 아니라 프로세스가 동작하는 데 필요한 모든 메모리의 크기를 말합니다. 즉, RES 값과 heap 크기의 차이가 크다면, 프로세스 내 Go가 관리하지 않는 곳에서 사용하는 메모리 때문일 수 있습니다. Go 버전에 따른 커널의 메모리 동작 Go 애플리케이션의 RES 값을 확인할 때, Go 버전에 따라 RES 값이 달라질 수 있다는 점을 알아두어야 합니다. 리눅스는 madvise라는 시스템 콜을 제공합니다. madvise에 어떤 값을 설정하느냐에 따라 커널이 프로세스의 메모리를 관리하는 정책이 달라집니다. Go 1.12부터 1.15까지는 madvise에 MADV_FREE를 사용하고 그 전 버전이나 그 후 버전은 MADV_DONTNEED 을 사용합니다. MADV_FREE는 성능이 좋은 대신 메모리 압력이 없는 한 커널이 메모리 할당 해제를 미룹니다. 반면 MADV_DONTNEED는 지정한 메모리 영역의 semantic을 변경하여 커널의 메모리 할당 해제를 유도합니다. 다음 그래프는 Go로 작성한 GraphQL DB인 Dgraph가 데이터를 주기적으로 로드할 때 MADV_DONTNEED를 적용한 경우(왼쪽)와 MADV_FREE를 적용한 경우(오른쪽)의 메모리 사용량을 비교한 것입니다. 출처: Benchmarks using GODEBUG=madvdontneed environment variable - Dev - Discuss Dgraph 왼쪽은 데이터 로드 이후 사용한 메모리를 커널이 할당 해제해 메모리 사용량이 감소하는 구간이 존재합니다. 반면 오른쪽은 커널이 메모리를 할당 해제하지 않아 메모리 사용량이 감소하는 구간이 없습니다. 만약 사용하고 있는 Go 버전이 1.12 이상 1.15 이하라면 메모리가 heap에서 할당 해제되었어도 커널이 할당 해제하지 않아 RES에 포함될 수 있습니다. Noir는 Go 최신 버전을 사용하기 때문에 이 이슈에는 해당하지 않았습니다. valgrind를 통한 cgo 코드의 메모리 누수 탐지 Noir는 RES 값과 heap 크기의 차이가 컸습니다. 그래서 메모리 사용량 증가 현상이 Go가 관리하는 메모리가 아닌 부분에서 발생할 확률이 높다고 추측했습니다. Noir는 C++로 작성된 라이브러리를 사용합니다. 보통 Go에서 C 코드를 다루기 위해서는 cgo를 사용합니다. cgo를 사용하면 C++처럼 메모리 할당 및 할당 해제를 개발자가 직접 해야 하는데, 실수로 메모리를 할당 해제하지 않는다면 메모리 누수가 일어날 수 있습니다. 메모리 누수 탐지에 유용한 도구 중 하나는 valgrind입니다. 다만 빌드된 Go 프로그램 바이너리를 그대로 valgrind에 사용하면 많은 경고가 발생합니다. Go는 GC 언어인데, valgrind가 Go 런타임 동작을 정확히 알 수 없기 때문입니다. 경고를 최소화하기 위해 cgo 코드만 사용하는 테스트 코드를 따로 빌드하여 valgrind에 사용하면 메모리 누수를 쉽게 탐지할 수 있습니다. 테스트 코드를 빌드 후 valgrind를 적용하여 다음과 같은 결과를 확인했습니다. ... ==15605== LEAK SUMMARY: ==15605== definitely lost: 19 bytes in 2 blocks ==15605== indirectly lost: 0 bytes in 0 blocks ==15605== possibly lost: 3,552 bytes in 6 blocks ==15605== still reachable: 0 bytes in 0 blocks ==15605== suppressed: 0 bytes in 0 blocks possibly lost는 Go 런타임이 관리하는 메모리를 valgrind가 경고한 것으로, 메모리 누수가 아닙니다.(실행 중인 스레드에 할당된 메모리이므로 잃을 수 있는 '가능성'이 있다고 경고하는 것입니다.) 하지만 definitely lost는 프로그램에는 더 이상 포인터가 존재하지 않는데 가리키는 heap의 메모리 할당이 해제되지 않았을 때 발생하는 오류로, 명백한 메모리 누수입니다. 확인 결과 cgo 코드에서 String 객체를 할당한 후 할당 해제하는 로직이 빠져 있었습니다. 할당 해제 함수를 추가한 후 valgrind를 다시 실행하여 definitely lost 오류를 해결했습니다. ... ==25027== LEAK SUMMARY: ==25027== definitely lost: 0 bytes in 0 blocks ==25027== indirectly lost: 0 bytes in 0 blocks ==25027== possibly lost: 2,960 bytes in 5 blocks ==25027== still reachable: 0 bytes in 0 blocks ==25027== suppressed: 0 bytes in 0 blocks GC 주기에 따른 애플리케이션의 메모리 사용량 변화 GC 주기를 잘 설정하면 애플리케이션 코드를 변경하지 않고도 메모리 사용량을 크게 낮추고 메모리 사용량 증가 문제도 해결할 수 있습니다. 예를 들어 GC가 메모리를 할당 해제하는 속도에 비해 heap에 메모리가 할당되는 속도가 빨라 메모리 사용량이 증가하는 경우 GC 주기 조절이 해결법이 될 수 있습니다. GC의 주기를 단순히 짧게 만들어서 해결되는 것은 아닙니다. 오히려 GC가 너무 자주 일어나면 오버헤드가 커져 메모리 사용량이 더 빠르게 증가할 수도 있습니다. Go의 GC는 크게 Mark와 Sweep의 두 단계로 구성됩니다. 그리고 mark 단계는 STW(stop the world)가 발생하는 구간과 그렇지 않은 구간으로 나눌 수 있어, GC 로그에서 3개의 단계를 확인할 수 있습니다. 보통 Mark and Sweep 방식을 떠올리면 먼저 Mark가 이루어지고 그 후 Sweep이 이루어지는 것으로 생각합니다. 하지만 Go의 GC 가이드 문서나 gctrace 문서를 보면 Sweep(STW 발생) → Mark and Scan → Mark 종료(STW 발생) 순서로 동작하는 것을 알 수 있습니다. 실제 GC 로그를 살펴보겠습니다(Go 1.21 기준). gc 45093285 @891013.129s 8%: 0.54+13+0.53 ms clock, 21+0.053/17/0+21 ms cpu, 336-&gt;448-&gt;235 MB, 392 MB goal, 0 MB stacks, 0 MB globals, 40 P gc 45093285: 45093285번째 GC 로그입니다. @891013.129s: 프로그램이 시작된 지 891013.129초가 지났습니다. 8%: 프로그램 시작 이후 GC가 사용한 시간의 비율입니다. 0.54+13+0.53 ms clock: wall-clock 시간으로 Sweep(STW) 0.54ms, Mark and Scan 13ms, Mark 종료(STW) 0.53ms 걸렸습니다. 21+0.053/17/0+21 ms cpu: CPU 시간으로 Sweep(STW) 21ms, Mark and Scan(allocation 0.053ms, background 17ms, idle 0ms), Mark 종료(STW) 21ms 걸렸습니다. 336 → 448 → 235MB: 각각 GC 시작 시 heap 크기, GC가 끝났을 때 heap 크기, live heap 크기를 의미합니다. 392MB: 이번 GC 시작 시 목표 heap 크기입니다. GC는 시작 전 목표 heap 크기를 설정하고 heap 크기를 그 이하로 줄이려고 합니다. 위 로그는 GC가 끝난 후 heap 크기가 448MB이므로 목표 heap 크기인 392MB보다 크므로 목표를 달성하지 못했습니다. Mark 중에 애플리케이션이 메모리를 할당할 수 있기 때문에 위 로그처럼 목표 달성에 실패할 수도 있습니다. GC가 끝난 후 live heap 크기(위 로그에서 235MB)를 기준으로 다음 목표 heap 크기가 정해지고, heap 크기가 다음 목표 heap 크기를 초과할 것으로 예상될 때 다음 GC가 발생합니다. GC가 자주 일어나면 GC가 사용한 시간의 비율(로그의 3번째 칼럼)이 높아집니다. GC가 자원을 많이 사용하면 애플리케이션의 로직이 사용할 자원이 적어지므로 GC 주기를 적절히 조절할 필요가 있습니다. GC 주기를 조절할 수 있는 파라미터 Go에서 GC를 조절할 수 있는 파라미터는 GOGC와 GOMEMLIMIT입니다. GOGC GOGC는 목표 heap 크기를 조절할 수 있는 파라미터입니다. 목표 heap 크기는 다음 식에 따라 설정됩니다. 목표 heap 크기 = live heap + (live heap + GC roots) * GOGC / 100 GOGC 기본값은 100으로 설정되어 있습니다. live heap이 비해 충분히 GC roots가 작다면 다음 목표 heap 크기는 live heap의 2배 정도라고 생각할 수 있습니다. 예를 들어 위에서 본 로그에서 다음 목표 heap 크기는 235MB의 두 배인 470MB 정도로 설정됩니다. 실제로 다음 GC 로그를 보면 470MB와 유사한 471MB로 설정된 것을 확인할 수 있습니다. gc 45093286 @891013.160s 8%: 0.39+7.5+0.75 ms clock, 15+1.8/16/0+30 ms cpu, 406-&gt;467-&gt;196 MB, 471 MB goal, 0 MB stacks, 0 MB globals, 40 P GOGC를 높이면 목표 heap 크기는 증가하고 GC 주기는 길어집니다. GOGC를 낮추면 목표 heap 크기가 작아지고 GC 주기가 짧아집니다. GOMEMLIMIT GOGC만 사용해 목표 heap 크기를 조절하면 문제가 발생할 수 있습니다. 예를 들어 순간적으로 메모리를 많이 사용하는 애플리케이션은 OOM이 발생하지 않도록 GOGC를 낮게 설정해야 하는데, 그러면 피크가 발생하지 않는 시간에는 낮은 GOGC로 인해 GC가 너무 자주 일어날 수 있습니다. 이 경우 목표 heap 크기의 상한값을 설정하면 GOGC를 높게 설정할 수 있습니다. 목표 heap 크기의 상한값을 설정하는 파라미터가 GOMEMLIMIT입니다. GOGC와 GOMEMLIMIT에 따른 애플리케이션의 메모리 사용량 변화 테스트 환경에서 GOGC와 GOMEMLIMIT을 변화시키며 Noir의 메모리 사용량 변화를 측정했습니다. Noir에 약 2주간 다량의 검색 요청을 보내고 gctrace 로그로 heap 크기 변화를 확인했습니다. 아무것도 설정하지 않은 경우(GOGC=100) 시간이 갈수록 heap 크기가 증가했습니다. 반면 GOGC를 높게 설정하면 heap 크기 증가 현상이 사라졌습니다. GC CPU 사용량 live heap 평균값 GC 시작 시 heap 평균값 live heap 실험 전후 변화량 GC 직전 heap 실험 전후 변화량 GOGC=50 9% 342.01MiB 470.22MiB -4.26MiB -4.6MiB GOGC=50, GOMEMLIMIT=800MiB 17% 640.48MiB 697.12MiB -19.23MiB -12.35MiB GOGC=100(기본값) 8% 149.95MiB 260.16MiB 32.01MiB 55.79MiB GOGC=200 6% 102.95MiB 273.76MiB 0.95MiB 2.96MiB GOGC=200, GOMEMLIMIT=800MiB 5% 99.94MiB 267.46MiB -0.87MiB -0.89MiB GOGC=300 4% 98.45MiB 358.30MiB -1.08MiB -2.67MiB GOGC=300, GOMEMLIMIT=800MiB 4% 95.71MiB 350.82MiB 0.63MiB 4.44MiB GOGC=400 3% 97.60MiB 451.43MiB 1.79MiB 6.5MiB GOGC=400, GOMEMLIMIT=800MiB 3% 90.74MiB 425.45MiB -3.2MiB -10.29MiB GOGC=600 2% 93.91MiB 623.61MiB 1.75MiB 9.48MiB GOGC=600, GOMEMLIMIT=800MiB 8% 500.11MiB 677.98MiB -91.57MiB -33.89MiB GOGC가 100일 때와 600일 때의 heap 크기 그래프를 비교하면 GOGC가 100일 때 heap 크기 증가가 두드러지는 반면 600은 증가가 거의 없습니다. 표를 보면 메모리 사용량 증가 외에도 주목해 볼 만한 지점이 있습니다. GOGC가 작을수록 GC가 자주 일어나 GC가 사용하는 CPU 사용률이 높아지지만, heap 크기는 커지는 경향을 보입니다. GOMEMLIMIT은 GOGC 값이 적당한 경우에는 heap 크기를 줄여주지만(GOGC=200, 300, 400), GOGC가 극단적이면(GOGC=50, 600) CPU 사용량과 함께 heap 크기가 오히려 늘어나는 모습을 보입니다. 따라서 GOGC와 GOMEMLIMIT을 적용할 때는 꼭 실험을 통해 적당한 값을 찾은 후 적용해야 합니다. 프로파일러를 통한 과다 메모리 사용 탐지 Go 애플리케이션에 heap 프로파일링을 적용하면 불필요한 메모리 사용이 있는 코드 부분을 탐지할 수 있습니다.(Go 애플리케이션의 프로파일링은 프로파일링 적용기 - 당신의 Go 애플리케이션은 좀 더 나아질 수 있다에서 자세히 설명합니다.) 먼저 Go의 메모리 allocator의 동작을 살펴보겠습니다. 출처: Visualizing memory management in Golang mheap은 Go 프로그램의 모든 heap 메모리 공간을 관리합니다. Resident set은 페이지들(일반적으로 8KB)로 나뉘고, mheap은 mspan과 mcentral을 통해 페이지들을 관리합니다. mspan은 가장 기본이 되는 구조체입니다. mspan은 다음 그림처럼 이중 연결 리스트(doubly linked list)로 이뤄지고 시작 페이지의 주소, span 크기 클래스, span에 속한 페이지 개수 정보가 저장됩니다. 출처: Visualizing memory management in Golang mcentral은 같은 크기 클래스의 두 개의 mspan 리스트를 관리합니다. 사용하고 있는 mspan의 리스트와 그렇지 않은 mspan 리스트를 관리하고, 가지고 있는 모든 mspan 리스트를 사용하면 mheap에 추가 페이지를 요청합니다. 객체가 할당되는 방식은 객체의 크기에 따라 달라집니다. ~ 8byte: mcache의 allocator가 할당 8byte~32KB: mspan의 크기 클래스(8byte ~ 32KB)에 속해 span에 할당 32KB~: mheap이 페이지를 직접 할당 이때 span에 할당되는 객체나 mheap에 의해 직접 할당되는 객체는 내부 단편화(internal fragmentation)가 일어날 가능성이 높습니다. 예를 들어볼까요. 다음은 8byte~32KB 크기의 객체가 할당되는 golang의 크기 클래스 중 일부입니다. // class bytes/obj bytes/span objects tail waste max waste min align ... // 56 12288 24576 2 0 11.45% 4096 // 57 13568 40960 3 256 9.99% 256 13KB 객체를 할당한다고 가정해봅시다. 13KB는 12288byte보다 크고 13568byte보다 작으므로 객체는 57번째 크기 클래스에 할당됩니다. 할당된 후 객체가 사용하는 메모리의 비율은 13000/13568 = 95.8%입니다. 약 4.2%의 내부 단편화가 발생합니다. 크기 클래스와 span 사이에서 발생하는 단편화도 존재합니다. 57번째 크기 클래스는 40960byte span에 3개가 들어갑니다. 3개의 클래스가 span에 채워져도 남는 메모리 40960-3*13568 = 256byte가 발생합니다(tail waste). 극단적으로 단 한 개의 13KB 객체만 생성한다면 40960byte의 span을 13KB 객체 하나만 사용하므로 약 68%의 내부 단편화가 발생합니다. 이번에는 35KB 객체를 할당하는 경우를 생각해봅시다. 크기가 32KB 이상인 객체는 페이지를 직접 할당하므로 크기 클래스를 사용하지 않습니다. // mcache.go // allocLarge allocates a span for a large object. func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan { if size+_PageSize &lt; size { throw("out of memory") } npages := size &gt;&gt; _PageShift if size&amp;_PageMask != 0 { npages++ } // Deduct credit for this span allocation and sweep if // necessary. mHeap_Alloc will also sweep npages, so this only // pays the debt down to npage pages. deductSweepCredit(npages*_PageSize, npages) spc := makeSpanClass(0, noscan) ... } 32KB 이상의 객체를 할당하는 allocLarge 함수는 객체 크기에 맞는 페이지 개수(npage)를 계산합니다. 그 후 해당 객체를 위해 span을 만듭니다(makeSpanClass). OS의 페이지 크기가 8KB인 경우 35KB가 사용하는 페이지 개수는 5개이고 총 메모리는 40KB입니다. 이 경우 35KB/40KB = 87.5%이므로 12.5%의 내부 단편화가 발생합니다. 이제 8byte 이상의 객체의 경우 내부 단편화가 발생할 수 있음을 알았습니다. Go는 8byte부터 32KB까지 총 67개의 크기 클래스를 촘촘하게 존재하여 너무 큰 객체가 아니라면 대부분은 내부 단편화가 문제 되지 않습니다. 하지만 불필요한 메모리 복사 등 잘못된 코드 로직이 중첩된다면 영향이 커질 수 있습니다. 애플리케이션에 내부 단편화가 발생할 가능성이 있는 객체를 쉽게 확인하는 방법이 있습니다. heap 프로파일링을 수행하면 다음 그림처럼 각 함수가 사용하는 객체의 개수(inuse_objects)와 객체들이 사용하는 메모리 크기(inuse_space)를 알 수 있습니다. 이를 이용하면 (객체들이 사용하는 메모리 크기)/(객체의 개수)로 객체 크기의 평균을 계산할 수 있습니다. 앞서 보았다시피 크기가 큰 객체의 개수가 많다면 내부 단편화가 메모리의 사용량에 영향을 끼칠 가능성이 높습니다. 그래서 애플리케이션의 메모리 최적화 작업을 진행할 때 크기가 큰 객체를 사용하는 함수 위주로 객체 할당을 줄이는 것이 좋습니다. 객체는 내부적으로 span에 할당되기 때문에, 같은 크기의 수많은 객체를 순차적으로 할당 후 일부만 사용하는 것도 지양해야 합니다. 포인터를 사용해 할당되는 객체를 분리하더라도 위험합니다. 다음 코드는 16byte 객체를 가리키는 포인터를 슬라이스로 할당 후 인덱스가 512의 배수인 포인터만 사용하는 예입니다. // Allocate returns a slice of the specified size where each entry is a pointer to a // distinct allocated zero value for the type. func Allocate[T any](n int) []*T // Copy returns a new slice from the input slice obtained by picking out every n-th // value between the start and stop as specified by the step. func Copy[T any](slice []T, start int, stop int, step int) []T // PrintMemoryStats prints out memory statistics after first running garbage // collection and returning as much memory to the operating system as possible. func PrintMemoryStats() // Use indicates the objects should not be optimized away. func Use(objects ...any) func Example3() { PrintMemoryStats() // (1) heapUsage: 0.41 MiB, maxFragmentation: 0.25 MiB slice := Allocate[[16]byte](1 &lt;&lt; 20) Use(slice) PrintMemoryStats() // (2) heapUsage: 24.41 MiB, maxFragmentation: 0.24 MiB badSlice := Copy(slice, 0, len(slice), 512) slice = nil Use(slice, badSlice) PrintMemoryStats() // (3) heapUsage: 16.41 MiB, maxFragmentation: 16.19 MiB newSlice := Allocate[[32]byte](1 &lt;&lt; 19) Use(slice, badSlice, newSlice) PrintMemoryStats() // (4) heapUsage: 36.39 MiB, maxFragmentation: 16.17 MiB Use(slice, badSlice, newSlice) } 출처: Memory Fragmentation in Go | Standard Output badSlice가 일부 포인터만 복사했지만 16*512=8KB(페이지 크기)이므로 페이지마다 사용하는 16byte 객체가 하나씩 남게 됩니다. 그 결과 포인터가 가리키는 객체가 사용하는 span은 하나도 할당 해제되지 못합니다. (2)와 (3)을 비교하면 포인터가 사용하는 메모리 8MB만 할당 해제되고 객체가 사용하는 메모리는 할당 해제되지 못하여 16MB 단편화가 발생하는 것을 확인할 수 있습니다. Noir의 경우 프로파일링을 통해 크기 13KB 이상의 객체를 다량으로 할당하는 것을 발견했습니다. 해당 부분에 불필요한 메모리 복사 등이 존재함을 확인했고 수정하여 전체 메모리 사용량의 30%를 줄일 수 있었습니다. 마치며 이 글에서 설명한 작업의 결과로 Noir는 메모리 사용량 증가 현상을 고쳤을 뿐만 아니라 메모리 사용량도 줄일 수 있었습니다. Go 프로그램의 메모리 문제로 골머리를 앓는 다른 분들께도 이 글이 도움이 되기를 바랍니다.