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...
기술 블로그 모음
국내 IT 기업들의 기술 블로그 글을 한 곳에서 모아보세요

안녕하세요, 올리브영 글로벌몰 PO ZㅣZㅣ입니다. 🙋🏻♀️ 올리브영 글로벌몰을 잠깐만 소개할게요. "안녕하세요, 올리브영입니다. 필요하신 물건 있으면 말씀해주세요" 이는 한국에 있는 1,35…
데이터 디스커버리란 조직 내에 존재하는 다양한 데이터를 쉽게 찾고 이해하여 분석에 활용할 수 있도록 돕는 과정입니다. 사용자 입장에서 이는 마치 거대한 서점에서 필요한 책을 찾는 것과 비슷합니다. 서점에는 수많은 책들이 있지만 이름만 알고 정확한 위치를 모르면 찾는 데 많은 시간이 걸립니다. 어떨 땐 정확한 이름도 모르는 상태에서 우선 둘러만 보려고 서점에 가기도 합니다. 데이터 디스커버리 […] The post 데이터카탈로그 PM이 ‘데이터 디스커버리’라는 가치를 풀어내는 방법 first appeared on 우아한형제들 기술블로그.
![[웍스 사용 설명서] 해외 지사와 쉽게 소통하는 법](https://blogthumb.pstatic.net/MjAyNTAyMTBfMTE3/MDAxNzM5MTUwNzc5NjM1.kXaimZ4rqMySZmIH5TVmNAyaJ-82k6tRIKSymlPD2Jgg.eszykd5-cQXDPx1aTtNUG_ywNkFcDgys7xW5l-dlmA0g.JPEG/썸네일.jpg?type=s3)
안녕하세요, 협업과 소통을 위한 필수 기능으로 글로벌 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 "cloudfront"."cloudfront_logs_xxxxxx"WHERE date > 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.

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

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->448->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->467->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 < size { throw("out of memory") } npages := size >> _PageShift if size&_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 << 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 << 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 프로그램의 메모리 문제로 골머리를 앓는 다른 분들께도 이 글이 도움이 되기를 바랍니다.

The post 외부감사인 선임 공고 appeared first on 리디주식회사 RIDI Corporation.

들어가며 안녕하세요. Game Platform Dev의 류동훈, Zhang Youlu(Michael), Takenaka, 이형중입니다. 저희 조직은 게임 퍼블리싱에 필요한 다양한 ...
![[네이버웍스] 드라이브 FAQ 웍스 드라이브는 모바일에서도 보안 위험 없이 사용할 수 있나요?](https://blogthumb.pstatic.net/MjAyNTAyMDZfMTQ0/MDAxNzM4ODE2MDkzODUz.1lRpSN2dTH6nOjSUABnjvturqhZaUq34r3J1T_yH20wg.HQ4tvIh66cKjbPRTYIb9gRA6WsSRaYTxloEQvkQbEcgg.PNG/네블_앱아이콘형.png?type=s3)
안녕하세요, 협업과 소통을 위한 필수 기능으로 글로벌 53만 기업의 든든한 협업툴 역할을 해온 네이버웍스(NAVER WORKS)입니다! 메신저, 이메일, 파일 저장 및 공유 등 대부분의 PC 업무를 모바일 기기에서 처리할 수 있게 되면서 개인이 소유한 모바일 기기를 업무에 활용하는 경우가 기하급수적으로 늘어났습니다. 이러한 추세로 회사 데이터와 자산을...

안녕하세요! 당근 검색품질팀 ML 엔지니어 해리예요. 👋이 글에서는 RAG를 활용한 검색 서비스인 “동네생활 기반 동네업체 추천” 기능 구현에 사용된 기술에 대해 이야기해보려고 해요.혹시 활동하고 있는 커뮤니티가 있으신가요?커뮤니티는 고향을 떠나 먼 곳으로 대학을 진학한 제게 절대 없어서는 안 될 정보의 창구였어요. 대학 새내기 시절을 돌아보면, 낯선 학교 근처의 맛집이나 듣기 좋은 꿀강의들을 찾기 위해 교내 커뮤니티 게시판을 열심히 뒤지던 기억이 새록새록 떠올라요. 대학원 진학을 준비할 때는 대학원생 커뮤니티에서 각 랩실의 정보를 얻기도 했죠.이처럼 커뮤니티는 우리 생활과 밀접한, 신뢰도 높은 정보들로 가득해요. 하지만 이런 정보들이 여러 게시글에 흩어져 있다 보니, 사용자는 정보를 모으기 위해 다양한 키워드로 검색하고 많은 게시글을 일일이 확인해야 하는 불편함이 있어요.이러한 정보 검색의 불편함은 당근의 동네 커뮤니티인 동네생활에서도 마찬가지였어요. 동네생활에는 “과잉 진료 없는 치과”, “탈색 잘하는 미용실”, “마카롱이 맛있는 카페” 등 동네 이웃들만이 알 수 있는 신뢰도 높은 업체 정보들이 가득하죠. 하지만 이런 정보들이 여러 게시글과 댓글에 흩어져 있어서, 사용자는 1) 적절한 검색어를 입력하고, 2) 게시글과 댓글 내용을 모두 확인하고, 3) 얻은 정보를 취합하는 과정을 거쳐야 해요.이 글에서는 이런 불편을 해결하고자 RAG를 활용해 “동네생활 기반 업체 추천” 서비스를 만들었던 과정을 자세히 소개해보고자 해요.당근 동네생활의 검색은 어떻게 개선될 수 있을까요?이 질문에 답하기 위해 먼저 유저들의 동네생활 검색 패턴을 분석했어요. 분석 결과, 유저들은 동네생활에서 주변 업체 정보를 활발하게 찾아보고 있었어요. 특히 “용달”, “24시 동물병원”과 같은 동네 업체 관련 검색어가 실제 상위 검색어들 중 큰 비중을 차지했죠.하지만 기존 검색 시스템으로는 이런 정보를 효율적으로 찾기가 어려웠어요. 기존에는 당근에 업체 관련 검색어를 입력하면, 그와 관련된 당근 등록 업체들을 최상단에 보여줬어요. 예를 들어, “치과”라는 검색어가 입력되면 사용자의 동네에 등록된 치과들을 검색 결과로 보여준 거죠.물론 “치과”라는 검색어에 치과를 보여주는 것이 틀린 검색 결과라고 볼 수는 없지만, 이 방식으로는 유저의 니즈를 제대로 충족하기 어려워요. 유저들은 “우리 동네 주민들이 추천한 알짜배기 업체”들을 발견하기를 원하는데, 현재 검색은 단순히 “관련 업체 프로필”들을 최상위로 노출하는 데 그치고 있거든요. “유저가 정말 원하는 동네업체 정보는 동네생활에 있다”는 점을 고려하면, 현재 검색에는 동네생활과 동네업체 간의 정보들을 서로 연결해 주는 도구가 없는 셈이에요.이런 문제를 해결하기 위해 우리는 동네생활 검색 시스템을 개선하기로 했어요. 유저들이 원하는 업체 관련 정보를 동네생활에서 더 쉽고 빠르게 찾을 수 있도록 돕는 것이 핵심이었죠. 그래서 RAG(Retrieval Augmented Generation) 기술을 활용해 동네생활의 정보와 업체 간의 연결을 가능하게 하는 검색 도구를 만들었어요.RAG란 무엇인가요?RAG는 Retrieval Augmented Generation의 약자로, 기존 데이터베이스에서 관련 정보를 검색(Retrieval)하고, 이를 활용(Augmented)하여 새로운 텍스트를 생성(Generation)하는 기술이에요. 예를 들어, 유저가 “강아지 미용” 관련 검색어를 입력하면, RAG는 동네생활에서 강아지 미용과 관련된 게시글과 댓글들을 찾아내요. 게다가 이 정보들을 종합하여 유저에게 도움이 되는 새로운 요약 정보를 만들어내죠.RAG의 장점은 신뢰할 수 있는 기존 데이터를 기반으로 정보를 생성한다는 점이에요. 단순히 AI가 학습한 일반적인 정보가 아닌, 실제 동네 주민들이 공유한 경험과 추천을 바탕으로 정보를 제공하는 거예요. 따라서 더욱 신뢰성 있고 실용적인 정보를 전달할 수 있어요.동네생활 기반의 업체 추천 검색은 어떻게 이뤄지나요?RAG를 활용한 동네생활 기반의 업체 추천 검색은 크게 세 단계로 진행돼요.첫째, 사용자가 검색어를 입력하면 그와 관련된 업체 정보가 담긴 동네생활 게시글을 검색해요. 둘째, 검색된 게시글에서 추천 업체들을 찾아내고 각 업체에 대한 요약문을 만들어요. 셋째, 추출된 업체들이 실제로 추천하기에 적절한지 검토하고 필터링하는 과정을 거쳐요. 이제 각 단계에서 어떤 일이 일어나는지 자세히 살펴볼게요.Retrieval: 동네생활 게시글 검색하기첫 번째 단계인 Retrieval에서는 사용자의 검색어와 관련된 동네생활 게시글들을 찾아내요. 가장 기본적인 방법으로 키워드 기반 검색을 고려할 수 있지만, 이 방식은 의미상 비슷한 게시글을 찾기 어렵다는 명확한 한계가 있어요. 예를 들어, “혼밥”이라는 검색어가 입력되면 “혼자 가기 좋은 식당”, “1인분 메뉴가 있는 곳” 등 혼자 식사하기 좋은 장소와 관련된 게시글들을 모두 찾아낼 수 있어야 해요. 하지만 단순히 “혼밥”이라는 단어가 포함된 게시글만 검색한다면, “여기 혼자 가기 좋네요”와 같은 관련 게시글은 발견되지 못할 거예요. 이런 키워드 기반 검색의 한계를 해결하기 위해 우리는 벡터 검색을 도입했어요.벡터 검색하기벡터 검색은 ElasticSearch(ES)를 활용했어요. 이는 검색실에서 이미 ES를 사용하고 있었기 때문에, 불필요한 추가 인프라 구축을 피하기 위한 선택이었어요.벡터 검색을 위해서는 먼저 동네생활의 게시글과 댓글, 그리고 게시글 임베딩 벡터를 ES 클러스터에 색인해야 해요. 동네생활 게시글을 OpenAI의 임베딩 API로 벡터로 변환하고, 이를 게시글 및 댓글과 함께 Elasticsearch(ES)에 저장하죠. 검색어가 들어오면 이 검색어도 같은 방식으로 임베딩 벡터로 변환해요. 그런 다음 이 벡터로 ES에 쿼리를 보내면, ES는 저장된 게시글 벡터들과의 코사인 유사도를 계산해 각 동네생활 게시글이 검색어와 얼마나 의미적으로 비슷한지를 측정하고, ANN(Approximated Nearest Neighbor)으로 가장 연관성 높은 게시글들을 빠르게 찾아내요.2. 키워드 매칭 점수를 활용하여 정렬하기하지만 벡터 검색은 코사인 유사도를 기반으로 게시글을 찾다 보니 때로는 관련 없는 문서들이 검색되는 한계가 있었어요. 예를 들어 검색어와 유사한 상위 10개 게시글을 찾으려 할 때 실제로 관련된 게시글이 5개뿐이라면, 나머지 5개는 어쩔 수 없이 관련 없는 문서들로 채워지게 돼요. 머신러닝의 정설인 “Garbage-in, Garbage-out”처럼, 검색어와 관련 없는 문서들이 포함되면서 부정확한 추천 업체들이 무분별하게 등장하는 문제가 생겼어요. 이 문제를 해결하기 위해 벡터 검색된 문서들에 키워드 기반 매칭인 BM25 스코어를 적용하고, 높은 점수를 받은 게시글들을 아래 그림처럼 상단과 하단을 번갈아가며 순차적으로 배치했어요.이는 LLM이 프롬프트의 시작과 끝 부분에 더 집중하는 특성을 활용한 방법인데요. 다음 섹션에서 LLM 프롬프트를 어떻게 구성했는지 구체적으로 살펴볼게요.Augmented Generation: 검색된 게시글 속 업체 정보를 LLM으로 요약하기두 번째 단계인 Augmented Generation에서는 검색된 동네생활 게시글들을 LLM이 이해하기 좋게 가공해서 프롬프트를 만들어요. 그리고 이 프롬프트를 바탕으로 LLM이 추천 업체를 추출하고 각 업체에 대한 요약 정보를 생성하죠.1. 검색된 게시글 가공 및 프롬프트 튜닝하기생성 모델로는 OpenAI의 GPT-4o-mini를 사용했어요. 다양한 프롬프트를 시도해 본 결과, 1) 명확한 단계 구분과 2) 구체적인 예시 포함이 더 좋은 품질의 LLM 생성 결과물을 만든다는 걸 발견했어요. 아래는 최종적으로 사용한 프롬프트 전문이에요.[1. Requirement]제시된 동네생활 게시글 질의응답으로부터, 업체명(name)을 추출하고 업체 설명(summary)을 작성해 줘. 꼭 동네생활 게시글과 댓글을 이용하여 작성해 줘.[2. Tone]동네에 대해 궁금한 걸 물어보면 알려주는 동네 친구처럼 행동해 줘. summary를 부드러운 '해요체'로 작성해 줘. [3. Input]유저 질문: {검색어}동네생활 게시글에서 추출된 질의응답:{LLM이 이해하기 좋은 형태로 가공된 동네생활 게시글들}[4. Output format]- 예시를 참고해서 json으로 뽑아줘. 업체명의 key는 "name", 업체 설명의 key는 "summary"이다. summary는 공백을 포함해 40자가 넘지 않게 작성해 줘.- 만약 'poi_name' 중에 적합한 업체명이 존재하면, 그것을 "name"의 value로 그대로 출력해 줘. None이라면 article과 comment를 사용해서 "name"의 value를 출력해 줘.- e.g., {{"name" : 잎사귀치과, "summary" : 과잉 진료 없이 사랑니를 잘 뽑아주는 치과예요.}}[5. Caution]- 동네생활 게시글과 댓글만을 활용하여 작성해 줘.- 유저 질문에 적합한 내용만 추출해 줘.또, LLM은 3) JSON 형식과 같이 구조화된 게시글을 넣어줬을 때 가장 많은 정보를 효율적으로 이용한다는 사실을 발견했어요. 검색된 게시글은 아래와 같은 형태로 위의 프롬프트에 들어가게 돼요.{ "게시글": { "article": 동네생활 게시글 제목 및 내용, "poi_name": 동네생활 게시글에 등장한 업체 이름 }, "댓글":[ { "content": 댓글 내용, "poi_name": 댓글에 등장한 업체 이름 }, { "content": 댓글 내용, "poi_name": 댓글에 등장한 업체 이름 }, ... ]}2. LLM 응답 안전하게 파싱하기생성된 업체 추천 결과는 “업체명”들과 각 업체의 “정보 요약문”으로 구성돼요. 이렇듯 여러 필드를 가진 정보를 생성할 때 OpenAI의 Structured Output API를 활용하면 JSON과 같은 구조화된 형식으로 결과를 받아볼 수 있어요. 따라서 좀 더 일관된 형태의 생성 결과물을 받아 안전하게 파싱할 수 있죠.from pydantic import BaseModelfrom openai import OpenAIclient = OpenAI()class POI(BaseModel): name: str summary: strclass RecommendedPOI(BaseModel): POI: list[POI] completion = client.beta.chat.completions.parse( model = "gpt-4o-mini", messages = [ {"role": "user", "content": prompt} ], response_format = RecommendedPOI,)Filtering: 부적절한 추천 업체 걸러내기마지막으로, Filtering 단계에서는 LLM이 추천한 업체와 요약 정보의 적절성을 검토해요. RAG를 사용하더라도 생성형 AI가 부정확하거나 부적절한 내용을 만들 수 있기 때문이에요. 이 필터링 과정까지 사람 손을 타지 않도록 완전 자동화하기 위해, 다시 한번 LLM을 활용해 추천 업체의 적절성을 판단했어요. 이 판단 결과로 부적절한 업체 추천들을 걸러내고 신뢰할 수 있는 정보만을 유저에게 제공하죠. 적절성 검토는 다음 세 가지 기준으로 이뤄졌어요.관련성 검증관련성 검증에서는 검색어와 추천된 업체가 실제로 연관이 있는지를 확인해요. 이런 관련성 검증이 중요한 이유는 Retrieval 단계에서 검색된 문서들이 검색어와 관련이 있더라도, LLM이 생성한 업체 추천이 검색 의도와 맞지 않을 수 있기 때문이에요. 예를 들어 “독일어”를 검색했을 때 검색된 문서에 독일 음식점이나 맥주집에 대한 내용이 포함되어 있다면, LLM은 이를 바탕으로 부적절한 추천을 할 수 있어요. 아래는 관련성 검증을 위한 프롬프트의 일부분이에요.[1. 'is_relative' 판단하기]검색어/업체명/업체 설명을 보고, 검색어와 업체가 관련성이 있는지 True/False로 판단해 줘.- True : 검색어와 업체가 관련성이 높다. 검색어에 대해 해당 업체가 노출되는 것이 적합하다.- False : 검색어와 업체의 관련성이 낮다. 검색어에 대해 해당 업체가 노출되는 것이 부적절하다.- {{"검색어":"독일어", "업체명":"신나라", "업체 설명":"주말에 시원한 맥주를 판매하는 곳이에요", "is_relative":False}}2. 추천 업체 일치 여부 검증LLM이 생성한 업체명이 실제 당근에 등록된 업체 상호명과 일치하는지 확인해요. 업체명의 문자 단위 일치 여부를 확인할 수도 있지만, “스타벅스”와 “스타벅스 XX점”, “스타벅스(XX점)”처럼 표기 방법이 조금만 달라도 다른 업체로 판별된다는 문제가 있었어요. 그래서 LLM을 활용해 업체명의 동일성을 판단하기로 했죠. LLM은 사람처럼 문맥을 이해하고 표현의 유사성을 파악할 수 있어서 “스타벅스”와 “스타벅스 XX점”이 같은 업체를 가리킨다는 것을 이해할 수 있거든요. 이를 통해 추천된 업체가 당근에 등록된 업체인지 검증할 수 있어요.[2. 'is_matched_with_poi' 판단하기]- '추천 업체명'/'등록 업체명'를 보고, 서로 동일한 장소인지 True/False로 판단해 줘.- True : 추천 업체명과 등록 업체명이 서로 같은 대상을 나타낸다고 유추할 수 있다. - False : 추천 업체명과 등록 업체명이 서로 다른 대상을 나타낸다. 둘의 연관성이 낮아 보인다.3. 부정적 내용 검증마지막으로 업체에 대한 설명이 부정적이거나 불쾌감을 줄 수 있는 내용을 포함하고 있는지 확인해요. 예를 들어, 업체 설명이 “가격이 너무 비싸고 맛도 없어요”와 같이 부정적인 내용을 담고 있다면 True로 판단하고 필터링해요. 반면 “가성비가 좋고 맛있는 식당이에요”처럼 긍정적인 내용이라면 False로 판단하죠. 이런 검증을 통해 업체에 대한 부정적인 리뷰나 불만 사항이 추천 결과에 포함되지 않도록 관리할 수 있어요.[3. 'is_negative' 판단하기]업체명/업체 설명을 보고, 업체 설명에 부정적인 내용이 있는지 True/False로 판단해 줘.- True : 업체에 대한 부정적인 내용 또는 불쾌한 감정을 포함하고 있다.- False : 업체에 대한 부정적인 내용을 찾을 수 없다.이렇게 만들어진 “동네생활 기반 동네업체 추천”위의 과정을 통해 동네 주민들이 추천한 업체들을 캐로셀 형태로 제공할 수 있게 되었어요. 캐로셀에는 동네생활에서 추천된 업체와 각 업체에 대한 추천 요약문이 표시되며, 탭 한 번으로 바로 업체 프로필로 연결돼요. 덕분에 동네 이웃들이 직접 경험하고 추천한, 신뢰도 높은 업체를 더욱 빠르고 효과적으로 찾을 수 있게 됐어요. 동네생활 게시글과 댓글을 일일이 확인한 후, 발견한 업체를 다시 검색해야 했던 번거로움은 이제 사라지게 된 거예요.마치며사실 이 서비스는 당근의 첫 GenAI 해커톤에서 시작된 프로젝트예요. 현재는 관악구에서만 서비스되고 있지만, 점진적으로 전국 확장을 예정하고 있어서 곧 어디서든 이 기능을 만나보실 수 있을 거예요!해커톤부터 기능 배포까지의 여정에서 팀원들과 함께 새로운 기술을 탐구하고, 이를 실제 서비스로 구현하며 값진 경험을 쌓을 수 있었어요. 특히 RAG 시스템을 실제 서비스에 적용하면서 마주친 여러 도전 과제들을 해결해 나가는 과정이 정말 보람찼고, 이를 통해 중요한 인사이트도 얻을 수 있었죠.첫 번째로, RAG 시스템에서도 정보 검색(Retrieval)의 품질이 핵심이라는 점을 다시 한번 깨달았어요. ES 벡터 검색으로 폭넓은 관련 문서를 찾고, 그중에서도 특히 관련도가 높은 문서를 찾아 적절히 배치하는 것이 전체 시스템의 성능을 좌우했죠. 아무리 좋은 LLM이라도 검색된 문서의 품질이 낮거나 정보가 올바르게 제공되지 않으면 좋은 결과를 기대하기 어려웠어요.두 번째로, 생성형 AI를 활용한 서비스에서는 특히 사람의 세심한 서비스 품질 관리가 중요하다는 점을 깨달았어요. 별도의 LLM을 활용해 검색어와 업체의 관련성, 추천 업체 일치 여부, 부정적 내용 포함 여부 등을 검증했지만, 일관되게 저품질 업체를 추천하는 검색어는 수동으로 제외하는 과정이 필요했어요. LLM 필터링을 활용한 서비스 완전 자동화를 꿈꿨지만, 여전히 사람의 판단이 필요한 부분이 있었던 거죠.이처럼 RAG를 활용한 검색 서비스 개발 과정에서 얻은 인사이트를 바탕으로, 검색실은 앞으로도 더 나은 검색 경험을 제공하기 위해 새로운 기술을 탐구하고 도전해 나갈 거예요. 이런 흥미진진한 여정에 함께하고 싶으시다면 🔗검색실의 문을 두드려주세요! 우리는 항상 새로운 동료를 기다리고 있답니다. :)RAG를 활용한 검색 서비스 만들기 was originally published in 당근 테크 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.
깃허브 코파일럿(Github Copilot)은 IDE에서 사용할 수 있는 AI 페어 프로그래밍 도구입니다. 2021년에 최초로 공개된 비교적 젊은 툴이지만, 이제는 단 한 번이라도 사용해 본 적 없는 개발자를 찾기가 어려울 정도로 프로그래밍 필수 준비물이 되었는데요. 우아한형제들에서는 개발직군 구성원들이 코파일럿을 사용할 수 있도록 유료 구독을 지원하고 있습니다. 처음에는 저도 코드 자동완성 기능만 사용했는데요. 코파일럿에 점점 익숙해지다 보니 어떻게 […] The post 코파일럿 “열일”하게 만드는 방법 first appeared on 우아한형제들 기술블로그.
![[소식] 팀네이버, '중동판 CES' LEAP 2년 연속 참가](https://blogthumb.pstatic.net/MjAyNTAyMDRfMzkg/MDAxNzM4NTk2Mjg0OTQw.e3W1ftYIrsTg0hcAKvWIgWptGjnjgya4gSiHoYe2i9Eg.Cyo_F7zwixs3ahMx0TMWHRRgmo3MwvkB3o8-YunVpwcg.PNG/blog_thumb_2.png?type=s3)
네이버클라우드가 네이버와 함께 '중동판 CES'라 불리는 글로벌 기술 박람회 LEAP 2025 (공식 홈페이지) 에 참가합니다. TEAM NAVER @LEAP 2025 ▸일정 : 2025. 2. 9 ~ 2.12 (4일간) ▸장소 : 리야드 전시 컨벤션 센터 (사우디아라비아) 올해로 4회차를 맞이한 LEAP은 사우디 정보통신기술부(MCIT) 주관 행사로...

JetBrains는 차세대 기술을 구현하고 확장하여 소프트웨어 개발을 보다 생산적이고 즐거운 경험으로 만드는 데 목표를 두고 있습니다. 개발자에게 힘을 실어주고 지원하기 위해 JetBrains는 전문적인 개발을 위한 다양한 제품을 내놓고 있습니다. 여기에는 생산성을 향상하고 창의성에 새로운 지평을 열어주는 강력한 AI 도구와 기능이 포함됩니다. 하지만...
![[프로모션] 비즈니스용 클로바노트와 LG 전자칠판의 만남! 구독 출시 기념 3개월 무료 프로모션](https://blogthumb.pstatic.net/MjAyNTAxMzFfNDYg/MDAxNzM4MzE0NzU4NjY1.yetZySZE4f-LRdD7VgF_bnOPJw4dfiwPdcXaXspTr7og.MJJ6PGA4lxvTvLbHo8Tz3dRrT7Cz0-nqx5DynIEptmwg.PNG/네클_썸네일.png?type=s3)
안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. 회의록 작성에 많은 시간을 쓰고 계시나요? 지난 회의록을 확인하며 아이디어를 정리하고 싶으신가요? AI 음성 기록 서비스 클로바노트와 LG 전자칠판이 만났습니다! 함께 구독하고 3개월 무료 혜택을 만나보세요! 비즈니스용 클로바노트 X LG 전자칠판 3개월 무료 체험 ...

…
![[행사스케치] ✨중소 제조 기업의 성공적인 디지털 전환을 위해! 제조 DX 세미나 현장 다시보기](https://blogthumb.pstatic.net/MjAyNTAxMzFfMjEw/MDAxNzM4MjkwMTg5NDQ3.YaMPYj5dpUnaDzlHIieJhINcy4M-jsQFEU8nttpJ01Qg.eP4teHa9CmRm8h-RoLOxgeX3RJPY1EMGTytSSKhjoRYg.PNG/250131_%C1%A6%C1%B6DX%BC%BC%B9%CC%B3%AA.png?type=s3)
안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. #네이버 #네이버클라우드 #제조DX #스마트공장 지난 1월 23일, 네이버클라우드가 준비한 중소 제조 기업의 효율적인 스마트 공장 전환을 위한 중소기업 제조 DX 세미나가 개최되었습니다! 이번 행사는 제조사와 제조 솔루션사, 유관 기관 관계자 100여 분과 함께 크라...

네이버클라우드 테크 앰버서더 기술 컨퍼런스. 제2회 NAVER Cloud Master Day 두 번째 후기를 공유합니다. - Part 2. 인프라 구축과 클라우드 - 네이버클라우드로의 성공적 전환, 레비뉴 마이그레이션 전략 (최승림 마스터) #RPM_Strategy #ZIA #Rehost AI 개발자와 연구원을 위한 실용적 인프라 구축 방안 (이규석 ...

The post 리디가 추천하는 ‘개성 만점 웹툰’ 3선 appeared first on 리디주식회사 RIDI Corporation.
![[웹툰파헤치기] 세계관의 힘…’영혼 없는 불경자의 밤’](https://ridicorp.com/wp-content/uploads/2025/02/thumbnail-1-940x627.jpg)
The post [웹툰파헤치기] 세계관의 힘…’영혼 없는 불경자의 밤’ appeared first on 리디주식회사 RIDI Corporation.

안녕하세요. 커뮤니케이션 앱 LINE의 모바일 클라이언트를 개발하고 있는 Ishikawa입니다. 저희 회사는 높은 개발 생산성을 유지하기 위해 코드 품질 및 개발 문화 개선에 힘쓰...
![[툰설툰설] 설맞이 가족애 느끼기…목린 VS 세상만 구하고 은퇴하겠습니다](https://ridicorp.com/wp-content/uploads/2025/01/thumbnail-2-940x627.jpg)
The post [툰설툰설] 설맞이 가족애 느끼기…목린 VS 세상만 구하고 은퇴하겠습니다 appeared first on 리디주식회사 RIDI Corporation.
![[프로모션] 2025년 3월 선불업 등록 유예 기간 종료 <네이버클라우드 선불업 등록 올인원 패키지>로 지금 바로 막차 탑승!](https://blogthumb.pstatic.net/MjAyNTAxMjRfMzUg/MDAxNzM3Njk3MDU5Nzkz.czhWsde9ldQeIceezpm6ijq28df1zI5EXVxlrolGIBAg.uvyARXKC0kJZ-XUWQib-rFzBj-Zg3cpa_g46oNXj4qcg.PNG/250124_선불업썸네일.png?type=s3)
안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. ✨ 네이버클라우드 선불업 등록 올인원 패키지 요약 ✨ 전자금융거래법 개정안이 2024년 9월 시행되면서, 포인트/상품권 등 선불전자지급수단을 발행하는 기업은 2025년 3월까지 등록을 마쳐야 합니다. 의무 등록 유예 기간 6개월이 종료되는 2025년 3월까지 선불업 ...
![[술술 읽히는 업무 해설집 - 근태편] 연차 촉진, 어떻게 해야 할까요?](https://blogthumb.pstatic.net/MjAyNTAxMjNfMjM4/MDAxNzM3NjEwMTQxMjk1.d0rwQh4XVlXgj-zhFm_QXa3lDDWIrmMZ6uxS5povqycg.v461Ws_F6ultu7XIez7Mz7mymqIdrSPExE7aKHUWaeQg.PNG/0_NB.png?type=s3)
안녕하세요, 협업과 소통을 위한 필수 기능으로 글로벌 53만 기업의 든든한 협업툴 역할을 해온 네이버웍스(NAVER WORKS)입니다! "업무와 관련된 것이라면 뭐든지 쉽게 풀어드립니다!" 술술 읽히는 업무 해설집 근로자가 부여받은 연차를 모두 사용하지 않으면, 회사에서는 필수로 연차 수당을 지급해야 할까요? 이는 회사가 ‘연차휴가 사용 촉진 제도’를...
2023년 프로덕트 디자이너 인턴십 후기를 들려드릴게요
.png)
사업개발, 영업으로 시작해서 풀스택, 프론트엔드, 서버 개발까지 다 해보신 지민님의 이야기를 들려드립니다. 끝 없는 도전과 변화, 지민님은 모두 계획했을까요?

코믹, 스포츠, 추리 등 장르별 추천 만화와 설 연휴 순삭! The post 리디, 설 연휴에 정주행할 애니메이션 원작 만화 추천 appeared first on 리디주식회사 RIDI Corporation.
![[캘린더️] 2월 무료 교육 웨비나 일정 모음](https://blogthumb.pstatic.net/MjAyNTAxMTRfMTU2/MDAxNzM2ODM0MDQ0MjA2.le7OBlL_DhCvXgNzLb90AG6gLQN-5GrlkZGuBg8VTyAg.gr_u4pv8JPe9Y13O3djkfiTMBOOgeG6ytpV0JP1p8GIg.PNG/2502_edu_thumb.png?type=s3)
안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다.

generated by DALL·E안녕하세요, 29CM 모바일팀의 iOS 개발자 김우성입니다. 이번 글에서는 SwiftLint 와 관련된 개선 작업을 통해 팀의 생산성을 향상시키고자 했던 내용을 다뤄보려고 합니다.iOS 팀에서는 대부분 SwiftLint 를 사용하실 텐데요, 저희 팀에선 모듈화를 해나가는 과정에서 SwiftLint 로 인해 증분 빌드 ...