고객을 향한 JetBrains의 약속 JetBrains는 지금까지 그래왔듯이 YouTrack을 고객의 요구 사항에 따라 발전하는 플랫폼으로 만들기 위해 최선을 다하고 있습니다. 이러한 노력의 일환으로 YouTrack Server와 Cloud 두 버전을 계속 제공하여 고객의 조직 상황과 데이터 거버넌스 정책에 가장 적합한 호스팅 옵션을 자유롭게 선택할 ...
기술 블로그 모음
국내 IT 기업들의 기술 블로그 글을 한 곳에서 모아보세요
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, 이형중입니다. 저희 조직은 게임 퍼블리싱에 필요한 다양한 ...
안녕하세요, 협업과 소통을 위한 필수 기능으로 글로벌 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 2025 (공식 홈페이지) 에 참가합니다. TEAM NAVER @LEAP 2025 ▸일정 : 2025. 2. 9 ~ 2.12 (4일간) ▸장소 : 리야드 전시 컨벤션 센터 (사우디아라비아) 올해로 4회차를 맞이한 LEAP은 사우디 정보통신기술부(MCIT) 주관 행사로...
JetBrains는 차세대 기술을 구현하고 확장하여 소프트웨어 개발을 보다 생산적이고 즐거운 경험으로 만드는 데 목표를 두고 있습니다. 개발자에게 힘을 실어주고 지원하기 위해 JetBrains는 전문적인 개발을 위한 다양한 제품을 내놓고 있습니다. 여기에는 생산성을 향상하고 창의성에 새로운 지평을 열어주는 강력한 AI 도구와 기능이 포함됩니다. 하지만...
안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. 회의록 작성에 많은 시간을 쓰고 계시나요? 지난 회의록을 확인하며 아이디어를 정리하고 싶으신가요? AI 음성 기록 서비스 클로바노트와 LG 전자칠판이 만났습니다! 함께 구독하고 3개월 무료 혜택을 만나보세요! 비즈니스용 클로바노트 X LG 전자칠판 3개월 무료 체험 ...
…
안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 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.
The post [웹툰파헤치기] 세계관의 힘…’영혼 없는 불경자의 밤’ appeared first on 리디주식회사 RIDI Corporation.
안녕하세요. 커뮤니케이션 앱 LINE의 모바일 클라이언트를 개발하고 있는 Ishikawa입니다. 저희 회사는 높은 개발 생산성을 유지하기 위해 코드 품질 및 개발 문화 개선에 힘쓰...
The post [툰설툰설] 설맞이 가족애 느끼기…목린 VS 세상만 구하고 은퇴하겠습니다 appeared first on 리디주식회사 RIDI Corporation.
안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. ✨ 네이버클라우드 선불업 등록 올인원 패키지 요약 ✨ 전자금융거래법 개정안이 2024년 9월 시행되면서, 포인트/상품권 등 선불전자지급수단을 발행하는 기업은 2025년 3월까지 등록을 마쳐야 합니다. 의무 등록 유예 기간 6개월이 종료되는 2025년 3월까지 선불업 ...
안녕하세요, 협업과 소통을 위한 필수 기능으로 글로벌 53만 기업의 든든한 협업툴 역할을 해온 네이버웍스(NAVER WORKS)입니다! "업무와 관련된 것이라면 뭐든지 쉽게 풀어드립니다!" 술술 읽히는 업무 해설집 근로자가 부여받은 연차를 모두 사용하지 않으면, 회사에서는 필수로 연차 수당을 지급해야 할까요? 이는 회사가 ‘연차휴가 사용 촉진 제도’를...
2023년 프로덕트 디자이너 인턴십 후기를 들려드릴게요
사업개발, 영업으로 시작해서 풀스택, 프론트엔드, 서버 개발까지 다 해보신 지민님의 이야기를 들려드립니다. 끝 없는 도전과 변화, 지민님은 모두 계획했을까요?
코믹, 스포츠, 추리 등 장르별 추천 만화와 설 연휴 순삭! The post 리디, 설 연휴에 정주행할 애니메이션 원작 만화 추천 appeared first on 리디주식회사 RIDI Corporation.
안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다.
generated by DALL·E안녕하세요, 29CM 모바일팀의 iOS 개발자 김우성입니다. 이번 글에서는 SwiftLint 와 관련된 개선 작업을 통해 팀의 생산성을 향상시키고자 했던 내용을 다뤄보려고 합니다.iOS 팀에서는 대부분 SwiftLint 를 사용하실 텐데요, 저희 팀에선 모듈화를 해나가는 과정에서 SwiftLint 로 인해 증분 빌드 ...
안녕하세요. 커뮤니케이션 앱 LINE의 모바일 클라이언트를 개발하고 있는 Ishikawa입니다. 저희 회사는 높은 개발 생산성을 유지하기 위해 코드 품질 및 개발 문화 개선에 힘쓰...
안녕하세요. 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. 공공기관의 서비스가 디지털화되고 점점 고도화 됨에 따라, 공공기관에서도 사용자 행동 분석 도구의 활용은 엔터프라이즈 기업과 동일하게 선택이 아닌 필수가 되어가고 있습니다. 사용자 데이터 분석을 어디서부터 시작해야 할지 막막하다면, 대안이 없어 외산 분석 솔루션을 사용...
DevPlay 계정에 대해 알아보고 이를 통해 얻을 수 있는 다양한 혜택을 알아봅니다.
안녕하세요 네이버 MYBOX 팀입니다. MYBOX 서비스가 24년 12월 MYBOX+ (유료 사용자) 200만명을 돌파했습니다. MYBOX+ 200만 명을 기념하여, 또 그동안 많은 사랑을 주신 고객님들께 감사한 마음을 담아 이벤트를 준비했답니다. MYBOX가 그동안 걸어온 길, 그리고 재미있는 데이터도 확인하시고, 200만 기념으로 특별 제작한 굿즈...
안녕하세요, 협업과 소통을 위한 필수 기능으로 글로벌 53만 기업의 든든한 협업툴 역할을 해온 네이버웍스(NAVER WORKS)입니다! 회의 일정을 잡을 때 마다 회의 참석자들이 모두 가능한 요일과 시간대를 일일이 물어보고, 회의 장소를 찾는데 많은 시간을 쓰고 계시나요? 네이버웍스 캘린더에서 참석자에게 묻지 않고, 장소를 찾지 않고도 회의 일정을 빠...