<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Infra on 얼쑤 · Team Edo</title><link>https://team-edo.github.io/categories/infra/</link><description>Recent content in Infra on 얼쑤 · Team Edo</description><generator>Hugo</generator><language>ko-kr</language><lastBuildDate>Mon, 09 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://team-edo.github.io/categories/infra/index.xml" rel="self" type="application/rss+xml"/><item><title>MySQL 복제가 끊기면 원본 쓰기를 막아야 할까?</title><link>https://team-edo.github.io/posts/2026/11/mysql-replication-write-policy/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0000</pubDate><guid>https://team-edo.github.io/posts/2026/11/mysql-replication-write-policy/</guid><description>&lt;p>운영 DB의 시간당 백업을 대기 서버로 보내고 있었다. 최근 24시간의 수신 기록에서 대기 서버가 가진 최신 스냅샷의 나이는 최대 &lt;strong>1시간 29초&lt;/strong>였다. 그때 원본 서버를 잃었다면 스냅샷 이후에 저장한 기록은 백업에서 복구할 수 없었다. 전송이 밀리면 이 구간은 더 길어질 수 있다.&lt;/p>
&lt;p>대기 서버에 MySQL 복제본을 두면 백업 사이의 변경도 따라갈 수 있다. 그러자 다른 질문이 생겼다. &lt;strong>두 서버 사이 연결만 끊겼을 때, 아직 정상인 원본은 계속 저장을 받아야 할까?&lt;/strong>&lt;/p></description></item><item><title>백엔드 무중단 배포</title><link>https://team-edo.github.io/posts/2026/11/docker-compose-blue-green-drain/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0000</pubDate><guid>https://team-edo.github.io/posts/2026/11/docker-compose-blue-green-drain/</guid><description>&lt;p>백엔드 컨테이너 하나를 새 버전으로 교체하면, 이전 컨테이너가 내려간 시점과 새 컨테이너가 요청을 받을 수 있는 시점 사이에 공백이 생긴다. 이 공백을 없애려면 순서를 바꿔야 한다. &lt;strong>이전 버전이 요청을 받는 동안 새 버전을 준비하고, 연결을 넘긴 뒤에도 이미 받은 요청과 작업을 마칠 때까지 이전 버전을 살려 둔다.&lt;/strong> 얼쑤는 기존 Docker Compose와 Nginx에 이 순서를 적용했다.&lt;/p>
&lt;h2 id="같은-호스트에-백엔드-슬롯-두-개를-뒀다">같은 호스트에 백엔드 슬롯 두 개를 뒀다&lt;/h2>
&lt;p>슬롯은 백엔드 한 인스턴스를 실행하는 자리다. Compose에 블루와 그린을 정의하고, 두 슬롯이 같은 런타임 설정을 쓰도록 했다. 배포할 때 비활성 슬롯에 새 버전의 JAR을 붙인다. 두 슬롯은 같은 MySQL을 사용한다. 사용자는 평소와 같은 HTTPS 주소로 접속하고, Nginx가 활성 슬롯으로 요청을 보낸다.&lt;/p></description></item><item><title>컨테이너에서만 타임아웃 난 MySQL SSH 터널 추적기</title><link>https://team-edo.github.io/posts/2026/11/ssh-tunnel-container-mysql-timeout/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0000</pubDate><guid>https://team-edo.github.io/posts/2026/11/ssh-tunnel-container-mysql-timeout/</guid><description>&lt;p>MySQL 복제를 준비하면서 원본 DB에 닿는 경로를 SSH 터널로 제한했다. 원본 서버의 전달 포트는 루프백에만 열었다. 대기 서버에서는 SSH 클라이언트가 전용 브리지의 호스트 주소에 포트를 열어 복제 컨테이너의 연결을 받도록 했다.&lt;/p>
&lt;p>대기 서버 호스트에서 터널 포트에 접속하자 MySQL이 첫 응답을 보냈다. 같은 포트에 &lt;strong>복제 컨테이너에서&lt;/strong> 접속하자 &lt;code>ERROR 2003&lt;/code>과 함께 연결 시간이 초과됐다.&lt;/p>
&lt;h2 id="응답이-끊긴-구간-찾기">응답이 끊긴 구간 찾기&lt;/h2>
&lt;p>연결 경로를 세 위치에서 비교했다.&lt;/p></description></item><item><title>APK는 받을 수 있는데 QA 목록 제목은 왜 비었을까</title><link>https://team-edo.github.io/posts/2026/11/apk-metadata-batch/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/apk-metadata-batch/</guid><description>&lt;p>얼쑤의 Android QA 페이지에서는 테스트할 앱 설치 파일인 APK를 고를 수 있습니다. 목록에는 파일을 만든 빌드와 연결된 변경 요청(MR)의 제목과 작성자가 함께 나옵니다. 제목이 비어 있어도 보관된 파일은 내려받을 수 있었지만, 테스터는 어떤 변경이 들어간 빌드인지 목록에서 확인하기 어려웠습니다. 11월 9일 첫 점검에서는 목록에 연결된 MR 136개 중 37개의 상세 정보가 비어 있었습니다.&lt;/p>
&lt;h2 id="설치-파일과-설명이-오는-길">설치 파일과 설명이 오는 길&lt;/h2>
&lt;p>Jenkins는 빌드한 APK를 보관하고 어떤 변경 요청과 연결되는지 목록에 남깁니다. 별도의 동기화 작업은 이 목록에 있는 MR 번호로 GitLab을 조회해 제목과 작성자를 채웁니다. APK는 이미 보관된 파일이어서 이 조회가 실패해도 다운로드할 수 있습니다. 빈 제목을 고치려면 APK를 다시 만드는 대신, 설명을 채우는 작업을 확인해야 했습니다.&lt;/p></description></item><item><title>Docker 로그 6종 중 4종만 파일에 남은 이유</title><link>https://team-edo.github.io/posts/2026/11/docker-log-delivery/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/docker-log-delivery/</guid><description>&lt;p>11월 9일 얼쑤의 운영 구성을 정리하면서, 나중에 문제가 생기면 각 프로그램의 기록을 어디에서 읽을 수 있는지 확인했습니다. Docker에는 로그 여섯 종류를 보내도록 설정돼 있었습니다. 그런데 점검일의 저장 폴더에서 찾을 수 있는 건 네 종류뿐이었습니다. 빠진 두 종류는 프로그램이 실행 중이었는데도 사후에 기록을 읽을 곳이 없었습니다.&lt;/p>
&lt;h2 id="로그가-지나가는-세-곳을-확인했습니다">로그가 지나가는 세 곳을 확인했습니다&lt;/h2>
&lt;p>컨테이너에서 실행되는 프로그램은 일을 하다가 필요한 내용을 출력합니다. Docker의 &lt;strong>로그 드라이버&lt;/strong>는 그 출력을 어디로 보낼지 정합니다. 얼쑤에서 확인한 여섯 프로그램은 모두 &lt;code>syslog&lt;/code> 방식으로 로그를 보냅니다. 같은 서버에서 기록을 받는 &lt;code>rsyslog&lt;/code>는 태그(출처 이름표)를 보고 어떤 기록을 파일로 남길지 결정합니다.&lt;/p></description></item><item><title>Docker 용량 조회 하나가 점검표의 네 칸을 비웠다</title><link>https://team-edo.github.io/posts/2026/11/docker-disk-stats-timeout/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/docker-disk-stats-timeout/</guid><description>&lt;p>얼쑤 운영 점검에서 Docker가 응답하는데도 채팅 점검표의 Docker 상태와 저장공간이 ‘확인 불가’로 나왔습니다. 수집 순서와 코드가 오류를 처리하는 범위를 확인하니, 이미 읽은 값들이 마지막 조회의 시간 초과에 함께 지워지고 있었습니다.&lt;/p>
&lt;h2 id="저장공간에-대한-두-질문">저장공간에 대한 두 질문&lt;/h2>
&lt;p>저장공간에는 두 종류의 숫자가 있었습니다. &lt;strong>사용 중인 공간&lt;/strong>은 이미지, 컨테이너에 새로 쓴 파일, 볼륨, 빌드 캐시 등이 지금 차지한 양입니다. &lt;strong>정리 가능 공간&lt;/strong>은 Docker가 정리하면 확보할 수 있다고 보고한 양입니다. 서버 디스크 전체에 지금 남은 공간은 파일시스템에서 확인하는 별도 숫자입니다.&lt;/p></description></item><item><title>서버 두 대로 어디까지 장애를 견딜 수 있을까</title><link>https://team-edo.github.io/posts/2026/11/two-host-availability/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/two-host-availability/</guid><description>&lt;p>운영 서버 한 대가 꺼졌다고 가정해 보겠습니다. 두 번째 서버에서 앱이 떠 있어도 사용자가 쓰던 주소가 그 서버로 향하고, 앱이 기존 데이터를 읽고 새 데이터를 저장할 수 있어야 서비스가 돌아옵니다. 얼쑤에서는 서버 두 대를 유지하고 추가 유료 인프라를 전제하지 않는 조건으로 Docker Compose를 확장하는 안과 가벼운 Kubernetes 배포판인 K3s를 쓰는 안을 비교했습니다. 비교 기준은 각 도구가 앱 실행, 접속, 쓰기 복구 중 어디까지 맡을 수 있는가였습니다.&lt;/p></description></item><item><title>읽기 전용 컨테이너에서 Gunicorn이 기동 오류를 남긴 이유</title><link>https://team-edo.github.io/posts/2026/11/gunicorn-control-socket-read-only-rootfs/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/gunicorn-control-socket-read-only-rootfs/</guid><description>&lt;p>얼쑤 기술 블로그의 편집 API를 점검하던 중 Gunicorn 기동 로그에서 같은 오류가 반복되는 것을 발견했습니다. 편집 설정과 조회수 API는 계속 HTTP 200을 돌려줬는데, 컨테이너를 시작할 때마다 &lt;code>/.gunicorn&lt;/code>을 만들 수 없다는 메시지가 한 번씩 남았습니다. Gunicorn이 시작하면서 어떤 작업에 실패했는지부터 살펴봤습니다.&lt;/p>
&lt;h2 id="요청은-처리됐는데-어떤-소켓이-실패했을까">요청은 처리됐는데 어떤 소켓이 실패했을까&lt;/h2>
&lt;p>Gunicorn은 HTTP 요청을 받는 리스너 외에 프로세스 관리를 위한 Unix 소켓을 엽니다. &lt;code>gunicornc&lt;/code>는 이 제어 소켓으로 워커 상태를 조회하거나 재시작을 요청할 수 있습니다. 소켓 경로를 지정하지 않으면 Gunicorn은 &lt;code>XDG_RUNTIME_DIR&lt;/code>을 우선 사용하고, 그 값이 없으면 홈 아래의 &lt;code>.gunicorn&lt;/code> 디렉터리를 사용합니다. &lt;a href="https://gunicorn.org/reference/settings/#control_socket">Gunicorn 제어 소켓 설정&lt;/a>&lt;/p></description></item><item><title>팀 채팅에서 서버 상태만 읽게 한 방법</title><link>https://team-edo.github.io/posts/2026/11/mattermost-readonly-server-checks/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/mattermost-readonly-server-checks/</guid><description>&lt;p>새 버전을 올린 뒤 서비스가 요청을 받을 준비가 됐는지 확인하고 싶을 때가 있습니다. 얼쑤에서는 팀 채팅에서 정해진 명령을 실행하면 서버가 답한 시각과 점검 결과를 표로 받을 수 있게 했습니다. 채팅 명령이 서버까지 이어지는 만큼, 어떤 입력을 받을지와 서버에서 무엇을 실행할지를 각각 정했습니다.&lt;/p>
&lt;h2 id="채팅-명령을-세-가지-질문으로-나누기">채팅 명령을 세 가지 질문으로 나누기&lt;/h2>
&lt;p>두 서버에 각각 서버 자원, 컨테이너, 준비 상태를 보는 세 가지 명령을 두었습니다. 디스크가 얼마나 남았는지와 특정 앱이 요청을 받을 수 있는지는 다른 곳에서 읽어야 합니다.&lt;/p></description></item><item><title>폐기용 MySQL을 실행 단위로 분리한 이유</title><link>https://team-edo.github.io/posts/2026/11/isolate-disposable-mysql-tests/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/isolate-disposable-mysql-tests/</guid><description>&lt;p>MySQL 통합 테스트는 실제 스키마를 만들고 데이터를 씁니다. 같은 데이터베이스와 계정을 다음 실행에 재사용하면 앞선 실행이 남긴 데이터가 새 결과에 영향을 줄 수 있습니다. 얼쑤에서는 실행마다 테스트 DB와 전용 계정을 만들고, 다른 DB에서 쓰는 접속 정보와 분리했습니다. 서버의 기동 조건이나 연결 한도를 시험할 때는 MySQL 인스턴스도 따로 띄웠습니다.&lt;/p>
&lt;h2 id="어디까지-분리할까">어디까지 분리할까&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>시험에서 바꾸는 것&lt;/th>
 &lt;th>실행마다 분리할 대상&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>테이블, 테스트 데이터, 스키마 변경&lt;/td>
 &lt;td>데이터베이스와 해당 DB에만 접근하는 전용 계정&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>기동 옵션, 최대 연결 수&lt;/td>
 &lt;td>MySQL 인스턴스, 데이터 폴더, 접속 주소&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Gradle의 통합 테스트에는 &lt;code>TEST_DB_URL&lt;/code>, &lt;code>TEST_DB_USERNAME&lt;/code>, &lt;code>TEST_DB_PASSWORD&lt;/code>를 그 실행에만 전달했습니다. 새 계정은 시험에 필요한 권한을 받고, 데이터 쓰기 권한은 해당 DB에 한정했습니다. 실패한 실행도 정리할 수 있게 종료 절차를 테스트의 성공 여부와 분리했습니다.&lt;/p></description></item><item><title>Flyway 선실행과 배포 롤백</title><link>https://team-edo.github.io/posts/2026/11/flyway-deploy-rollback/</link><pubDate>Fri, 06 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/flyway-deploy-rollback/</guid><description>&lt;p>새 버전이 배포된 뒤 readiness는 &lt;code>UP&lt;/code>이었는데, 모범 음원 조회는 HTTP 500을 반환했습니다. 새 앱은 음원의 저장소 객체 키를 읽으려 했고, 운영 DB에는 그 컬럼이 아직 없었습니다. 실행 중인 앱을 그대로 두고 밀린 마이그레이션을 적용하자 같은 요청은 게시된 음원이 없을 때의 응답인 404로 바뀌었습니다. DB 연결이 정상이어도 새 쿼리에 필요한 컬럼이 준비됐는지는 별도로 확인해야 했습니다.&lt;/p>
