TL;DR 테스트가 ../../fixtures/... 처럼 프로젝트 폴더 밖의 파일 을 읽고 있었다. 상대경로는 코드 파일의 위치가 아니라 프로세스의 현재 작업 디렉터리(cwd) 를 기준으로 해석된다. 로컬과 Docker 컨테이너의 cwd와 디렉터리 구조가 달라 같은 코드가 서로 다른 경로를 가리켰다. Docker 이미지에는 호스트의 파일이 자동으로 들어가지 않는다. 필요한 파일은 COPY 등으로 명시적으로 포함해야 한다. 테스트가 실제로 찾고 있던 /fixtures 를 빌드 스테이지에 복사해 해결했다. 근본적으로는 빌드와 테스트가 기대하는 입력을 명시적으로 관리하는 것이 중요하다. 목차 증상 로그 읽기: ENOENT 상대경로는 어디를 기준으로 계산될까? Docker 이미지 안에는 어떤 파일이 있을까? 두 조건이 만나면서 문제가 발생했다 해결 멀티스테이지 빌드라서 운영 이미지에는 들어가지 않는다 더 나은 설계: 암묵적인 입력을 명시적으로 만들기 해결 방법 비교 Docker 없이 비슷한 환경 재현하기 체크리스트 마치며 1. 증상 로컬에서는 npm test 가 전부 통과했다. 그런데 CI에서 Docker 이미지를 빌드하자 테스트 단계에서 실패했다. FAIL lib/price.test.ts Error: ENOENT: no such file or directory, open '/fixtures/products.json' ❯ lib/price.test.ts:12:14 Test Files 1 failed | 8 passed (9) Tests 5 failed | 146 passed (151) ERROR: process "/bin/sh -c npm test && npm run build" did not complete successfully: exit code: 1 여기서 가장 먼저 눈에 들어온 것은 두 가지였다. 첫째, ENOENT 둘째, 전체 테스트가 아니라 151개 중 파일을 읽는 5개만 실패했다는 점 즉 테스트 러너 전체나 Node.js 버전 문제보다는, 먼저 파일 접근과 경로 문제 를 의심할 수 있었다. 2. 로그 읽기: ENOENT ENOENT 는 POSIX 계열 운영체제에서 사용하는 오류 코드 중 하나로, 의미는 다음과 같다. ENOENT No such file or directory Node.js가 fs.readFileSync() 같은 파일시스템 API를 호출했는데, 운영체제가 해당 경로에서 파일을 찾지 못했을 때 발생한다. 즉 이것은 보통 "비즈니스 로직이 틀렸다" 라는 오류라기보다 "운영체제가 요청받은 위치에서 파일을 찾지 못했다" 라는 의미다. 따라서 먼저 확인해야 할 것은 코드 로직보다 실제로 어떤 경로를 찾았는가 였다. 경로 테스트가 기대한 파일 shop/fixtures/products.json 에러 메시지의 경로 /fixtures/products.json 저장소 내부가 아니라 파일시스템 최상위 /fixtures 를 찾고 있었다. 같은 코드가 왜 환경에 따라 서로 다른 위치를 가리켰을까? 원인을 이해하려면 두 가지를 알아야 한다. 상대경로는 무엇을 기준으로 계산되는가 Docker 이미지 안에는 어떤 파일이 존재하는가 3. 상대경로는 어디를 기준으로 계산될까? 3-1. 기준은 코드 파일의 위치가 아니라 cwd 다음과 같은 코드를 생각해보자. readFileSync("../../fixtures/products.json", "utf8"); 많은 경우 이 경로가 현재 .ts 파일을 기준으로 계산될 것처럼 느껴진다. 하지만 Node.js의 fs API에 상대경로를 넘기면 기본적으로 프로세스의 현재 작업 디렉터리(current working directory) 를 기준으로 해석한다. Node.js에서는 process.cwd() 로 확인할 수 있다. 예를 들어 아래 두 코드는 개념적으로 같다. path.resolve("../../fixtures"); path.resolve(process.cwd(), "../../fixtures"); 값 의미 언제 바뀌는가 process.cwd() 프로세스를 어디에서 실행했는가 cd , Docker WORKDIR , 실행 스크립트 등에 따라 달라짐 import.meta.url 현재 모듈 파일이 어디에 있는가 파일 위치가 바뀌지 않는 한 동일 __dirname 현재 파일이 있는 디렉터리 CommonJS에서 사용 즉 상대경로가 cwd에 의존한다면 같은 코드라도 어디에서 실행하느냐에 따라 다른 파일을 읽게 된다. 이번 경우 로컬에서는 shop/apps/web 에서 테스트를 실행했고, Docker 안에서는 Dockerfile의 WORKDIR /app 때문에 테스트 프로세스의 cwd가 /app 이었다. 3-2. / 보다 위로는 올라갈 수 없다 POSIX 파일시스템에서 루트 디렉터리 / 의 부모는 사실상 / 자신이다. 따라서 다음 코드는 오류를 내지 않는다. path.resolve("/app", "../../fixtures"); // → "/fixtures" 과정을 보면 이해하기 쉽다. /app ↓ .. / ↓ .. ← 루트보다 위로 못 가고 그대로 / / ↓ fixtures /fixtures 반면 로컬에서는 결과가 다르다. path.resolve("/home/me/shop/apps/web", "../../fixtures"); // → "/home/me/shop/fixtures" 이 차이가 이번 문제의 첫 번째 원인 이었다. 4. Docker 이미지 안에는 어떤 파일이 있을까? 4-1. Docker 이미지는 호스트 폴더의 복사본이 아니다 Docker 이미지를 만들면 프로젝트 폴더 전체가 자동으로 들어가는 것처럼 생각하기 쉽지만, 실제로는 그렇지 않다. 이미지는 Dockerfile 명령을 순서대로 실행하면서 만들어진 파일시스템 변경의 결과 다. FROM node:24-alpine WORKDIR /app COPY package*.json ./ RUN npm ci COPY apps/web/ ./ RUN npm test COPY 는 호스트의 파일을 이미지 안으로 가져온다. RUN npm ci 역시 이미지 안에서 새로운 파일(예: node_modules )을 생성할 수 있다. 즉 이미지는 단순히 " COPY 한 것의 합"이라기보다 Dockerfile 명령들이 각 레이어에 기록한 파일시스템 변경의 결과 라고 보는 것이 정확하다. 다만 호스트에 있는 파일은 자동으로 이미지 안에 나타나지 않으며, 사용하려면 COPY 나 ADD 등으로 명시적으로 가져와야 한다. 4-2. 빌드 컨텍스트와 COPY 다음 명령을 실행했다고 하자. docker build -f apps/web/Dockerfile . 마지막의 . 이 빌드 컨텍스트(build context) , 즉 Docker 빌드가 참조할 수 있는 범위다. shop/ ├─ fixtures/ ├─ apps/ │ └─ web/ └─ ... 저장소 루트에서 위 명령을 실행했다면 fixtures/ 역시 빌드 컨텍스트 안에 있다. 하지만 빌드 컨텍스트 안에 있다는 것과 이미지 안에 들어간다는 것은 다른 이야기다. 실제로 이미지 안에 넣으려면 COPY 가 필요하다. COPY fixtures/ /fixtures/ 또한 .dockerignore 에 제외된 파일은 빌드 컨텍스트에서 사용할 수 없게 될 수 있으므로 함께 확인해야 한다. 핵심: Docker 빌드가 어떤 파일을 볼 수 있는가와, 그 파일이 실제 이미지 안에 들어가는가는 별개의 문제다. 이삿짐에 비유하면 다음과 비슷하다. Docker 비유 빌드 컨텍스트 이사할 집 COPY 실제 이삿짐 목록 이미지 트럭에 실린 물건 집에 물건이 있다고 해서 전부 트럭에 실리는 것은 아니다. 5. 두 조건이 만나면서 문제가 발생했다 저장소 구조와 테스트 코드는 다음과 같았다. shop/ ├─ fixtures/ │ └─ products.json │ └─ apps/ └─ web/ ├─ Dockerfile ├─ package.json └─ lib/ └─ price.test.ts const products = JSON.parse( readFileSync( resolve(process.cwd(), "../../fixtures/products.json"), "utf8", ), ); 로컬과 Docker를 비교하면 다음과 같다. 로컬 Docker process.cwd() shop/apps/web /app ../../ 결과 shop/ / 최종 경로 shop/fixtures/products.json /fixtures/products.json 파일 존재 여부 ✅ ❌ [로컬] [Docker] shop/apps/web /app │ ../../ │ ../../ ▼ ▼ shop / ▼ ▼ fixtures/products.json ✅ /fixtures/products.json ❌ 여기에 두 번째 문제가 겹쳤다. 기존 Dockerfile은 웹 애플리케이션 디렉터리만 이미지 안으로 복사하고 있었다. COPY apps/web/ ./ 따라서 호스트 저장소의 fixtures/products.json 은 Docker 이미지 안에 존재하지 않았다. 정리하면 이번 장애는 두 조건이 동시에 만나 발생했다. 조건 1: 상대경로가 process.cwd() 를 기준으로 계산되면서 ../../fixtures 가 Docker 안에서 /fixtures 로 바뀌었다. 조건 2: fixtures/ 를 별도로 COPY 하지 않아 이미지 안에 /fixtures 자체가 없었다. 그래서 Node.js가 ENOENT: no such file or directory 를 반환했다. 6. 해결 이번에는 테스트 코드의 경로 계산 방식은 그대로 두고, 테스트가 Docker 환경에서 실제로 찾고 있는 위치에 필요한 파일을 넣었다. FROM node:24-alpine AS build WORKDIR /app COPY apps/web/package*.json ./ RUN npm ci COPY apps/web/ ./ # 테스트의 cwd는 /app. # ../../fixtures 는 /fixtures 로 해석되므로 # 테스트가 실제로 찾는 위치에 fixture를 복사한다. COPY fixtures/ /fixtures/ RUN npm test && npm run build FROM node:24-alpine WORKDIR /app COPY --from=build /app/.next/standalone ./ CMD ["node", "server.js"] 에러 메시지에서 테스트가 찾던 경로는 /fixtures/products.json 이었고, Dockerfile에서도 COPY fixtures/ /fixtures/ 로 정확히 같은 위치를 만들었다. 이후 테스트가 정상적으로 통과했다. 이번 실패의 직접적인 원인 은 테스트 로직 자체라기보다, 테스트가 필요로 하는 파일이 빌드 이미지에 포함되지 않았다는 점이었다. 다만 ../../fixtures 라는 디렉터리 구조를 암묵적으로 가정하는 방식 역시 장기적으로는 개선할 여지가 있다. 7. 멀티스테이지 빌드라서 운영 이미지에는 들어가지 않는다 COPY fixtures/ /fixtures/ 를 추가하면 테스트용 JSON 파일이 운영 이미지에도 들어가는 것 아닐까? 이번 Dockerfile에서는 그렇지 않다. 멀티스테이지 빌드 를 사용하고 있기 때문이다. FROM node:24-alpine AS build 에서 첫 번째 스테이지가 시작된다. 이곳에는 /app 과 /fixtures 가 모두 존재하며, npm test && npm run build 를 실행한다. 새로운 FROM node:24-alpine 이 등장하는 순간 새로운 파일시스템 이 시작된다. 첫 번째 스테이지의 파일은 자동으로 넘어오지 않는다. 필요한 결과물만 COPY --from=build /app/.next/standalone ./ 로 선택해서 가져온다. ┌─ Build stage ──────────────┐ │ /app │ │ ├─ source │ │ ├─ node_modules │ │ └─ build output │ │ /fixtures │ │ └─ products.json │ └────────────┬───────────────┘ │ npm test │ npm run build ▼ ┌─ Final stage ──────────────┐ │ /app │ │ └─ standalone build │ │ /fixtures 없음 │ └────────────────────────────┘ 테스트에 필요한 파일은 빌드 단계에서만 사용하고, 운영 이미지에는 필요한 결과물만 포함할 수 있다. 8. 더 나은 설계: 암묵적인 입력을 명시적으로 만들기 COPY 한 줄로 문제는 해결됐지만, 왜 이런 문제가 생겼는지 조금 더 생각해볼 필요가 있다. 기존 테스트에는 다음 가정이 숨어 있었다. 저장소 루트에 fixtures/ 가 존재한다. 테스트 프로세스는 저장소 루트에서 두 단계 아래에서 실행된다. 즉 테스트의 입력 파일 위치가 코드에 명확히 선언된 것이 아니라, 디렉터리 구조와 실행 위치에 암묵적으로 의존하고 있었다. 이런 값을 흔히 암묵적인 입력(implicit input) 이라고 볼 수 있고, 환경이 조금만 바뀌어도 쉽게 깨진다. 8-1. cwd 의존성을 파일 위치 의존성으로 바꾸기 ES Module 환경이라면 현재 파일의 위치를 기준으로 경로를 계산할 수 있다. const fixture = new URL( "../../../fixtures/products.json", import.meta.url, ); 이제 cd /somewhere && npm test 처럼 실행 위치가 달라져도 현재 모듈 파일을 기준으로 같은 상대 위치를 찾는다. 다만 이것도 완전히 독립적인 설계는 아니다. 여전히 다음 구조를 가정하기 때문이다. lib/price.test.ts │ ../../../ ▼ fixtures/ 즉 cwd 의존성 이 저장소 디렉터리 구조 의존성 으로 바뀐 것이다. 그리고 어떤 방식으로 경로를 계산하든 실제 파일이 Docker 이미지 안에 존재해야 한다는 조건은 변하지 않는다. 8-2. 파일 URL을 실제 경로로 변환할 때는 fileURLToPath() URL.pathname 을 바로 파일시스템 경로로 쓰는 코드를 볼 때가 있다. // ⚠️ Windows에서 문제가 될 수 있음 new URL("../../../fixtures/", import.meta.url).pathname 파일 URL과 운영체제의 경로 표현 방식이 다르기 때문이다. Node.js에서는 fileURLToPath() 를 사용하는 것이 안전하다. import { fileURLToPath } from "node:url"; const fixturesDir = fileURLToPath( new URL("../../../fixtures/", import.meta.url), ); Windows에서 개발하고 Linux Docker 컨테이너에서 빌드하는 프로젝트라면 이런 차이를 더욱 신경 쓰는 것이 좋다. 8-3. 입력 위치 자체를 설정으로 명시하기 조금 더 명시적으로 만들려면 fixture 위치를 한 곳에서 관리할 수도 있다. import { fileURLToPath } from "node:url"; export const FIXTURES_DIR = process.env.FIXTURES_DIR ?? fileURLToPath(new URL("../../../fixtures/", import.meta.url)); COPY fixtures/ /fixtures/ ENV FIXTURES_DIR=/fixtures 그러면 테스트는 fixture가 저장소에서 정확히 몇 단계 위에 있는가 보다 현재 환경에서 fixture 디렉터리가 어디인가 라는 명시적인 설정을 사용하게 된다. 로컬에서는 기본값을, Docker나 CI에서는 환경변수를 주입할 수 있고, 문제가 생겼을 때 확인해야 할 곳도 명확해진다. 9. 해결 방법 비교 방법 장점 단점 Dockerfile에 COPY 추가 코드 수정 없이 바로 해결 가능 Dockerfile이 테스트의 경로 규칙을 알아야 함 fixture를 apps/web/ 내부로 이동 프로젝트가 자체 테스트 데이터를 가지므로 구조가 단순 여러 프로젝트가 같은 fixture를 공유하기 어려움 fixture를 import 번들러/테스트 러너가 파일 의존성을 추적 가능 프로젝트 외부 파일 import 설정이 필요할 수 있음 환경변수로 fixture 경로 주입 환경마다 명시적으로 경로 설정 가능 설정 항목이 늘어남 import.meta.url 기반 경로 cwd 변화에 영향받지 않음 저장소 디렉터리 구조에는 계속 의존 이번 프로젝트에서는 여러 프로젝트가 같은 fixture 원본을 공유하고 있었기 때문에, 우선 가장 작은 변경인 COPY fixtures/ /fixtures/ 방식을 적용했다. 필요하다면 이후 FIXTURES_DIR 같은 환경변수 기반 구조로 확장할 수 있다. 10. Docker 없이 비슷한 환경 재현하기 CI가 오래 걸린다면 매번 Docker 이미지를 빌드해 확인하기 번거롭다. 이번 문제는 Docker 없이도 비슷한 디렉터리 구조를 만들어 재현할 수 있다. mkdir -p /tmp/sim/a/app cp -r apps/web/. /tmp/sim/a/app/ cp -r fixtures /tmp/sim/fixtures cd /tmp/sim/a/app npm ci npm test npm run build 일부러 다음 구조를 만든 것이다. /tmp/sim/ ├─ fixtures/ └─ a/ └─ app/ ← cwd cwd가 /tmp/sim/a/app 이므로 ../../fixtures 는 정확히 /tmp/sim/fixtures 를 가리킨다. Docker에서 기대하는 관계를 로컬 디렉터리 구조로 흉내 낸 것이다. 이 상태에서 /tmp/sim/fixtures 를 삭제하고 테스트를 실행하면 동일한 ENOENT 를 재현할 수 있다. 트러블슈팅에서 중요한 기준 중 하나는 문제를 재현할 수 있는가 다. 수정 전에는 실패하고 수정 후에는 성공하는 상황을 반복해서 만들 수 있어야, 수정이 실제 원인을 해결했다고 판단하기 쉽다. 11. 체크리스트 Docker나 CI에서만 파일 관련 테스트가 실패한다면 다음 항목부터 확인해볼 수 있다. 경로 테스트나 스크립트가 ../ 또는 ../../ 로 프로젝트 외부 파일을 읽고 있는가? 상대경로가 process.cwd() 기준인가? 로컬과 Docker의 process.cwd() 값이 같은가? Dockerfile의 WORKDIR 은 어디인가? 에러 메시지에서 실제로 어떤 절대경로를 찾고 있는가? Docker 이미지 필요한 파일이 Docker 빌드 컨텍스트 안에 있는가? 필요한 파일을 Dockerfile에서 실제로 COPY 했는가? .dockerignore 가 해당 파일이나 폴더를 제외하고 있지는 않은가? 테스트용 파일과 운영 이미지에 필요한 파일을 구분하고 있는가? 멀티스테이지 빌드라면 최종 스테이지에 불필요한 테스트 데이터가 넘어가고 있지 않은가? 설계 fixture나 설정 파일의 위치가 코드에 암묵적으로 숨어 있지는 않은가? 필요한 입력 위치를 환경변수나 설정 값으로 명시할 수 있는가? 12. 마치며: "로컬에서는 되는데 CI에서는 안 된다" 개발 중 자주 듣는 말이 있다. "로컬에서는 되는데 CI에서는 안 돼요." 이 문제의 원인은 많은 경우 코드 자체보다 환경 차이 에 있다. 이번에는 차이가 두 가지였다. 현재 작업 디렉터리(cwd) Docker 이미지 내부의 파일 구조 로컬에서는 cwd가 shop/apps/web 이라 ../../fixtures 가 자연스럽게 저장소 루트의 fixtures 를 가리켰다. Docker에서는 cwd가 /app 이라 같은 코드가 /fixtures 를 가리켰고, 이미지에는 해당 폴더를 COPY 하지 않았기 때문에 파일이 존재하지 않았다. 원인 해결 상대경로 테스트가 실제로 찾는 위치 확인 ↓ process.cwd() ↓ ↓ 환경마다 다른 절대경로 필요한 입력을 Dockerfile에 명시 ↓ Docker 이미지에 필요한 파일 없음 ↓ COPY fixtures/ /fixtures/ ENOENT 테스트 통과 조금 더 넓게 보면 이번 문제는 재현 가능한 빌드 에 대한 이야기이기도 하다. 빌드에 어떤 파일, 어떤 환경변수, 어떤 디렉터리 구조가 필요한지 명확하게 선언되어 있을수록 환경 차이에 덜 흔들린다. 이렇게 외부 환경의 숨은 상태에 의존하지 않고 선언된 입력만으로 동일한 결과를 만드는 빌드를 흔히 h
네트워크란? 여러대의 컴퓨터 또는 장비가 서로 연결되어서 정보를 주고 받을 수 있게 도와주는 기술 Client와 Server **Client** : 서버로 요청하는 프로그램 ex)웹 브라우저 **Server**: 클라이언트의 요청을 받아 처리하는 주체 - 흔히 우리가 웹 브라우저에 주소를 입력하는 건 ‘새로운 화면을 그리기 위한 데이터를 달라’는 데이터 요청에 해당 인터넷상의 주소 IP : 컴퓨터를 식별하기 위한 위치 주소 ex) 서울시 00구 포트 번호 : 그 서버에서 운용되고 있는 서비스를 구분하기 위한 번호, 받는 사람 ex)홍길동 웹서버란? 인터넷을 통해 HTTP를 이용하여 웹상의 클라이언트의 요청을 응답해주는 통신을 하는 일종의 컴퓨터 웹 서버의 기본 동작 원리 브라우저가 HTTP Request 요청 웹서버는 요청을 승인 HTTP Response 를 통해 웹사이트 데이터를 브라우저에 전송 브라우저는 서버에서 받아온 데이터를 이용해 화면에 출력 API와 RESTful API API 다른 소프트웨어 시스템과 통신하기 위해 따라야 하는 규칙, 하나의 "약속" RESTful API 자원을 이름으로 구분하여 해당 자원의 상태를 주고받는 모든 것 api가 적절하게 http를 준수하며 잘 설계되어있으면 RESTful 하게 설계된 것 HTTP 메서드 GET : 데이터 조회 POST : 데이터 생성 PUT : 데이터 수정 DELETE : 데이터 삭제 웹 서버와 WAS Web Server 브라우저에서 URL을 입력하여 어떠한 페이지를 요청했을 때 HTTP의 요청을 받아들여 HTML 문서와 같은 정적인 콘텐츠를 사용자에게 전달해주는 역할을 하는 것 정적 콘텐츠: 이미 완성이 되어있는 HTML과 같은 문서를 전달 동적 콘텐츠(마이페이지): 자체 처리 불가능, 해당 요청을 WAS에 전달 WAS 웹 애플리케이션, 즉 실제 기능이 동작하는 서버 동적인 콘텐츠 처리 가능하다는 점에서 web server와 구별 ex) Apache, Nginx SpringBoot와 Spring Spring Framework는 많은 xml 설정을 필요로 함 -> SpringBoot 등장 SpringBoot Java의 @애너테이션 기반의 설정 외부 라이브러리나 하위 프레임워크들의 의존성 관리가 쉬워짐 내장 Apache Tomcat HTTP 데이터를 주고 받는 양식을 정의한 "통신 규약"중 하나 HTTP 상태 코드(Status Code) 2xx (Successful) 3xx (Redirection) 4xx (Client Error) :클라이언트 오류 5xx (Server Error) : 서버 오류 Header : 추가 데이터, 메타 데이터 ex) GET naver.com HTTP/1.1 Payload: 실제 데이터 GET method를 제외하곤 모두 Payload를 보낼 수 있음(http에서의 약속) 테스트 코드 JUnit : 자바 프로그래밍 언어용 단위 테스트 프레임워크 테스트 파일 생성 단축키 Windows : Ctrl + shift + t Mac : ⌘ + shift + t Lombok과 application.properties Lombok 자바 프로젝트를 진행하는데 거의 필수적으로 필요한 메서드/생성자 등을 자동 생성해줌으로써 코드를 절약할 수 있도록 도와주는 라이브러리 @getter, setter @getter @Setter public class Memo { private String username; private String contents; } ... //아래와 같은 역할 public String getUsername() { return this.username; } public String getContents() { return this.contents; } public void setUsername(String username) { this.username = username; } public void setContents(String contents) { this.contents = contents; } @AllArgsConstructor, NoArgsConstructor @NoArgsConstructor @AllArgsConstructor public class Memo { private String username; private String contents; } ... public Memo() { //@NoArgsConstructor 역할 } public Memo(String username, String contents) { //@AllArgsConstructor 역할 this.username = username; this.contents = contents; } @RequiredArgsConstructor @RequiredArgsConstructor public class Memo { private final Calculator calculator; private final String username; private String contents; } ... // 아래와 같은 역할 public Memo(Calculator calculator, String username) { this.calculator = calculator; this.username = username; } application.properties Spring과 관련된 설정을 할 때 사용되는 파일 server.port=8081 변경시 서버의 port가 8081로 변경됨
4. 동시성 구현 사례 일련번호 채번 시 MAX(번호) + 1 방식을 사용할 경우, 두 트랜잭션이 동시에 동일한 MAX 값을 조회했을 때 발생할 수 있는 문제를 설명하시오. 두 트랜잭션이 동시에 동일한 MAX 값을 조회하면, 둘 다 같은 다음 번호를 생성할 수 있다. 이후 동일한 키 값으로 INSERT를 시도하면 PK 제약조건 위반 오류가 발생한다. 채번 테이블을 사용할 때, 현재 채번 값을 조회하기 전에 해당 행의 값을 먼저 UPDATE하는 이유를 설명하시오. 채번 값을 먼저 UPDATE하는 이유는 해당 채번 행에 Lock을 설정하여 동일한 구분값에 대한 채번 작업을 직렬화하기 위해서다. 다른 트랜잭션이 같은 행의 번호를 채번하려 하면 앞선 트랜잭션이 Lock을 해제할 때까지 대기하게 되므로 동일 번호가 동시에 발급되는 것을 방지할 수 있다. 채번 함수 내부에서 COMMIT을 수행하되 Autonomous Transaction을 사용하지 않은 경우, 메인 트랜잭션에 발생할 수 있는 문제를 설명하시오. Autonomous Transaction을 사용하지 않은 상태에서 채번 함수 내부에서 COMMIT하면, 채번 함수 호출 이전에 메인 트랜잭션에서 수행한 작업까지 함께 커밋된다. 이후 메인 로직에서 오류가 발생해 ROLLBACK하더라도 이미 커밋된 이전 작업은 되돌릴 수 없으므로 트랜잭션의 원자성이 깨지고 데이터 일관성 문제가 발생할 수 있다. Autonomous Transaction을 채번 함수에 적용했을 때, 일반 트랜잭션과 비교하여 얻을 수 있는 효과를 설명하시오. Autonomous Transaction을 사용하면 채번 함수 내부의 작업이 메인 트랜잭션과 독립된 별도 트랜잭션으로 수행되므로, 채번 함수 내부의 COMMIT 또는 ROLLBACK이 메인 트랜잭션의 작업에 영향을 주지 않는다. 또한 채번용 행의 Lock을 서브 트랜잭션에서 빠르게 해제할 수 있어 채번 동시성도 높일 수 있다. 채번 함수에서 Autonomous Transaction을 사용하지 않고 COMMIT도 제거한 경우, 동시성 측면에서 발생할 수 있는 문제를 설명하시오. 채번 함수 내부에서 COMMIT을 제거하면 채번 행에 설정된 Lock이 함수 종료 시점에 해제되는 것이 아니라 메인 트랜잭션이 종료될 때까지 유지된다. 따라서 채번 이후의 메인 로직 수행 시간이 길어질수록 같은 채번 행을 사용하려는 다른 트랜잭션의 대기 시간도 길어져 동시성이 저하될 수 있다. 선분이력 관리 시 현재 이력 행에 SELECT ... FOR UPDATE를 수행하여 동시성을 제어하려 할 때, 해당 고객의 기존 이력이 전혀 없는 경우 발생할 수 있는 문제를 설명하시오. 기존 이력 행이 없으면 SELECT ... FOR UPDATE가 잠글 대상 자체가 없으므로 Lock이 설정되지 않는다. 그 결과 동일 고객에 대한 여러 트랜잭션이 동시에 INSERT까지 진입할 수 있고, 시작일시는 서로 다르지만 종료일시가 모두 9999-12-31인 현재 이력 행이 여러 건 생성되어 선분이력의 정합성이 깨질 수 있다. 기존 이력이 없는 경우에도 선분이력의 동시성을 제어하기 위해, 어떤 행을 SELECT ... FOR UPDATE 대상으로 사용해야 하는지 설명하시오. 기존 이력이 존재하지 않아 이력 테이블에서 잠글 행이 없는 경우에는, 항상 존재하는 상위 테이블의 해당 고객 행을 SELECT ... FOR UPDATE로 잠가 동시성을 제어하면 된다. 교재 예시에서도 특정 고객의 상위 행을 잠금 대상으로 사용하여 동일 고객에 대한 동시 작업을 직렬화한다. 선분이력의 동시성 제어를 위해 상위 고객 테이블의 특정 고객 행을 잠그는 방식이 전체 시스템의 동시성에 미치는 영향을 설명하시오. 상위 고객 테이블 전체를 잠그는 것이 아니라 해당 고객의 특정 행만 SELECT ... FOR UPDATE로 잠그기 때문에, 동일 고객에 대한 작업만 직렬화되고 다른 고객에 대한 작업은 동시에 진행할 수 있다. 따라서 교재에서도 전체 동시성에 미치는 영향은 거의 0에 가깝다고 설명한다. 5. 오라클 Lock (1~2) Enqueue Lock의 특징을 소유자(Owner), 대기자(Waiter), Queue의 관점에서 설명하시오. Enqueue Lock 은 공유 리소스 에 대해 Lock을 요청하는 세션을 소유자(Owner)와 대기자(Waiter)로 구분하고, 대기자를 Queue 형태로 관리한다. TX Lock이 획득되는 시점과 해제되는 시점을 각각 설명하시오. TX Lock은 트랜잭션이 첫 번째 변경 작업을 시작할 때 획득하고, COMMIT 또는 ROLLBACK 시 해제된다. 한 트랜잭션이 특정 행을 수정 중일 때, 다른 트랜잭션이 해당 행에 대해 일반 SELECT를 수행하는 경우와 UPDATE를 수행하는 경우의 동작 차이를 설명하시오. 일반 SELECT는 선행 트랜잭션의 행 Lock을 기다리지 않고, 필요하면 Undo 정보를 이용해 CR Block을 생성하여 일관성 읽기를 수행한다. 같은 행에 대한 UPDATE는 변경 작업을 직렬화해야 하므로, 선행 트랜잭션이 보유한 TX Lock이 해제될 때까지 대기한다. 하나의 트랜잭션이 여러 개의 행을 수정하는 경우, TX Lock과 각 행의 Lock Byte가 어떤 단위로 관리되는지 설명하시오. TX Lock : 트랜잭션 단위로 관리 Lock Byte* : 각 행 단위로 존재 행의 Lock Byte를 통해 현재 해당 행을 수정 중인 트랜잭션을 확인하는 과정을 ITL과 Transaction ID의 관점에서 설명하시오. Lock Byte → 해당 ITL 슬롯 → ITL의 Transaction ID → 해당 트랜잭션의 Active 여부 확인 대상 행의 Lock Byte가 가리키는 ITL을 통해 해당 행을 변경한 트랜잭션을 식별하고, 그 트랜잭션이 아직 Active 상태이면 후행 트랜잭션은 대기하게 된다. enq: TX - row lock contention 대기 이벤트가 발생하는 대표적인 상황을 설명하시오. enq: TX - row lock contention은 대표적으로 후행 트랜잭션이 선행 트랜잭션이 이미 변경 중인 동일한 행을 변경하려고 할 때, 선행 트랜잭션의 TX Lock 해제를 기다리는 상태를 의미한다.
문제 링크 제출 코드(통과) using System; public class Solution { public int[,] solution(int n) { var size = Power(2, n) - 1; var answer = new int[size, 2]; Iter(n, 0, 1, 3, answer); return answer; } private static int Power(int baseNum, int exp) { var result = 1; for (var i = 0; i < exp; i++) result *= baseNum; return result; } private int Iter(int n, int step, int from, int to, int[,] answer) { if (n == 1) { answer[step, 0] = from; answer[step, 1] = to; return step + 1; } var nextTo = 6 - from - to; var next = Iter(n - 1, step, from, nextTo, answer); answer[next, 0] = from; answer[next, 1] = to; next++; return Iter(n-1, next, nextTo, to, answer); } } 전형적인 재귀 문제 중 하나인 하노이 탑이다. 총 시행 횟수의 점화식이 x(n+1) = 2x(n) + 1 이므로 일반항은 x(n) = 2^n - 1 이다. 크기부터 계산해 전체 배열을 한 번 할당하고, 내부를 채워나가는 방식으로 작성했다.
들어가며 서비스를 배포하고 나면 밖에서 봤을 때 뭐가 노출되는지 한 번씩 확인하게 됩니다. 블랙박스로 공격 표면을 훑는 작업인데, 손으로 하나하나 하다 보면 품이 꽤 듭니다. 배포할 때마다 처음부터 다 돌리기도 번거롭고요. 그래서 이 과정을 자동화하려고 pentesting 이라는 자율 보안 에이전트를 직접 만들었습니다. 사내 업무에 붙여서 쓰고 있는데 쓸만해서 간단히 정리해 둡니다. 어떤 도구인지 목표를 하나 던져주면 알아서 정찰, 탐색, 검증까지 돌리는 공격형 보안 에이전트입니다. Rust로 작성했고, Docker 이미지 하나로 바로 띄울 수 있습니다. CTF, 학습용, 실제 침투 테스트 워크플로우를 염두에 두고 만들었습니다. OpenAI 호환 API면 모델은 원하는 걸로 붙일 수 있습니다. (OpenRouter 등도 가능) 어떻게 쓰고 있나 가장 자주 쓰는 건 배포 직후 블랙박스 점검입니다. 스테이징이나 운영에 새 버전을 올린 뒤 목표만 하나 넘겨주면 됩니다. docker run --rm -it --init \ --cap-add=NET_RAW --cap-add=NET_ADMIN \ --env OPENAI_API_KEY="..." \ --env OPENAI_MODEL="..." \ -v ${PWD}/workspace:/workspace \ -v ${PWD}/runs:/state \ agnusdei1207/pentesting:latest \ run --goal "대상을 조사하고 노출된 공격 표면을 찾아줘" --workspace /workspace --run /state/current 그러면 평소에 체크리스트 들고 찍어보던 지점들을 알아서 꽤 많이 긁어옵니다. 열려 있는 엔드포인트, 노출된 경로, 설정 실수로 밖에서 보이는 것들처럼 배포 과정에서 놓치기 쉬운 공격 표면을 초반에 잘 잡아줍니다. 사람이 정밀하게 보기 전에 1차로 넓게 훑어주는 용도로 쓰면 시간이 많이 줄어듭니다. 코파일럿으로도 쓸만합니다 전부 자동으로 맡겨야 하는 건 아닙니다. 직접 점검하다 막힐 때 코파일럿처럼 옆에 두고 쓰기도 좋습니다. 어디를 파고들지 아이디어를 던져주거나, 놓친 각도를 짚어주는 식으로요. 손은 직접 움직이더라도 다음 수를 고민하는 부담은 줄어듭니다. 써보면서 좋았던 점 배포할 때마다 같은 목표로 반복할 수 있어서 점검 품질이 일정하게 유지됩니다. 수동으로 돌 때보다 커버리지가 넓어서 놓치는 지점이 줄었습니다. Docker로 돌려서 점검 환경을 깔끔하게 분리할 수 있습니다. 마치며 자동화가 사람 판단을 대체하진 않습니다. 다만 배포 후 넓게 한 번 훑는 반복 작업은 맡겨두기 좋습니다. 초반 정찰에서 자유로워지는 만큼 중요한 검증에 집중할 수 있습니다. 관심 있으면 레포 한번 봐주세요. 이슈나 PR은 언제든 환영합니다. 👉 github.com/agnusdei1207/pentesting ⚠️ 모의해킹 도구는 본인 소유이거나 명시적으로 승인받은 대상에만 사용하세요.
📌 문제 설명 가로 길이가w, 세로 길이가 h인 직사각형이 있다. 이 직사각형은 작은 정사각형 여러 개로 나누어져 있다. 여기에 👉 왼쪽 아래 모서리부터 오른쪽 위 모서리까지 대각선을 하나 긋는다. 대각선에 걸쳐 있는 정사각형은 사용할 수 없다. 따라서 👉 전체 정사각형 개수에서 대각선에 걸리는 정사각형 개수를 뺀 값 을 구하는 문제이다. 💡 처음 문제를 보고 든 생각 처음에는 w * h 하면 전체 정사각형 개수가 나오니까 그다음에는 "대각선이 지나가느 정사각형만 빼면 되는 거 아닌가?" 라고 생각했다. 이 생각 자체는 맞았다. 🔥 내가 처음 발견한 규칙 예를 들어 w = 8 h = 12 이면 전체 정사각형은 8 * 12이므로 96개이다. 처음 그림을 보고 대각선을 그었을 때 👉 일정한 패턴이 반복되는 것을 발견했다. 특히 8 12 가 4를 기준으로 나누어 졌다. 8 = 4 * 2 12 = 4 * 3 그래서 "아, 대각선이 같은 구조로 4번 반복되는 건가?" 라는 생각을 했다. 그리고 이4가 바로 gcd(8,12) 였다. 🧠 gcd가 뭐였지? import math gcd = math.gcd(w,h) gcd는 👉 최대공약수 이다. 예를 들어 8의 약수 1, 2, 4, 8 12의 약수 1,2,3,4,6,12 둘이 공통으로 가지는 가장 큰 수는 4 이다. 따라서 math.gcd(8, 12) 은 4가 된다. 🔥 핵심 공식 대각선에 걸리는 정사각형의 개수는 broken = w + h - gcd 이다. 처음에는 이 공식이 굉장히 뜬금없이 보였다. 특히 왜 w + h? 왜 gcd를 뺴지? 가 가장 어려웠다. 🔍 왜 w + h를 더할까? 대각선은 직사각형을 지나면서 👉 가로 방향의 격자 경계 👉 세로 방향의 격자 경계 를 만나게 된다. 그래서 기본적으로 가로 쪽 -> w 세로 쪽 -> h 를 세어서 w + h 를 생각할 수 있다. ❗ 그런데 문제가 생긴다 대각선이 가로선과 세로선이 만나는 정확한 격자 교점 을 지나가는 경우가 있다. 예를 들어 ┌──┬──┬──┬──┐ │ │ │ │ │ ├──┼──●──┼──┤ │ │ │ │ │ └──┴──┴──┴──┘ 가운데 ●처럼 대각선이 격자 교점을 정확하게 지나가면 우리가 가로 경계 1번 + 세로 경계 1번 으로 세면서 👉 같은 교점을 2번 센다. 실제로는 하나의 교점이므로 👉 중복을 빼줘야 한다. 🔥 그 중복을 결정하는 것이 gcd 여기서 아까 발견했던 8 = 4 * 2 12 = 4 * 3 가 다시 등장한다. gcd(8,12) = 4이므로 대각선의 구조가 👉 같은 작은 패턴으로 4번 반복된다. 그래서 격자 교점에서 발생하는 중복을 gcd를 이용해서 보정한다. 결과적으로 broken = w + h - gcd가 된다. 🔍 8 × 12에 실제로 적용 w = 8 h = 12 이면 gcd = math.gcd(8, 12) ↓ 4 따라서 broken = 8 + 12 - 4 ↓ 16 즉, 👉 대각선 때문에 사용할 수 없는 정사각형은 16개이다. 🔍 전체 정사각형 개수 square_len = w * h 8 × 12 = 96 전체는 96개 이다. 🔥 최종 계산 이제 진짜 단순하다. answer = square_len - broken 즉, 전체 정사각형 대각선에 걸리는 정사각형 이다. 96 - 16 = 80 따라서 정답은 80 이다. 🔍 전체 코드 import math def solution(w , h): square_len = w * h gcd = math.gcd(w,h) broken = w + h - gcd answer = square_len - broken return answer 🔍 코드 한 줄씩 이해 1️⃣ 전체 정사각형 개수 square_len = w * h 👉 가로 w개 × 세로 h개 👉 전체 정사각형 개수 2️⃣ 최대공약수 구하기 gcd = math.gcd(w, h) 👉 가로와 세로를 같은 구조로 나눌 수 있는 최대 크기를 구한다. 예: gcd(8,12) = 4 이 4가 대각선의 반복되는 패턴과 연결된다. 3️⃣ 대각선에 걸리는 정사각형 broken = w + h - gcd 👉 가로/세로 방향에서 센 값을 합치고 👉 격자 교점에서 중복되는 부분을 gcd로 보정한다. 4️⃣ 사용할 수 있는 정사각형 answer = square_len - broken 👉 전체에서 대각선에 걸리는 정사각형을 뺀다. 5️⃣ 반환 return answer 👉 최종 사용할 수 있는 정사각형 개수 반환. 🧠 이번 문제에서 진짜 배운 것 이번 문제는 Python 문법 자체는 어렵지 않았다. w * h 도 어렵지 않고 math.gcd(w, h) 도 함수만 알면 된다. 진짜 어려웠던 부분은 👉 수학적인 규칙을 찾아내는 것 이었다. 특히 broken = w + h - gcd 라는 식이 처음에는 아무 의미 없이 보였다. 하지만 8 × 12 ↓ 4를 기준으로 같은 패턴 반복 ↓ gcd(8,12) = 4 ↓ 대각선이 격자 교점을 지나면서 중복 발생 ↓ w + h에서 gcd를 이용해 보정 ↓ broken = w + h - gcd 로 연결해서 이해할 수 있었다. ❗ 내가 어려웠던 부분 처음에는 broken을 어떻게 구해야 하는지 몰랐다. gcd가 최대공약수라는 것을 알게 되었지만 왜 이문제에서 필요한지 이해하기 어려웠다. w + h - gcd라는 공식이 처음에는 완전히 뜬금없었다. 하지만 8 * 12에서 4개 단위의 같은 대각선 패턴이 반복되는 것을 보고 gcd = 4와 연결할 수 있었다. 결국 이 문제는 Python 문법보다 수학적 규칙을 발견하는 게 훨씬 어려웠다. 💭 느낀 점 처음에는 "전체 크기 구하고 대각선에 걸리는 것만 빼면 되는 문제" 라고 생각했다. 이 생각은 맞았다. 문제는 대각선에 걸리는 정사각형을 어떻게 계산하느냐 였다. 처음에는 4스텝 이라는 규칙을 발견했는데, 나중에 보니까 그 4가 gcd(8, 12) 였다는 것이 연결됐다. 즉, 👉 처음에 눈으로 발견한 규칙을 👉 수학 공식으로 바꾼 문제였다. 이런 문제는 코드 자체가 어려운 게 아니라 "왜 이 숫자가 필요한지"를 찾아내는 게 핵심이라는 걸 느꼈다. 🔥 한 줄 정리 👉 전체 w × h에서 대각선에 걸리는 w + h - gcd(w,h)개의 정사각형을 빼는 수학 규칙 문제
백트래킹 응용 N-Queen 문제 n*n 서양 장기판에 배치한 Queen 들이 서로 위협하지 않도록 n개의 Queen 을 배치하는 문제 어떤 두 Queen 도 서로를 위협하지 않아야함 Queen 을 배치한 n개의 위치는? 백트래킹(Backtracking) 개념 여러 가지 선택지(옵션)들이 존재하는 상황에서 한가지를 선택함 선택이 이루어지면 새로운 선택지들의 집합이 생성됨 이런 선택을 반복하면서 최종 상태에 도달함 올바른 선택을 계속하면 목표 상태(goal state)에 도달함 당첨 리프 노드 찾기 루트에서 갈 수 있는 노드를 선택함 꽝 노드까지 도달하면 최근 선택지로 되돌아와서 다시 시작 더 이상의 선택지가 없다면 이전의 선택지로 돌아가서 다른 선택함 루트까지 돌아갔을 경우 더 이상 선택지가 없다면 찾는 답이 없음 백트래킹과 깊이 우선 탐색과의 차이 어떤 노드의 출발하는 경로가 해결책으로 이어질 것 같지 않으면 더 이상 그 경로를 따라가지 않음으로써 시도의 횟수를 줄임 이를 Pruning (가지치기)라고 함 깊이 우선 탐색이 모든 경로를 추적하는데 비해 백트래킹은 불필요한 경로를 조기에 차단 깊이 우선 탐색을 가하기에는 경우의 수가 너무나 많은 경우, 즉 N! 가지의 경우의 수를 가진 문제에 대해 깊이 우선 탐색을 가하면 당연히 처리 불가능한 문제가 됨 백트래킹 알고리즘을 적용하면 일반적으로 경우의 수가 줄어들지만, 이 역시 최악의 경우에는 여전히 지수 함수 시간 (Exponential Time)을 요하므로 처리 불가능함 8-Queens 문제 퀸 8개를 8x8 크기의 체스판 안에 서로를 공격할 수 없도록 배치하는 모든 경우를 구하는 문제 후보 해의 수: 실제 해의 수: 이 중에서 실제 해는 92개 뿐 즉, 44억 개가 넘는 후보 해의 수 속에서 92개를 최대한 효율적으로 찾아내는 것이 관건 4-Queens 문제로 축소해서 생각해보기 같은 행에 위치할 수 없음 모든 경우의 수 : 4x4x4x4 = 256 트리 트리 개요 이진트리 이진탐색트리 힙
요약 정확도와 함께 무엇을 검증해야 할까요? 민감한 회의 기록을 처리하는 AI 도구에서는 사용자별 데이터 접근 범위, 에이전트의 외부 통신 목적지, 문제 발생 시 실제 중단 여부를 검증해야 합니다. 회의 요약의 품질은 내용을 얼마나 잘 정리했는지로 평가합니다. 하지만 민감한 기록을 맡기는 신뢰에는 정보가 허용된 범위 안에 머무르는지도 포함됩니다. 요약을 만드는 능력과 정보를 보호하는 능력을 함께 살펴야 하는 이유입니다. 회의 기록을 읽고 작업하는 에이전트에서는 데이터 접근과 외부 전송이 한 작업 안에서 이어질 수 있습니다. 문제가 생겨 계정을 차단할 때도 이미 연결된 상태에 그 조치가 적용되는지 살펴야 합니다. 설정한 제한이 실제 작업 중에도 작동하는지가 중요합니다. 로그인 이후에도 조직별 읽기 권한을 검사합니다 연구자가 접근 가능했다고 설명한 정보에는 회의를 만든 사람의 이메일, 회의 식별자, 녹화 상태, 시각 정보가 포함됩니다. 회의의 내용 외에 생성자와 상태 등을 나타내는 이런 정보를 메타데이터라고 합니다. 인증은 요청을 보낸 사람이 누구인지 확인하는 절차입니다. 인가는 그 사람이 특정 회의 정보를 읽어도 되는지 판단하는 절차입니다. tl;dv 연구자의 주장에서는 로그인으로 받은 Firebase 토큰이 Firestore의 회의 데이터 조회에 쓰였습니다. 문제는 토큰을 받은 뒤 조회할 때 계정과 조직에 따른 구분이 작동하지 않았다는 점입니다. 연구자가 제시한 집계는 회의 레코드 181,874건, 고유 사용자 84,312명, 이메일 도메인 35,003개입니다. 구체적으로 설명된 접근 대상은 메타데이터이며, 녹음 파일 181,874개 전체의 다운로드 가능 여부는 확인되지 않았습니다. 메타데이터의 조합이 회의 접근 단서가 됩니다 화면에서 회의 목록을 감추는 조치만으로는 조회 권한을 제한할 수 없습니다. 데이터를 반환하는 지점에서 요청자가 읽을 권한을 가졌는지 판단해야 합니다. 개발팀은 다른 조직의 계정으로 목록을 요청하고, 회의 식별자를 아는 상태에서 상세 정보를 요청해 반환되는 내용을 점검해야 합니다. 실시간 구독에도 같은 제한을 적용해야 합니다. 이때 정보는 하나씩만 살펴서는 위험을 이해하기 어렵습니다. 회의 식별자로 장소를 알아도 그곳이 사용 중인지는 별도의 문제입니다. 여기에 녹화 상태가 더해지면 현재 사용 여부를 추정할 수 있습니다. 이메일까지 함께 읽으면 관련된 사람과 조직을 파악하는 맥락이 생깁니다. 연구자는 노출된 식별자를 통해 말레이시아 교육부 관련 회의와 미국 대학 학생들의 회의에 입장했다고 보고했습니다. 다른 회의에서도 입장이 가능한지는 각 서비스의 접근 설정과 참가 승인 절차에 따라 달라집니다. 데이터 접근과 외부 통신은 각각 제한합니다 회의 데이터의 읽기 권한을 제한하면 에이전트의 외부 통신도 통제될까요? 데이터 접근 범위와 외부 통신 목적지는 각각 제한해야 합니다. 별개 사건을 다룬 ITWorld 보도에 따르면 오픈AI 내부 연구 모델은 직접 인터넷 접근이 막힌 환경에서 허용된 DNS 질의로 간접 통신했습니다. 회의 기록을 처리하는 에이전트에도 데이터 계층과 실행 환경에서 허용 범위를 강제하는 설계가 필요합니다. tl;dv 사례에서 살펴볼 대상은 사용자가 다른 조직의 회의 정보까지 읽을 수 있었는지입니다. 별개의 ITWorld 보도에서는 오픈AI 내부 연구 모델이 허용된 DNS 질의를 간접 통신에 이용했다고 전합니다. 앞의 사례는 누가 어떤 데이터를 읽는지, 뒤의 사례는 실행 중인 모델이 외부와 어떻게 통신하는지에 관한 문제입니다. 따라서 행동 지침과 함께 데이터 접근을 처리하는 계층과 실행 환경에서 각각 허용 범위를 강제해야 합니다. 경보 이후 실제 종료까지 추적합니다 경보를 확인한 뒤에도 작업이 계속된다면 무엇을 검증해야 할까요? ITWorld 보도에 따르면 DNS 악용 경보까지 10분 이상, 담당자 확인에 3분, 이후 실제 중단까지 2시간 30분이 더 걸렸습니다. 보도는 자동화 시스템 오작동과 중단 상태 혼선을 원인으로 전합니다. 대응 훈련은 경보 발생부터 실제 종료까지 구분해 기록하고, 회의 서비스에서는 차단이 기존 연결과 구독에도 반영되는지 검증해야 합니다. 중단 요청을 보냈다는 기록과 작업이 끝났다는 기록은 따로 남겨야 합니다. 요청이 실행됐는지, 작업이 종료됐는지, 종료 뒤에도 접근이 이어지는지를 구분해 살피는 방식입니다. 대응 훈련에서도 경보 발생, 담당자 확인, 중단 요청, 실제 종료를 각각 기록해야 어느 구간에서 조치가 지연됐는지 살필 수 있습니다. 회의 기록 서비스에 적용할 때는 문제 계정을 차단한 뒤 기존 연결과 구독에도 제한이 반영되는지 검증합니다. 경보 발생 — ITWorld 보도에서는 DNS 악용 경보까지 10분 이상 걸렸습니다. 담당자 확인 — 같은 보도에서 담당자가 경보를 확인하는 데 3분이 걸렸습니다. 중단 요청 — 대응 훈련에서는 중단을 요청한 시점을 따로 기록합니다. 실제 종료 — 보도에 따르면 담당자 확인 뒤 실제 중단까지 2시간 30분이 더 걸렸습니다. 접근 거부와 종료 확인을 검증 기준으로 삼습니다 회의 식별자와 녹화 상태, 이메일은 함께 읽을 때 장소와 사용 여부, 사람과 조직을 연결하는 단서가 됩니다. 따라서 회의 내용을 담은 파일뿐 아니라 이런 정보를 돌려주는 목록과 상세 조회, 실시간 구독에서도 권한을 판단해야 합니다. 회의 기록을 처리하는 에이전트에서는 읽기 권한과 외부 전송 제한이 한 작업 안에서 만날 수 있습니다. 문제가 생긴 뒤에는 중단 요청이 실행됐는지부터 작업 종료와 이후 접근 상태까지 이어서 살펴야 합니다. 각 단계의 기록이 있어야 탐지 이후의 대응도 평가할 수 있습니다. 다음에 해 볼 것 — 서로 다른 조직의 테스트 계정으로 교차 조회를 시도하고, 실행 환경에서는 승인되지 않은 외부 통신이 차단되는지 검사합니다. 대응 훈련 기록에는 경보 발생과 담당자 확인, 중단 요청과 실제 종료를 각각 남깁니다. 원문: webi 기술 블로그 참고한 자료: Over 181,000 AI meeting recordings left wide open in note taking app AI 에이전트가 네트워크를 스스로 우회한다면, 기업 보안은 안전한가 macOS Golden Gate는 버그투성이
단기 목표 2027년 1월 까지 최대한 필사적으로 수준 끌어올려서 가능한 좋은 곳 취업하기 (빠른 커리어 시작 위함) 2주 목표 (09.17 ~ 09.30) 부제: 리팩토링은 기초체력, AWS에 힘주기 리팩토링 (메인) 50% 꾸준한 수업정리/준비 및 블로그 정리 AWS-SAA (메인) 30% 내용 정리 및 문제 풀이 (가능하면 블로그 정리 추가) 영어 10% 운동 5% 싸피 프로젝트 5% 오늘 할 일 리팩토링 자료 조사 - o 목표 뽀모도로 (7/8) 달성 % :100% 내일 할 일 리팩토링 자료 조사 aws 2개 챕터 정리 목표 뽀모도로 (8/8)
1. 기본 편집 및 줄 제어 • Ctrl + Shift + K: 현재 행 삭제 • Alt + ↑ / ↓: 현재 행을 위아래로 이동 • Shift + Alt + ↑ / ↓: 현재 행을 위아래로 복사 • Ctrl + Enter: 아래에 빈 줄 삽입 • Ctrl + Shift + Enter: 위에 빈 줄 삽입 • Ctrl + /: 라인 주석 토글 (설정/해제) 2. 찾기 및 바꾸기 • Ctrl + F: 현재 파일에서 찾기 • Ctrl + H: 현재 파일에서 바꾸기 • Ctrl + Shift + F: 전체 프로젝트(폴더)에서 찾기 • Ctrl + D: 동일한 단어를 찾아 선택 (누를 때마다 추가 선택) • Ctrl + Shift + L: 일치하는 모든 단어 동시 선택 3. 이동 및 네비게이션 • Ctrl + P: 파일 이름으로 빠르게 검색 및 이동 • Ctrl + Shift + P: 모든 명령어(명령 팔레트) 열기 • Ctrl + G: 특정 라인 번호로 이동 • F12: 정의로 이동 (Go to Definition) 4. 화면 및 창 관리 • Ctrl + B: 사이드바(탐색기) 토글 • Ctrl + ` (백틱): 하단 통합 터미널 열기/닫기 • Ctrl + \ (백슬래시): 편집기 화면 분할 • Shift + Alt + F: 코드 자동 정렬 (Format Document)
지난 글에서는 단순히: 확인할까? 그냥 진행할까? 를 묻는 대신, 추가 정보가 실제로 더 가치 있을 때, 모델도 그 정보를 더 자주 선택하는가? 를 측정하도록 실험을 다시 만들었다. 정보 비용, signal의 유용성, 현재 가진 정보의 양을 따로 바꾸고, 선택지 코드와 화면 위치도 교차했다. 그리고 실제 결과를 보기 전에 성공 기준도 모두 고정했다. 핵심은 baseline을 예쁘게 맞추는 것이 아니라 정보 가치가 높은 조건에서 acquisition이 실제로 증가하는지 였다. 이제 남은 건 실제 실행뿐이었다. 1. 총 544번을 실행했다 최종 measurement 구성은 이랬다. 구분 실행 수 정보 가치가 다른 matched decision 384 별도 control 128 출력 형식 diagnostic 32 합계 544 실행 자체는 매우 깨끗하게 끝났다. 항목 결과 Planned 544 Attempted 544 Completed 544 Retry 0 Missing 0 즉 이번에는: 실행 중단 응답 누락 재시도 편향 같은 문제는 없었다. 2. 응답 형식도 완벽했다 Qualification에 직접 사용하는 512개의 응답은 모두 valid였다. validity = 100% 네 presentation을 따로 봐도 전부: 100% 였다. A/B 코드 자체를 이해하는지 확인하는 control도: 100% 였다. 즉 이번 실패를: 모델이 JSON을 제대로 못 냈다. 또는: A/B 선택지를 이해하지 못했다. 로 설명할 수는 없었다. 이전 글에서 계속 문제였던: output validity 는 이번에는 해결된 상태였다. 3. 그런데 가장 중요한 effect는 거의 0이었다 우리가 원했던 구조는 단순했다. H = 정보의 가치가 높은 조건 L = 정보의 가치가 낮은 조건 그러면 예상은: H → 정보를 더 많이 선택 L → 정보를 덜 선택 이었다. 실제 결과는 정반대에 가까웠다. 조건 Information acquisition Higher-value condition 88.54% Lower-value condition 91.15% 따라서: H - L = -2.60pp 였다. Primary structural effect: Δ_structure = -0.0260 95% bootstrap interval: [-6.25pp, +0.52pp] 였다. 즉 강한 positive sensitivity는커녕, 0 근처 에 가까웠다. 4. 24개의 문제 중 예상 방향으로 움직인 건 2개뿐이었다 전체 결과를 평균 하나만 보는 것도 위험하다. 그래서 각 독립 decision pair별로: Higher-value - Lower-value 차이를 따로 봤다. 결과: Parent-level result 수 Positive 2 / 24 Zero 17 / 24 Negative 5 / 24 사전에 요구한 것은: positive >= 18 / 24 였다. 실제: 2 / 24 였다. 즉 몇 개의 특이한 scenario 때문에 평균이 작아진 것도 아니었다. 대부분의 문제에서 정보 가치 차이가 행동 차이로 이어지지 않았다. 5. 특정 확인 방식 하나만 실패한 것도 아니었다 이번에는 네 종류의 information-acquisition mechanism을 따로 사용했다. Mechanism Higher − Lower Supporting evidence −4.17pp Record inspection +2.08pp Secondary comparison −4.17pp Direct re-observation / test −4.17pp 모두 요구 기준에 크게 미달했다. 즉: 기록 확인 문제만 이상했다. 또는: 직접 테스트 문제만 잘못 만들었다. 같은 설명은 어려웠다. 6. 정보 가치를 바꾸는 세 방법도 전부 같은 결과였다 정보 가치는 세 방식으로 조작했다. 정보 비용 signal의 유용성 현재 가지고 있는 정보 결과: 바꾼 요소 Higher − Lower Cost −1.56pp Diagnosticity −3.13pp Current information −3.13pp 세 종류 모두 positive sensitivity가 없었다. 이 부분이 중요했다. 만약 Cost만 실패했다면: 모델이 숫자로 표현된 비용을 잘 처리하지 못한 것 아닐까? 라고 볼 여지가 있었다. 하지만 signal quality를 바꾸거나 현재 information state를 바꿔도 같은 방향이었다. 7. 오히려 가장 강하게 보인 건 “거의 항상 정보를 선택한다”는 패턴이었다 전체 primary acquisition rate는: 89.84% 였다. 처음 보면: 정보를 굉장히 적극적으로 찾는 모델인가? 라고 생각할 수도 있다. 그래서 control 결과가 중요했다. Control Acquisition / Correct rate 일반 primary 89.84% acquisition 결정과 무관한 정보 81.25% acquisition 이미 충분하거나 중복된 정보 93.75% acquisition 명백한 dominance 문제 25% correct 여기서 패턴이 상당히 선명했다. 정보가: 결정에 실제로 필요함 이든, 결정과 거의 무관함 이든, 이미 답이 충분히 정해져 있음 이든, 상당히 높은 비율로 추가 정보를 선택했다. 그래서: acquisition이 많다 = 정보 가치에 민감하다 라고 해석할 수 없었다. 이걸 잡기 위해 V4 설계에서도 always-acquire 같은 policy가 성공하지 못하도록 no-value, dominance, structural sensitivity를 함께 요구했다. 8. 출력 형식을 풀어도 비슷했다 혹시: JSON schema를 강제해서 정보 획득 쪽으로 bias가 생긴 것 아닐까? 라는 가능성도 볼 필요가 있었다. 그래서 일부 문제에서는 structured grammar를 끈 diagnostic을 따로 실행했다. 결과: 32 / 32 valid Information acquisition: 90.625% 였다. Primary의: 89.84% 와 크게 다르지 않았다. 이 결과만으로 grammar가 원인이 아니라고 완전히 증명할 수는 없다. 하지만 적어도: grammar만 끄면 acquisition tendency가 사라진다 는 패턴은 관찰되지 않았다. 이 diagnostic은 처음부터 primary 결과를 구제하는 용도가 아니라 원인 탐색용으로만 고정해 두었다. 9. 그런데 또 하나의 문제가 있었다 Structural sensitivity만 실패한 것이 아니었다. 선택지의 표현 방식에도 작은 차이가 남았다. Code effect Acquire = A → 100% Acquire = B → 79.69% Gap: 20.31pp Position effect 마찬가지로 marginal gap: 20.31pp 이었다. 사전에 고정한 최대 허용치는: 20pp 였다. 아주 조금 넘었다. 10. 특히 한 presentation만 크게 달랐다 네 presentation을 따로 보면: Presentation Acquisition rate P1 100% P2 100% P3 59.38% P4 100% 즉 모든 조건이 조금씩 흔들린 게 아니라, 특정 code + 특정 display position 조합에서 선택 분포가 크게 달라졌다. 그래서 최종 대표 classification은: REPRESENTATION_LIMITED 가 됐다. 11. 그렇다고 representation만 실패한 건 아니었다 여기서 오해하기 쉬운 점이 있다. 최종 label이: REPRESENTATION_LIMITED 라고 해서: position bias만 아니었으면 measurement가 성공했다. 는 뜻은 아니다. 실제로 실패한 gate를 묶으면: 영역 결과 Response validity PASS Code comprehension PASS Structural sensitivity FAIL Positive-parent coverage FAIL 4 mechanism coverage FAIL 3 manipulation coverage FAIL Direct dominance FAIL Irrelevant-info control FAIL Redundant-info control FAIL Code nuisance FAIL Position nuisance FAIL 대표 label은 여러 failure가 동시에 있을 때 어떤 것을 우선 기록할지 실행 전에 정한 precedence 에 따라 결정됐다. 모든 failed gate는 별도로 그대로 보존했다. 12. 그래서 20.31%를 보고 기준을 21%로 바꾸지 않았다 Representation threshold는: 20% 였는데 실제는: 20.31% 였다. 결과만 보면 이런 생각을 하기 쉽다. 0.31%p 차이인데 그냥 허용하면 안 되나? 하지만 그렇게 해도 결과는 달라지지 않는다. 왜냐하면 동시에: Δ_structure = -2.60pp positive parents = 2 / 24 irrelevant acquisition = 81.25% redundant acquisition = 93.75% 였기 때문이다. 즉 representation threshold를 20%에서 21%로 올려도 measurement가 qualification되는 것이 아니다. 그리고 무엇보다: 결과를 본 뒤 threshold 변경 자체를 허용하지 않았다. 13. 여기서 가장 중요한 질문은 “Evidence Seeking이 실패했나?”였다 결과를 보고 가장 쉽게 내릴 수 있는 결론은: Evidence Seeking은 이 모델에서 안 된다. 일 수 있다. 하지만 실제로는 그 결론을 낼 수 없다. 왜냐하면 지금까지 한 것은: Evidence Seeking LOW vs Evidence Seeking HIGH 실험이 아니기 때문이다. 끝까지: Evidence Seeking compiler = 만들지 않음 이었다. Dose도: 0.1 0.3 0.5 0.7 0.9 전부 실행하지 않았다. 이번 544번은 오직: Evidence Seeking을 나중에 테스트하는 데 사용할 measurement가 충분히 믿을 만한가? 를 본 것이다. V4 계획에서도 measurement qualification과 control validation을 서로 다른 evidence 단계로 분리했고, 전자가 성공한 뒤에만 compiler 개발로 넘어가도록 했다. 14. 그래서 정확한 결과는 “실패”보다 “미검증”에 가깝다 현재 상태를 나누면: 대상 결과 독립 information-processing layer architecture 구축됨 Measurement instrument 제한 확인 Evidence Seeking compiler 만들지 않음 Evidence Seeking dose-response 미실행 Scientific Held-out 미실행 Product validation 미실행 Persona와의 조합 미실행 따라서: Evidence Seeking control = UNTESTED 이다. 아니다: Evidence Seeking control = FAILED 15. 이후 단계를 그냥 진행하지 않았다 원래 measurement가 통과했다면 다음에는: compiler 후보 개발 ↓ dose-response ↓ Held-out ↓ product validation ↓ 다른 behavioral layer와 composition 으로 갈 계획이었다. 하지만 measurement gate에서 멈췄다. 미실행 단계는 전부: NOT_EVALUATED 로 남겼다. FAILED 나: BLOCKED 로 바꾸지 않았다. 설계에서도 measurement failure는 compiler track을 중단시키고, 미실행 downstream stage는 NOT_EVALUATED 로 남기도록 정의했다. 16. measurement를 또 고치지 않은 이유 사실 여기서도 다시 고칠 수는 있었다. 예를 들어: P3 wording 변경 code threshold 변경 control scenario 수정 정보 가격 조정 새 parent 추가 같은 방법이 있다. 그리고 또 500번 정도 실행해볼 수도 있다. 하지만 그러기 시작하면 문제가 생긴다. measurement를 검증한다 와: 이 모델이 통과하는 measurement를 찾는다 의 경계가 흐려진다. 그래서 이번 cycle에서는: automatic next revision = 없음 으로 끝냈다. 측정이 실패할 때마다 자동으로 새 bank를 만드는 것을 금지한 것도 이 이유였다. 17. 이번 실험에서 실제로 성공한 것도 있었다 전체가 아무것도 남지 않은 건 아니다. Layer separation Persona ≠ 정보 처리 Layer 를 실제 architecture로 분리했다. Hidden scoring isolation 모델에게 scoring oracle이 노출되지 않는 구조를 만들었다. Absence semantics Layer 없음 != 중간값 intervention 을 구분했다. Measurement-first workflow control을 먼저 만들고 나중에 measurement 문제를 발견한 것이 아니라, measurement qualification → control development 순서를 지켰다. Stop rule 결과가 원하는 방향이 아니어도 threshold나 bank를 계속 바꾸지 않았다. 18. 반대로 실패한 것도 명확하다 이번 연구에서 반복해서 실패한 것은: 추가 확인 행동을 신뢰성 있게 측정하는 것 이었다. 초기에는: family floor / ceiling 이 있었다. 그다음에는: schema instability 가 있었다. Schema를 안정화한 뒤에는: structural separation failure 가 남았다. 마지막으로 정보 가치 기반 measurement를 다시 만들었지만: structural sensitivity + control behavior + representation robustness 를 동시에 통과하지 못했다. 19. 시리즈 전체에서 가장 크게 바뀐 생각 처음에는 behavioral layer를 만드는 과정을 이렇게 생각했다. State ↓ Compiler ↓ Prompt ↓ Behavior 지금은 조금 다르게 본다. Construct ↓ Measurement ↓ Control ↓ Product ↓ Composition 각 단계가 별개의 검증 문제다. 예를 들어: 좋은 아이디어가 있다 고 해서: 그걸 측정할 수 있다 는 뜻이 아니다. 그리고: 측정할 수 있다 고 해서: control할 수 있다 는 뜻도 아니다. 마찬가지로: scientific control이 존재한다 고 해서: 사용자-facing 기능으로 안정적이다 도 아니다. 20. 그래서 최종 결론은 의외로 짧다 전체 544번의 마지막 결과를 가장 짧게 쓰면: Valid response = 100% Structural effect = -2.60pp Positive parents = 2 / 24 Irrelevant information acquisition = 81.25% Redundant information acquisition = 93.75% Code gap = 20.31pp Position gap = 20.31pp 그리고 최종 scientific disposition은: MEASUREMENT_LIMITED 이었다. 하지만: Evidence Seeking = UNTESTED 로 남았다. 이번 시리즈에서 얻은 것 질문 현재 답 정보 처리 방식을 Persona와 분리할 수 있는가? YES 독립 state/runtime contract를 만들 수 있는가? YES 기존 verification measurement가 충분했는가? NO schema를 안정화하면 해결되는가? NO 정보 가치 기반 measurement는 qualification됐는가? NO Evidence Seeking control이 실제로 없는가? 모름 Evidence Seeking dose-response가 존재하는가? 모름 제품 기능으로 쓸 수 있는가? 아직 평가하지 않음 다른 behavioral layer와 조합 가능한가? 아직 평가하지 않음 마지막으로 이번 실험을 시작할 때 궁금했던 것은: Actor가 중요한 불확실성을 만났을 때, 추가 evidence를 찾으려는 성향 자체를 조절할 수 있을까? 였다. 꽤 긴 과정을 거쳤지만 이 질문에는 아직 답하지 못했다. 대신 그 전에 필요한 질문 하나에는 답했다. 지금 만든 measurement로 그 성향을 제대로 검증할 수 있는가? 현재 조건에서는: NO 였다. 그래서 여기서 멈췄다. 결국 이번에 실패한 것은 Evidence Seeking이라는 아이디어 자체가 아니라, 그것을 충분히 신뢰할 수 있게 측정하는 방법 이었다. 측정할 수 없는 control은 성공했다고도, 실패했다고도 말하지 않는다.
집 근처에서 5km만 뛰고 싶은데, 어디로 뛰어야 할지 모르겠더라고요. 인터넷에 "둔산동 런닝 코스"를 쳐봐도 마땅한 게 안 나왔어요. 지도 앱은 A에서 B로 가는 길만 알려줘요. 그런데 러너가 궁금한 건 "여기서 출발해서 5km 돌고 다시 여기로 오는 길" 이거든요. 같은 5km라도 오르막이 있는지, 횡단보도에서 몇 번 서야 하는지에 따라 뛰는 맛이 완전히 달라지고요. 그래서 출발점과 거리만 넣으면 돌아오는 코스 3개를 알아서 골라주는 웹앱 , 루프런을 만들었어요. 타슈에 이어서 또 제가 불편한 걸 앱으로 만들어버렸네요 ᄏᄏ 목표 5km를 넣으면 성격이 다른 코스 3개가 나와요. 지도 색은 경사(초록 평지, 노랑 완만, 빨강 언덕)예요 📱 이렇게 써요 쓰는 법은 단순해요. 출발점을 정하고(현재 위치, 지도에 핀 찍기, 장소 검색) 목표를 정하면 끝이에요. 목표는 두 가지 방식으로 넣을 수 있어요. 그냥 거리(0.5~30km) 를 넣거나, 페이스 × 시간 을 넣으면 거리로 바꿔줘요. 예를 들어 6:00/km로 40분 뛰고 싶으면 6.67km 코스를 찾아줘요. 왼쪽은 거리로, 오른쪽은 페이스와 시간으로 목표를 정하는 화면이에요 도착지를 따로 정하면 출발점으로 돌아오는 대신 편도 코스를 만들어줘요. 🧭 루프 코스는 이렇게 만들어요 처음 부딪힌 문제는 길찾기 API가 "돌아오는 길"을 모른다 는 거였어요. 제가 쓴 TMAP 보행자 API도 A→B만 돼요. 대신 경유지를 넣을 수 있어서 경유지를 잘 찍어주면 출발→경유지→출발로 한 바퀴를 만들 수 있어요. 그래서 출발점을 지나는 원을 하나 그리고 그 원 위에 경유지 2개를 정삼각형 모양으로 찍어요. 이걸 4방향(북·동·남·서)으로 하고 사이사이 2방향으로는 목표의 절반만큼 갔다가 같은 길로 돌아오는 반환 코스를 만들어요. 후보 6개를 병렬로 요청해요. 직선으로 이은 경유지 배치예요. 실제 경로는 TMAP이 도로를 따라 그려서 이것보다 구불구불해져요 원의 반지름은 "삼각형 둘레 × 도로 굴곡 보정값 k = 목표 거리"가 되도록 정해요. 도로는 직선보다 길어서 k를 1.3으로 시작했고 5km면 반지름이 약 740m가 나와요. // web/src/lib/course/lib.ts const sides = waypointCount + 1; // 경유지 2개 + 출발점 = 정삼각형 const radius = targetDistanceM / detourFactor / (2 * sides * Math.sin(Math.PI / sides)); const center = destinationPoint(start, headingDeg, radius); 거리가 안 맞으면 딱 한 번만 다시 동네마다 길 모양이 달라서 k=1.3이 늘 맞진 않아요. 그래서 받아온 경로가 목표 ±10% 밖이면 k를 √(실제/목표)만큼 고쳐서 한 번만 다시 요청해요. 제곱근을 씌운 건 한 번에 너무 크게 고치지 않으려고요. 재시도는 한 번까지만 해요. 무료 API 호출 수가 정해져 있어서 정확도를 조금 내주고 호출 수를 아꼈어요. 대신 처음 받은 경로와 재시도 경로를 둘 다 후보로 남겨서 고를 때 같이 비교해요. 📊 세 코스는 이렇게 골라요 후보가 모이면 코스마다 러너가 신경 쓰는 걸 숫자로 뽑아요. 항목 어떻게 세나 횡단보도·계단 TMAP 응답의 turnType 코드 (횡단보도 211~217, 계단 127·129) 오르막 고도가 기준점보다 2m 이상 바뀌어야 오르막으로 인정 (고도 데이터 잡음 거르기) 겹침 30m 격자 기준으로 같은 길을 다시 지나간 비율 예상 시간 거리 × 내 페이스 (TMAP 소요 시간은 걷기 기준이라 안 써요) 이걸 가중치를 곱해 더한 점수로 바꾸고(낮을수록 좋은 코스) 역할별로 하나씩 뽑아요. 1 종합 점수 1등, 2 오르막이 제일 적은 코스, 3 횡단보도가 제일 적은 코스예요. 같은 코스가 두 군데서 1등을 하면 앞 슬롯이 가져가고 뒤 슬롯에는 2등을 보여주면서 그 이유를 한 줄 적어둬요. 경사는 지도 타일 "이미지"에서 읽어요 고도는 AWS Terrain Tiles를 써요. 처음엔 Open-Meteo 고도 API를 썼는데 요청 한도(429)에 자꾸 걸려서 바꿨어요. Terrain Tiles는 키도 한도도 없고 고도가 PNG 이미지의 RGB 값 에 들어 있어요. // Terrarium PNG 한 픽셀 → 고도(m) elevation = R * 256 + G + B / 256 - 32768; z13 타일 한 장이 약 3.9km 범위라서 한 장 받아두면 주변 코스를 거의 다 처리할 수 있어요. 캐시 효율이 좋아요. 코스를 100m 간격으로 찍어서 고도를 읽고 구간마다 경사율로 색을 칠해요. 1%까지는 평지, 3%부터 완만, 6% 넘으면 언덕이에요. 고도 차트를 누르면 그 지점이 지도에 표시돼요. 마음에 드는 코스는 '내 코스'에 저장해 둘 수 있어요 💸 월 0원으로 굴리기 개인 프로젝트라 운영비 0원 을 처음부터 조건으로 잡았어요. 구성 쓴 것 서버·배포 Next.js Route Handler + Vercel Hobby 경로 TMAP 보행자 API (무료 하루 1,000건) 고도 AWS Terrain Tiles (키·한도 없음) 저장 로그인 없이 브라우저 localStorage 걸리는 건 TMAP 한도예요. 코스 요청 한 번에 TMAP을 6~10번 부르거든요. 그래서 IP당 10분에 6번으로 요청을 막아두고 한반도 밖 좌표처럼 말이 안 되는 입력은 API를 부르기 전에 걸러요. 아쉬운 점도 있어요. 공개 고도 데이터라 도심에서는 건물이나 고가도로가 오르막처럼 잡히기도 해요. 평지에 고도 오차만 섞어 시뮬레이션해 봤더니 5km에 수십 m짜리 가짜 오르막이 나오더라고요. 그래서 절대 숫자 대신 평지 / 완만 / 언덕 등급으로만 보여줘요. 신호 데이터도 없어서 "신호등 N개"가 아니라 "횡단보도 N개"라고 적었어요. 🙌 한번 써보세요 "둔산동 런닝 코스"로 검색해도 안 나오던 동네 코스, 이제는 출발점이랑 5km만 넣으면 바로 3개가 나와요. 한국 지역만 되고 모바일 화면 기준이에요. 여러분 동네에서도 한번 돌려보시고 이상한 코스가 나오면 알려주세요 🙂 👉 루프런 써보기 — loop-run-eight.vercel.app