🔒 IS NULL https://school.programmers.co.kr/learn/courses/30/lessons/157340 🖋문제 다음은 어느 자동차 대여 회사의 자동차 대여 기록 정보를 담은 CAR_RENTAL_COMPANY_RENTAL_HISTORY 테이블입니다. CAR_RENTAL_COMPANY_RENTAL_HISTORY 테이블은 아래와 같은 구조로 되어있으며, HISTORY_ID, CAR_ID, START_DATE, END_DATE 는 각각 자동차 대여 기록 ID, 자동차 ID, 대여 시작일, 대여 종료일을 나타냅니다. CAR_RENTAL_COMPANY_RENTAL_HISTORY 테이블에서 2022년 10월 16일에 대여 중인 자동차인 경우 '대여중' 이라고 표시하고, 대여 중이지 않은 자동차인 경우 '대여 가능'을 표시하는 컬럼(컬럼명: AVAILABILITY)을 추가하여 자동차 ID와 AVAILABILITY 리스트를 출력하는 SQL문을 작성해주세요. 이때 반납 날짜가 2022년 10월 16일인 경우에도 '대여중'으로 표시해주시고 결과는 자동차 ID를 기준으로 내림차순 정렬해주세요. 💻코드 SELECT CAR_ID, CASE WHEN SUM(CASE WHEN (START_DATE<='2022-10-16' AND END_DATE >='2022-10-16') THEN 1 #'대여중' ELSE 0 #'대여 가능' END ) > 0 THEN '대여중' ELSE '대여 가능' END AS AVAILABILITY FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY GROUP BY CAR_ID ORDER BY CAR_ID DESC 🔑풀이 일단 문제가 CAR_ID 별로 2022-10-16 에 대여 가능 상태인지. 즉, START_DATE ~ END_DATE 사이에 2022-10-16 이 있는 데이터가 있는지에 대한 문제이다. SELECT CAR_ID, FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY GROUP BY CAR_ID ORDER BY CAR_ID DESC 일단 CAR_ID 별로니까 GROUP BY CAR_ID 인 것 까지는 알겠는데.. 처음에는 CAR_ID 로 그룹핑하고 그 그룹내에서 16일에 대여중인 데이터를 뽑아내기 위해 HAVING을 쓰면되지 않을까 생각했다. HAVING (START_DATE<='2022-10-16' AND END_DATE >='2022-10-16') 하지만 HAVING을 사용하면 2가지 문제가 발생했다. 첫번째는 GROUP BY CAR_ID 를 하면 하나의 CAR_ID 그룹 안에 여러 개의 START_DATE , END_DATE 가 존재한다. HAVING에서 START_DATE <= 처럼 컬럼을 직접 사용하면 이 그룹에 START_DATE가 여러 개 있는데 어떤 행의 START_DATE를 기준으로 판단해야 하는가? 라는 문제가 생긴다. 두번째는 첫번째 문제를 무시한다 쳐도 HAVING은 그룹화된 결과에서 조건을 만족하는 그룹만 남기기 때문에 HAVING으로 대여중인 기록을 조건으로 걸면 대여중인 CAR_ID만 출력되고 대여 가능한 CAR_ID는 결과에서 사라진다. 문제에 대해 GPT에게 힌트를 물어본 결과 힌트는 집계함수 였다. SELECT COUNT(SCORE) FROM CAR GROUP BY ID 집계함수는 각 그룹의 여러행을 대상으로 계산을 해준다. 일단 집계함수는 그룹내에서 계산을 해주기 때문에 어떤 집계함수를 쓰면 좋을지 고민했다. 각 CAR_ID의 그룹 안에 2022-10-16에 대여중인 데이터가 하나라도 있는지를 확인하면 된다. 그래서 각 대여 기록을 먼저 CASE WHEN을 이용해서 1과 0으로 만들어서 그 합이 1 이상이라면 해당 그룹 내에 2022-10-16 대여중인 데이터가 있다는것을 파악할 수 있다. 이렇게 하면 각 대여 기록마다 2022-10-16 에 대여중인지 여부를 1 또는 0 으로 표현할 수 있다. CASE WHEN START_DATE <= '2022-10-16' AND END_DATE >= '2022-10-16' THEN 1 ELSE 0 END 물론 저렇게만 사용하면 HAVING을 사용할 때 발생하는 첫번째 문제와 같은 문제가 발생한다. 하지만 CASE WHEN 을 집계함수 안에서 사용하면, GROUP BY 로 만들어진 각 그룹의 행들을 대상으로 CASE WHEN 이 각각 평가되고, 그 결과를 집계함수가 계산한다. 그래서 SUM() 으로 더하면 해당 자동차가 2022-10-16 에 대여중인 기록이 몇 개 있는지 알 수 있다. SUM() > 0 인지 확인하면 해당 CAR_ID 에 2022-10-16 에 대여중인 기록이 하나라도 있는지 알 수 있다. 이 결과를 다시 CASE WHEN 으로 처리해서 1개 이상이면 대여중, 0이면 대여 가능으로 표시하면 된다. 🧐후기 GROUP BY 를 했다고 해서 그룹 안의 여러 행이 하나의 행으로 사라지는 것이 아니라, 집계함수를 이용하면 그룹 안의 여러 행을 대상으로 계산할 수 있다. WHERE 과 HAVING 은 조건에 맞지 않는 행을 제거하고 출력한다. WHERE 과 HAVING 차이는 WHERE 는 GROUP BY 이전에 개별 행을 필터링 하고 HAVING 은 GROUP BY 이후에 그룹을 필터링 한다. CASE WHEN 은 데이터를 필터링하는 것이 아니라 조건에 따라 값을 변환 할 수 있다. 집계함수 내에서 CASE 문을 활용할 수 있다.
구현 시작 할 일 단계 할 일 코드 작성 1 Python, Git, VS Code 설치 없음 2 프로젝트 열고 테스트·dry-run 없음 3 키 발급, Slack 연결, 실제 실행 없음 4 GitHub에 올리고 자동 실행 없음 — 운영하면서 코드 공부 병행 읽기 위주 5 MVP1 마무리: 수집 결과를 주간 리포트 파일로 저장 직접 작성 6 MVP2: 주간 보고 스킬 만들기 SKILL.md 작성 위 모든 명령어는 windows + PowerShell 기준으로 작성할 것이다. 1단계. 개발 환경 설치 일단 vscode 랑 python, git 등을 암것도 없는 내 회사 노트북에 설치해주었다. (해당 내용들은 생략) 도구 역할 Python 3.12 수집 프로그램을 실행하는 언어 Git 코드 변경 기록 관리 VS Code 코드 편집기 회사 노트북이라 관리자 권한이 없어서 셋 다 내 계정에만 설치하는 방식 을 골랐다. 2단계. 프로젝트 첫 실행 # (1) 자동 실행 설정 파일을 GitHub이 읽는 위치로 옮기기 mkdir .github move workflows .github\workflows # (2) 가상환경 만들기 python -m venv .venv # (3) 라이브러리 설치 .venv\Scripts\python -m pip install -r requirements.txt pytest # (4) 테스트 .venv\Scripts\python -m pytest -q # (5) Slack 없이 화면에만 출력 .venv\Scripts\python -m radar competitors --dry-run 한 줄씩 무슨 뜻인가 명령 하는 일 왜 필요한가 (1) .github\workflows 로 옮기기 자동 실행 설정 파일( radar.yml , tests.yml ) 위치 맞추기 GitHub은 이 경로에 있는 파일만 읽고 실행한다 (2) venv 이 프로젝트 전용 Python 공간 만들기 프로젝트마다 라이브러리 버전을 따로 관리. Java의 Gradle 의존성 관리와 비슷한 역할 (3) pip install -r requirements.txt 필요한 라이브러리 설치 requests (HTTP 호출), PyYAML (설정 파일 읽기) 두 개가 전부 (4) pytest 테스트 실행 인터넷·API 키 없이도 코드가 의도대로 동작하는지 확인 (5) --dry-run 실제로 수집하되, 보내지도 저장하지도 않음 결과만 눈으로 먼저 확인 activate 대신 .venv\Scripts\python 을 직접 부른 이유: PowerShell의 Activate.ps1 은 회사 PC 스크립트 실행 정책에 막히는 경우가 많다. 정책을 바꾸지 않고도 같은 효과를 낸다. 테스트 13개는 뭘 확인하나 인터넷에 실제로 접속하지 않고, 가짜 응답(Fake) 을 넣어서 코드의 판단 로직만 검사한다. 대상 확인하는 것 나라장터 수집 여러 키워드 결과 합치기와 중복 제거, 제외 키워드, 100건 넘을 때 페이지 넘기기, 인증키 오류 메시지 CVE 수집 CVSS 7.0 미만 거르기, 최신 점수 체계 우선 사용 페이지 감시 Oracle 패치 링크 찾기, 페이지 구조가 바뀌면 "0건"이 아니라 오류로 알리기 비교표 "EOL"이 "geolocation"에 걸리지 않게 단어 단위로 매칭, 오래된 칸 찾기, AI 판단이 실패하면 키워드 결과로 돌아가기 전체 흐름 같은 공고는 두 번 알리지 않기, 첫 실행엔 기준선만 저장, 한 출처가 실패해도 나머지는 계속 dry-run 결과 키가 없어도 NVD, GitHub, Oracle, Tomcat은 공개돼 있어서 실제 데이터가 나올 수 있다. NVD는 키가 없으면 요청 사이에 6.5초씩 쉬게 해 두어서 1분 정도 걸린다. 사내망에서 외부 사이트가 막혀 있으면 '수집 실패'가 보이는데, 이것도 정상이다. 멈추지 않고 실패를 보고하도록 만들었기 때문이다. 3. 이 프로그램은 어떻게 동작하나 흐름 python -m radar competitors │ ▼ __main__.py 명령어 해석, 설정(config.yaml)·비밀값(.env) 읽기 │ ▼ jobs.py 전체 순서 지휘 │ ├─▶ collectors/ 출처별로 데이터 수집 │ g2b.py 나라장터 API │ nvd.py CVE (NVD API) │ github_releases GitHub 릴리즈 │ page_watch.py API 없는 페이지 감시 │ ├─▶ state.py 이미 알린 것 거르기 (state/seen.json) ├─▶ comparison.py 비교표에서 다시 볼 칸 찾기 └─▶ notify/slack Slack으로 전송 │ ▼ 전송 성공 후 "본 것"으로 기록 → seen.json 저장 핵심 설계 네 가지 모든 수집기는 같은 모양( Item )을 돌려준다. 출처가 달라도 뒤쪽 단계는 데이터가 어디서 왔는지 신경 쓰지 않는다. 새 출처를 붙일 때 수집기 하나만 추가하면 된다. 같은 걸 두 번 알리지 않는다 (멱등성). 알린 항목의 ID를 state/seen.json 에 기록한다. 몇 번을 실행해도 새 것만 알린다. Slack 전송이 성공한 뒤에 기록한다. 전송이 실패하면 기록도 안 하니까, 다음 실행 때 다시 시도된다. 한 출처가 죽어도 나머지는 돈다. 출처마다 오류를 따로 잡아서, 실패한 사실은 Slack 메시지 맨 아래에 남긴다. 조용히 실패해서 "이번 주는 조용하네"로 착각하는 일을 막는다. 4. 배치 파이프라인이 정확히 뭔가 두 단어를 나눠 보면 이렇다. 용어 뜻 이 프로젝트에서 배치(Batch) 사람이 요청할 때마다가 아니라, 정해진 시간에 한꺼번에 처리하는 작업 평일 08:30, 월요일 09:00에 실행 파이프라인 데이터가 여러 단계를 차례로 거치는 흐름 수집 → 중복 제거 → 비교표 대조 → 전송 → 기록 그래서 배치 파이프라인 = 정해진 시간에 깨어나서, 데이터를 여러 단계로 처리하고, 끝나면 종료되는 프로그램. 반대 개념은 웹 서버 같은 상시 실행 서비스 다. 서버는 계속 켜져서 요청을 기다리지만, 배치는 할 일을 마치면 꺼진다. 이 프로젝트는 서버가 없다. 실행될 때만 잠깐 존재한다. 5. GitHub Actions의 역할, 그리고 이게 CI/CD인가 GitHub Actions란 GitHub이 제공하는 자동 실행 환경 이다. 정해진 조건(코드 push, 정해진 시간 등)이 되면 GitHub이 임시 가상 컴퓨터를 하나 빌려서 지정한 명령을 실행하고, 끝나면 그 컴퓨터를 버린다. 이 프로젝트에서 맡는 역할은 두 가지 파일 언제 실행되나 역할 이름 붙이면 tests.yml 코드를 push할 때마다 테스트를 자동으로 돌려서 깨진 코드가 들어오는지 확인 CI radar.yml 평일 08:30, 월 09:00 (cron) 수집 프로그램 실행 → Slack 전송 → seen.json 커밋 배치 실행 환경 (스케줄러) radar.yml 이 실행될 때 일어나는 일: GitHub이 임시 가상 컴퓨터(Ubuntu)를 켠다 저장소 코드를 내려받는다 Python을 설치하고 라이브러리를 설치한다 python -m radar bids 실행 (API 키는 GitHub Secrets에서 주입) 바뀐 state/seen.json 을 저장소에 커밋한다 → 다음 실행이 기억을 이어받는 방법 가상 컴퓨터는 사라진다 실행할 때마다 컴퓨터가 새로 만들어지고 사라지기 때문에, "이미 알린 것" 기록을 git 커밋으로 남겨야 한다. DB 대신 git 저장소가 상태 저장소 역할을 하는 셈이다. 그래서 CI/CD인가? 반은 맞고 반은 아니다. 용어 뜻 이 프로젝트 CI (Continuous Integration, 지속적 통합) 코드가 바뀔 때마다 자동으로 빌드·테스트 ✅ tests.yml CD (Continuous Delivery/Deployment, 지속적 배포) 테스트를 통과한 코드를 서버 등에 자동으로 배포 ❌ 배포할 서버가 없음 스케줄 배치 정해진 시간에 작업 실행 ✅ radar.yml radar.yml 은 CI/CD가 아니라 GitHub Actions를 스케줄러(cron) 대신 쓴 것 이다. 보통 회사에서는 Jenkins나 서버의 cron, Spring의 @Scheduled 로 하는 일을, 서버 없이 GitHub에 맡겼다. 정리하면: GitHub Actions는 도구이고, CI와 배치 실행은 그 도구로 하는 일 이다. 참고 GitHub의 cron 시간은 UTC 기준 이다. 한국시간 평일 08:30은 UTC로 전날 23:30이라 30 23 * * 0-4 (일~목)로 적었다. 예약 실행은 GitHub 사용량에 따라 몇 분~수십 분 늦어질 수 있다. 무료 계정의 Private 저장소는 월 사용 시간 한도가 있지만, 이 정도 작업이면 충분하다. 막혔던 것과 해결 문제 : 내 추측 : 실제 원인 : 해결 : 새로 배운 것 git config 는 GitHub 연결이 아니라 커밋 이름표다 배치 = 정해진 시간에 한꺼번에 처리하고 끝나는 작업, 파이프라인 = 단계를 차례로 거치는 흐름 GitHub Actions는 도구고, CI와 스케줄 배치는 그걸로 하는 일이다. 이 프로젝트에 CD는 없다 실행마다 사라지는 환경에서 상태를 이어가려면 어딘가에 저장해야 한다 (여기선 git 커밋) 다음 할 일 공공데이터포털 키 발급, Slack 웹훅 연결 로컬에서 실제 실행 → GitHub에 올리고 자동 실행 걸기
네트워크 면접 질문 #1 - L2(Data Link Layer)와 L3(Network Layer)의 차이 ❓ 면접 질문 L2와 L3의 차이점을 설명해주세요. OSI 7계층에서 L2와 L3 OSI 7계층은 네트워크 통신을 역할별로 나눈 모델이다. 그중 L2(Data Link Layer) L3(Network Layer) 는 실제 네트워크 통신에서 가장 많이 등장하는 계층이다. 실무에서도 "L2는 된다." "L3가 안 열린다." 와 같은 표현을 자주 사용한다. L2(Data Link Layer) L2는 같은 네트워크 안에서 실제 장비(MAC 주소)까지 데이터를 전달하는 계층 이다. 대표적인 특징은 다음과 같다. 항목 설명 주소 MAC Address 대표 장비 Switch 전송 단위 Frame 대표 프로토콜 Ethernet L2에서는 MAC Address를 이용하여 같은 네트워크 내부의 장비를 찾는다. 예를 들어 PC1 ↓ Switch ↓ PC2 처럼 같은 LAN 안에서 통신하는 역할을 담당한다. MAC Address란? MAC Address는 랜카드(Network Interface Card)에 부여된 고유한 물리 주소이다. 예를 들어 00:1A:2B:3C:4D:5E 와 같은 형태를 가진다. IP 주소는 변경될 수 있지만 MAC 주소는 일반적으로 장비 고유의 주소이다. L3(Network Layer) L3는 목적지 네트워크(IP)를 찾아 데이터를 전달하는 계층 이다. 대표적인 특징은 다음과 같다. 항목 설명 주소 IP Address 대표 장비 Router 전송 단위 Packet 대표 프로토콜 IP L3는 현재 목적지가 어느 네트워크에 있는지를 판단한다. 즉 192.168.0.10 ↓ 10.10.20.30 처럼 다른 네트워크까지 데이터를 전달하는 역할을 한다. Switch와 Router의 차이 Switch는 MAC Address를 보고 같은 네트워크 내부에서 데이터를 전달한다. PC ↓ Switch ↓ Printer Router는 IP Address를 보고 다른 네트워크로 데이터를 전달한다. 회사 네트워크 ↓ Router ↓ 인터넷 ↓ 네이버 서버 즉 Switch는 L2 Router는 L3 장비이다. L2와 L3를 쉽게 이해하기 택배를 예로 들면 IP 주소는 서울특별시 강남구 ○○로 처럼 목적지 주소이다. MAC 주소는 101호 홍길동 처럼 건물 안에서 실제 누구에게 전달할지를 의미한다. 즉 IP ↓ 어느 건물? ↓ MAC ↓ 몇 호? 라고 이해하면 쉽다. 왜 L2와 L3를 같이 알아야 할까? 예를 들어 192.168.0.20 으로 데이터를 보내려고 한다. L3에서는 목적지 IP 확인 까지는 가능하다. 하지만 Ethernet 통신을 하려면 목적지의 MAC 주소도 알아야 한다. 그래서 IP 확인 ↓ MAC 모름 ↓ ARP 요청 ↓ MAC 확인 ↓ Frame 전송 과정이 수행된다. 즉 ARP는 L3(IP)와 L2(MAC)를 연결하는 프로토콜이다. 그래서 L2와 L3를 이해해야 ARP도 이해할 수 있다. 실무에서 자주 듣는 표현 "L2는 된다." 보통 케이블 연결 Switch 연결 VLAN 등 L2 통신은 정상이라는 의미로 사용된다. "L3가 안 된다." 보통 IP 설정 Subnet Gateway Routing 문제를 의미하는 경우가 많다. 예를 들어 ping 실패 라면 L3 문제를 의심할 수 있다. "L3는 되는데 포트가 안 열린다." 예를 들어 ping 성공 하지만 10.0.0.20:8080 접속이 안 된다면 L3는 정상이다. 대신 L4(TCP/UDP) 또는 Port Firewall 서버 프로세스 문제를 의심해야 한다. 핵심 정리 L2 L3 MAC Address IP Address Switch Router Frame Packet 같은 네트워크 통신 다른 네트워크 통신 Ethernet IP 면접 답변 L2(Data Link Layer)는 MAC 주소를 이용하여 같은 네트워크 내부에서 데이터를 전달하는 계층이며, 대표적인 장비는 Switch입니다. L3(Network Layer)는 IP 주소를 이용하여 다른 네트워크까지 데이터를 전달하는 계층이며, 대표적인 장비는 Router입니다. 실제 통신에서는 L3에서 목적지 IP를 결정한 후, ARP를 이용해 목적지의 MAC 주소를 알아내어 L2에서 Ethernet Frame을 전송합니다.
오늘은 프로젝트의 더블노드 구성을 돌아보다가 질문 하나가 생겼다. “우리 분산 처리한 부분 있잖아. Kafka를 쓸지, YARN을 쓸지 선택했던 거야?” Kafka도 분산 시스템이고, YARN도 분산 처리할 때 등장한다. 여기에 Kubernetes까지 나오니 머릿속에서는 전부 비슷한 자리에 놓여 있었다. 이름은 들어봤는데, 서로 무엇을 대신할 수 있는지는 흐릿했던 것이다. 이 질문을 시작으로 이야기를 이어가다 보니 프론트엔드와 백엔드 뒤에 있는 구조가 조금씩 보이기 시작했다. 나중에 또 헷갈릴 것 같아서, 오늘 이해한 흐름을 남겨보려고 한다. 이 글은 기술별 도입 가이드보다는, 각 기술이 서비스의 어느 부분을 맡는지 정리한 학습 기록이다. 일단 Kafka와 YARN은 같은 선택지가 아니었다 가장 먼저 정리한 건 역할이었다. 기술 맡는 역할 떠올릴 질문 Kafka 이벤트를 받아 보관하고 전달한다 들어오는 데이터를 어떻게 전달하고 다시 읽을까? Spark 데이터를 나눠 실제 계산을 수행한다 이 많은 데이터를 어떻게 집계할까? YARN 여러 서버의 실행 자원을 관리한다 계산할 프로그램에 어느 서버의 CPU와 메모리를 줄까? HDFS 파일을 저장하고 접근하게 한다 처리할 원본과 결과를 어디에 둘까? 이렇게 놓으니 조금 명확해졌다. Kafka와 YARN은 둘 중 하나를 골라 끼우는 부품이 아니었다. Kafka는 데이터가 흘러가는 쪽에 있고, YARN은 그 데이터를 처리할 프로그램의 실행 자원을 관리하는 쪽에 있다. 실제 계산은 Spark가 한다. 예를 들어 이런 조합도 가능하다. 수집 프로그램 → Kafka → Spark → 처리 결과 저장 ↑ 실행 자원은 YARN이 관리 물론 모든 서비스에 이 구성이 필요한 것은 아니다. 중요한 건 함께 사용할 수 있는 서로 다른 역할이라는 점이다. 그럼 우리 더블노드에서는 누가 무엇을 했을까? 우리 Classet에서 이야기한 더블노드는 기존 데이터를 Spark로 처리하면서, 데이터 서버와 서비스 서버 양쪽에 계산을 맡기는 구성이었다. HDFS 는 양쪽에서 접근할 입력·출력 파일을 제공했다. YARN 은 두 서버에서 Spark가 실행될 자원을 관리했다. Spark executor 는 할당된 자원 위에서 실제 계산 작업을 수행했다. executor는 Spark 애플리케이션의 계산 작업을 실행하는 프로세스다. Spark의 driver가 작업을 조정하고 executor들에게 태스크를 보낸다. 우리 구성에서는 서비스 서버에 YARN의 NodeManager를 추가해 계산에 참여시켰다. 여기서도 하나 구분할 게 있다. 계산에 참여하는 서버가 두 대라는 것과, HDFS 저장 노드를 두 대로 늘렸다는 것은 다른 이야기다. 해당 분산 처리 실험에서는 HDFS DataNode를 추가하지 않았다. 이제 이 작업을 설명한다면 이렇게 말하는 게 맞다. “Spark의 분산 연산을 두 EC2에 배치하기 위해 YARN을 활용했다.” 기존 파일을 묶어서 계산하는 작업이었으니, 두 노드로 분산하기 위해 Kafka를 추가할 필요는 없었다. 또 이 구성은 필요할 때 전환해서 사용한 것이지, 두 노드가 항상 떠 있다는 의미도 아니다. Kafka는 실시간 처리할 때만 쓰는 걸까? 처음에는 “아, Kafka는 실시간성이 중요할 때 쓰는 거구나” 하고 이해했다. 틀린 방향은 아니지만, 그것만으로 설명하기에는 빠지는 부분이 있었다. Kafka는 계속 들어오는 데이터를 다룰 때 유용하다. 동시에 데이터를 만드는 쪽과 처리하는 쪽의 속도가 달라도 일을 이어갈 수 있게 해준다. 뉴스가 평소에는 조금씩 들어오다가 어느 순간 몰린다고 해보자. 처리 프로그램이 전부 즉시 처리하지 못해도 Kafka에 보관된 이벤트를 자기 속도에 맞춰 읽을 수 있다. 서로 다른 프로그램이 같은 이벤트를 각자 활용하거나, 보관 기간 안의 데이터를 다시 읽는 것도 가능하다. 주문 서비스로 생각하면 더 와닿는다. 주문 발생 → 주문 데이터 저장 → 사용자에게 응답 │ └→ 주문 이벤트 → Kafka ├→ 알림 처리 ├→ 매출 통계 └→ 추천 반영 주문 하나 때문에 알림, 통계, 추천이 모두 끝날 때까지 기다릴 필요가 없도록 후속 작업을 나눌 수 있는 것이다. 다만 그림처럼 선을 연결했다고 끝나는 건 아니다. 주문은 저장됐는데 이벤트 전달에 실패하면 어떻게 할지, 같은 이벤트를 두 번 받으면 어떻게 할지, 통계 반영이 늦어져도 괜찮은지 등을 설계해야 한다. Kafka를 넣으면 자동으로 모든 처리가 실시간이 되는 것도 아니다. 소비자가 처리하는 속도가 느리면 데이터는 계속 쌓인다. 도구가 해결해 주는 문제와 새로 관리해야 하는 문제가 함께 생긴다. Kubernetes는 YARN과 조금 더 가까웠다 그다음 질문은 자연스럽게 이거였다. “그러면 Kubernetes는 또 다른 역할이야?” Kubernetes는 여러 서버에서 컨테이너가 어디에, 몇 개, 어떤 상태로 실행될지 관리한다. 컨테이너가 종료되면 원하는 실행 상태를 회복하고, 배포나 확장도 관리한다. YARN과는 관리하는 대상과 방식에 차이가 있지만, Spark를 여러 서버에서 실행할 자원을 관리한다는 면에서는 역할이 겹친다. 그래서 Spark를 실행할 클러스터 관리자를 고른다면 다음이 비교 대상이 된다. YARN Spark Standalone Kubernetes 우리는 기존 Hadoop·YARN 환경을 확장해서 두 서버가 계산에 참여하도록 구성했다. Kubernetes를 사용했다면 Spark 실행 환경도 그에 맞게 구성해야 했을 것이다. 그리고 Docker와 Kubernetes도 구분해야 한다. Docker는 프로그램과 필요한 환경을 이미지로 묶어 컨테이너로 실행하는 데 사용하고, Kubernetes는 여러 서버에 걸친 컨테이너 실행을 관리한다. Docker를 쓴다고 Kubernetes가 반드시 따라오는 건 아니다. 작은 서비스라면 한 서버와 Docker Compose로도 충분히 운영할 수 있다. 여기서부터 서비스가 조금 다르게 보이기 시작했다 지금까지 서비스를 떠올릴 때 가장 먼저 그렸던 구조는 이거였다. 사용자 → 프론트엔드 → 백엔드 → DB 틀린 그림은 아니다. 사용자의 요청이 어떻게 처리되는지 잘 보여준다. 다만 이 그림에는 아직 답하지 않은 질문이 많다. “백엔드는 어느 컴퓨터에서 계속 실행하지?” “사용자가 입력한 도메인은 어떻게 그 서버를 찾아가지?” “DB에 들어 있는 통계 데이터는 누가 만들어 놓았지?” “프로그램이 죽거나 데이터 갱신이 멈추면 어떻게 알지?” 이 질문들을 따라가면 서비스에 적어도 세 가지 흐름이 있다는 걸 알 수 있다. 1. 사용자의 요청을 처리하는 흐름 사용자가 상품 목록을 열면 프론트엔드가 API를 호출하고, 백엔드가 DB를 조회해서 결과를 돌려준다. 실제 환경에서는 도메인을 주소로 연결하는 DNS, 통신을 암호화하는 HTTPS, 요청을 전달하는 프록시나 로드밸런서도 관여한다. 브라우저 → 서비스 진입점 → 백엔드 → 서비스 DB HTTPS 요청 전달·분산 프론트엔드와 백엔드가 하나씩 있다고 서버도 두 대인 건 아니다. 한 서버에 여러 프로그램을 띄울 수도 있고, 같은 백엔드를 여러 서버에 복제해서 실행할 수도 있다. 2. 서비스에 필요한 데이터를 준비하는 흐름 오늘의 인기 상품, 월별 매출 통계, 수많은 뉴스에서 뽑아낸 이슈는 처음부터 DB에 완성된 형태로 들어 있지 않다. 원본을 가져오고, 중복과 오류를 정리하고, 집계나 분석을 거쳐 만들어야 한다. 외부 API·파일·사용자 행동 기록 ↓ 수집 ↓ 원본 저장 ↓ 정제·분석·집계 ↓ 서비스용 결과 저장 ↓ 백엔드에서 조회 이 과정이 데이터 파이프라인이다. 예를 들어 최근 5년 통계를 보여줄 때마다 원본부터 전부 계산하면 화면이 느릴 수밖에 없다. 미리 월별로 집계해 두면 백엔드는 작은 결과만 읽으면 된다. 빠른 API 뒤에 미리 계산해 둔 데이터가 있을 수 있다는 것. 오늘 이해한 내용 중 특히 기억해 두고 싶은 부분이다. 3. 앞의 두 흐름을 실행하고 지켜보는 흐름 작업이 매일 정해진 시간에 실행돼야 하고, 수집에 성공했을 때만 정제를 시작해야 한다면 작업의 일정과 순서를 관리해야 한다. 서버가 여러 대라면 프로그램을 어디에 배치할지도 정해야 한다. 실행 중에 오류가 나면 알아차릴 방법도 있어야 한다. 여기에 Airflow, YARN, Kubernetes, 모니터링 도구가 등장한다. 도구 주로 관리하는 것 Airflow 작업의 일정, 순서, 의존 관계, 재시도 YARN 데이터 처리 애플리케이션의 실행 자원 Kubernetes 컨테이너의 배치, 개수, 실행 상태 Prometheus 시스템과 애플리케이션의 수치 지표 수집 Grafana 지표 등을 대시보드로 시각화 Loki 로그 수집·저장·조회 Airflow가 직접 Spark의 집계 연산을 대신하는 것은 아니다. 예를 들면 이런 관계다. 매일 새벽 Airflow가 Spark 작업을 시작한다. Spark는 YARN에서 실행 자원을 확보한다. executor들이 계산을 마치면 Airflow가 다음 결과 반영 작업을 시작한다. 전부 “관리 도구”처럼 들렸는데, 무엇을 관리하는지 나누니 차이가 보였다. 실시간과 분산도 따로 생각해야 했다 두 개를 자꾸 한 덩어리로 생각해서 함께 정리했다. 구분 질문 배치·스트리밍 데이터를 모아서 작업 단위로 처리할까, 계속 들어오는 데이터를 지속적으로 처리할까? 실시간성 들어온 데이터가 결과에 반영되기까지 얼마나 걸릴까? 단일·분산 한 컴퓨터에서 실행할까, 여러 컴퓨터가 나눠 실행할까? 과거 10년치 데이터를 여러 서버에서 계산하면 분산 배치 처리 다. 실시간으로 들어오는 데이터라도 양이 적으면 한 서버에서 처리할 수 있다. Spark와 Flink도 각각 배치 전용, 실시간 전용으로 외우면 안 된다. 둘 다 배치와 스트리밍을 지원하며, 어떤 처리 방식과 지연 요구에 맞는지를 봐야 한다. 이름이 너무 많아서 역할별로 놓아봤다 새로 알게 된 이름을 전부 깊게 파기보다는, 일단 어느 위치에 있는지 정리했다. 같은 줄에 있다고 완전히 같은 도구라는 뜻은 아니다. 역할 대표 기술 외부 시스템 연결·변경 데이터 수집 Kafka Connect, Debezium 이벤트 전달·작업 큐 Kafka, RabbitMQ 대량 원본 보관 HDFS, Amazon S3 데이터 계산 Spark, Flink 작업 순서·일정 관리 Airflow, Dagster SQL 중심 분석 데이터 변환 dbt 분석용 데이터 저장·조회 BigQuery, Snowflake, ClickHouse 여러 저장소 통합 SQL 조회 Trino 서버·네트워크 같은 인프라 생성 Terraform 패키지 설치·서버 설정 자동화 Ansible 빌드·테스트·배포 자동화 Jenkins, GitHub Actions Kubernetes 배포 상태를 Git 설정과 동기화 Argo CD HDFS는 분산 파일 시스템이고 S3는 객체 저장소다. Kafka와 RabbitMQ도 메시지를 전달한다는 공통점이 있지만, 이벤트 보관·재처리와 작업 큐·라우팅 등에서 주로 활용하는 방식이 다르다. 이 표는 도입 후보를 바로 정하는 비교표보다는, 공부할 때 길을 찾기 위한 지도에 가깝다. 그렇다고 전부 넣어야 하는 건 아니다 이름을 이렇게 늘어놓고 보니 “서비스 하나 만들려면 이걸 다 알아야 하나?” 싶은 생각도 든다. 하지만 실제로 필요한 구성은 서비스의 요구와 문제에 따라 달라진다. 처음에는 서버 한 대에 프론트엔드, 백엔드, DB를 두고 기본적인 배포·백업·모니터링을 갖추는 것으로 충분할 수 있다. 데이터가 커져서 계산을 나눠야 할 때 분산 처리를 검토하고, 여러 후속 작업을 분리해야 할 때 메시지 전달 구조를 검토하고, 실행할 컨테이너와 서버가 많아질 때 관리 방식을 고민하는 것이다. 운영에서는 “떠 있다”와 “정상이다”도 다르다. API가 살아 있어도 응답이 너무 느릴 수 있고, 화면은 멀쩡해도 파이프라인이 실패해서 어제 데이터가 보일 수 있다. 서버 자원뿐 아니라 응답 시간, 오류율, 데이터의 마지막 갱신 시각도 봐야 한다. 오늘 바뀐 질문 오늘 대화가 끝날 때 이런 말을 했다. “지금까지 백엔드, 프론트엔드가 중요하다고 생각했는데, 지금 완전 그 뒤쪽 이면 구조를 파악해 가는 과정인 것 같아.” 화면을 만들고 API를 연결하는 일이 서비스의 큰 부분인 건 여전하다. 이제는 그 코드가 어디에서 실행되고, 화면에 나올 데이터가 어떻게 준비되며, 그 과정이 계속 돌아가도록 무엇이 받쳐 주는지도 함께 보인다. 다음에 새로운 기술 이름을 만나면 먼저 세 가지를 물어보려고 한다. 무엇을 입력받고, 무엇을 내보낼까? 데이터를 저장·전달·계산할까, 프로그램의 실행을 관리할까? 지금 우리 서비스의 어떤 문제를 해결해 줄까? Kafka와 YARN 중 뭘 고르는지 묻던 데서 시작했는데, 이제는 각 기술이 들어갈 자리를 먼저 찾아볼 수 있을 것 같다. 참고 자료 Apache Kafka 소개 Spark 클러스터 구조 Kubernetes 개념 Airflow 소개 Prometheus 시작하기
ChatGPT나 Claude에게 "다운로드 폴더 정리해줘"라고 하면, 보통은 방법만 알려주고 끝납니다. 그게 답답해서 AI가 내 Mac에서 직접 일하게 하는 도구 를 만들었습니다. 이름은 BCD입니다. 어떻게 동작하나 Mac에 작은 에이전트를 설치합니다 (터미널에 curl -fsSL https://bcd.snack-wrap.com | bash 한 줄). ChatGPT·Claude·Codex에 원격 MCP 커넥터( https://bcd.snack-wrap.com/mcp )를 추가합니다. 이제 채팅에서 부탁하면 AI가 BCD 도구로 내 Mac의 파일을 읽고, 옮기고, 명령을 실행하고, 결과를 확인합니다. 폰의 ChatGPT에서도 됩니다. 실제로 해본 것 (전부 실제 화면) 1. 다운로드 폴더 정리 "Desktop의 Downloads demo 폴더를 파일 종류별로 정리해줘" → 파일 23개가 폴더 9개로 정리됐습니다. https://youtube.com/shorts/IGgzeGHeYdI 2. 회의록 PDF에서 할 일만 "회의록 PDF 3개 읽고 아직 안 끝난 할 일만 담당자·마감일과 함께 뽑아줘" → 끝난 건 빼고 4개만. https://youtube.com/shorts/m8BgBhEcyuY 3. 엑셀 안 열고 매출 비교 "sales_2026.xlsx에서 9월과 8월 매출을 제품별로 비교해줘" → 비교표가 채팅에 바로. https://youtube.com/shorts/pinaw3FoAN4 4. 영상 편집 "raw.mp4를 0:20~0:32만 9:16 쇼츠로, 2배속, 위에 제목 넣어서 저장해줘" → Mac의 AVFoundation으로 편집. CapCut·ffmpeg 없이, 업로드도 없이. https://youtube.com/shorts/Lij26gsAEjU 안전장치 AI에게 내 컴퓨터를 맡기는 거라 이 부분에 가장 신경 썼습니다. Mac 에이전트는 바깥으로만 연결합니다. 포트를 열지 않습니다. 새 컴퓨터는 읽기 전용 으로 시작합니다. 쓰기·명령 실행·화면 제어는 대시보드에서 컴퓨터마다 직접 켭니다. 허용한 폴더 밖 경로는 막히고, 심볼릭 링크로 빠져나가는 것도 막습니다. 공식 MCP Registry에 com.snack-wrap.bcd/bcd 로 등재돼 있습니다. 써보기 Mac 1대는 무료이고, 가입하면 한 달간 Personal(Mac 3대)을 무료로 씁니다. 카드 등록은 없습니다. 👉 https://bcd.snack-wrap.com/?lang=ko&utm_source=velog 10월 6일(월) 오후 4시에는 Product Hunt에도 올립니다. 써보시고 막히는 곳이나 "AI가 이것도 해줬으면" 하는 게 있으면 댓글로 알려주세요.
안녕하세요. 저는 원티드에서 주관하는 AI 해커톤 대회에 참가한 남예지입니다. 사진 세 장 올리면 웨딩 화보 만들어 주는 기능을 넣어 홈페이지를 만들어 보았습니다. 프로젝트 명은 STUDIO ZERO PROJECT : 스드메에서 '스'를 뺐습니다 입니다. 투표를 독려하고 싶으나 표를 받지 않더라도 제 프로젝트가 아무에게도 닿지 않아 시현조차 돌려지지 않는다면 열심히 만들었는데 아쉬울 것 같아 이렇게 글을 작성해 봅니다. 또한 이 프로젝트를 어떻게 기획하게 되었고, 어떤 어려움이 있었는지, 해결 과정과 경험을 기록하고자 하는 용도도 있습니다. 우선 아래 링크로 들어가시면 투표 사이트가 나오니 한 번 씩 구경해보시기 바랍니다. 홈페이지에서 실제 사진을 넣어 시현을 돌려 볼수도 있습니다. (증명사진을 추천드립니다.) https://event.wanted.co.kr/ai-championship/2026/projects/57 목적 저의 이야기를 잠깐 해야될 것 같습니다. 흔히 몇 년을 사귀어야 장기연애로 불리우는지 개인마다 다를테지만, 저는 8년차 연애중으로 나름 주변에서 장기연애 커플로 불리고 있습니다. 그러다보니 다른 이들의 결혼 정보에 더 관심을 기울이게 되더라고요. 이제 나이가 나이인지라 주변에서 결혼을 많이들 해서 경험담을 자주 들어볼 수 있었습니다. 스튜디오 촬영을 위해선 반드시 하루를 빼고, 그 많은 사진 중 셀렉해서 보정맡기는게 일이라고요. 비용은 100~200만원에 피로도가 상당하다고 했습니다. 들어보니 정말 그럴거 같더군요. 드레스도 고르고, 메이크업도 하고... 생각해보니 이게 본식이랑 다른게 별로 없는거예요. 본식날 예쁘게 차려입고, 예쁜 결혼식장을 배경으로 사진을 엄청 남기는 걸 봤거든요. 그럼 저 스튜디오 사진은 정말 프로필 사진/결혼식장 DP/청첩장 정도의 쓸모만 있구나. 그렇다면 나는 그냥 AI로 뽑고 싶은데.(진심입니다) 이게 STUDIO ZERO PROJECT 의 시작이었습니다. 부르는게 값인 웨딩시장에서 드레스, 메이크업과 스튜디오, 사진작가를 정하는데 들어가는 시간과 비용 + 촬영에만 하루라는 시간 + 카메라 앞에서 어색하게 경직되는 경험들... 나처럼 웨딩 촬영에 로망이 없거나, 비용과 피로도는 줄이고 싶은 그런 사람들이 있지 않을까? 그런 사람들에게 도움이 되는걸 이 프로젝트의 목적 달성 1순위로 잡았습니다. 제 생각은 그래요. 어차피 그거 본식날에 다 할텐데. 예쁘게 입고, 메이크업 하고, 사진 남기기. 이런 경험 2번할 필요 있을까요? (반박 시 여러분 말이 맞음) 스드메에서 '스'를 뺐습니다 — 2주 개발 기록 브레인스토밍 GPT API 연결하기 아 그러면 요즘 GPT가 사진을 잘 뽑으니 GPT API를 연결해보면 되겠군. 가설을 세우고 바로 실험을 돌려보니 실제로 어느정도 가능했고, 퀄리티도 좋았습니다. 문제는 예상 금액이 너무 높았습니다. 장당 대략 500원이 예상 되었거든요. 모델 직접 만들기 내가 생성형 AI를 학습시켜서 얼굴만 넣으면 그 사람처럼 그려주면 될까? 생각해봤는데요. 찾아보니 가능은 하지만 실패 가능성과 희박한 시간으로 접었습니다. 체험을 무료로 열어 둘 생각이었는데, 학습 방식은 누가 한 번 눌러 볼 때마다 GPU가 몇 분씩 돌아요. 실험 기한도 길어질 것으로 예상해 대회기간 내에 무리라고 판단했습니다. 이미 있는 모델 사용하기 서치 결과 Face ID라는 모델이 있었는데요. 라이센스가 문제였습니다. 상업용으로는 안된다는 건데요. 개발 당시에는 잘되면 상업적으로 판매까지 생각했던 터라 FACE ID와 비슷한 다른 상업 사용이 가능한 모델을 선택했습니다. 최종적으로 이 방법이 금액도 제일 저렴했어요. 금액 계산 : 한 사람이 한 번 주문하면 화보 여덟 장이 나갑니다. 그걸 기준으로 계산해 봤습니다. 방향 여기서 한 번 크게 생각을 바꿨어요. 처음에는 "사진 몇 장을 넣으면 AI가 웨딩 화보를 그려준다"를 그대로 만들려고 했거든요. 그런데 이 방식은 매번 배경도, 드레스도, 포즈도 새로 그려야 합니다. 그러면 결과가 매번 달라지고, 퀄리티도 같은 걸 넣어도 다르게 출력되더라고요. 어떤 날은 드레스가 예쁘게 나오고 어떤 날은 손가락이 여섯 개인 사진이 나오는 거죠. 화보는 한 장만 잘 나오면 되는 게 아니라 여덟 장이 모두 팔 만해야 하는데, 이걸 운에 맡길 수는 없다고 판단했습니다. 글이 길어져서 실제 적용하고 경험했던 기술 관련 이슈와 해결 방법은 다음 글에서 이어보겠습니다.
현재 재직중 경력 짧음 (많이 쳐줘도 2년) 앞으로 미래전망을 쉽게 예측할 수 없음 AI가 바꿔놓은 개발자의 삶 혼자 힘으로 유의미한 결과물을 생산하는 능력 크게 향상됨 조직에서 AI사용으로 인한 책임보단, 개인적으로 진행하는 프로젝트가 가벼움 일과와 패시브 인컴을 동시에 구축 언제든 일자리가 끊어질 수 있음을 각오 그를 대비하기 위한 다양한 수익 창출 루트 계획 , 구축중 시도 중인 인컴 파이프라인 구글 웹 개발 - 크롬 익스텐션 게임개발 - Godot 엔진 - 스팀 / iOS, 안드로이드 (모바일) 쿠팡 파트너스 - SNS 홍보 게시 자동화 이미지스톡 이미지 등록 기타 구글애드 넣을 수 있는 개인 사이트
LLaVA: Visual Instruction Tuning Visual Instruction Tuning Haotian Liu, Chunyuan Li, Qingyang Wu, Yong Jae Lee NeurIPS 2023 기존 Vision-Language Model은 image captioning, VQA, detection 등 다양한 visual task에서 좋은 성능을 보여왔다. 하지만 많은 경우 수행해야 하는 task가 모델이나 학습 방식에 미리 정해져 있다는 한계 가 있었다. 반면 Large Language Model은 자연어 instruction 자체를 하나의 universal interface 로 사용할 수 있다. "요약해줘" "번역해줘" "둘을 비교해줘" "이유를 설명해줘" 하나의 모델이 instruction에 따라 서로 다른 task를 수행할 수 있는 것이다. LLaVA는 여기서 출발한다. LLM에서 효과적이었던 Instruction Tuning을 Vision-Language Multimodal 영역으로 확장하면 어떨까? 이를 위해 LLaVA는 GPT-4를 이용해 Visual Instruction Data를 생성하고, CLIP Vision Encoder와 Vicuna를 연결해 Visual Instruction Tuning 을 수행한다. 1. Background 1.1 기존 Vision-Language Model의 한계 기존 vision model은 이미 여러 task를 수행할 수 있었다. Classification Image → Class Detection Image → Bounding Boxes Captioning Image → Caption 하지만 대부분은 수행할 task가 어느 정도 모델 설계에 암묵적으로 포함 되어 있었다. 예를 들어 captioning model의 기본 interface는 보통 Image → "A dog is running in a park." 처럼 이미지를 설명하는 것이다. 사용자가 갑자기 "이 장면에서 이상한 점은 뭐야?" "이 사람이 어떤 어려움을 겪고 있는 것 같아?" "다음에 무슨 일이 일어날 것 같아?" 처럼 자연어로 원하는 task를 자유롭게 지정하기는 어렵다. 즉 기존 모델에서도 language를 사용했지만, 주로 visual information을 language semantics로 표현하는 수단 으로 사용되었다. LLaVA는 language의 역할을 한 단계 확장한다. Language를 이미지 설명의 결과물이 아니라, 사용자가 원하는 task를 지정하는 interface 로 사용한다. 1.2 Instruction Tuning LLM의 기본 pre-training은 보통 이전 token을 보고 다음 token을 예측하는 방식이다. 즉 모델은 다음 확률을 학습한다. $$ P(x_t \mid x_{<t}) $$ 하지만 next-token prediction을 잘한다고 해서 모델이 사용자의 instruction을 반드시 잘 따르는 것은 아니다. 그래서 다음과 같은 형태의 데이터를 추가로 학습한다. Instruction: Summarize the following paragraph. Input: ... Response: ... 즉, Instruction + Input → Response 형태로 supervised fine-tuning을 수행한다. 이를 Instruction Tuning 이라고 한다. 중요한 점은 Instruction Tuning이라고 해서 새로운 language modeling objective를 사용하는 것은 아니라는 것이다. 여전히 정답 response를 token 단위로 생성하도록 학습한다. $$ \mathcal{L} = -\sum_t \log P_\theta (y_t \mid x, y_{<t}) $$ 여기서 x 는 instruction과 input이고, y 는 정답 response이다. Instruction Tuning의 핵심은 모델이 다양한 task를 각각 별도의 classifier처럼 배우는 것이 아니라, 자연어 instruction 자체를 task specification으로 사용하는 방법을 학습한다는 것 이다. 이러한 학습은 새로운 instruction에 대한 zero-shot / few-shot generalization에도 도움이 된다. 2. Visual Instruction Tuning 그렇다면 Instruction Tuning을 multimodal 환경으로 확장하면 어떻게 될까? 기존 Instruction Tuning이 Instruction → LLM → Response 이었다면 LLaVA는 Image + Instruction → Multimodal LLM → Response 로 확장한다. 이를 논문에서는 Visual Instruction Tuning 이라고 부른다. 예를 들어 같은 이미지라도 instruction에 따라 task가 달라질 수 있다. Image + "Describe this image." → "A dog is running in a park." Image + "What color is the dog?" → "Brown." Image + "What is unusual about this scene?" → 상황에 대한 설명 Image + "What might happen next?" → visual reasoning 단순히 Image → Caption 을 생성하는 것이 아니라, Image와 Language Instruction을 함께 보고, 사용자가 원하는 형태의 Response를 생성하는 것 이 목표다. 3. GPT-assisted Visual Instruction Data Generation Visual Instruction Tuning을 하려면 당연히 다음과 같은 데이터가 필요하다. Image + Instruction + Response 문제는 당시 이런 multimodal instruction-following data가 충분하지 않았다는 것이다. 반면 이미 세상에는 COCO, CC, LAION과 같은 데이터셋을 통해 Image + Caption 형태의 데이터가 매우 많이 존재했다. LLaVA는 여기서 기존 Image-Text Pair를 GPT-4를 이용해 Instruction-following Data로 변환 한다. 3.1 가장 단순한 방법 이미 이미지와 caption이 있다면 다음처럼 바꿀 수 있다. Human: Describe this image. Assistant: [Original Caption] 논문에서 이미지, caption, question을 각각 X_v , X_c , X_q 라고 하면 대략 Human: X_q + X_v Assistant: X_c 형태가 된다. 하지만 이렇게 단순하게 만들면 대부분의 질문이 Describe this image. What is shown in the image? 와 비슷해진다. 따라서 instruction과 response 모두 다양성이 부족하고, 깊은 reasoning을 학습시키기도 어렵다. 3.2 GPT-4는 이미지를 못 보는데 어떻게 데이터를 만들까? 당시 사용한 GPT-4는 text-only model이었다. 즉 GPT-4에게 실제 이미지를 직접 보여줄 수 없었다. 그래서 LLaVA는 이미지를 대신 표현할 수 있는 두 종류의 symbolic representation 을 사용한다. Captions 하나의 이미지를 여러 관점에서 설명하는 caption을 제공한다. A group of people standing outside a black vehicle. People try to fit all their luggage in an SUV. The SUV is being packed for a trip. Bounding Boxes 이미지 안에 어떤 object가 있고 어디에 위치하는지를 함께 제공한다. person: [x1, y1, x2, y2] backpack: [x1, y1, x2, y2] suitcase: [x1, y1, x2, y2] 즉 실제 흐름은 다음과 같다. Image ↓ Captions + Bounding Boxes ↓ Text-only GPT-4 ↓ Instruction + Response 이미지를 직접 보여주는 대신, LLM이 읽을 수 있는 textual / symbolic representation으로 이미지를 표현한 것 이다. Table 1의 위쪽에는 GPT-4에게 제공한 Captions + Bounding Boxes 가 있고, 아래쪽에는 GPT-4가 생성한 세 종류의 response가 있다. 중요한 점은 Table에 보이는 실제 이미지는 GPT-4에게 입력되지 않았다는 것 이다. 3.3 세 종류의 Instruction Data LLaVA는 GPT-4를 이용해 세 종류의 데이터를 생성한다. Conversation 이미지에 대해 자연스럽게 질문하고 답하는 형태이다. 질문에는 다음과 같은 내용이 포함된다. Object type Object counting Object action Object location Relative position Q: What type of vehicle is featured in the image? A: The image features a black SUV. 특히 확실하게 답할 수 있는 질문만 생성하도록 GPT-4에게 지시 한다. Detailed Description 단순 caption보다 훨씬 상세하게 이미지를 설명하도록 한다. Describe the following image in detail. 와 같은 instruction에 대해, 누가 어디에 있고, 어떤 물체가 있으며, 무엇을 하고 있고, 전체 장면이 어떤 상황인지 까지 자세하게 생성한다. Complex Reasoning 마지막은 이미지에서 직접 보이는 정보만 설명하는 것이 아니라, 이를 기반으로 reasoning하도록 한다. 예를 들어 많은 짐을 SUV에 넣고 있는 이미지에서 Q: What challenges do these people face? 라고 질문하면, 많은 짐을 제한된 차량 공간에 효율적으로 배치해야 하며, 승객의 공간과 운전자의 시야도 고려해야 한다. 와 같은 reasoning response를 생성한다. 즉 Visual Evidence ↓ Reasoning ↓ Response 형태를 학습시키려는 것이다. 최종적으로 총 158K 개의 instruction-following sample을 생성한다. Type Samples Conversation 58K Detailed Description 23K Complex Reasoning 77K Total 158K 3.4 Prompt는 어떻게 설계했을까? GPT-4에게 단순히 데이터를 만들어달라고 요청한 것은 아니다. Prompt에서는 다음과 같은 조건을 명시했다. 다양한 종류의 visual question을 생성할 것 확실하게 답할 수 있는 질문만 생성할 것 Object count, action, location, relative position 등을 포함할 것 필요한 경우 complex reasoning도 포함할 것 사람이 작성한 few-shot example을 참고할 것 즉 LLaVA의 synthetic data는 GPT-4 자체의 성능뿐만 아니라 prompt design과 few-shot examples에도 크게 의존한다. 4. LLaVA Architecture 이제 이렇게 만든 데이터로 학습할 실제 모델을 보자. LLaVA의 구조는 생각보다 매우 단순하다. 전체 구조를 단순화하면 다음과 같다. Image ↓ CLIP Vision Encoder ↓ Visual Feature ↓ Linear Projection ↓ Visual Tokens \ → Vicuna → Response / Language Instruction 4.1 Vision Encoder 입력 이미지를 X_v 라고 하자. LLaVA는 pretrained CLIP의 ViT-L/14 를 Vision Encoder로 사용한다. Vision Encoder를 g 라고 하면, $$ Z_v = g(X_v) $$ 이다. 여기서 Z_v 는 이미지 하나를 나타내는 단일 값이 아니라, ViT가 image patch들을 처리해서 만든 visual feature sequence 이다. 개념적으로는 다음처럼 생각할 수 있다. $$ Z_v = [z_1, z_2, \cdots, z_N] $$ 즉 이미지가 여러 patch로 나뉘고 각각에 대응하는 visual representation이 만들어진다. Image ↓ Image Patches ↓ CLIP ViT ↓ z1, z2, ..., zN 4.2 Linear Projection 문제는 CLIP이 만든 visual representation을 Vicuna에 그대로 넣을 수 없다는 것이다. CLIP의 hidden representation과 Vicuna가 사용하는 word embedding은 서로 다른 representation space를 사용한다. 그래서 LLaVA는 학습 가능한 linear projection W 를 사용한다. $$ H_v = W Z_v $$ 여기서 Z_v : CLIP이 만든 visual feature W : trainable projection matrix H_v : LLM이 사용할 visual token 이다. Projection 이후 H_v 는 LLM의 word embedding과 동일한 dimensionality를 갖는다. 중요한 점은 이것이 단순히 숫자의 dimension만 맞춰주는 것 은 아니라는 것이다. Training을 통해 W 는 CLIP의 visual representation을 LLM이 사용할 수 있는 representation으로 mapping 하도록 학습된다. 결과적으로 $$ H_v = [h_1, h_2, \cdots, h_N] $$ 이라는 visual token sequence 를 얻는다. 4.3 Visual Token과 Language Token 사용자의 language instruction 역시 token embedding으로 변환된다. 이를 H_q 라고 하면 LLM에는 결국 Visual Tokens H_v + Language Tokens H_q 가 함께 들어간다. 즉 LLM 입장에서는 대략 [visual][visual][visual] ... [visual] [What][is][happening][in][this][image][?] 처럼 하나의 multimodal token sequence를 처리한다고 볼 수 있다. 이 부분은 이후 Efficient MLLM 연구에서도 중요하다. LLaVA는 여러 image patch에서 만들어진 visual token들을 LLM으로 전달한다. 그러면 자연스럽게 다음 질문이 생긴다. 이 많은 visual token을 정말 모두 LLM에 넣어야 할까? 이 질문이 이후 visual token pruning, selection, adaptive visual compute 등의 연구로 이어진다. 4.4 BLIP-2와 비교하면? BLIP-2를 먼저 읽었다면 구조 차이가 더욱 명확하다. BLIP-2 LLaVA Vision Encoder Frozen Vision Encoder CLIP ViT-L/14 Connector Q-Former Linear Projection 핵심 Vision-Language representation bridging Visual Instruction Following 학습 특징 Q-Former 중심 Instruction Data 중심 BLIP-2는 Vision Encoder와 LLM 사이에 비교적 복잡한 Q-Former 를 사용한다. 반면 LLaVA는 단순한 Linear Projection만 사용한다. 논문에서도 이러한 lightweight architecture를 선택함으로써 data-centric experiment를 빠르게 반복할 수 있었다 고 설명한다. 즉 LLaVA의 핵심 contribution은 복잡한 connector 자체보다는 Visual Instruction Data와 Training 방식 에 더 가깝다. 5. Training Objective LLaVA의 training objective 자체는 특별한 multimodal loss가 아니다. 기존 LLM처럼 autoregressive next-token prediction 을 사용한다. Assistant response를 X_a 라고 하면, $$ p(X_a \mid X_v, X_{\text{instruct}}) = \prod_{i=1}^{L} p_\theta \left( x_i \mid X_v, X_{\text{instruct}}, X_{a,<i} \right) $$ 이다. 복잡해 보이지만 의미는 단순하다. 이미지 + instruction + 지금까지 생성한 answer token을 보고 다음 answer token을 예측한다. 예를 들어 정답이 The dog is running. 이라면, $$ P(\text{The} \mid Image, Instruction) $$ 다음으로 $$ P(\text{dog} \mid Image, Instruction, \text{The}) $$ 다음으로 $$ P(\text{is} \mid Image, Instruction, \text{The dog}) $$ 를 차례대로 학습하는 것이다. 또한 loss는 Assistant가 생성해야 하는 response token에 대해서만 계산 한다. Human: What is happening in this image? → Loss X Assistant: A dog is running in the park. → Loss O Human instruction은 모델이 맞혀야 하는 target이 아니라 response를 생성하기 위한 condition 이다. 6. Two-Stage Training LLaVA는 한 번에 모든 모델을 학습하지 않고 두 단계로 나눈다. 6.1 Stage 1: Pre-training for Feature Alignment 첫 번째 단계의 목적은 CLIP과 Vicuna를 연결하는 것 이다. CLIP Vision Encoder → Frozen Projection W → Train Vicuna → Frozen 사용하는 데이터는 CC3M에서 filtering한 595K image-text pairs 이다. 각 sample은 단순한 형태로 바꾼다. Image Question: Describe this image briefly. Answer: [Original Caption] 이 단계에서 학습되는 parameter는 오직 projection W 뿐이다. $$ \theta = W $$ 즉 CLIP도 이미 충분히 좋은 visual representation을 가지고 있고, Vicuna도 이미 language를 잘 처리하므로 둘을 다시 학습시키지 않는다. 그 대신 CLIP Representation ↓ W ↓ LLM Representation 을 연결하는 projection만 먼저 학습한다. 논문에서는 이를 training a compatible visual tokenizer for the frozen LLM 이라고 표현한다. 즉 frozen LLM이 이해할 수 있는 형태로 visual feature를 변환하는 방법을 먼저 배우는 단계라고 생각하면 된다. 6.2 Stage 2: Fine-tuning End-to-End Stage 1에서 vision-language 연결을 만들어 놓았다면, 이제 실제 instruction-following을 학습한다. CLIP Vision Encoder → Frozen Projection W → Train Vicuna → Train 학습 가능한 parameter는 $$ \theta = {W,\phi} $$ 이다. 여기서 W 는 projection parameter이고, φ 는 Vicuna parameter이다. 이 단계에서는 앞에서 만든 LLaVA-Instruct-158K 를 사용한다. 즉 실제로 Image + Instruction → Response 를 학습한다. 두 stage를 정리하면 다음과 같다. Stage 1 Stage 2 목적 Feature Alignment Instruction Following Vision Encoder Frozen Frozen Projection Train Train LLM Frozen Train Data CC-595K LLaVA-Instruct-158K 형태
개요 Ubuntu 26.04에서는 Greenbone 공식 컨테이너(Docker) 방식을 권장합니다. Greenbone 공식 문서가 지원하는 배포판 목록에는 Debian stable(bookworm) , Ubuntu 24.04 LTS , Fedora 35/36 , CentOS 9 Stream 만 있고 26.04는 아직 없습니다. 컨테이너 방식은 운영체제, 설치된 소프트웨어, 툴체인과 관계없이 로컬 네트워크를 스캔할 수 있어서 새 배포판에서 소스 빌드할 때 생기는 의존성 문제를 피할 수 있습니다. 최소 권장 사양 피드 데이터가 크기 때문에 권장 사양 이상을 추천합니다. 구분 CPU RAM DISK 최소 2 Core 4 GB 20 GB 권장 4 Core 8 GB 60 GB 설치 Docker 사용자 설정 Docker 패키지 설치 자체는 root/sudo 권한으로 진행하고, 설치 후 Docker CLI 를 docker 사용자로 실행하도록 구성합니다. root └─ Docker 패키지 설치/시스템 설정 ↓ docker 사용자 └─ docker 그룹 소속 ↓ docker 명령 실행 ↓ /var/run/docker.sock docker user 생성 useradd -m -g docker -s /bin/bash docker passwd docker Docker 설치 (공식 Repo) Docker 공식 문서는 Ubuntu Resolute 26.04 (LTS) 를 지원 대상으로 명시하고 있습니다. Ubuntu 기본 저장소의 docker.io는 버전이 뒤처지므로 공식 저장소를 쓰는 편이 좋습니다. # 충돌 패키지 제거 (설치돼 있지 않으면 무시해도 됩니다) sudo apt remove docker.io docker-compose docker-compose-v2 docker-doc podman-docker # 필수 패키지와 GPG 키 apt update -y apt install -y ca-certificates curl install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc # 저장소 등록 tee /etc/apt/sources.list.d/docker.sources <<EOF Types: deb URIs: https://download.docker.com/linux/ubuntu Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") Components: stable Signed-By: /etc/apt/keyrings/docker.asc EOF # 설치 apt update -y apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # docker 사용자를 docker 그룹에 추가 (적용하려면 재로그인 또는 newgrp) usermod -aG docker docker newgrp docker # 자동 재시작 설정 systemctl enable docker --now # 사용자 변경 su - docker # 동작 확인 docker run --rm hello-world docker ps docker version Greenbone Community Edition 컨테이너 실행 # 작업 디렉터리 export DOWNLOAD_DIR=$HOME/greenbone-community-edition && mkdir -p $DOWNLOAD_DIR # 공식 compose 파일 다운로드 curl -f -O -L https://greenbone.github.io/docs/latest/_static/compose.yaml --output-dir "$DOWNLOAD_DIR" # 이미지 받기 → 백그라운드 실행 docker compose -f $DOWNLOAD_DIR/compose.yaml pull docker compose -f $DOWNLOAD_DIR/compose.yaml up -d # 로그 확인 (Ctrl+C로 빠져나오기) docker compose -f $DOWNLOAD_DIR/compose.yaml logs -f compose 파일은 상시 변경됩니다. 따라서 항상 최신 compose.yaml 을 사용해야 합니다. 관리자 비밀번호 변경 default 계정인 admin / admin 계정이 만들어지는데, 아래와 같이 새 비밀번호로 바꿔줍니다. docker compose -f $DOWNLOAD_DIR/compose.yaml \ exec -u gvmd gvmd gvmd --user=admin --new-password='$new_password' 비밀번호 값에 $ 같은 특수문자가 있으면 작은따옴표로 감싸서 입력합니다. 피드 로딩 대기 후 웹 접속 기본 설정대로 진행했다면 WEB UI(GSA) 는 nginx로 127.0.0.1:443 에 바인딩 됩니다. 서버 자체 브라우저에서는 https://127.0.0.1 로 접속하면 되지만 외부에서 접속 시에는 포트 포워딩이나 compose.yaml 내부 바인딩 설정(compose.yaml의 nginx ports에서 127.0.0.1:443:443)을 변경하고 up -d 옵션으로 재실행해서 적용한 뒤 접근 가능합니다. 웹 초기 접근 시 자체 서명 인증서라서 브라우저에 경고가 뜬다면 예외로 진행하면 됩니다. 서비스가 모두 시작되고 피드 데이터가 전부 로드된 뒤에 WEB UI(GSA) 를 쓸 수 있습니다. 처음에는 VT , SCAP , CERT 데이터를 DB에 저장하는 데 꽤 오래 걸립니다. WEB UI의 Administration → Feed Status 에서 모든 항목이 Current 가 될 때까지 기다린 후에 스캔 작업을 시작할 수 있습니다.
저자 : Guoshun Cai, Guodong Yin, Jinxiang Wang, Jiwei Feng, Shuo Bai, Jingyu Hu 소속 : Southeast University, Liaocheng University 게재지 : Control Engineering Practice , Vol. 172, 106893, 2026 DOI : 10.1016/j.conengprac.2026.106893 Abstract 요약 자율주행차의 궤적 추종에서는 종방향 속도와 타이어 코너링 강성이 계속 변하고, 외란과 상태 추정 오차까지 동시에 존재함. 이 논문은 이러한 불확실성을 다면체 LPV 모델로 표현하고, quadratic boundedness 기반 관측기로 추정 오차의 크기를 매 순간 갱신하며, 그 오차경계를 min-max 강건 MPC에 직접 포함하는 방법을 제안함. 여기에 횡슬립각–요레이트 위상평면 제약과 제어 장벽 함수(CBF) 기반 추종 안전영역을 함께 넣어 안정성과 안전성을 동시에 다룸. 계산량이 큰 LMI는 오프라인에서 풀고 온라인에서는 현재 스케줄링 변수에 맞춰 보간함으로써 0.01 s 샘플링 조건을 만족함. HIL과 실차 시험에서 기존 LMI 기반 preview 제어기와 tube MPC보다 추종 정확도와 차량 안정성이 개선됨을 확인함. 이 논문의 질문은 단순히 “경로를 잘 따라갈 수 있는가”가 아님. 실제 차량에서 직접 측정하기 어려운 상태가 있고, 모델이 계속 변하며, 안전영역을 넘지 않으면서도 실시간 계산이 가능해야 한다는 네 가지 요구를 하나의 제어 구조에서 만족할 수 있는지가 핵심임. 논문의 해법은 관측기가 불확실성을 숫자로 경계 짓고, 강건 MPC가 그 경계의 최악 조건을 대비하며, 위상평면과 CBF가 각각 차량 동역학 안정성과 추종 안전성을 제한하는 역할 분담임. 1. Introduction 기존 궤적 추종 제어는 모델 불확실성, 외란, 상태 추정 오차를 따로 다루는 경우가 많음. 그러나 실제 차량에서는 이 세 요소가 동시에 작용하므로 하나만 무시해도 이론적으로 얻은 안정성 보장이 약해질 수 있음. 일반적인 상태 관측기는 추정값만 제공하지만, 강건 MPC에는 “추정 오차가 최대 어느 범위에 있는가”도 필요함. 이 논문은 quadratic boundedness를 사용하여 관측 오차의 타원체 경계를 계산하고 온라인에서 줄여 나감. 차량 안정성은 횡슬립각과 요레이트가 만드는 위상평면의 안정영역으로, 추종 안전성은 횡방향 오차와 헤딩 오차가 만드는 CBF 안전집합으로 표현함. 두 제약을 MPC 상태 제약으로 합치는 것이 제안 구조의 특징임. 주요 기여는 시변 타이어 강성과 속도를 반영한 다면체 LPV 모델, 온라인 오차경계 갱신 관측기, 오차를 포함한 min-max 강건 MPC, CBF와 위상평면의 통합, 오프라인·온라인 분리를 통한 실시간 구현으로 정리됨. 2. System modeling 2.1 Vehicle dynamics model 차량 횡방향 운동은 횡슬립각 $\beta$와 요레이트 $\gamma$를 중심으로 한 2자유도 모델로 표현됨. $$ M v_x(\dot{\beta}+\gamma)=F_{yf}\cos\delta_f+F_{yr} \tag{1} $$ $$ I_z\dot{\gamma}=l_fF_{yf}\cos\delta_f-l_rF_{yr}+\Delta M_z \tag{1b} $$ $M$은 차량 질량, $v_x$는 종방향 속도, $I_z$는 요 관성모멘트, $F_{yf},F_{yr}$은 전·후륜 횡력, $\delta_f$는 전륜 조향각, $\Delta M_z$는 추가 요 모멘트임. 타이어 횡력은 Magic Formula로 계산한 뒤 작동점 주변에서 등가 코너링 강성으로 나타냄. $K_f=\varepsilon_fC_f$, $K_r=\varepsilon_rC_r$로 두면 $\varepsilon_f,\varepsilon_r$가 노면과 비선형 타이어 효과를 담는 불확실성 계수가 됨. preview point의 횡방향 오차 $e_y$와 헤딩 오차 $e_\varphi$를 차량 상태에 포함함. 이 선택은 단순한 차량 안정화가 아니라 미래 경로를 미리 보며 추종하는 제어 문제로 바꾸기 위함임. $$ x=\begin{bmatrix}e_y&e_\varphi&\beta&\gamma\end{bmatrix}^{T},\quad u=\begin{bmatrix}\delta_f&\Delta M_z\end{bmatrix}^{T},\quad d=\rho_r \tag{6} $$ $d=\rho_r$는 기준 경로 곡률이며, 제어 출력은 $z=Cx+Hd$로 정의됨. 2.2 LPV system $v_x$, $\varepsilon_f$, $\varepsilon_r$가 변하면 시스템 행렬도 변함. 이를 하나의 고정 모델로 근사하는 대신 스케줄링 변수 $\sigma_1\sim\sigma_7$의 affine 결합으로 표현함. $$ A(\sigma)=A_0+\sum_{i=1}^{7}\sigma_iA_i,\qquad B(\sigma)=B_0+\sigma_1B_1+\sigma_5B_5 \tag{7} $$ 이론적으로 모든 변수의 최솟값과 최댓값을 조합하면 128개 꼭짓점이 필요하지만, 변수 사이의 종속성을 이용해 네 개의 집계 꼭짓점으로 축소함. 이는 이후 LMI의 개수를 크게 줄이는 핵심 계산 기법임. 이산화한 다면체 모델은 다음과 같이 네 꼭짓점의 볼록결합으로 표현됨. $$ x(k+1)=E(\eta)x(k)+F(\eta)u(k)+G(\eta)d(k),\qquad \sum_{q=1}^{4}\eta_q(k)=1 \tag{10} $$ $\eta_q(k)$는 현재 속도와 타이어 상태에 따른 꼭짓점 가중치임. 따라서 비선형 차량을 매 시점 하나의 LPV 선형모델로 갱신할 수 있음. 이 단계의 목적은 비선형 차량을 완전히 선형화해 버리는 것이 아니라, 가능한 모델 변화 전체를 네 꼭짓점의 볼록껍질 안에 가두는 것임. 이후 모든 꼭짓점에서 LMI가 성립하면 그 사이의 모든 모델에서도 조건이 성립한다는 논리임. 3. Observer-based robust MPC design 3.1 Off-line observer design 직접 측정하기 어려운 상태를 얻기 위해 LPV 관측기를 구성함. $$ \hat{x}(k+1)=E(\eta)\hat{x}(k)+F(\eta)u(k)+L(\eta)\left[z(k)-C\hat{x}(k)\right] \tag{12} $$ 관측 오차 $x_o=x-\hat{x}$에 대해 $V_o=x_o^TP_ox_o$를 정의하고, 외란 에너지보다 오차 에너지가 큰 영역에서는 $V_o$가 감소하도록 요구함. 이 조건이 quadratic boundedness임. $$ V_o(k)\geq \lVert d(k)\rVert_{Q_w}^{2} \Rightarrow V_o(k+1)-V_o(k)<0 \tag{14} $$ Theorem 1은 이 조건을 S-procedure와 Schur complement로 LMI화함. 네 LPV 꼭짓점에서 LMI를 풀어 $P_o$와 $L_q=P_o^{-1}R_q$를 오프라인에서 구함. 여기서 관측기는 오차를 정확히 0으로 만든다고 주장하지 않음. 외란이 존재해도 오차가 알려진 타원체 안에 머무는 것을 보장하는 구조임. 3.2 On-line refreshment of EES 고정된 큰 오차경계를 계속 사용하면 MPC가 지나치게 보수적이 됨. 논문은 현재 관측 오차 타원체의 크기 $\varsigma(k)$를 매 순간 최소화하여 estimation error set(EES)을 갱신함. $$ V_o(k+1)\leq 1+(1-\theta)\left[V_o(k)-1\right] \tag{20} $$ $0<\theta<1$이면 큰 초기 경계가 외란이 정한 정상상태 경계 1을 향해 수축함. Theorem 2의 LMI는 다음 시점의 최소 $\varsigma(k+1)$를 계산함. Algorithm 1은 관측기 이득은 오프라인에서 구하고, 상태 추정과 EES 크기만 온라인에서 갱신하는 절차임. 3.3 Optimization problem formulation 제어입력은 현재 LPV 가중치에 따른 상태 피드백으로 놓음. $$ u(c|k)=N(\eta)\hat{x}(c|k) \tag{24} $$ 추정 상태와 관측 오차를 결합한 확장 상태 $\xi=[\hat{x}^T,x_o^T]^T$를 사용함. 이렇게 해야 MPC가 추정값만 믿는 대신 관측 오차가 만들 수 있는 최악의 상태까지 함께 예측할 수 있음. 목적함수는 모든 허용 외란에 대한 무한시간 성능비용의 최댓값을 최소화하는 min-max 문제임. 동시에 종단집합, Lyapunov 감소, 입력 한계, 상태 한계를 만족해야 함. $$ \min_{N(\eta)}\max_{d}\sum_{c=0}^{\infty} \left(\lVert z(c|k)\rVert_T^2+\lVert u(c|k)\rVert_U^2\right) \tag{26} $$ 3.4 Robust MPC design Theorem 3은 위 min-max 문제를 LMI 최적화로 바꾸고 성능 상계 $\Upsilon(k)$를 최소화함. LMI에는 관측 오차 경계, 외란 경계, 입력 제약, 상태 제약이 동시에 들어감. 중요한 점은 제어기와 관측기를 분리 설계하더라도 MPC가 관측 오차를 명시적으로 고려한다는 점임. 따라서 추정값과 실제값의 차이가 허용 타원체 안에 있는 한 제약 만족과 안정성 논리를 유지할 수 있음. 3.5 On-line and off-line solution 모든 LMI를 매 샘플 온라인에서 풀면 계산량이 너무 큼. 논문은 가능한 스케줄링 영역에서 제어이득과 Lyapunov 행렬을 오프라인 계산하고, 온라인에서는 현재 $\eta_q$에 따라 보간함. Algorithm 1은 완전 온라인 방식이고, Algorithm 2는 오프라인·온라인 분리 방식임. 두 방식의 성능은 유지하면서 Algorithm 2가 실시간 구현을 담당함. 이 논문의 실용적 핵심은 강건 MPC 공식을 제시한 것만이 아니라, 계산의 대부분을 오프라인으로 이동시켜 10 ms 제어주기 안에 들어오도록 설계한 것임. 4. Stability and dynamic envelopes 4.1 Closed-loop system stability 명목계에서 Lyapunov 함수가 매 스텝 감소하면 폐루프 안정성이 보장됨. $$ V(k+1)-V(k)\leq-\lVert z(k)\rVert_T^2-\lVert u(k)\rVert_U^2<0 \tag{47} $$ 또한 무한시간 비용은 최적화로 얻은 상계 $\Upsilon(k)$보다 작음. 따라서 $\Upsilon$는 단순한 튜닝 숫자가 아니라 추종 성능과 안정성의 인증값 역할을 함. 4.2 Phase-plane 4.2.1 Phase-plane 차량 횡방향 안정성은 $\beta$–$\gamma$ 위상평면에서 정의한 평행사변형 영역으로 제한함. $$ -\Psi_1\leq \aleph_1\beta+\varphi_1\gamma\leq\Psi_1,\qquad -\Psi_2\leq \aleph_2\beta+\varphi_2\gamma\leq\Psi_2 \tag{50} $$ 이 경계는 타이어가 포화되는 불안정 영역으로 상태가 진입하지 못하도록 하는 동역학적 안전봉투임. 4.2.2 Transformation to state constraint form 위상평면 부등식은 MPC에 넣을 수 있도록 선형 상태 제약으로 변환됨. $$ \Phi_{\mathrm{PPB}}x(k)\leq\chi_{\max}^{\mathrm{PPB}} \tag{53} $$ 상태벡터의 세 번째와 네 번째 성분이 각각 $\beta$와 $\gamma$이므로 해당 열에 위상평면 계수를 배치하면 됨. 4.3 CBF-based dynamic safety envelope 4.3.1 Safety set and CBF 위상평면이 차량의 동역학 안정성을 제한한다면, CBF는 추종 오차가 허용 범위를 벗어나지 않도록 제한함. 안전집합은 $e_y$와 $e_\varphi$의 타원으로 정의됨. $$ h_s(x)=1-\frac{e_y^2}{e_{ym}^2}-\frac{e_\varphi^2}{e_{\varphi m}^2} \tag{54} $$ $h_s>0$이면 안전영역 내부, $h_s=0$이면 경계, $h_s<0$이면 안전영역 밖임. 4.3.2 Discrete-Time CBF condition 이산시간에서 안전집합의 전방 불변성을 위한 조건은 다음과 같음. $$ h_s(k+1)\geq(1-b_rT_s)h_s(k) \tag{57} $$ 모델 오차나 갑작스러운 외란 때문에 최적화가 불가능해지는 상황을 막기 위해 slack $b_s\geq0$를 추가함. $$ h_s(k+1)\geq(1-b_rT_s)\left[h_s(k)-b_s\right] \tag{58} $$ $b_r$는 안전영역 내부로 복귀시키는 속도, $b_s$는 순간적인 완화 여유를 결정함. 완화는 안전을 제거하는 것이 아니라 최적화의 실행 가능성을 유지하면서 다시 엄격한 집합으로 복귀시키기 위한 장치임. 4.3.3 Dynamic safety envelope definition 시점 $k$의 동적 안전봉투는 $\mathcal{E}(k)={x:h_s(x(k))\geq0}$로 정의됨. 현재 추종 오차와 $b_r,b_s$에 따라 다음 시점의 허용 경계가 달라지므로 고정된 오차 제한이 아니라 상태 의존적 안전봉투가 됨. 4.3.4 Linearized CBF integration into robust MPC 타원형 CBF 제약은 결합 비선형 제약임. 논문은 이를 MPC의 선형 부등식으로 넣기 위해 각 오차를 독립적으로 제한하는 보수적 근사를 사용함. $$ e_{ym}^{\mathrm{eff}}(k)=e_{ym}\sqrt{R(k)},\qquad e_{\varphi m}^{\mathrm{eff}}(k)=e_{\varphi m}\sqrt{R(k)} \tag{60} $$ $$ \Phi_{\mathrm{CBF}}x(k)\leq\chi_{\max}^{\mathrm{CBF}}(k) \tag{61} $$ 이 근사는 원래 타원보다 엄격할 수 있지만, 선형 LMI 기반 MPC 안에서 안전조건을 직접 계산할 수 있다는 장점이 있음. 4.4 Integrated constraint formulation 최종 상태 제약은 CBF 추종 안전영역과 위상평면 동역학 안정영역을 수직으로 결합함. $$ \Phi_{\mathrm{total}}x(k)\leq\chi_{\max}^{\mathrm{total}}(k),\qquad \Phi_{\mathrm{total}}=\begin{bmatrix}\Phi_{\mathrm{CBF}}\\Phi_{\mathrm{PPB}}\end{bmatrix} \tag{62} $$ 조향각과 추가 요 모멘트도 각각 $|\delta_f|\leq\bar{\delta}_f$, $|\Delta M_z|\leq\Delta\bar{M}_z$로 제한됨. 4.5 Torque allocation design 상위 제어기가 요구한 $\Delta M_z$를 네 인휠모터의 종력으로 배분함. 목적함수는 전체 타이어 마찰 사용률과 네 바퀴 사이 사용률 편차를 함께 최소화함. $$ \min J=\sum_{\tau=1}^{4}\zeta_\tau+\phi\sqrt{\frac{1}{4}\sum_{\tau=1}^{4}\left(\zeta_\tau-\frac{1}{4}\sum_{j=1}^{4}\zeta_j\right)^2} \tag{64} $$ $\zeta_\tau=(F_{x\tau}^2+F_{y\tau}^2)/(\mu F_{z\tau})^2$는 각 바퀴의 마찰 사용률임. 요 모멘트 등식과 모터 토크·마찰원 제약을 만족하는 quadratic programming 문제로 계산됨. 5. Test and analysis 5.1 HIL experiment dSPACE MicroAutoBox에서 제어기를 실행하고 NI PXI의 CarSim 차량모델과 연결한 HIL 환경을 사용함. 샘플링 시간은 0.01 s이며, 마찰계수 $\mu=0.8$, 속도는 10–15 m/s로 변화하는 double lane change 조건임. 비교 대상은 LMI 기반 preview 제어기(LMI-P), 오프라인 tube MPC(TMPC), 제안한 RMPC-P임. 추종 오차 한계는 $e_{ym}=0.2$ m와 $e_{\varphi m}=0.03$ rad임. EES는 약 1 s 안에 $\varsigma(k)=1$로 수렴하고, 성능 상계는 평균 4.63%의 비율로 감소하여 0.5 s 안에 $\Upsilon(k)=48.57$에 도달함. 초기 구간에서 CarSim과 예측모델의 mismatch로 추정 편차와 일부 반대 방향 추세가 나타남. 저자들은 이를 숨기지 않고 관측기 재튜닝, 모델 정밀도 향상, 적응 보상의 필요성을 한계로 제시함. 외란은 실험 전체에서 $\lVert d(k)\rVert_{Q_w}^2\leq1$을 만족함. RMPC-P의 위상평면 궤적이 원점에 가장 가깝고, TMPC와 LMI-P 순으로 큰 범위를 보임. RMPC-P는 세 제어기 중 횡방향·헤딩 오차가 가장 작고 $0.2$ m 안전경계 안에 머묾. 안전 제약을 별도 후처리가 아니라 MPC 내부에 넣었으므로 입력을 갑자기 수정하지 않고 최적화 단계에서 안전한 입력을 생성함. 평균 계산시간은 LMI-P 0.0001 s, TMPC 0.0089 s, Algorithm 2 0.0076 s, 완전 온라인 Algorithm 1 0.1251 s임. Algorithm 1은 0.01 s 제어주기를 넘지만, 오프라인·온라인 분리 방식인 Algorithm 2는 실시간 조건을 만족함. 5.2 Real vehicle test 네 개의 인휠모터와 EPS를 갖춘 ADDEV 시험차에 GPS/INS, IMU, VCU, ROS 기반 mini PC를 탑재함. 평탄하고 건조한 아스팔트에서 DLC 시험을 수행함. 세 제어기 모두 주행을 완료했으나 RMPC-P가 가장 작은 횡방향·헤딩 오차를 보였고, CBF에 의해 횡방향 오차가 안전경계 안에 제한됨. RMPC-P의 $\beta$–$\gamma$ 궤적이 원점에 더 가깝고 조향각과 추가 요 모멘트도 더 작아, 추종 정확도 향상이 과도한 제어입력의 결과만은 아님을 보여 줌. 5.3 Quantitative performance analysis HIL에서 RMPC-P의 RMS 값은 $e_y=0.0684$ m, $e_\varphi=0.0082$ rad, $\beta=0.0025$ rad, $\gamma=0.0529$ rad/s임. 실차에서는 각각 0.0737 m, 0.0083 rad, 0.0143 rad, 0.0624 rad/s임. 실차 기준으로 RMPC-P는 두 비교기보다 RMS 횡방향 오차를 최소 17.19%, RMS 횡슬립각을 최소 4.67%, RMS 요레이트를 최소 8.77% 줄였음. 다섯 번의 독립 HIL 반복시험에서 RMS 횡방향 오차는 $0.0686\pm7.0103\times10^{-4}$ m, RMS 헤딩 오차는 $0.0082\pm5.7890\times10^{-5}$ rad의 95% 신뢰구간을 보임. 반복시험의 좁은 신뢰구간은 동일한 HIL 조건에서 결과의 재현성이 높다는 증거임. 다만 이는 다양한 노면·고속 조건 전체에 대한 일반화 증거는 아니며, 논문도 고속 실차시험을 향후 과제로 남김. 6. Conclusion 제안 구조는 관측 오차를 경계 짓는 관측기, 그 최악값을 고려하는 강건 MPC, 동역학 안정성을 제한하는 위상평면, 추종 안전성을 제한하는 CBF를 하나의 LMI 기반 제어기에 통합함. 논문이 보고한 핵심 성과는 성능 상계의 샘플당 평균 4.63% 감소, 횡방향 오차의 0.2 m 안전경계 유지, LMI-P 대비 최대 요레이트 20.95% 감소와 최대 횡슬립각 29.67% 감소, 평균 온라인 계산시간 0.0076 s임. 이 결과는 단순 추종 정확도 향상보다 “추정 오차와 모델 불확실성이 있어도 안전·안정 제약을 함께 보장하면서 실시간 실행이 가능한가”라는 질문에 대한 실험적 답변임. 7. Future work 현재 실차 시험장은 안전상의 이유로 저속 시험만 허용함. 따라서 고속·고동특성 조건에서 제안한 안전봉투와 관측 오차 보장이 유지되는지는 아직 검증되지 않음. 고속 전용 시험장에서 공격적인 주행 조건을 검증하는 것이 후속 과제로 제시됨. Appendix A LPV 모델의 $A_0\sim A_7$, $B_0,B_1,B_5,D_7$ 기저행렬을 제시함. 본문의 식 (7)이 스케줄링 변수와 고정행렬의 affine 결합으
apps/web-e2e 를 next dev (동적) 단일 기반에서 static-server.mjs (정적) 병행 기반으로 바꾸는 과정에서 겪은 이슈들과 대응, 남은 항목을 정리한다. 작업 진행 순서를 참고하되, 내용은 주제별로 묶었다. 1. 배경 — 왜 static 기반이 필요했나 이 프로젝트( output: "export" , 정적 익스포트)는 프로덕션엔 서버가 없다. 그런데 e2e는 지금까지 next dev (라이브 서버) 기준으로만 돌아갔다. 팀 내 피드백으로는: "이전에 이 프로젝트는 SSR 사용했었음 → 모든 e2e 환경은 여기에 맞춰져있음 → static으로 바꾸면서 e2e에서 오류가 보인 것" 즉 next dev (동적)로는 "실제 배포에서 사용자가 잘못된 URL로 들어왔을 때" 같은 시나리오를 정확히 재현할 수 없었고, 이게 not-found 관련 문제의 근본 원인이었다. 2. 구축한 것 — static 기반 e2e 파이프라인 파일 역할 apps/web-e2e/static-server.mjs apps/web/out (빌드 산출물)을 그대로 서빙하는 최소 정적 파일 서버. 없는 경로는 실제 404.html 을 404 상태코드로 반환 apps/web-e2e/playwright.config.ts E2E_STATIC=true 면 static-server.mjs , 아니면 기존 next dev 를 띄우도록 webServer.command 분기 apps/web-e2e/project.json e2e:local 타겟 추가 — E2E_STATIC=true 로 playwright 실행 루트 package.json pnpm e2e:local 스크립트 추가 검증 결과(static 기반) : 전체 스위트 반복 실행 시 not-found·navigator 포함 모두 안정적으로 통과(자세한 내용은 5절). 3. not-found(500 ↔ 404) 이슈 — 해결됨 3-1. 환경별로 다른 게 정답인 이유 환경 정답 이유 next dev 500 Next 자체 안전장치( next-dev-server.js )가 output:"export" + generateStaticParams() 에 없는 id를 감지해 의도적으로 런타임 에러를 던짐 static( static-server.mjs ) 404 파일이 실제로 없어서 나는 진짜 404 — 실제 배포와 동일 왜 dev에서는 404가 아니라 500이 뜨는가 — 상세 generateStaticParams() 는 빌드 시점에 "이 동적 라우트( [id] )에 대해 어떤 id들의 파일을 실제로 만들지" 목록을 정한다. 이 목록에 없는 id는 정적 빌드에서 파일 자체가 안 만들어지므로, 실제 배포에서는 그냥 파일이 없어서 404가 뜨는 게 정상이다. 문제는 next dev 가 이 상황을 다르게 처리한다는 점이다. next dev 는 기술적으로는 목록에 없는 id도 그냥 렌더링해서 보여줄 수 있다(코드 자체는 있으니까). 그런데 일부러 그렇게 하지 않고 런타임 에러를 던지도록 만들어져 있다: // node_modules/next/dist/server/dev/next-dev-server.js:585-594 (요약) if (this.nextConfig.output === 'export') { if (!prerenderedRoutes.some((item) => item.pathname === urlPathname)) { throw new Error(`Page "${page}" is missing param "${pathname}" in "generateStaticParams()"...`); } } 이 체크는 dynamicParams 설정과 무관하게, 오직 output === 'export' 인지만 보고 무조건 실행 된다. 의도는 이렇다 — 만약 dev가 이 상황에서도 친절하게 페이지를 보여주면, 개발자는 "잘 되네?"라고 착각하고 넘어간다. 근데 실제 빌드·배포하면 그 페이지는 파일 자체가 없어서 조용히 깨진다. 그래서 Next는 "나중에 조용히 깨지느니 지금 시끄럽게 알려주자"는 의도로, dev 단계에서 일부러 크게 에러(500)를 던지도록 설계했다. 즉 이 500은 코드 버그도, dev의 결함도 아니라 Next.js의 의도된 안전장치다. 테스트 스펙에 환경별 기대값을 분기하는 코드로 반영: const expectedStatus = process.env.E2E_STATIC === 'true' ? 404 : 500; expect(response?.status()).toBe(expectedStatus); 3-2. dev 전체 스위트에서만 나던 flaky — 원인과 해결 증상 : 이 테스트를 단독으로 돌리면 항상 500(정상). 근데 dev로 전체 스위트 를 같이 돌리면 가끔 404가 떴다(실패로 표시). 원인 : next dev 는 라우트를 요청 시점에 한 번만 컴파일하고 캐시한다. 다른 스펙( product-detail.spec.ts 등)이 같은 [id] 라우트를 유효한 id로 먼저 방문하면, 그 라우트가 컴파일되어버려서 — 그 뒤로는 잘못된 id로 접근해도 안전장치(500)가 안 걸리고 그냥 404가 남. 서버 프로세스의 영구적 상태 변화 라 재시도로 해결 불가능. 왜 "수직(격리) 구조"를 택했는가 — 다른 flaky 이슈(5절)에서 나온 개념을 여기 적용한 것 이 테스트는 CPU 타이밍 문제가 아니라 "다른 테스트가 먼저 그 라우트를 건드렸는가"라는 이진 상태 문제 다. 그래서 retries/workers/타임아웃 조정(5절 이슈에 썼던 방법들) 자체가 원천적으로 안 통한다 — 같은 서버에 재시도해봤자 여전히 같은 서버, 여전히 404. "수직구조로 떼어낸다"는 개념 자체는 원래 5절의 flaky 이슈를 놓고 나온 팀 내 피드백 이었다 — "이 테스트만 타임아웃을 주게 되면 다른건 병렬구조인데 얘만 수직구조가 될 것, 스케줄을 top/bottom 어디에 둘 것인지"라는 우려, 그리고 나중 피드백("진행 중인 것들 finish된 걸 보고, 환경 클리어한 이후 일정 시간 지나서 테스트")은 CPU 경합을 완화하려는 취지 였다(다른 작업이 다 끝나서 CPU 여유가 생긴 뒤에 타이밍 민감한 테스트를 돌리자는 것). 이 개념(전체와 분리해서 따로 돌린다)을 원인이 다른 not-found에도 같은 틀로 가져와 적용 한 것이다. not-found에선 왜 top/bottom 순서 자체가 중요하지 않은지 : 격리 실행이 실제로는 sh -c '...; ...' 로 완전히 별도의 playwright test 프로세스 를 띄우는 방식이라(첫 번째 명령이 끝나면 그 웹서버는 종료되고, 두 번째 명령이 자기 서버를 완전히 새로 켠다) — 이 테스트가 메인 스위트보다 먼저 도나 나중에 도나, 어차피 한 번도 안 건드려진 새 서버를 상대하는 건 똑같다. 그래서 "나중(bottom)"으로 둔 건 지시를 그대로 따른 게 아니라, 그냥 메인 스위트를 먼저 보여주고 이 격리 확인을 뒤에 덧붙이는 게 자연스러워서 고른 순서 일 뿐, 순서 자체가 결과에 영향을 주는 요인은 아니다(둘 다 서버가 항상 새것이라 어느 쪽이든 500이 보장됨, 실제로 검증도 이 순서로만 했다). 5절 이슈 쪽은 다르다 : "환경 클리어 후 나중에" 라는 그 원래 취지대로 테스트해봤지만(완전 격리, workers=1), 5절에서 보듯 여전히 60% 실패 가 나와 이 추론이 그쪽에서는 실제로 검증되지 않았다. 해결 : 이 테스트를 전체 스위트에서 완전히 분리 — e2e 실행 타겟을 2단계로 재구성: "command": "sh -c 'playwright test --grep-invert \"존재하지 않는 상품\"; e1=$?; playwright test --grep \"존재하지 않는 상품\"; e2=$?; exit $((e1>e2?e1:e2))'" 1단계: 이 테스트를 제외한 나머지 실행 2단계: 이 테스트만 단독 실행(항상 컴파일 안 된 새 서버 상대) sh -c 로 감싸 두 단계 모두 항상 실행되고, 둘 중 하나라도 실패하면 전체 종료 코드에 반영됨 pnpm e2e 한 번 으로 자동 실행됨(수동으로 두 번 돌릴 필요 없음). 검증 완료 — 반복 실행 시 이 테스트가 항상 500/passed로 격리 확인됨. 주의 : 이 격리 구조는 pnpm e2e (nx 타겟)를 통해서만 적용된다. pnpm exec playwright test --ui 처럼 nx를 거치지 않고 직접 실행하면 이 분리 효과가 없다 — 같은 효과를 원하면 --grep-invert "존재하지 않는 상품" 을 직접 붙여야 한다. 실제 사용 명령어 정리 상황 명령어 평소 전체 확인(자동, 신뢰 가능) pnpm e2e — 격리 구조 자동 적용됨 --ui 로 전체 다 보고 싶을 때(이 테스트는 가끔 빨갛게 뜰 수 있음, 이유 있는 것이니 무시 가능) pnpm exec playwright test --ui --ui 로 이 테스트만 빼고 나머지 안정적으로 보고 싶을 때 pnpm exec playwright test --ui --grep-invert "존재하지 않는 상품" 이 테스트만 단독 확인 pnpm exec playwright test --ui --grep "존재하지 않는" static 모드로 전체(둘 다 자동으로 안정적) E2E_STATIC=true pnpm exec playwright test --ui --ui 는 대화형 도구라 pnpm e2e 처럼 "끝나면 자동으로 이어서 실행"하는 체이닝이 안 된다 — 필요하면 직접 두 번(위 표의 grep-invert → grep) 나눠서 확인해야 한다. 4. goBackWithMarker 타이밍 버그 — 해결됨 증상 : static 모드로 전체 스위트를 돌릴 때만 웹 진입 시나리오 테스트의 "critical user journey" 항목에서 Execution context was destroyed, most likely because of a navigation 에러. 원인 : goBackWithMarker 헬퍼가 page.goBack() 직후 곧바로 이전 문서를 대상으로 page.evaluate() 를 호출했다. next dev 는 응답이 느려서 이 사이 시간이 우연히 확보됐는데, static-server.mjs 는 응답이 훨씬 빨라 그 여유가 사라지면서 원래 있던 타이밍 결함이 드러났다. 해결 : 이 evaluate() 는 판정 로직과 무관한 순수 시각 표시(Inspector 하이라이트 제거)라, 실패해도 무시하도록 처리: await page.evaluate(() => { ... }).catch(() => {}); 5. navigator.spec.ts CPU 경합 flaky — 미해결 5-1. 무엇이 문제인가 레이아웃 리플로우 감지 관련 테스트는 requestAnimationFrame 기반 리플로우 감지를 MutationObserver 로 관찰한다. 이건 "언젠가 참이 되면 통과"하는 일반 assert와 달리, 그 순간에 실제로 스케줄된 이벤트가 발화하는가 를 재는 방식이라 CPU 경합에 유난히 취약하다 — 브라우저 프로세스가 CPU를 못 받으면 이벤트 자체가 통째로 무산될 수 있다(그냥 "늦게 오는" 게 아니라). 참고로 CPU 경합 자체는 이 테스트만의 문제는 아니다 — 이 테스트를 붙잡고 있다가 알게 된 것뿐이고, 비슷한 실시간 타이밍 방식으로 짜인 테스트라면 다른 곳에서도 같은 현상이 생길 수 있다. 5-2. 시도했지만 안 됐던 것들 시도 결과 retries 0→1 부분 개선 retries 1→2 개선 없음/악화(재시도가 워커 점유 시간을 늘려 경합 창을 넓히는 역효과) workers 6→4 개선 없음 expect.poll 타임아웃 5초→10초 악화(같은 이유) 배경 앱(Cursor, Claude 데스크톱 앱, Slack) 종료 부분 개선(load average 27→11.5) — 완전 해결 아님 완전 격리(workers=1, 이 테스트만 단독) 여전히 5번 중 3번(60%) 완전 실패 — "격리하면 항상 성공"이라는 초반 결론 자체가 표본 부족으로 틀렸음이 나중에 드러남 5-3. 배경 CPU 조사 — 새로 발견한 것 테스트 도는 동안 load average가 유휴 대비 급증(4→27)했고, 이 중 상당 부분이 Playwright/Chromium이 아니라 회사 보안·관리 에이전트 , macOS 자체 서비스(Spotlight mds , system_profiler )였다. 유휴 상태에서는 이 프로세스들이 거의 안 보이다가, 테스트가 도는 동안에만 뚜렷하게 치솟는 패턴을 확인(우연이 아니라는 근거로 유휴-테스트중 비교 실측함). 이건 로컬(회사 보안 소프트웨어) 요인이라 직접 끄거나 조정할 수 없다. 5-4. 유일하게 통했던 것 — static 모드 static 모드( E2E_STATIC=true )로 전체 스위트를 반복 실행하면 이 테스트를 포함해 전부 안정적으로 통과했다(3회 이상 연속 클린 확인). 추정 이유: static은 응답이 거의 즉시라 전체 스위트가 훨씬 빨리 끝나고, dev처럼 "요청마다 컴파일하느라 오래 걸리는" 시간 동안 배경 프로세스들의 경합이 쌓일 기회 자체가 없다. 주의 : not-found를 격리해도 이 테스트의 실패율(60%)엔 변화가 없었다(5회 반복 검증) — 두 문제가 서로 얽혀있던 게 아니라 독립적이다. 5-5. 팀 피드백과의 관계 "workers=1로 잡으면 CPU 경합 자체가 안 생길 것"이라는 예상이 있었는데, 실측(완전 격리 후에도 60% 실패)이 이를 뒷받침하지 못했다 — Playwright 자체 워커 격리는 되지만, Playwright 바깥의 배경 프로세스(에디터, 보안 에이전트 등)까지는 격리 못 하기 때문 으로 추정. 6. 작업 진행 순서 (요약) 정적 빌드 산출물 기반 e2e 환경 추가( E2E_STATIC ) goBack 직후 실행 컨텍스트 파괴로 인한 flaky 수정 not-found 시나리오를 dev/static 양쪽 환경 자동 검증으로 확장 CPU 경합 flaky 완화용 로컬 retries 1회 적용 dev 전체 스위트에서 not-found 500 테스트를 격리 실행하도록 재구성 7. 남은 것 navigator CPU 경합 flaky — 최종 해결 방법 미정, 현재 실용적 대응: static 모드를 주력 검증 경로로 사용, dev 모드에서 이 테스트만 실패하면 재실행으로 확인