&lt;p>당시 앱 배포는 자동이었고 Flyway 적용은 수동이었습니다. 앱 교체와 스키마 변경의 순서를 사람이 맞춰야 했습니다. 이 배포 흐름을 바꾸는 작업에 참여하면서, 새 앱이 필요한 스키마를 먼저 준비하고 이전 앱도 그 스키마에서 계속 동작할 수 있어야 한다는 두 가지 조건을 함께 다뤘습니다.&lt;/p></description></item><item><title>백엔드는 자동, 웹은 검증한 SHA로 배포</title><link>https://team-edo.github.io/posts/2026/11/split-manual-web-deployment/</link><pubDate>Fri, 06 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/split-manual-web-deployment/</guid><description>&lt;p>얼쑤의 Jenkins는 개발 브랜치 변경을 감지하면 백엔드를 검사하고 배포했습니다. 뒤이어 웹 잡도 자동으로 예약되는 구조여서, 백엔드 변경을 운영에 올릴 때도 별도의 웹 빌드가 시작됐습니다. 웹 배포 시점을 따로 정할 수 있게 트리거를 나누되, 운영 웹이 어떤 소스에서 나왔는지는 계속 확인할 수 있어야 했습니다.&lt;/p>
&lt;h2 id="수동으로-실행해도-소스를-지정하게-했다">수동 배포에서 소스 SHA 지정&lt;/h2>
&lt;p>백엔드의 자동 배포는 두고, 웹은 트리거가 없는 전용 잡으로 옮겼습니다. 운영자는 성공한 백엔드 빌드에서 검증한 전체 커밋 SHA를 웹 잡에 입력합니다. 웹 잡은 체크아웃 결과가 입력과 같고 해당 SHA가 개발 브랜치 이력에 포함되는지 확인한 뒤 빌드를 시작합니다. 기존 웹 잡은 비활성화해 이력 조회용으로 남겼습니다.&lt;/p></description></item><item><title>테스트한 JAR을 이미지에 그대로 넣기</title><link>https://team-edo.github.io/posts/2026/11/reuse-verified-jar-in-image/</link><pubDate>Fri, 06 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/reuse-verified-jar-in-image/</guid><description>&lt;p>얼쑤의 백엔드 CI는 Jenkins에서 Gradle로 검사하고 실행 JAR을 만든 뒤, Docker로 배포 후보 이미지를 빌드했습니다. 오래 걸린 빌드의 로그를 보니 Dockerfile이 소스와 Gradle Wrapper를 받아 &lt;code>bootJar&lt;/code>를 한 번 더 실행하고 있었습니다. Docker 레이어 캐시가 적중하지 않은 한 실행에서 이미지 안의 재빌드만 1분 51초가 걸렸습니다.&lt;/p>
&lt;h2 id="실행할-jar을-먼저-확정했다">실행할 JAR 먼저 확정&lt;/h2>
&lt;p>배포 후보 이미지의 역할을 확인해 보니, 배포 도구는 후보를 띄워 확인한 뒤 그 안에서 JAR을 꺼내 운영 실행 이미지에 연결하고 있었습니다. 후보 안에서 JAR을 다시 만들어야 할 이유가 없어, Jenkins가 이미 검증한 실행 JAR을 이미지에 넣도록 바꿨습니다.&lt;/p></description></item><item><title>Jenkins Wasm 최적화 956초에서 237초로</title><link>https://team-edo.github.io/posts/2026/11/jenkins-wasm-build-bottleneck/</link><pubDate>Wed, 04 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/jenkins-wasm-build-bottleneck/</guid><description>&lt;p>얼쑤의 Jenkins 빌드가 길어졌을 때, 저는 완료 화면의 전체 시간부터 단계별로 쪼개 봤습니다. 백엔드 빌드에서는 시작 기록이 남은 뒤 첫 단계까지 14분 넘게 걸렸고, 별도의 웹 빌드에서는 실제로 실행기에 올라간 뒤 Wasm 최적화 도중 시간 제한에 걸렸습니다. 두 경우 모두 빌드가 오래 걸렸지만, 손봐야 할 곳은 달랐습니다.&lt;/p>
&lt;h2 id="첫-단계가-시작되기까지-얼마나-걸렸나">첫 단계까지의 대기 시간&lt;/h2>
&lt;p>빌드 기록의 시작·종료 시각을 Jenkins의 단계별 시작·종료 시각과 맞춰 봤습니다. 빌드 시작부터 첫 단계까지, 첫 단계부터 마지막 단계가 끝날 때까지, 마지막 단계 이후로 나누면 어느 구간이 길었는지 보입니다.&lt;/p></description></item><item><title>파일 교체 뒤에도 남은 컨테이너 설정</title><link>https://team-edo.github.io/posts/2026/11/reload-file-bind-mount-in-place/</link><pubDate>Wed, 04 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/reload-file-bind-mount-in-place/</guid><description>&lt;p>운영 Nginx 설정에 압축 옵션을 반영할 때, 설정 파일을 읽는 쪽이 어디인지부터 확인했습니다. Nginx 컨테이너는 호스트의 설정 디렉터리 전체 대신 설정 파일 하나를 읽기 전용으로 연결하고 있었습니다. 호스트에서 경로가 같은 파일을 새로 만들어 넣어도, 실행 중인 컨테이너는 이전 파일을 계속 볼 수 있는 구성입니다.&lt;/p>
&lt;h2 id="이름을-유지하는-것과-파일을-유지하는-것">같은 이름과 같은 파일의 차이&lt;/h2>
&lt;p>Linux에서 경로는 파일을 찾는 이름입니다. 파일 하나를 bind mount하면 마운트는 그때 찾은 파일의 실체에 연결됩니다. 임시 파일을 만든 뒤 &lt;code>rename&lt;/code>으로 기존 이름을 교체하면 호스트에서 같은 경로를 열 때 새 파일을 읽습니다. 컨테이너가 이미 연결한 파일은 그대로 남습니다. 반대로 기존 파일을 열어 내용을 덮어쓰면 inode를 유지하므로 컨테이너에서도 같은 파일의 바뀐 내용을 읽습니다.&lt;/p></description></item><item><title>Flyway 체크섬의 줄바꿈과 공백</title><link>https://team-edo.github.io/posts/2026/11/flyway-line-checksum/</link><pubDate>Tue, 03 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/flyway-line-checksum/</guid><description>&lt;p>앱을 교체하기 전에 후보 JAR의 SQL이 운영 DB에 적용된 마이그레이션과 같은지 확인하고 싶었습니다. 적용할 마이그레이션이 없으면 배포 도구는 Flyway를 실행하지 않습니다. 그때도 이미 적용한 SQL이 바뀌었는지 가려내려면 JAR의 파일에서 Flyway 이력과 같은 방식으로 체크섬을 계산해야 했습니다.&lt;/p>
&lt;h2 id="sql을-읽는-방식까지-맞췄다">SQL 읽기 방식 맞추기&lt;/h2>
&lt;p>프로젝트에서 사용한 Flyway는 파일을 UTF-8로 읽고, 첫 줄의 BOM과 각 줄의 줄바꿈 문자를 제외한 바이트를 순서대로 CRC32에 넣었습니다. 그래서 LF를 CRLF로 바꾸거나 마지막 줄바꿈을 빼도 체크섬이 같습니다. 줄 끝의 공백은 SQL을 읽는 줄에 그대로 남아 계산에 들어갑니다.&lt;/p></description></item><item><title>Play 배포판의 App Links가 빠진 이유</title><link>https://team-edo.github.io/posts/2026/11/android-app-links-signing/</link><pubDate>Tue, 03 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/android-app-links-signing/</guid><description>&lt;p>얼쑤에서 Android 소셜 로그인은 외부 브라우저를 열고, 인증 뒤 App Links로 앱에 돌아오는 흐름을 사용했습니다. Play에서 설치한 일반 앱이 브라우저에서 돌아오지 못해 운영 서버의 &lt;code>assetlinks.json&lt;/code>을 읽었습니다. 서버는 기동 상태가 정상이었고, 연결 문서에는 QA 패키지 한 항목만 있었습니다. 우선 Play 설치판에 해당하는 연결 항목을 복구했습니다.&lt;/p>
&lt;h2 id="설치한-앱에-찍힌-서명이-필요했다">Play 앱 서명 지문 확인&lt;/h2>
&lt;p>App Links의 연결 문서에는 패키지 이름과 서명 인증서 지문이 함께 들어갑니다. 직접 빌드해 설치한 앱과 Play가 배포한 앱의 서명이 다르면, 한 쪽을 확인한 지문으로 다른 쪽까지 연결됐다고 볼 수 없습니다. Play에서 받은 설치본에 해당하는 지문은 Play Console의 앱 서명 정보에 나옵니다.&lt;/p></description></item><item><title>백업 도구를 바꿔도 남은 오류</title><link>https://team-edo.github.io/posts/2026/11/diagnose-backup-status-by-host-role/</link><pubDate>Tue, 03 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/diagnose-backup-status-by-host-role/</guid><description>&lt;p>DB를 내보내는 서버와 백업을 받는 서버가 분리된 환경에서 백업 도구를 교체했습니다. 교체 직후 보내는 서버에서 새 버전의 &lt;code>status&lt;/code>를 실행했더니 &lt;code>no completed backup: db&lt;/code>가 나왔습니다. 설치 직후라 도구의 변경을 먼저 의심할 만했지만, 이 메시지가 어느 서버의 어떤 저장소를 조회한 결과인지 확인해야 했습니다.&lt;/p>
&lt;h2 id="같은-호스트에서-버전만-바꿔-봤다">같은 호스트에서 버전만 비교&lt;/h2>
&lt;p>교체 전에 보관한 이전 버전으로 같은 서버에서 같은 &lt;code>status&lt;/code> 명령을 실행했습니다. 이전 버전도 동일하게 실패했습니다. 두 버전이 같은 위치에서 같은 결과를 냈으므로, 우선 두 버전이 공통으로 읽는 경로를 확인했습니다. 이 비교에서는 백업을 새로 보내거나 기존 데이터를 바꾸는 명령 대신, 현재 상태만 읽는 명령을 사용했습니다.&lt;/p></description></item><item><title>업로드 재시도에서 기존 파일 지키기</title><link>https://team-edo.github.io/posts/2026/11/retry-safe-object-upload-conditional-put/</link><pubDate>Tue, 03 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/retry-safe-object-upload-conditional-put/</guid><description>&lt;p>팀에서 음원 파일이나 폴더를 R2에 올리는 공용 명령을 만들었습니다. 폴더를 올리다 중간에 연결이 끊기면 같은 명령을 다시 실행하고 싶습니다. 그런데 같은 키에 일반 PUT을 보내면, 이미 올라간 파일이 지금 로컬에 있는 내용으로 바뀔 수 있습니다. 기본 실행은 빈 키만 채우고, 기존 파일을 바꿀 때는 교체 옵션을 명시하도록 했습니다.&lt;/p>
&lt;h2 id="쓰기-요청에-빈-키라는-조건을-넣었다">비어 있는 키에만 쓰기&lt;/h2>
&lt;p>먼저 HEAD로 키가 비어 있는지 확인하고 뒤이어 PUT을 보내면, 두 요청 사이에 다른 작업자가 같은 키에 파일을 올릴 수 있습니다. 저는 기본 PUT에 &lt;code>If-None-Match: *&lt;/code>를 넣었습니다. R2는 이 조건을 객체 쓰기와 함께 판단합니다. 키가 이미 있으면 &lt;code>412 PreconditionFailed&lt;/code>가 돌아오고, 도구는 그 파일을 건너뛰었다고 표시한 뒤 다음 파일로 넘어갑니다. &lt;code>--overwrite&lt;/code>를 준 경우에는 이 조건을 붙이지 않습니다. &lt;a href="https://developers.cloudflare.com/r2/api/s3/extensions/#conditional-operations-in-putobject">Cloudflare R2의 조건부 PUT 문서&lt;/a>&lt;/p></description></item><item><title>테스트 결과에서 재현 절차 찾기</title><link>https://team-edo.github.io/posts/2026/11/trace-qa-results-to-acceptance-criteria/</link><pubDate>Tue, 03 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/trace-qa-results-to-acceptance-criteria/</guid><description>&lt;p>팀의 테스트 문서는 한 시나리오 안에 완료 조건, 따라 할 단계, 수행 결과가 있었습니다. 기존 결과를 보면 일부 결함이 있다는 메모를 남겨도 어느 조건을 다시 실행해야 하는지 단번에 찾기 어려웠습니다. 반대로 단계에 있는 확인 절차를 조건과 대조하면, 그 동작의 기대 결과가 문서 어디에도 없는 경우가 있었습니다.&lt;/p>
&lt;h2 id="번호-하나가-판정과-재현을-함께-가리키게-했다">번호로 판정과 재현 연결&lt;/h2>
&lt;p>완료 조건 번호와 가이드 단계 번호를 맞췄습니다. 예를 들어 조건 ④가 곡을 지정하지 않을 때 가장 최근에 연주한 곡이 선택된다는 내용이면, 단계 ④에는 곡 ID 없이 요청하고 응답과 DB에서 비교할 위치를 적습니다. 조건은 기대 결과를, 단계는 그 결과를 볼 방법을 담습니다. 로그인과 같은 공통 준비는 시작 상태에 적고, 결과나 결함은 같은 번호 줄에 남기도록 했습니다.&lt;/p></description></item><item><title>Git 브랜치 갱신 전 확인할 것</title><link>https://team-edo.github.io/posts/2026/11/guard-git-ref-updates/</link><pubDate>Mon, 02 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/guard-git-ref-updates/</guid><description>&lt;p>병합 뒤 로컬 기준 브랜치를 정해진 커밋으로 옮겨야 했습니다. 다른 작업을 열어 둔 상태라 기준 브랜치로 체크아웃하지 않고 처리했습니다. 이때 필요한 확인은 브랜치가 아직 제가 확인한 커밋에 있는지, 그리고 옮길 커밋이 현재 이력을 그대로 포함하는지였습니다.&lt;/p>
&lt;h2 id="움직여도-되는-참조인지-확인했다">현재 참조와 조상 관계 확인&lt;/h2>
&lt;p>먼저 &lt;code>git merge-base --is-ancestor &amp;quot;$old&amp;quot; &amp;quot;$new&amp;quot;&lt;/code>로 이전 커밋이 새 커밋의 조상인지 확인했습니다. 종료 코드가 0이어야 빨리 감기입니다. 다음으로 &lt;code>git worktree list --porcelain&lt;/code>에서 옮길 브랜치를 사용하는 체크아웃이 없는지 확인했습니다. 체크아웃한 브랜치의 참조를 직접 바꾸면 그 폴더의 인덱스와 파일이 새 커밋에 맞춰 갱신되지 않기 때문입니다.&lt;/p></description></item><item><title>약관 배포와 시행 사이의 시차</title><link>https://team-edo.github.io/posts/2026/11/separate-terms-publication-from-effective-time/</link><pubDate>Mon, 02 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/separate-terms-publication-from-effective-time/</guid><description>&lt;p>새 약관을 배포한 날 저녁, 웹의 &lt;code>current&lt;/code> 주소는 새 문서를 보여줬습니다. 그런데 앱이 조회하는 약관 목록에 새 판이 들어간 건 자정 뒤였습니다. 문서 버전에 시행일을 적어 두었지만, 웹과 API가 버전을 고르는 방식은 서로 달랐습니다.&lt;/p>
&lt;h2 id="배포와-시행이-갈라진-시간">자정 뒤 전환된 약관 목록&lt;/h2>
&lt;p>11월 2일 19:27 KST에 새 판이 들어 있는 변경을 병합했고, 19:36 배포 확인에서 &lt;code>current&lt;/code>가 시행일 11월 3일인 문서를 보여주는 것을 확인했습니다. 이미 11월 2일 판이 있어서 같은 날짜를 새 문서에 다시 쓸 수는 없었습니다. 병합부터 자정까지 약 4시간 반 동안 문서의 날짜와 현재라고 표시되는 시점이 어긋났습니다.&lt;/p></description></item><item><title>Jenkins가 새 Compose 파일을 못 읽은 이유</title><link>https://team-edo.github.io/posts/2026/10/atomic-compose-install-permissions/</link><pubDate>Thu, 29 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/atomic-compose-install-permissions/</guid><description>&lt;p>운영 Compose 파일을 바꾼 뒤 Jenkins 배포가 설정 검사에서 멈췄습니다. 같은 파일을 root로 검사하니 통과했고, Jenkins 계정으로 실행하니 읽기 권한이 없었습니다. 파일은 &lt;code>root:root&lt;/code> 소유에 모드가 &lt;code>0600&lt;/code>이었습니다. 파일의 내용은 그대로 두고 Jenkins 그룹에 읽기 권한을 복구하자 같은 커밋의 재실행이 배포까지 성공했습니다. 이후 같은 문제가 다시 생겨 배포 네 건이 연속으로 실패했습니다.&lt;/p>
&lt;h2 id="바뀔-파일을-실제-계정으로-먼저-읽었다">Jenkins 계정으로 교체 전 검사&lt;/h2>
&lt;p>같은 파일시스템에서 새 파일을 준비한 뒤 이름을 바꾸면, 읽는 쪽에는 완성된 파일만 보입니다. 이때 새 경로의 권한은 옛 파일에서 이어지는 값이 아니라 교체해 넣는 파일이 가진 값입니다.&lt;/p></description></item><item><title>원격 명령은 어디서 막혔을까</title><link>https://team-edo.github.io/posts/2026/10/trace-shell-and-ssh-command-failures/</link><pubDate>Thu, 29 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/trace-shell-and-ssh-command-failures/</guid><description>&lt;p>운영 기록을 읽으려던 명령이 실패하면 먼저 원격 서버 상태를 의심하기 쉽습니다. 세 번의 실패를 다시 보니 Git에 인수를 넘기기 전, SSH에 인수를 넘기는 순간, SSH가 원격 명령을 시작하기 전이라는 서로 다른 위치에서 막혔습니다.&lt;/p>
&lt;h2 id="zsh가-만든-인수를-먼저-본다">zsh가 만든 인수 확인&lt;/h2>
&lt;p>첫 사례에서는 변수에 든 Git ref 뒤에 콜론과 경로를 바로 붙였습니다. zsh에서 뒤 문자열이 &lt;code>:a&lt;/code>로 시작하면 변수 확장의 경로 수정자로 읽힐 수 있습니다. &lt;code>git show &amp;quot;${ref}:archive/report.txt&amp;quot;&lt;/code>처럼 중괄호로 변수 이름을 닫으면 콜론은 Git이 받는 문자열에 그대로 남습니다. &lt;a href="https://zsh.sourceforge.io/Doc/Release/Expansion.html">zsh 확장 문서&lt;/a>&lt;/p></description></item><item><title>Docker가 Mac의 여유 공간과 파일을 못 본 이유</title><link>https://team-edo.github.io/posts/2026/10/docker-macos-vm-boundary/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/docker-macos-vm-boundary/</guid><description>&lt;p>Mac에서 분석 서버 이미지를 만들 때 &lt;code>apt&lt;/code>가 패키지를 내려받을 공간이 부족하다고 했습니다. Mac에는 약 &lt;strong>20 GiB&lt;/strong>가 남아 있었습니다. 컨테이너 안에서 &lt;code>df -h /&lt;/code>를 실행하자 가상 디스크 파일시스템은 &lt;strong>40G&lt;/strong> 규모였고, 사용 가능량 &lt;strong>0&lt;/strong>, 사용률 **100%**로 표시됐습니다. 호스트의 남은 용량과 Linux 컨테이너가 실제로 쓸 수 있는 용량이 따로 움직이고 있었습니다.&lt;/p>
&lt;h2 id="검증에-필요한-이미지-수를-줄였다">실행 이미지 하나로 검증&lt;/h2>
&lt;p>당시에는 실행 이미지와 별도로 테스트용 빌드 단계까지 유지하려 했습니다. 로컬 Docker 디스크가 부족해 비슷한 크기의 이미지를 추가로 만들기 어려웠습니다. 테스트 전용 단계를 빼고, 실행 이미지에서 일회용 컨테이너를 띄워 개발 의존성을 받아 검사하는 방식으로 바꿨습니다. 검증 기록에서 실행 이미지는 약 &lt;strong>1.65 GB&lt;/strong>였고, 그 이미지로 컨테이너를 만들어 실행한 테스트 &lt;strong>63개&lt;/strong>가 통과했습니다. 디스크를 정리한 뒤 실패했던 리뷰의 빌드를 같은 조건으로 다시 돌렸다는 기록은 없습니다.&lt;/p></description></item><item><title>FFmpeg를 유지하며 Python 3.12로 맞추기</title><link>https://team-edo.github.io/posts/2026/10/pin-managed-python-in-shared-base-image/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/pin-managed-python-in-shared-base-image/</guid><description>&lt;p>녹음 업로드는 백엔드에서 검사하고, 채점은 Python 분석 서비스에서 실행합니다. 두 서비스에서 같은 FFmpeg 패키지를 쓰기 위해 분석 이미지의 기반을 백엔드와 같은 Ubuntu &lt;strong>26.04&lt;/strong>로 맞췄습니다. 당시 기반 이미지의 시스템 Python은 &lt;strong>3.14&lt;/strong>였고, 분석 프로젝트는 &lt;strong>3.12&lt;/strong>를 사용했습니다. Python 버전을 맞추려고 기반 이미지를 바꾸면 함께 맞춰 둔 FFmpeg 패키지부터 다시 확인해야 했습니다.&lt;/p>
&lt;h2 id="시스템-python과-프로젝트-python을-분리했다">시스템 Python과 프로젝트 Python 분리&lt;/h2>
&lt;p>이미지에 uv를 넣고 &lt;code>UV_PYTHON_PREFERENCE=only-managed&lt;/code>로 설정했습니다. 프로젝트의 &lt;code>.python-version&lt;/code>에 있는 버전을 읽어 uv가 관리하는 Python을 설치하게 했습니다. 런타임 계정도 읽을 수 있는 공용 위치에 설치하고, 앱이 시작될 때는 그 Python으로 만든 가상 환경을 사용하도록 했습니다. uv를 가져오는 이미지도 다이제스트로 지정해 빌드가 참조하는 도구의 버전을 고정했습니다. &lt;a href="https://docs.astral.sh/uv/concepts/python-versions/#adjusting-python-version-preferences">uv의 관리형 Python 설정&lt;/a>&lt;/p></description></item><item><title>Flyway를 끈 운영 프로필에서 API 켜기</title><link>https://team-edo.github.io/posts/2026/10/spring-profile-ops-terms-route/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/spring-profile-ops-terms-route/</guid><description>&lt;p>얼쑤의 백엔드는 운영에서 &lt;code>ops&lt;/code> 프로필로 실행했습니다. 이 프로필에서는 앱이 이미 만들어진 DB에 연결하고, 스키마 변경은 별도 작업에서 적용합니다. 업무 API를 켜기 전의 구성을 확인해 보니 약관을 비롯한 업무 컨트롤러와 서비스가 &lt;code>db&lt;/code> 프로필에만 등록돼 있었습니다. 프로세스가 기동해도 &lt;code>ops&lt;/code>로 뜬 컨텍스트에는 약관 요청을 받을 컨트롤러가 없었습니다.&lt;/p>
&lt;h2 id="업무-빈과-스키마-변경의-조건을-분리했다">업무 빈과 마이그레이션 분리&lt;/h2>
&lt;p>처음에는 두 프로필을 함께 켜면 업무 빈도 만들 수 있다고 생각했습니다. 하지만 당시 &lt;code>db&lt;/code>는 Flyway를 켰고 &lt;code>ops&lt;/code>는 껐습니다. 두 프로필에 같은 설정 키가 있으면 뒤에 지정한 프로필의 값이 적용됩니다. 순서를 바꾸는 것만으로 운영 앱이 기동할 때 마이그레이션을 실행할 수 있는 구성이었습니다.&lt;/p></description></item><item><title>Git 병합 뒤 DB 테스트를 한 이유</title><link>https://team-edo.github.io/posts/2026/10/verify-git-merge-beyond-conflicts/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/verify-git-merge-beyond-conflicts/</guid><description>&lt;p>진행 중인 작업을 바꾸지 않고 기준 브랜치와 충돌할지 미리 보고 싶을 때 &lt;code>git merge-tree --write-tree HEAD origin/develop&lt;/code>를 사용했습니다. 작업 트리와 인덱스를 그대로 두고 두 커밋을 병합한 결과를 확인할 수 있습니다. 한 브랜치에서는 기준 브랜치에 새 커밋이 23개 들어왔을 때 검사에 충돌이 없었습니다. 이후 다른 변경이 먼저 병합되자 다시 실행한 검사에서 문서 충돌이 나타났고, 기준 브랜치를 실제로 합쳐 그 파일을 정리했습니다. 미리 본 결과는 그때 비교한 두 커밋에 대한 것이었습니다.&lt;/p></description></item><item><title>Jira 작업 생성 뒤 다시 읽는 이유</title><link>https://team-edo.github.io/posts/2026/10/verify-jira-task-after-create/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/verify-jira-task-after-create/</guid><description>&lt;p>개발 작업을 시작할 때 Jira에 작업을 만들고 담당자와 스프린트를 지정했습니다. 생성할 때 필드를 한꺼번에 보내면 요청 한 번으로 끝날 거라고 생각했습니다. 당시 사용한 연결 도구에서는 스프린트 값을 포함한 생성 요청이 &lt;strong>HTTP 400&lt;/strong>으로 거절됐습니다. 값을 빼고 작업을 만들자 요청은 성공했지만, 응답에 적힌 담당자는 요청에 넣은 사람과 달랐습니다. 요청을 받아들였는지와 기대한 값이 남았는지를 따로 확인해야 했습니다.&lt;/p>
&lt;h2 id="생성과-배정을-분리했다">생성과 배정을 나눠 재조회&lt;/h2>
&lt;p>먼저 스프린트 값을 빼고 작업을 만들었습니다. 생성 응답에서 실제 작업을 가리키는 키를 얻은 뒤, 그 작업에 상태 전환과 스프린트 배정을 요청했습니다. 생성 응답에서 다르게 표시된 담당자도 확인해 수정했습니다. 담당자가 바뀐 이유로 프로젝트 기본값을 의심했지만, 어느 설정이 적용됐는지까지는 확인하지 못했습니다.&lt;/p></description></item><item><title>MR 병합 뒤에도 운영 확인이 남았다</title><link>https://team-edo.github.io/posts/2026/10/mr-merge-and-operational-verification/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/mr-merge-and-operational-verification/</guid><description>&lt;p>새 약관 API를 운영에서 호출해 HTTP 200을 받는 것이 한 작업의 완료 조건이었습니다. MR이 병합되자 Jira는 작업을 완료로 표시했지만, 같은 시점에 운영 API는 &lt;strong>404&lt;/strong>를 반환했습니다.&lt;/p>
&lt;h2 id="자동-종료가-가리키는-작업을-확인했다">자동 종료 대상과 운영 응답 확인&lt;/h2>
&lt;p>저희 프로젝트는 MR이 병합되면 연결된 작업을 완료로 전환했습니다. 약관 사례에서는 배포 이력과 운영 응답을 계속 확인해 새 버전의 &lt;strong>200&lt;/strong> 응답까지 기록했습니다. 배포가 막힌 원인과 복구 과정은 별도 글에 정리했습니다. &lt;a href="https://team-edo.github.io/posts/2026/10/separate-deployment-health-probes/">인증 추가 뒤 자동 배포가 10번 실패한 이유&lt;/a>&lt;/p></description></item><item><title>인증 추가 뒤 자동 배포가 10번 실패한 이유</title><link>https://team-edo.github.io/posts/2026/10/separate-deployment-health-probes/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/separate-deployment-health-probes/</guid><description>&lt;p>얼쑤에서 새로 병합한 약관 조회 API를 운영에서 호출했더니 404가 돌아왔습니다. 기존 서비스는 계속 응답하고 있었지만, 실행 중인 JAR의 버전과 컨테이너가 시작한 시각을 확인하니 그 뒤의 변경은 배포되지 않은 상태였습니다. 처음 살펴본 Jenkins 실행 열 건은 연속으로 배포 검사에서 실패했습니다.&lt;/p>
&lt;p>Jenkins API에서 인증 때문에 실행 기록을 읽을 수 없어, 서버에서 빌드별 &lt;code>build.xml&lt;/code>과 콘솔 로그를 읽었습니다. &lt;code>build.xml&lt;/code>의 결과·시작 시각·커밋 SHA를 콘솔 로그의 마지막 실패 단계와 맞춰 보니, 서로 다른 커밋의 배포가 같은 검사에서 멈췄습니다.&lt;/p></description></item><item><title>재실행 중 덮어쓴 배포 백업</title><link>https://team-edo.github.io/posts/2026/10/rerun-safe-deployment-backup/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/rerun-safe-deployment-backup/</guid><description>&lt;p>배포 도구를 교체할 때 현재 파일을 백업하고, 준비한 새 파일을 설치하는 명령을 이어서 실행했습니다. 같은 명령을 다시 실행했더니 &lt;code>install&lt;/code>에서 후보 파일을 찾지 못했습니다. 실패 메시지는 마지막 단계에서 나왔지만, 그 전에 실행된 복사가 기존 백업을 이미 바꾼 뒤였습니다.&lt;/p>
&lt;p>첫 실행에서는 백업 파일에 이전 버전이 들어갔고 설치 경로에는 새 버전이 들어갔습니다. 두 번째 실행에서는 그 새 버전이 같은 이름의 백업 위에 복사됐습니다. 그 뒤에야 설치할 후보 파일이 없다는 오류가 났습니다. 이번에는 변경 전 도구가 저장소 이력에 있어 이전 내용을 찾을 수 있었습니다. 운영에서만 관리하는 파일이라면 동일한 복원 경로를 기대하기 어렵습니다. 실제로 도구를 이전 상태로 복구했거나, 수정한 절차로 설치를 다시 검증했다는 기록은 없습니다.&lt;/p></description></item><item><title>컨테이너 기동 후 첫 분석 요청이 500인 이유</title><link>https://team-edo.github.io/posts/2026/10/numba-cache-nonroot-first-request/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/numba-cache-nonroot-first-request/</guid><description>&lt;p>얼쑤의 녹음 분석 API를 Docker 이미지로 만들었습니다. 이미지 안에서 root로 실행한 테스트는 통과했고, 기본 실행 명령으로 서버도 떴습니다. 그런데 기본 계정으로 뜬 서버에 처음 녹음 분석을 요청하자 HTTP 500이 돌아왔습니다. 로그에는 &lt;code>RuntimeError&lt;/code>가 남았습니다. 서버가 요청을 받을 수 있게 된 시점과 실제로 분석 코드를 불러오는 시점 사이에 차이가 있었습니다.&lt;/p>
&lt;h2 id="분석-코드를-불러올-때-캐시가-필요했다">모듈을 불러올 때 필요한 Numba 캐시&lt;/h2>
&lt;p>분석에 쓰는 librosa에는 Numba가 JIT 컴파일하고 결과를 캐시하는 함수가 있습니다. 이번에는 분석 요청에서 관련 모듈을 불러올 때 Numba가 캐시 위치를 찾지 못해 &lt;code>cannot cache function … no locator available&lt;/code> 오류를 냈습니다. 요청 전체를 보내지 않고 같은 이미지에서 &lt;code>import librosa.core.notation&lt;/code>만 실행해도 이 오류를 확인할 수 있었습니다.&lt;/p></description></item></channel></rss>