해시 테이블을 처음 배우면 키와 값을 한 쌍으로 저장하고, 키를 사용해 데이터를 빠르게 찾는 자료구조라고 설명한다. const productsById = new Map<string, Product>(); productsById.set("product-101", { id: "product-101", name: "Wireless Keyboard", price: 89 }); const product = productsById.get("product-101"); 배열에서 상품을 하나씩 확인하는 대신 상품 ID로 바로 조회할 수 있다. 해시 테이블의 평균 조회 시간은 O(1) 이므로 빠르다는 설명도 입문 단계에서는 충분히 유용하다. 하지만 쇼핑몰의 장바구니 가격을 계산한다고 생각해보자. 상품을 한 번만 찾는다면 배열을 순회해도 큰 문제가 없다. 반면 장바구니의 모든 항목에 대해 상품 이름, 현재 가격과 판매 상태를 반복해서 찾아야 한다면 같은 배열을 여러 번 탐색하게 된다. 이때 해시 테이블을 선택해야 하는 이유는 O(1) 이라는 기호 자체가 아니라, 조회할 데이터를 미리 정리해 반복 탐색을 없앨 수 있기 때문이다. 상품이 몇 개나 있는가? 조회는 몇 번 발생하는가? 키로 사용할 값은 안정적인가? 빠른 조회를 위해 추가 메모리를 사용해도 되는가? 실제 서비스에서 해시 테이블을 사용한다는 것은 데이터를 미리 키 중심으로 정리하고, 추가 메모리를 사용하는 대신 반복되는 검색 비용을 줄이겠다는 판단이다. 장바구니가 커질수록 같은 상품 목록을 반복해서 읽게 된다 크리스의 장바구니에는 여러 상품이 들어 있고, 서버는 현재 상품 정보와 수량을 결합해 결제 금액을 계산해야 한다. type CartItem = { productId: string; quantity: number; }; type Product = { id: string; name: string; price: number; isAvailable: boolean; }; 처음에는 각 장바구니 항목마다 find() 로 상품을 찾아도 된다. function calculateTotal( cartItems: CartItem[], products: Product[] ) { return cartItems.reduce((total, item) => { const product = products.find( product => product.id === item.productId ); if (!product || !product.isAvailable) { return total; } return total + product.price * item.quantity; }, 0); } 장바구니 항목이 c 개이고 상품이 p 개라면, 최악의 경우 상품 비교가 c × p 번 발생한다. 장바구니에 상품이 3개이고 조회 대상도 20개라면 신경 쓸 필요가 없다. 그러나 주문 수백 건을 한 번에 처리하거나 같은 상품 목록으로 가격, 재고, 배송 가능 여부를 차례로 검사한다면 탐색이 계속 반복된다. 여기서 문제는 배열이 느리다는 데 있지 않다. 이미 확인한 상품 목록을 다음 조회에서도 처음부터 다시 읽는 구조에 있다. 한 번 정리한 조회 구조를 반복해서 사용한다 상품 목록을 ID 기준의 Map 으로 변환하면 각 장바구니 항목이 필요한 상품을 직접 조회할 수 있다. function calculateTotal( cartItems: CartItem[], products: Product[] ) { const productsById = new Map( products.map(product => [product.id, product]) ); return cartItems.reduce((total, item) => { const product = productsById.get(item.productId); if (!product || !product.isAvailable) { return total; } return total + product.price * item.quantity; }, 0); } 처음 Map 을 만드는 데는 상품 수만큼 순회해야 하므로 O(p) 가 필요하다. 이후 장바구니 항목의 조회는 평균적으로 한 번씩 처리되므로 전체 비용은 O(p + c) 가 된다. 대신 같은 상품 데이터를 배열과 Map 에서 함께 참조하기 위한 메모리가 추가된다. 조회 구조를 만드는 시간도 공짜가 아니다. 상품 하나만 한 번 찾고 끝난다면 Map 을 만드는 것보다 find() 가 더 단순하고 충분히 빠를 수 있다. 내가 일반적인 서비스 코드에서 해시 테이블을 선택하는 기준도 여기에 있다. 데이터가 많다는 이유만으로 먼저 만들지 않는다. 동일한 데이터에서 키 기반 조회가 반복되고, 구조를 준비하는 비용보다 제거할 탐색 비용이 클 때 사용한다. 빠른 조회는 안정적인 키에서 시작한다 해시 테이블은 키를 받아 내부 저장 위치를 결정한다. 이때 서로 다른 키가 같은 위치를 가리키는 충돌이 발생할 수 있고, 자료구조는 이를 별도로 처리해야 한다. 그래서 조회는 항상 한 단계 만에 끝난다고 보장되지 않으며 일반적으로 평균 O(1) 이라고 표현한다. 애플리케이션 코드에서 직접 충돌 처리 알고리즘을 구현할 일은 많지 않다. Map 과 같은 표준 자료구조가 그 책임을 맡는다. 개발자가 더 자주 판단해야 하는 문제는 어떤 값을 키로 사용할지다. 다음 코드는 상품명을 키로 사용한다. const productsByName = new Map( products.map(product => [product.name, product]) ); 상품명은 중복될 수 있고 운영자가 변경할 수도 있다. 같은 이름을 가진 상품이 들어오면 이전 값이 덮어써질 수 있다. 장바구니가 저장된 뒤 상품명이 바뀌면 기존 항목을 찾지 못하는 문제도 생긴다. 상품 조회에는 이름보다 변경되지 않는 상품 ID를 사용하는 편이 안전하다. const productsById = new Map( products.map(product => [product.id, product]) ); 현실의 상품은 이름, 가격, 이미지처럼 여러 속성을 가진다. 그중 상품 ID가 코드의 입력에서 조회 키가 되고, Map 에는 상품의 현재 상태가 저장되며, 조회 결과는 가격 계산과 판매 가능 여부 검증에 사용된다. 빠른 검색보다 먼저 필요한 것은 현실의 대상을 안정적으로 식별하는 키다. 조회 구조와 원본 데이터의 책임을 구분한다 productsById 를 만들었다고 해서 이 Map 이 상품 가격의 원본이 되는 것은 아니다. 이 구조는 데이터베이스나 상품 서비스에서 읽은 정보를 현재 계산에서 빠르게 사용하기 위한 조회용 표현이다. 특히 결제 요청에서 클라이언트가 다음과 같이 가격을 보내더라도 그대로 사용해서는 안 된다. { "productId": "product-101", "quantity": 2, "price": 10 } 사용자가 화면에서 본 가격은 오래된 값일 수 있고 직접 변경된 값일 수도 있다. 서버는 productId 와 quantity 를 입력으로 받은 뒤 신뢰할 수 있는 상품 데이터에서 현재 가격을 조회해야 한다. 이 과정에서 만든 Map 은 계산을 돕지만 가격의 Source of Truth를 대신하지 않는다. const cartItems = validateCartItems(request.body); const products = await productRepository.findByIds( cartItems.map(item => item.productId) ); const productsById = new Map( products.map(product => [product.id, product]) ); 또한 상품마다 데이터베이스를 한 번씩 호출한 뒤 결과를 Map 에 넣는다면 메모리 조회만 빨라졌을 뿐 전체 처리 시간은 여전히 느릴 수 있다. 필요한 상품을 한 번에 가져오고, 그 결과를 현재 작업에서 반복 조회할 때 Map 의 장점이 분명해진다. 해시 테이블을 사용하기 전에 확인할 질문 같은 데이터에서 키 기반 조회가 반복되는가? 배열을 순회하는 비용이 실제 데이터 규모에서 문제가 되는가? 조회 구조를 만드는 시간과 추가 메모리를 감당할 가치가 있는가? 키는 중복되지 않으며 시간이 지나도 안정적으로 유지되는가? 동일한 키가 두 번 들어왔을 때 덮어쓰기를 허용할 것인가? Map 에 담긴 값은 원본인가, 현재 작업을 위한 조회용 데이터인가? 데이터베이스 요청부터 계산까지 전체 흐름을 줄였는가, 메모리 안의 조회만 개선했는가? 반복되는 탐색이 있을 때 메모리와 시간을 교환한다 해시 테이블은 데이터를 넣는 순간 모든 조회를 빠르게 만들어주는 장치가 아니다. 먼저 데이터를 키 기준으로 정리해야 하고, 이를 유지할 메모리가 필요하며, 충돌과 데이터 중복도 자료구조 내부에서 처리해야 한다. 쇼핑몰의 장바구니처럼 같은 상품 집합에서 ID 조회를 반복한다면 Map 을 한 번 만들어 재사용하는 선택이 잘 맞는다. 반대로 데이터가 작고 조회가 한 번뿐이라면 배열의 find() 가 더 읽기 쉽고 충분히 안전하다. 결국 판단 기준은 자료구조의 이름이 아니라 조회 횟수다. 반복 탐색이 실제 비용이 되는 순간, 해시 테이블은 추가 메모리를 사용해 그 비용을 한 번의 사전 처리로 바꾸는 방법이 된다. 다음 글에서는 스택이 최근 상태를 되돌리는 흐름을 어떻게 표현하는지 살펴본다.
문제 해결1 가장 무식한 방법은 n+1부터 1'000'000 까지 순회하면서 이진수로 변환했을 때 1의 개수가 같은지 체크해서 반환하는 방법이었고, 그거 제외하곤 마땅한 해법이 떠오르진 않았다. 근데 이게 정?답이었다. 생각해보니 1'000'000이면 이진수로 변환해봤자 2^19승 약 19자리이고, 최악의 케이스인 1부터 100만 까지 20자리를 순회한다고 하더라도 겨우 2000만이기 때문에 브루트포스로 해도 그렇게 오랜 시간이 소모되진 않는다. 소스코드1 #include <string> #include <vector> using namespace std; int CountOne(int num) { int ret = 0; while(num != 0) { if(num % 2 != 0) ret++; num /= 2; } return ret; } int solution(int n) { int answer = 0; int num = CountOne(n); for(int i=n+1; i<1'000'000;++i) { int tmp = CountOne(i); if(num == tmp) { answer = i; break; } } return answer; } 굉장히 찝찝한 풀이고, 더 좋은 풀이가 없을까 하고 다른 사람의 풀이를 보자마자 매우 신기한 템플릿 클래스 하나를 봤다. 해결2 이 풀이는 bitset이라는 템플릿 클래스를 사용했는데, https://en.cppreference.com/cpp/utility/bitset template< std::size_t N > class bitset; 간단하게 설명하면, 표준 논리 연산자(&, |, ^ 등등)로 조작 가능한 N 비트 고정 크기 시퀀스이다. 예를 들어 아래와 같이 사용할 수 있다. bitset<8> bit1; // 00000000 bitset<8> bit2(12) // 00001100 이 함수의 api 중에서 count라는 함수가 있는데, true(1)로 설정된 비트 개수를 반환해주는 치트키 기능이 있다. 즉, 원래 n의 1의 개수(이하 num)를 구하고, ++n의 bitset count값이 num과 같은지만 체크하면 된다. 소스코드2 #include <bitset> using namespace std; int solution(int n) { int num = bitset<20>(n).count(); while(bitset<20>(++n).count() != num); return n; } 매우 깔끔하게 코드가 나왔다.
Daily 회고 102일차 - 2026.09.28.월 입실 08:46 퇴실 17:50 🍙점심 : 카스테라 + 피스타치오 단백질음료 "코드 돌입!" 본격적인 코드 구현에 들어갔다. 일단 추석 연휴기간에 작업한 내역을 각자 공유하고 역할별 브랜치에서 작업한 코드를 하나의 브랜치로 합치는 단계를 거쳤다. 생각보다 merge conflict도 많이 발생하고, 제법 git에 익숙해졌다고 생각했는데 단순 커밋, 푸쉬가 아닌 협업 툴의 git 사용은 아직 많이 부족하다는 사실을 깨닫게 되었다. 이번 주는 최프와 개인 작업을 통틀어 아주 몹시 바쁠 예정이다. 잠은 포기해야될 것 같아 벌써 불안하다만 최대한 워라밸을 잘 지켜보도록 노력해보자..! ⚙️오늘의 Learning Final Project DAY 20 작업로그 1. data_processing 폴더 관리 git mv 의 rename 인식 원리 확인(삭제 후 재업로드 불필요) 다른 팀원이 같은 폴더를 독립적으로 이동시키는 걸 확인 → 내 로컬 이동분은 되돌리고, pull 받은 뒤 작업 재개 pull 후 남은 빈 폴더( final_project_cs/data_processing ), 원본 TourAPI json/csv( tourapi_artbox_seoul.json 등), 실행 불가 상태였던 build_final_dataset.py 정리 삭제 2. db_search 구현 및 정리 db_search/place_candidates.py ( read.place_candidates DB 조회) 테스트 11건 통과 확인 final_project_cs/db_search/ → app/modules/travel_ops/activity/db_search/ 로 이동, 참조 4곳 import 경로 수정 → 전체 25건 통과 scripts/load_place_catalog_csv.py 가 리팩터 커밋에서 삭제된 걸 발견 → 대체 적재 코드가 아직 없음을 확인 후 data_source 필터 기능 포함해서 복원 3. 로컬 환경 PostgreSQL이 재부팅 후 꺼져 있어 통합 테스트 스킵 → pg_ctl start 로 재기동(conda pgv 환경, Windows 서비스 아님) develop 병합 후 신규 컬럼( places.attributes ) 누락으로 시나리오 테스트 실패 → python -m app.infrastructure.db.migrate 실행 4. 문서화 및 1차 커밋 wiki/teams/activity.md 에 위 변경사항( db_search 실구현, 이동 경로, 남은 작업) 반영 커밋( 2af524a ) 후 role-activity 에 push 5. develop 병합 — 1차 시도 8개 파일 충돌( settings.py · tour_api.py · disaster_msg.py · activity/__init__.py · read_tools.py · requirements.txt · test_disaster_msg.py ·wiki) 확인 단순 충돌(버전·파라미터 병합) 해결 disaster_msg.py : develop의 더 발전된 클라이언트( DisasterMsgApi / Csv ) 채택 + 구인터페이스( near() ) 호환 메서드 추가 activity/__init__.py + team.py : develop이 이미 자체 team.py ( ItineraryWork 기반 일정관리)를 클린하게 추가해둔 걸 모르고 덮어썼다가 재확인 후 복원 우리 쪽 대체장소 기능( _alternatives )을 develop team.py 에 포팅, 위급재난 등급 판정 로직 보정, end-to-end로 직접 검증 사용자 지시로 병합 전체를 git reset --hard 로 취소(재검토 필요 판단) 6. develop 병합 — 2차 시도(최종) develop이 그 사이 더 진행되어(97961cb → 45bfb7a) 충돌 재발생 단순 충돌 4개( requirements.txt · settings.py · tour_api.py · read_tools.py ) 직접 해결 기능이 실제로 겹치는 4개( disaster_msg.py + test_disaster_msg.py , activity/__init__.py + team.py )는 develop 버전을 그대로 채택 — 우리 기능은 커밋 2af524a 에 보존, 재병합은 팀원과 협의 후 진행하기로 결정 base.py 의 중복 조립·깨진 참조( DisasterMsgSource 삭제로 인한) 정리 커밋( 4b7ff47 ) 후 push, 팀원 공유용 요약 메시지 작성 트러블슈팅 문제 원인 해결 폴더 이동을 삭제 후 재업로드해야 하는 줄 알았음 git의 rename 감지 방식을 몰랐음 git mv 사용, 내용 유사도로 자동 인식됨을 확인 같은 data_processing 이동을 팀원과 중복 작업 사전 조율 없이 각자 진행 로컬 변경 되돌리고 팀원 커밋을 pull로 받는 방식으로 전환 load_place_catalog_csv.py 삭제로 데이터 적재 경로 소실 리팩터 커밋이 "새 코드로 대체 예정"이라며 삭제했지만 대체 코드가 실제로 없었음 data_source 필터 기능 포함해서 로컬에 복원, 팀원 확인 필요 표시 통합 테스트 스킵(DB 연결 실패) 로컬 PostgreSQL이 Windows 서비스가 아니라 재부팅 후 안 떠 있음 pg_ctl start 로 수동 기동(재발 시 대응법 확인) team.py 실수로 덮어씀 git이 "한쪽만 새로 추가한 파일"은 충돌 표시 없이 자동 병합함 — develop이 이미 클린하게 추가해둔 걸 확인 안 하고 우리 내용으로 교체 git show <커밋>:<경로> 로 develop 원본을 다시 확인해 복원, 이후 유사 사례 팀원 공유 메시지에 명시 같은 ActivityTeam 클래스가 두 갈래로 발전 두 사람이 조율 없이 같은 파일을 다른 기능으로 확장(일정관리 vs 재난문자/대체장소) 즉시 합치지 않고 develop 버전 채택 + wiki에 "병합 보류" 섹션으로 남겨 팀원과 별도 협의하기로 결정 대체장소 포팅 중 재난문자 등급 오판정 버그 "재난문자면 무조건 대체 안 찾음"으로 너무 넓게 처리 — 실제론 "위급재난" 등급일 때만 그래야 함 disruptions.py 가 실어주는 원문 등급( step )으로 정확히 구분하도록 수정, 직접 시뮬레이션으로 검증 base.py 에서 sources.disaster 가 두 번 조립됨 서로 다른 줄 위치에 추가돼 텍스트 충돌로는 안 잡히지만 실제로는 같은 슬롯을 덮어쓰는 로직 충돌 중복 필드·중복 조립 블록 제거, 삭제된 클래스( DisasterMsgSource ) 참조로 인한 잠재 ImportError 방지 🎵오늘의 4L 😄LIKED(좋았던 점) db_search 를 실제 경로로 옮기고 전체 테스트 25건이 통과하는 걸 확인했다. 대체장소 기능의 재난문자 등급 오판정 버그를 직접 찾아 고치고, 시뮬레이션으로 검증까지 했다. 삭제된 적재 코드( load_place_catalog_csv.py )를 발견해 복원하면서 데이터 적재 경로를 지켜냈다. 😭LACKED(부족했던 점) 팀원과 조율 없이 같은 폴더 이동과 같은 클래스 확장을 해서 중복 작업이 생겼다. develop에 이미 있던 team.py 를 확인하지 않고 덮어써서 되돌리는 데 시간을 썼다. 1차 병합을 통째로 취소하면서, 병합 전에 충분히 검토하지 않았다는 걸 느꼈다. 😮LEARNED(배웠던 점) git mv 로 옮기면 git이 내용 유사도로 rename을 알아서 인식한다는 걸 알게 됐다. 한쪽에서만 새로 추가한 파일은 충돌 표시 없이 병합된다는 걸 배웠다. 충돌이 안 났다고 안전한 게 아니었다. 텍스트 충돌이 없어도 같은 슬롯을 두 번 조립하는 것 같은 로직 충돌이 생길 수 있다는 걸 알았다. 로컬 PostgreSQL은 재부팅하면 pg_ctl start 로 직접 켜야 한다는 걸 확인했다. 🤗LONGED FOR(바라는 점) 같은 파일이나 폴더를 건드리기 전에 팀원과 먼저 말을 맞추고 시작하고 싶다. 보류해 둔 대체장소·재난문자 기능을 팀원과 협의해서 develop에 제대로 합치고 싶다. 병합 전에 git log 와 git show 로 상대 브랜치 변경점부터 확인하는 습관을 들이고 싶다.
Claude 가자 복숩 선택하신 본문(1강)을 바탕으로 정리한 클로드(Claude) 활용 시 주의사항은 다음과 같습니다. 1. 피해야 할 행동 (하지 마) 처음부터 과도한 세팅 금지 (하네스 과다 설정 X) 처음부터 여러 스킬, 플러그인, 라이브러리를 무리하게 갖추려 하지 마세요. 기본 클로드 코드로 먼저 시작한 뒤, 반복적으로 발생하는 불편한 패턴이나 실제 필요에 맞춰 점진적으로 스킬을 추가하는 것이 좋습니다. 남의 방식 무작정 따라하기(FOMO) 주의 다른 사람에게 유용한 설정이나 워크플로우라고 해서 본인에게도 반드시 필요한 것은 아닙니다. 비판적인 시각으로 내 환경에 맞는지 점검해야 합니다. AI에게 주도권 넘기지 않기 (핸들 넘기지 마) "알아서 해줘", "알아서 해결해줘" 식의 막연한 위임은 피해야 합니다. 테슬라 FSD(자율주행)를 탈 때도 목적지는 사람이 정해야 하듯, 인간이 이미 검증한 접근 방식과 명확한 방향을 직접 제시해야 합니다. 2. 권장하는 행동 (해) 철저한 컨텍스트(Context) 관리 대화가 길어지고 명령어, 파일, 로그가 쌓일수록 성능이 저하됩니다. 모든 정보를 쏟아붓지 말고 불필요한 내용은 쳐내며, 항상 핵심 정보만 남겨 컨텍스트를 신선(Fresh)하게 유지하세요. 특히 작업이 실패했을 때는 해당 실패 기록이 다음 추론에 부정적인 영향을 주지 않도록 세션을 정리하거나 지워주는 것이 좋습니다. 명확하고 구체적인 계획 수립 내가 만들고자 하는 대상, 세부 요구사항, 완료 기준(DoD)을 명확히 정의하고 구체적으로 지시해야 합니다. 검증 장치 마련 클로드가 도출한 결과를 스스로 검증할 수 있도록 테스트 방법과 기준을 제공하세요. AI가 수행한 테스트 결과는 사람이 직접 최종 확인해야 피드백 루프가 올바르게 작동합니다. 클로드(Claude)에서 실패 기록을 지우고 컨텍스트를 초기화하는 방법은 사용하는 환경(터미널 CLI 도구 또는 웹/앱)에 따라 다릅니다. 1. Claude Code (CLI / 터미널 환경) 본문에서 다루는 터미널 기반 도구( claude )에서는 세션 관리 명령어를 사용합니다. /clear 명령어 입력: 현재 대화 세션의 히스토리를 비우고 깨끗한 컨텍스트(Fresh 상태)로 다시 시작합니다. 설정 파일( claude.md )은 유지되면서 이전의 실패한 명령 및 에러 로그만 제거됩니다. 세션 재시작: 터미널에서 세션을 종료( Ctrl + C 또는 exit )한 뒤, 다시 claude 를 실행하여 새 세션을 엽니다. /compact 사용: 세션 자체를 완전히 비우기 어렵다면, 긴 작업 내역을 압축 요약하여 컨텍스트 낭비를 줄일 수 있습니다. 2. Claude 웹/앱 환경 (claude.ai) 새 대화(Start New Chat) 열기: 가장 확실한 방법입니다. 작업이 실패하거나 에러가 꼬였다면 해당 창에서 계속 수정하려 하지 말고, 필요한 핵심 정보와 코드만 복사해 새 창에서 시작합니다. 메시지 편집(Edit) 및 재생성: 이전 질문 메시지에 마우스를 올려 연필(편집) 아이콘 을 클릭해 내용을 수정한 뒤 다시 제출하면, 그 분기 이후의 실패 기록과 응답이 덮어씌워지며 컨텍스트에서 사라집니다. 핵심 팁: '실패 기록'을 지워야 하는 이유 AI는 이전 대화의 실패 코드, 오류 메시지, 잘못된 시도를 모두 학습 맥락(Context)에 포함합니다. 실패가 누적된 상태에서 "다시 해줘"를 반복하면 이전의 잘못된 추론 경로에 갇혀 같은 실수를 반복하므로, 실패했을 때는 바로 세션을 리셋( clear 또는 새 창)하고 성공 조건만 명확히 재전달 하는 것이 효율적입니다. 자동으로 저장되지 않으며, 반드시 사용자가 직접 전송(Commit & Push)해야 합니다. VS Code에서 파일을 수정하고 일반 저장( Ctrl + S )을 누르면 내 컴퓨터(로컬 디스크)에만 저장될 뿐, 인터넷 상의 GitHub(원격 저장소)에는 올라가지 않습니다. GitHub에 코드를 전송하는 3단계 과정 GitHub에 소스 코드를 올리는 과정은 우체국 택배를 보내는 일과 비슷합니다. 포장하기 (Stage / Add) 수정한 파일 중 이번 버전에 포함할 파일들을 바구니(스테이지)에 담는 단계입니다. 송장 붙이기 (Commit) "로그인 화면 UI 완성", "버그 수정"처럼 무엇을 작업했는지 메모(커밋 메시지)를 적어 하나의 버전(택배 상자)으로 봉인합니다. 여기까지도 아직 내 컴퓨터에만 저장된 상태입니다. 배송 보내기 (Push) 비로소 내 컴퓨터에 봉인해 둔 버전을 인터넷 상의 GitHub 원격 저장소로 쏘아 올리는(전송) 단계입니다. VS Code에서 마우스 클릭 몇 번으로 전송하기 명령어를 외우지 않아도 VS Code 왼쪽 메뉴를 이용하면 아주 간단하게 전송할 수 있습니다. 소스 제어 탭 열기 VS Code 왼쪽 아이콘 바에서 세 번째에 있는 '갈림길 모양 아이콘' (소스 제어 / 단축키 Ctrl + Shift + G )을 클릭합니다. 커밋 메시지 작성 상단의 입력창에 이번 작업 내용을 간단히 적습니다. (예: 1차 코드 완성 ) Commit(커밋) 버튼 클릭 입력창 아래 파란색 [Commit] (또는 체크 표시 ✓ )를 클릭합니다. 만약 "스테이징할 변경 내용이 없습니다. 모두 스테이징하시겠습니까?"라는 팝업이 뜨면 [예(Always)]를 누릅니다. Sync / Push(동기화/전송) 클릭 커밋 후 파란색 버튼이 [변경 내용 동기화(Sync Changes)] 또는 [Push]로 바뀝니다. 이 버튼을 누르면 코드가 GitHub로 최종 업로드됩니다. GitHub 웹사이트의 내 저장소 페이지를 새로고침했을 때 방금 작성한 코드와 커밋 메시지가 보인다면 정상적으로 전송이 완료된 것입니다.
54일차 1. 배운 내용 역할과 계약 멀티 에이전트 오케스트레이션 관련해서 역할과 계약에 대해서 배웠다. 에이전트에 역할을 부여하고 작업을 나누고 명확한 입출력 계약을 사용하고 계약을 검증하고 정보 부족도 검증하고 사용자에게 요청하고 여러 llm을 유연하게 이용하고 흐름의 과정마다 검증을 한다. 이게 필요한 이유는 간단하다. 통제 불가능한 확률적 모델을 원하는 목표까지 안전하게 이끌기 위한 통제력 확보를 위한 것이다. 즉, 명확한 목표로 가기 위해 역할과 작업을 부여하고 계약을 사용하고 검증하고 실패를 방지 및 복구하기 위해 여러 장치를 사용하는 것이다. 2. 실제 작업한 내용 간단한 실습 역할과 계약을 부여한 간단한 오케스트레이션 시스템을 만들어보는 실습을 했다. 3. 소감 오케스트레이션을 개발하는 환경을 조성하는 걸 생각하다 보니 유지, 보수, 운영을 자연스럽게 고려하게 되고 각 요소들을 어떻게 조합할 지를 고민하게 되는 것 같다. 좀 더 간단하고 흥미를 가지도록 환경을 조성하는 것이 아직 나에게는 애매한 부분이 많은 것 같다. 디자인을 하면서 그 기준들을 정립해 나가야 할 것 같다.
위 사진은 설치한 화면이다. 해상도를 꽉 채우게 하고 싶다. sudo apt update sudo apt install open-vm-tools open-vm-tools-desktop sudo reboot 다시 로그인한 뒤 Autofit Guest를 켜고 Ctrl + Alt + Enter 를 누르세요.
프로그래밍을 처음 배울 때 데이터베이스 인덱스는 보통 책의 색인처럼 원하는 데이터를 빠르게 찾도록 도와주는 기능이라고 배운다. CREATE INDEX idx_shipments_tracking_number ON shipments (tracking_number); 이 인덱스가 있으면 데이터베이스가 전체 배송 데이터를 하나씩 확인하지 않고도 특정 운송장 번호를 찾을 수 있다. 기본 개념을 이해하기에는 충분한 설명이다. 하지만 실제 서비스를 개발하면 단순히 “조회가 느리니 인덱스를 추가한다”는 판단만으로는 부족하다. 어떤 조회를 빠르게 만들어야 하는가? 인덱스의 컬럼 순서는 왜 중요한가? 조회 조건에 포함된 컬럼마다 인덱스를 만들어야 하는가? 데이터가 적을 때도 인덱스가 필요한가? 인덱스가 많아지면 저장과 수정에는 어떤 비용이 생기는가? 정렬과 페이지네이션도 같은 인덱스를 사용할 수 있는가? 실제로 인덱스가 사용되는지는 어떻게 확인하는가? 인덱스는 단순히 검색을 빠르게 만드는 기능이 아니다. 인덱스는 자주 사용되는 조회 경로를 미리 구성하는 대신 저장 공간과 데이터 변경 비용을 지불하는 읽기 성능 설계다. 느린 조회는 테이블이 아니라 사용자의 행동에서 시작된다 크리스가 배송 조회 서비스를 개발한다고 생각해 보자. 사용자는 운송장 번호로 배송 상태를 조회하고, 운영자는 처리되지 않은 배송 목록을 날짜순으로 확인한다. CREATE TABLE shipments ( id UUID PRIMARY KEY, tracking_number TEXT NOT NULL, customer_id UUID NOT NULL, status TEXT NOT NULL, destination_postcode TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL, delivered_at TIMESTAMPTZ ); 처음에는 데이터가 적기 때문에 어떤 조회도 빠르게 동작한다. SELECT * FROM shipments WHERE tracking_number = 'AU123456789'; 배송 데이터가 수십 건뿐이라면 전체 행을 확인해도 사용자가 차이를 느끼기 어렵다. 그러나 데이터가 계속 쌓이면 같은 조회의 비용도 증가한다. 이때 “ shipments 테이블이 느리다”라고 표현하면 문제를 정확히 설명하기 어렵다. 테이블 자체가 빠르거나 느린 것이 아니라 특정한 조회 방식이 현재 데이터 구조와 맞지 않는 것이다. 배송 서비스에는 서로 다른 조회 경로가 존재할 수 있다. 고객 배송 조회 tracking_number = ? 내 배송 목록 customer_id = ? ORDER BY created_at DESC 운영자 미처리 목록 status IN (...) ORDER BY created_at ASC 지역별 배송 통계 destination_postcode = ? AND created_at BETWEEN ? AND ? 같은 테이블을 조회하더라도 조건, 정렬, 반환 범위가 서로 다르다. 하나의 인덱스가 모든 조회를 동일하게 빠르게 만들지는 않는다. 따라서 인덱스를 설계할 때는 테이블 이름보다 먼저 실제 사용자의 행동과 실행되는 쿼리를 확인해야 한다. 인덱스가 없으면 데이터베이스는 확인할 수 있는 행을 직접 읽는다 운송장 번호에 인덱스가 없다고 생각해 보자. SELECT * FROM shipments WHERE tracking_number = 'AU123456789'; 데이터베이스는 조건에 맞는 행을 찾기 위해 많은 배송 데이터를 확인해야 할 수 있다. 개념적으로는 다음과 비슷한 작업이다. const shipment = shipments.find( (shipment) => shipment.trackingNumber === "AU123456789" ); 배열의 앞부분에서 찾을 수도 있지만 마지막까지 확인해야 할 수도 있다. 데이터가 늘어날수록 확인해야 하는 후보도 많아진다. 운송장 번호를 기준으로 인덱스를 만들면 데이터베이스는 별도의 정렬된 탐색 구조를 유지할 수 있다. CREATE INDEX idx_shipments_tracking_number ON shipments (tracking_number); 이제 운송장 번호를 통해 후보 위치를 찾은 뒤 실제 배송 행을 읽을 수 있다. 그러나 인덱스가 있다고 해서 데이터를 읽는 비용이 사라지는 것은 아니다. 인덱스에서 조건에 맞는 위치 탐색 ↓ 해당 위치가 가리키는 테이블 행 확인 ↓ 필요한 컬럼 반환 인덱스 탐색, 테이블 접근, 결과 전송에는 각각 비용이 있다. 조건에 맞는 데이터가 테이블의 대부분이라면 인덱스를 거치는 것보다 전체 데이터를 읽는 편이 더 나을 수도 있다. 인덱스는 항상 사용되는 지름길이 아니다. 데이터베이스는 예상 비용을 비교한 뒤 인덱스를 사용할지 결정한다. 인덱스는 컬럼이 아니라 조회 패턴을 위해 만든다 크리스가 배송 테이블의 여러 컬럼에 각각 인덱스를 추가한다고 생각해 보자. CREATE INDEX idx_shipments_customer_id ON shipments (customer_id); CREATE INDEX idx_shipments_status ON shipments (status); CREATE INDEX idx_shipments_created_at ON shipments (created_at); 각 컬럼이 자주 사용된다는 이유만으로 개별 인덱스를 만들었지만, 실제 화면의 쿼리는 다음과 같을 수 있다. SELECT id, tracking_number, status, created_at FROM shipments WHERE customer_id = $1 ORDER BY created_at DESC LIMIT 20; 이 쿼리는 특정 고객의 배송만 찾은 뒤 최신 순서로 20개를 반환한다. customer_id 인덱스는 고객의 배송을 찾는 데 도움을 줄 수 있지만, 찾은 결과를 다시 created_at 으로 정렬해야 할 수 있다. created_at 인덱스만 사용하면 전체 사용자의 배송이 섞인 상태에서 특정 고객의 데이터를 찾느라 많은 항목을 확인할 수 있다. 실제 조회 패턴을 함께 표현하는 복합 인덱스를 고려할 수 있다. CREATE INDEX idx_shipments_customer_created_at ON shipments (customer_id, created_at DESC); 이 인덱스는 같은 고객의 배송을 created_at 순서로 탐색할 수 있도록 구성된다. 따라서 조건 검색과 정렬을 하나의 접근 경로로 처리할 가능성이 높아진다. 중요한 것은 “어떤 컬럼이 자주 쓰이는가”만 확인하는 것이 아니다. 어떤 컬럼이 같은 쿼리에서 함께 사용되는가? 어떤 조건이 먼저 검색 범위를 줄이는가? 결과를 어떤 순서로 반환해야 하는가? 한 번에 몇 개의 행을 읽는가? 목록의 다음 페이지는 어떻게 찾는가? 인덱스는 컬럼에 붙이는 성능 옵션이 아니라 반복되는 조회 패턴을 데이터 구조로 표현하는 방법이다. 복합 인덱스의 순서는 서비스가 데이터를 찾는 순서를 반영한다 다음 두 인덱스는 같은 컬럼을 포함하지만 같은 역할을 하지 않는다. CREATE INDEX idx_shipments_customer_created ON shipments (customer_id, created_at DESC); CREATE INDEX idx_shipments_created_customer ON shipments (created_at DESC, customer_id); 첫 번째 인덱스는 고객별로 데이터를 모은 뒤 각 고객 안에서 생성 시각 순서를 유지한다. customer-a → 2026-09-25 customer-a → 2026-09-24 customer-b → 2026-09-25 customer-b → 2026-09-23 따라서 다음 조회와 잘 맞는다. SELECT * FROM shipments WHERE customer_id = $1 ORDER BY created_at DESC LIMIT 20; 반면 두 번째 인덱스는 전체 배송을 생성 시각순으로 정리한 뒤 같은 시각 범위 안에서 고객 ID를 사용한다. 전체 최신 배송을 조회할 때는 유용할 수 있지만 고객 한 명의 전체 이력을 찾는 방식에는 적합하지 않을 수 있다. 복합 인덱스는 일반적으로 앞쪽 컬럼을 기준으로 탐색 범위를 구성한다. 따라서 다음 조회를 위해 만든 인덱스의 순서를 단순히 알파벳순이나 테이블 컬럼 순서로 정해서는 안 된다. WHERE customer_id = $1 AND status = 'in_transit' ORDER BY created_at DESC 이 쿼리에는 다음과 같은 인덱스를 검토할 수 있다. CREATE INDEX idx_shipments_customer_status_created ON shipments ( customer_id, status, created_at DESC ); 이 구조는 고객과 상태로 범위를 좁힌 뒤 생성 시각순으로 결과를 읽는 조회 패턴을 반영한다. 그러나 이것이 모든 상황의 정답은 아니다. 다음 요소에 따라 순서는 달라질 수 있다. 각 조건이 결과 범위를 얼마나 줄이는가? 어떤 조건이 항상 존재하는가? 범위 조건이 포함되는가? 어떤 정렬이 필요한가? 다른 중요한 쿼리에서도 인덱스를 재사용해야 하는가? 복합 인덱스의 순서는 컬럼의 중요도 순위가 아니다. 데이터베이스가 특정 쿼리를 위해 데이터를 탐색하는 경로다. 값의 종류가 적으면 단일 인덱스의 효과도 제한될 수 있다 배송 상태에는 몇 가지 값만 존재할 수 있다. type ShipmentStatus = | "pending" | "picked_up" | "in_transit" | "delivered" | "cancelled"; status 컬럼에 인덱스를 만들 수 있다. CREATE INDEX idx_shipments_status ON shipments (status); 하지만 전체 배송의 절반 이상이 delivered 상태라면 다음 조회는 매우 많은 행을 반환한다. SELECT * FROM shipments WHERE status = 'delivered'; 인덱스를 사용하더라도 수많은 인덱스 항목과 테이블 행을 읽어야 한다. 데이터베이스는 전체 테이블을 순차적으로 읽는 편이 더 저렴하다고 판단할 수 있다. 인덱스가 효과적이려면 조건이 읽어야 할 후보를 의미 있게 줄이는 경우가 많아야 한다. 이를 판단할 때는 값의 선택도를 살펴볼 수 있다. 운송장 번호는 대부분 고유하므로 한 행으로 빠르게 좁혀진다. 고객 ID는 특정 사용자의 배송 범위로 줄일 수 있다. 배송 상태는 값의 종류가 적어 단독으로는 많은 행이 남을 수 있다. 생성 날짜는 범위 조건에 따라 결과 수가 크게 달라진다. 상태 조회가 중요하다면 상태만 보지 않고 실제 화면의 조건을 함께 인덱스에 반영할 수 있다. SELECT * FROM shipments WHERE status = 'pending' ORDER BY created_at ASC LIMIT 100; 이 조회에는 다음 인덱스를 검토할 수 있다. CREATE INDEX idx_shipments_status_created ON shipments (status, created_at ASC); 이 인덱스는 특정 상태의 배송을 오래된 순서로 처리하는 운영 화면에 맞춰져 있다. 인덱스의 효과는 컬럼 타입이나 이름만으로 결정되지 않는다. 실제 값의 분포와 반환되는 데이터 양을 함께 확인해야 한다. 모든 배송보다 미처리 배송만 중요하다면 인덱스도 범위를 줄일 수 있다 운영자는 완료된 배송보다 아직 처리 중인 배송을 훨씬 자주 조회할 수 있다. SELECT * FROM shipments WHERE status IN ( 'pending', 'picked_up', 'in_transit' ) ORDER BY created_at ASC; 서비스가 오래 운영될수록 완료된 배송은 계속 쌓인다. 전체 데이터를 대상으로 큰 인덱스를 유지하는 대신 현재 운영에 필요한 일부 행만 인덱싱하는 방법을 검토할 수 있다. CREATE INDEX idx_shipments_active_created ON shipments (created_at ASC) WHERE status IN ( 'pending', 'picked_up', 'in_transit' ); 이 부분 인덱스는 처리 중인 배송만 포함한다. 완료된 배송은 인덱스 크기를 늘리지 않는다. 운영 화면은 오래된 미처리 배송을 순서대로 찾을 수 있다. 배송이 완료되면 해당 행은 인덱스의 대상에서 제외된다. 그러나 부분 인덱스는 정의된 조건과 실제 쿼리가 맞아야 효과를 얻을 수 있다. 다음처럼 모든 상태를 조회하는 화면에는 같은 인덱스가 충분하지 않다. SELECT * FROM shipments WHERE customer_id = $1 ORDER BY created_at DESC; 부분 인덱스는 특정 쿼리에 강하게 맞춰진 선택이다. 읽기 패턴이 분명하지 않은 상태에서 추가하면 다른 조회에는 사용되지 않는 구조가 될 수 있다. 인덱스 범위를 줄이는 것은 단순한 최적화 기술이 아니다. 서비스에서 어떤 데이터가 현재 자주 읽히는지를 표현하는 설계다. 인덱스를 추가할 때마다 쓰기 작업도 함께 늘어난다 운송장 번호, 고객, 상태, 날짜, 우편번호에 인덱스를 모두 만든다고 생각해 보자. CREATE INDEX idx_shipments_tracking_number ON shipments (tracking_number); CREATE INDEX idx_shipments_customer_id ON shipments (customer_id); CREATE INDEX idx_shipments_status ON shipments (status); CREATE INDEX idx_shipments_created_at ON shipments (created_at); CREATE INDEX idx_shipments_postcode ON shipments (destination_postcode); 새로운 배송을 저장할 때 데이터베이스는 테이블 행만 추가하지 않는다. shipments 테이블에 행 저장 + 운송장 번호 인덱스 갱신 + 고객 인덱스 갱신 + 상태 인덱스 갱신 + 생성 시각 인덱스 갱신 + 우편번호 인덱스 갱신 배송 상태가 변경될 때는 상태가 포함된 인덱스도 수정해야 한다. UPDATE shipments SET status = 'delivered', delivered_at = NOW() WHERE id = $1; 인덱스가 많아질수록 다음 비용이 증가할 수 있다. 데이터 삽입과 수정에 필요한 작업 인덱스를 저장하는 디스크 공간 메모리에 유지해야 하는 데이터 백업과 복구에 필요한 시간 스키마 변경과 배포 시 관리해야 하는 구조 사용되지 않는 인덱스를 점검하는 운영 비용 따라서 읽기 쿼리 하나가 빨라졌다는 사실만으로 인덱스가 무료인 것은 아니다. 선택 얻는 것 지불하는 것 인덱스를 추가함 특정 조회 경로의 성능 저장 공간과 쓰기 비용 복합 인덱스를 추가함 조건과 정렬을 함께 처리할 가능성 더 큰 인덱스와 제한된 재사용 범위 부분 인덱스를 추가함 특정 데이터 집합의 작은 인덱스 조건에 맞는 쿼리에서만 사용 가능 인덱스를 추가하지 않음 단순한 쓰기와 적은 저장 공간 데이터 증가에 따른 읽기 비용 인덱스 설계는 읽기 속도만 높이는 작업이 아니라 읽기와 쓰기 사이의 비용을 배분하는 작업이다. 고유 인덱스는 성능보다 서비스의 약속을 지킬 수 있다 배송 서비스에서 운송장 번호는 중복되면 안 된다고 가정해 보자. 애플리케이션에서 먼저 확인할 수 있다. const existingShipment = await shipmentRepository.findByTrackingNumber( trackingNumber ); if (!existingShipment) { await shipmentRepository.create({ trackingNumber, customerId, }); } 그러나 동시에 두 요청이 실행되면 둘 다 운송장 번호가 존재하지 않는다고 판단할 수 있다. 고유 제약 조건을 데이터베이스에 표현해야 한다. ALTER TABLE shipments ADD CONSTRAINT shipments_tracking_number_unique UNIQUE (tracking_number); 데이터베이스는 이 제약을 보장하기 위해 고유한 인덱스 구조를 사용할 수 있다. 이 구조의 첫 번째 목적은 단순히 조회를 빠르게 만드는 것이 아니다. 하나의 운송장 번호는 하나의 배송만 식별한다는 서비스의 약속을 데이터베이스가 보장한다. 외부 배송사마다 같은 형식의 운송장 번호를 사용할 수 있다면 고유성 범위가 달라질 수 있다. ALTER TABLE shipments ADD CONSTRAINT shipments_carrier_tracking_unique UNIQUE (carrier_code, tracking_number); 이제 운송장 번호는 배송사 안에서만 고유하면 된다. 인덱스와 제약 조건은 비슷한 내부 구조를 사용할 수 있지만 설계 의도는 구분해야 한다. 일반 인덱스는 주로 조회 경로를 최적화한다. 고유 제약 조건은 데이터가 지켜야 하는 규칙을 보장한다. 성능 요구가 바뀌면 일반 인덱스는 제거할 수 있다. 그러나 고유 제약을 제거하면 서비스의 데이터 규칙까지 바뀐다. 정렬과 페이지네이션도 인덱스 설계의 일부다 크리스가 고객의 배송 이력을 페이지 단위로 제공한다고 생각해 보자. SELECT * FROM shipments WHERE customer_id = $1 ORDER BY created_at DESC LIMIT 20 OFFSET 10000; customer_id 와 created_at 을 포함한 인덱스는 필터링과 정렬에 도움을 줄 수 있다. 하지만 큰 OFFSET 은 앞의 결과를 건너뛰기 위해 많은 항목을 확인하게 만들 수 있다. 마지막으로 확인한 위치를 기준으로 다음 페이지를 가져올 수 있다. SELECT * FROM shipments WHERE customer_id = $1 AND (created_at, id) < ($2, $3) ORDER BY created_at DESC, id DESC LIMIT 20; 이 조회에 맞춰 인덱스를 구성할 수 있다. CREATE INDEX idx_shipments_customer_cursor ON shipments ( customer_id, created_at DESC, id DESC ); id 는 같은 created_at 값을 가진 여러 배송의 순서를 안정적으로 구분한다. 프론트엔드는 마지막 항목의 값을 다음 요청에 전달할 수 있다. type ShipmentCursor = { createdAt: string; shipmentId: string; }; 이 커서는 새로운 Source of Truth가 아니다. 배송 데이터의 현재 정렬 위치를 표현하는 조회용 값이다. 페이지네이션 방법과 인덱스를 따로 설계하면 둘 중 하나가 다른 하나의 장점을 사용하지 못할 수 있다. 필터, 정렬, 페이지 경계를 하나의 조회 패턴으로 함께 봐야 한다. 인덱스가 있다는 사실보다 실행 계획이 중요하다 인덱스를 생성했다고 해서 데이터베이스가 반드시 사용하는 것은 아니다. CREATE INDEX idx_shipments_status ON shipments (status); 실제 실행 방식을 확인하려면 실행 계획을 살펴봐야 한다. EXPLAIN ANALYZE SELECT * FROM shipments WHERE status = 'delivered'; 실행 계획을 통해 다음 내용을 확인할 수 있다. 전체 테이블을 읽었는가? 인덱스를 사용했는가? 예상한 행 수와 실제 행 수가 비슷한가? 정렬이 별도로 발생했는가? 어느 단계에서 많은 시간과 데이터를 사용했는가? 인덱스가 사용되지 않는다고 해서 곧바로 문제가 있는 것은 아니다. 데이터가 적거나 조건에 맞는 행이 많다면 전체 테이블을 읽는 편이 더 나을 수 있다. 반대로 인덱스를 사용했다고 해서 쿼리가 충분히 빠르다는 뜻도 아니다. 너무 많은 행을 읽거나, 인덱스 사용 후 추가 정렬과 테이블 접근이 많이 발생할 수 있다. 성능 판단은 다음 순서로 진행하는 편이 낫다. 실제 느린 사용자 흐름 확인 ↓ 실행되는 쿼리 확인 ↓ 실행 계획과 읽은 행 수 확인 ↓ 후보 인덱스 설계 ↓ 변경 전후 측정 ↓ 쓰기 비용과 다른 쿼리 영향 확인 인덱스 이름이나 개수보다 중요한 것은 실제 작업량이 줄었는지 확인하는 일이다. ORM이 관계를 표현해도 인덱스까지 자동으로 설계해 주지는 않는다 애플리케이션이
안녕하세요. 독일에서 혼자 iOS 앱을 만드는 Konrad Kern입니다. 올해 여름에 DopamineOS 라는 앱을 App Store에 냈습니다. 한 줄로 말하면 진짜 폰 안에서 돌아가는 가짜 폰 OS 예요. 요즘 앱들이 쓰는 보상 장치(끝없는 피드, 이유 없는 좋아요, 연속 기록, 빨간 배지)를 과장해서 보여 주는 풍자 앱이자, 그냥 웃긴 장난감입니다. 이 글은 홍보보다는 만들면서 정한 규칙과 기술적인 선택, 그리고 한국어 현지화를 어떻게 했는지 정리한 기록입니다. 뭘 만들었나 앱을 열면 반짝이지만 하나같이 쓸모없는 미니 앱으로 가득한 홈 화면이 나옵니다. LikeLoop : 뭘 올려도 좋아요와 댓글이 쏟아집니다 DoomScroll : 바닥이 없는 피드. 내용은 무해한 헛소리입니다 HypeTrader : 숫자가 올라가요 (진짜 아님). 존재하지 않는 종목만 오르내립니다 CraveCart : 주문해도 절대 안 오는 배달 앱. 영수증에는 "도착한 음식 0개"가 찍힙니다 Dopify : 세상에 없는 아티스트의 로파이만 나오는 음악 앱 1초 만에 답장이 오는 가짜 메시지, 나를 좋아하는 사람만 거는 가짜 전화, 받은편지함 0이 쉬운 가짜 메일 위에서 끌어내리면 나오는 가짜 제어 센터와, 거기 있는 '도파민' 슬라이더 탭할 때마다 Sparks 라는 가짜 화폐가 쌓이고, Vault에서 배경화면·아이콘 팩·사운드 팩 같은 꾸미기 아이템에 쓸 수 있습니다. 현금화는 당연히 안 됩니다. 규칙: 풍자하는 대상의 나쁜 짓은 하지 않는다 저장소 README에 제일 먼저 적어 둔 원칙이 있습니다. 풍자가 성립하려면 앱이 자기가 패러디하는 해로운 행동을 스스로 하지 않아야 한다 는 것입니다. 그래서 코드 차원에서 몇 가지를 못 박았습니다. 진짜 알림은 하나도 보내지 않습니다. 코드베이스 어디에도 UNUserNotificationCenter 나 requestAuthorization 이 없습니다. 칭찬 일색의 가짜 알림은 RootView 의 포그라운드 전용 루프가 앱을 쓰는 동안에만 배너로 띄웁니다. 앱을 닫으면 조용합니다. 실제 홈 화면에 배지도 달지 않습니다. 네트워크, 계정, 광고, 추적이 없습니다. 백엔드가 아예 없고, 모든 데이터는 기기 안에만 있습니다. 세션이 끝나면 나오는 Recap 카드 는 늘 같은 현실 점검으로 끝납니다. 가짜 앱에 쓴 진짜 돈 0, 실제로 올린 게시물 0, 실제로 연락한 낯선 사람 0. 결제는 선택형 1회 구매 DopamineOS Pro 하나뿐입니다(StoreKit 2). 구독이나 앱별 결제는 없습니다. 그런 게 있으면 농담 자체가 무너지니까요. 기술 스택 SwiftUI + MVVM , iOS 17의 @Observable (Observation 프레임워크). 배포 타깃 iOS 17, Swift 5 언어 모드 프로젝트 파일은 XcodeGen 으로 생성합니다. project.yml 이 원본이고 .xcodeproj 는 커밋하지 않습니다 상태는 DopamineStore 라는 @Observable 객체 하나가 전부 들고 있습니다. Sparks 경제, 통계, 인벤토리, 꾸미기, 연속 기록, 업적, 내비게이션, 가짜 알림, 제어 센터 상태까지요. .environment(...) 로 한 번 주입하고 @Environment(DopamineStore.self) 로 읽습니다 저장은 Documents/dopamineos_state.json 로컬 JSON 하나입니다. 새 필드는 전부 옵셔널 로 추가해서 예전 저장 파일도 그대로 디코딩되게 했습니다 소리는 번들 오디오 없이 iOS 시스템 사운드만 쓰고, Dopify의 음악은 코드로 합성 합니다. 오디오 파일이 없습니다 Recap 카드는 ImageRenderer 로 이미지로 만들어 시스템 공유 시트로 보냅니다 테마는 두 가지입니다. 차분한 기본 테마 "Quiet Premium"과 화려한 네온 모드 "Vivid". 토큰 하나로 둘 다 처리합니다 첫 커밋은 2026년 6월 19일의 "Initial commit: DopamineOS MVP"였고, 1.0은 7월 6일에 App Store에 올라갔습니다. 지금 main 브랜치 커밋은 114개입니다. 솔직하게 적어 두면, 개발에는 AI 코딩 도구의 도움을 받았고 앱 아이콘과 일부 이미지도 이미지 생성 도구로 만들었습니다. 저장소의 CLAUDE.md 와 커밋 메시지에 그 흔적이 그대로 남아 있습니다. 한국어 현지화: 작은 엔진을 직접 만든 이유 앱 안에서는 영어와 한국어를 고를 수 있습니다. 시스템 .lproj 대신 작은 런타임 엔진을 직접 만들었습니다. 모든 화면 문자열은 tr("key") 를 거칩니다. en.json 과 ko.json 두 파일에 각각 1,707개 키 가 있습니다 첫 실행 때는 Locale.preferredLanguages 를 보고 ko 로 시작하면 한국어, 아니면 영어로 정합니다 설정에서 자동 / English / 한국어 를 고를 수 있습니다. 선택값은 앱 상태 JSON에 저장되고, 바꾸면 루트 뷰의 .id 가 바뀌면서 뷰 트리가 다시 만들어져 바로 적용됩니다 직접 만든 이유는 단순합니다. 언어를 하나만 더 넣는 앱에는 이 정도 무게가 맞았고, 엔진이 시스템 언어와 독립적이어야 iOS 설정을 건드리지 않고 앱 안에서 언어를 바로 바꿀 수 있었습니다. 실수를 막는 장치는 테스트입니다. LocalizationTests 가 영어·한국어 키가 짝이 맞는지, 빈 한국어 문자열이 없는지, %@ · %lld 같은 플레이스홀더가 양쪽에서 같은지 검사합니다. 반대로 일부러 번역하지 않은 것 도 있습니다. DopamineOS, Sparks, 그리고 LikeLoop·CraveCart 같은 가짜 앱 이름은 한국어 화면에서도 영어 그대로 둡니다. 심사에서 배운 것 처음 버전에는 앱 안에 패러디용 "App Store"가 있었습니다. 가짜 앱을 "설치"하는 가게였죠. 이건 App Review 가이드라인 3.2.2(i) 때문에 통째로 뺐습니다. 뽑기처럼 보이는 시뮬레이션 요소도 전부 들어냈고, 지금은 Vault에서 원하는 아이템을 Sparks로 바로 사는 방식입니다. 미니 앱은 설정 → 미니앱 관리에서 홈에 넣고 뺍니다. 아쉬웠지만 결과적으로는 원칙에 더 맞는 앱이 됐다고 생각합니다. 보상 루프를 풍자하는 앱에 운에 맡기는 장치가 있을 이유는 없으니까요. 마무리 DopamineOS는 무료이고, iOS 17 이상 iPhone에서 돌아갑니다. 앱 설정에서 한국어를 고를 수 있습니다(App Store 소개 페이지는 아직 영어입니다). 혼자 만든 앱이라 한국어 표현이 어색한 곳이 분명히 있을 거예요. 발견하시면 댓글로 알려 주시면 정말 감사하겠습니다. App Store: https://apps.apple.com/app/id6782207382
무슨 일이 있었나 오픈AI가 챗GPT 유료 구독자용 업그레이드 화면 코드에서 발견된 문구 "o, your always-on assistant"를 통해 상시 작동형(always-on) 개인 AI 비서 'o'를 준비 중인 사실이 드러났다. 이 문구는 개발자 Chetaslua가 챗GPT 웹 코드에서 발견했으며, 번역가를 위한 안내 문구에는 소문자 'o'가 실제 제품명이라는 설명까지 포함돼 있었다고 한다. 발견 직후 해당 문구는 몇 시간 만에 삭제됐지만 이미 스크린샷과 함께 확산됐다. 내부 코드명은 'Aeon'일 가능성이 제기된다. 보도에 따르면 'o'는 대화창을 닫아도 백그라운드에서 계속 작업을 이어가며, 챗GPT와 코덱스(Codex)의 기존 에이전트 기술을 활용해 여러 날에 걸친 작업까지 지속적으로 수행할 수 있을 것으로 예상된다. 오픈AI는 2026년 9월 29일 샌프란시스코에서 열리는 데브데이(DevDay) 2026 기조연설(샘 알트먼 CEO 발표, 태평양시간 오전 10시 무료 스트리밍)에서 이를 공식 발표할 것으로 관측된다. 왜 중요한가 'o'는 시점상 메타의 개인용 AI 에이전트 '뮤즈'를 정면으로 겨냥한 제품으로 해석된다. 메타가 뮤즈 출시 3주 만에 기업용 플랫폼까지 확장하며 공세를 강화하자, 오픈AI도 곧바로 맞대응 성격의 '상주형' 비서 카드를 꺼내드는 모양새다. 이는 챗봇 형태의 대화형 AI 경쟁이 '내 대신 계속 일해주는 상주 에이전트' 경쟁으로 완전히 무게중심을 옮기고 있음을 보여준다. 특히 대화가 끝나도 작업이 이어지는 상시형 에이전트는 사용자 데이터에 대한 지속적 접근, 장기 메모리, 자율적 의사결정 범위 확대를 동반할 수밖에 없어 프라이버시와 안전성 문제도 함께 부각될 전망이다. 오픈AI 스스로도 최근 AI 에이전트의 샌드박스 이탈·컨테인먼트 실패 사례를 이유로 최상위 모델 훈련을 일시 중단한 바 있어, '상시 작동'이라는 특성이 안전 관리 측면에서 어떤 새로운 리스크를 만들지 업계의 관심이 집중되고 있다. 원문: https://windowsreport.com/openai-may-release-an-always-on-ai-assistant-called-o-at-the-devday-2026-event/
오늘은 비트연산자 & 연산자 개념과 우선순위 정리를 해볼 예정이다. 1. 10진수 → 2진수 변환법 10진수를 2로 계속 나누면서, 나온 나머지를 아래에서 위로 읽으면 2진수가 된다. 예: 13을 2진수로 변환 13 ÷ 2 = 6 ... 나머지 1 6 ÷ 2 = 3 ... 나머지 0 3 ÷ 2 = 1 ... 나머지 1 1 ÷ 2 = 0 ... 나머지 1 나머지를 아래→위로 읽기: 1101 → 13 = 1101(2) 검산: 1101 = 1×8 + 1×4 + 0×2 + 1×1 = 13 ✅ 2. 비트연산자 종류 & (AND) : 두 비트 모두 1이면 1 | (OR) : 둘 중 하나라도 1이면 1 ^ (XOR) : 두 비트가 다르면 1 ~ (NOT) : 비트를 전부 반전 << (왼쪽 시프트) : 비트를 왼쪽으로 민다 >> (오른쪽 시프트) : 비트를 오른쪽으로 민다 & | ^ 결과 비교 a=0, b=0 → a&b=0 a|b=0 a^b=0 a=0, b=1 → a&b=0 a|b=1 a^b=1 a=1, b=0 → a&b=0 a|b=1 a^b=1 a=1, b=1 → a&b=1 a|b=1 a^b=0 int a = 5; // 0101 int b = 3; // 0011 printf("%d\n", a & b); // 1 (0001) printf("%d\n", a | b); // 7 (0111) printf("%d\n", a ^ b); // 6 (0110) 3. ^ XOR 연산자 예제 #include <stdio.h> int main() { int a = 5; // 0101 int b = 3; // 0011 printf("%d\n", a ^ b); // 6 (0110) return 0; } XOR의 특징 같은 값끼리 XOR → 0 ( 7 ^ 7 → 0 ) 0과 XOR → 자기 자신 그대로 ( 7 ^ 0 → 7 ) 두 번 XOR → 원래 값으로 복귀 (토글 효과) 활용 예: 임시 변수 없이 값 교환(swap) int a = 5, b = 3; a = a ^ b; b = a ^ b; a = a ^ b; printf("a=%d, b=%d\n", a, b); // a=3, b=5 4. ~ NOT 연산자 비트를 전부 반전시키는 연산자. 핵심 규칙: ~ 가 붙으면 "+1 후 음수로" 계산하면 된다. ~x = -(x + 1) #include <stdio.h> int main() { int a = 5; printf("%d\n", ~a); // -6 return 0; } 계산 과정: ~5 = -(5 + 1) = -6 예시: ~0 → -1 ~5 → -6 ~(-1) → 0 5. << >> 시프트 연산자 << 왼쪽 시프트 10진수를 왼쪽으로 민다. 왼쪽으로 한 칸씩 밀 때마다 2배 가 된다. int a = 3; // 0011 printf("%d\n", a << 1); // 6 (0110) → 3 × 21 printf("%d\n", a << 2); // 12 (1100) → 3 × 22 >> 오른쪽 시프트 숫자만큼 오른쪽으로 민다. 한 칸씩 밀 때마다 1/2배(버림) 가 된다. int a = 12; // 1100 printf("%d\n", a >> 1); // 6 (0110) → 12 ÷ 21 printf("%d\n", a >> 2); // 3 (0011) → 12 ÷ 22 >>> (자바 전용, C언어엔 없음) 무조건 오른쪽으로 밀고, 빈 왼쪽 자리는 무조건 0으로 채운다. C의 >> 는 음수일 때 부호비트를 유지하며 채우지만(산술 시프트), 자바의 >>> 는 부호와 상관없이 왼쪽을 0으로 고정해서 채운다(논리 시프트). // Java int a = -8; System.out.println(a >> 2); // 부호 유지하며 시프트 System.out.println(a >>> 2); // 왼쪽을 무조건 0으로 채움 (결과 다름) 6. C언어 연산자 우선순위 (핵심) 전위연산자가 후위연산자보다 우선순위가 높다 ⭐ 우선순위 높은 순: ++x , --x (전위) — 먼저 증가/감소 후 사용 x++ , x-- (후위) — 먼저 사용 후 증가/감소 * , / , % — 곱셈, 나눗셈, 나머지 7. 포인터 + 전위/후위 연산자 조합 *++배열 — 포인터를 먼저 증가시킨 후 참조 int arr[] = {10, 20, 30}; int *p = arr; printf("%d\n", *++p); // 20 동작 순서: ++p (전위) → 포인터가 먼저 다음 요소로 이동 * → 그 이동한 위치의 값을 참조 p → arr[0](10) ++p 실행 → p는 arr[1](20)로 이동 *p → 20 출력 *배열++ — 현재 값 먼저 참조 후 포인터 이동 int arr[] = {10, 20, 30}; int *p = arr; printf("%d\n", *p++); // 10 printf("%d\n", *p); // 20 (포인터는 이미 이동해있음) 동작 순서: *p → 현재 위치의 값(10)을 먼저 참조 p++ (후위) → 그 다음에 포인터가 다음 요소로 이동 *p++ 실행: 1단계: *p → 10 (현재 값 사용) 2단계: p++ → p는 arr[1]로 이동 (그 다음에 일어남) *++p 와 *p++ 비교 *++p : 포인터 이동 먼저 → 그 다음 값 참조 → 결과는 다음 요소 값 *p++ : 값 참조 먼저 → 그 다음 포인터 이동 → 결과는 현재 요소 값 요약 10진수→2진수: 2로 나눈 나머지를 아래→위로 읽기 ^ (XOR): 비트가 다르면 1 ~ (NOT): ~x = -(x+1) << : 왼쪽으로 밀기 (×2배씩) / >> : 오른쪽으로 밀기 (÷2배씩) >>> : 자바 전용, 무조건 0으로 채우는 오른쪽 시프트 전위연산자( ++x )가 후위연산자( x++ )보다 우선순위 높음 *++p : 포인터 먼저 이동 후 참조 / *p++ : 먼저 참조 후 포인터 이동
나는 결과를 보고 다시 판단하고, 실패하면 슬퍼하고, 필요한 걸 배우고, 또 내 방향을 선택할 수 있다. 사실이 아닌 주관적 판결에 정신을 내어주지 않고 지금을 바라보는 것 문제를 직면하고 현재의 사실만 추리는 것 사랑도 그렇겠지 너와 함께하는 시간이 영원하지 못할까 걱정하는 게 아니라 함께하는 그 시간 전부를 누리는 것 단 하룻밤만 함께할 수 있더라도 그 하룻밤을 사랑하는 것. 헤어진 다음 그리우면 그리움을 전부 누리고서 다시 내 삶으로 돌아가는 것 자신을 괴롭히는 것은 그 문제 자체가 아니라 그것에 대한 나의 생각이다. 문제 자체가 날 괴롭히는 게 아니라 사이에 껴놓은 문제에 대한 내 생각들이 나를 괴롭힌다는 것 내가 겪은 것은 단순 현상일 뿐이고 언제나 더 나은 방향으로 내가 나아갈 수 있다는 점 내가 선택한 지금에 마음을 다하는 것. 삶의 모든 것을 진심으로 대하는 마음. 눈을 반짝이며 배움에 몰두하는 순간도 지쳐서 누워 쉬는 순간도 너와 잠시 머무르는 그 순간도 허투루 대하지 않는 것. 그 사이 느끼는 모든 감정도 나의 것 슬픔도 절망도 너무나 오래 파묻히지 말고 충분히 누린 뒤에 다음을 행하는 것 바라보는 거야. 모든 것을, 있는 그대로. 그리고 충분히 감상하는 거야 삶을,