🍕 一个主编、三个工种、两本手册:搭一支 AI 调研队
掘金
一、先看编制表:一个主编 + 三个工种 整个系统的骨架在 agent.mjs 里: 六个参数,就是一个团队的完整编制: 参数 作用 类比 systemPrompt 主 Agent 的职责说明 主编的岗
Score: 57.35Confidence: 54%
View offerLoading the catalog…
THE AI OPPORTUNITY INDEX
Find your next AI tool. Explore free access, trials, and credits — all in one place.
掘金
一、先看编制表:一个主编 + 三个工种 整个系统的骨架在 agent.mjs 里: 六个参数,就是一个团队的完整编制: 参数 作用 类比 systemPrompt 主 Agent 的职责说明 主编的岗
Score: 57.35Confidence: 54%
View offerAIタグが付けられた新着記事 - Qiita
AI の活用範囲は企業ソフトウェアのあらゆる部分へ広がっています。コード生成やデータ分析から、Agent、自動化、業務支援まで、既存システムの使い方そのものを見直す企業が増えています。以前の記事では、企業向けオープンソース AI Agent プラットフォーム 10 選を取り...
Score: 57.34Confidence: 54%
View offerReadhub
三安光电公告,公司于 2026 年 10 月 5 日收到监察机关出具的《解除留置通知书》,监察机关已解除对副董事长兼总经理林科闯的留置措施,目前林科闯已能正常履行职责,公司生产经营情况正常。
Score: 57.34Confidence: 54%
View offerReadhub
谷歌推出全新 macOS 应用 AI Edge Foresight,向苹果用户展示 EmbeddingGemma 2 端侧多模态模型的实力,该应用支持完全离线运行,可调用 Mac 的麦克风和系统音频,结合用户笔记整理润色线上、线下会议纪要,还支持基于会议内容和其他知识库的实时问答功能,兼容搭载 Apple 芯片的 Mac 计算机,用户可免费下载。
Score: 57.34Confidence: 54%
View offer掘金
开源 App AgentDeck:在手机上启动、审批电脑里的 Claude Code、Codex 等 agent,直接看它产出的图表和模拟器实时画面。端到端加密,内置免费 agent。
Score: 57.31Confidence: 54%
View offer掘金
先看两组数据 让 Agent 总结一个网页,最常见的做法是把整页内容读给它。问题是:整页内容往往不是 Agent 需要的东西。 场景一:总结一个 1 小时的 YouTube 演讲 方式 Token 耗
Score: 57.3Confidence: 54%
View offeriThome 新聞
10月2日美國網路與基礎設施安全局(CISA)更新已遭利用漏洞名單(KEV),增列開源IT服務與客服系統Zammad漏洞CVE-2026-102489與CVE-2026-102490,而這兩個漏洞的發現與揭露,源自於9月下旬荷蘭漏洞揭露協會(DIVD)發生的資安事故。
Score: 56.83Confidence: 54%
View offervelog
시스템 보안 · 취약점 › G. 내부 확장 · 측면 이동 징후 · 4/50편 (전체 304/450) 학습 단계: 측면 이동 기본 개념 실습 표기: 이 글의 명령어·출력·로그는 로컬 VMware 테스트 VM(Rocky Linux 9 / Ubuntu 22.04) 기준의 「실습 예시」 이며, IP·계정·호스트명은 가상의 값입니다. 선행 학습 303. 내부 확장·측면 이동 — 측면 이동 감사의 기반 구성 004. 주요 파일 권한 기준 점검 매트릭스(A영역) 1. 개념 측면 이동 탐지의 전제는 정상 내부 연결 기준선 입니다. 내부망에는 정상적인 호스트 간 통신(웹→DB, 관리→전체)이 많으므로, 무엇이 정상 경로인지 알아야 이상 이동을 구분할 수 있습니다. 측면 이동 기준선 요소 정상 연결 맵: 어떤 호스트가 어떤 호스트에 연결하나 web→db(정상), app→cache(정상) 정상 인증 출발지: 관리 호스트(mgmt)에서만 SSH 정상 포트: 서비스별 정해진 포트 정상 시점: 업무 시간·배포 창 정상 계정: 호스트별 사용 계정 기준선 밖 연결 = 측면 이동 후보 내부 연결은 대부분 정해진 패턴(웹→DB 등)을 따릅니다. 기준선(정상 연결 맵)에서 벗어난 새로운 경로가 측면 이동 후보입니다. 2. 왜 중요한가 내부망엔 정상 호스트 간 통신이 많아, 기준선 없이는 이상 이동을 구분할 수 없습니다. 정상 연결 맵(누가 누구에게)·인증 출발지·포트·시점이 비교 기준입니다. 기준선은 네트워크 구조(서비스 의존 관계) 이해에서 나옵니다. 3. 핵심 명령어 / 설정 기준선 요소 정상 이상 신호 연결 맵 web→db·app→cache 새 경로(web→dc) 인증 출발지 mgmt 호스트 서비스 서버 발 포트 서비스 정해진 비표준·다수 시점 업무·배포 새벽·비정기 계정 호스트별 정해진 교차·공유 4. 실습 (실습 예시) # 측면 이동 기준선 수립 (분석 방법) # 정상 내부 연결 맵(출발지→목적지:포트) sudo ausearch -k lateral -i --start week 2>/dev/null | grep -oE 'addr=[0-9.]+' | sort | uniq -c | sort -rn > /root/baseline/conn_map.txt # 정상 인증 출발지 sudo grep Accepted /var/log/secure 2>/dev/null | grep -oE 'from [0-9.]+' | sort | uniq -c > /root/baseline/auth_src.txt cat /root/baseline/auth_src.txt 5. 정상 상태 $ (정상 인증 출발지) 450 from 10.0.0.10 (mgmt01) 30 from 10.0.0.11 (backup01) → 관리·백업 호스트에서만 내부 SSH(정상 맵) 내부 SSH가 관리·백업 호스트에서만 발생하고, 연결 맵이 서비스 의존 관계와 일치하는 상태가 정상입니다. 6. 이상 상태 $ (정상 인증 출발지) 450 from 10.0.0.10 (mgmt01) 22 from 10.0.0.50 (web01 — 신규 출발지) → web01에서 내부 SSH 발생(기준선에 없던 출발지) web01(웹 서버)에서 내부 SSH 출발 → 기준선 이탈(웹 서버는 SSH 출발지가 아님) 정상 연결 맵(web→db만)을 벗어난 web→app·web→file 경로 출현 기준선 밖 출발지·경로 → 측면 이동 후보 → 정탐 7. 로그 분석 (분석 방법) 측면 이동 기준선 비교 흐름입니다(가상의 예시). [기준선] 정상 연결 맵·인증 출발지(mgmt)·포트·시점 ↓ (비교) [관측] web01 → db01,app01 SSH (04시) ↓ [이탈 판정] 출발지: web01(관리 호스트 아님) → 이탈 경로: web→app(맵에 없음) → 이탈 시점: 04시(비업무) → 이탈 ↓ [측면 이동 후보] 6축 판정(G08) 축 기준선 관측 판정 출발지 mgmt web01 이탈 경로 web→db web→app 이탈 시점 업무 04시 이탈 기준선 대비 이탈 축이 많을수록 측면 이동 가능성이 높습니다. 8. SOC 관제 포인트 측면 이동 탐지는 정상 내부 연결 기준선 (연결 맵·인증 출발지·포트·시점) 수립이 전제입니다. 내부망엔 정상 통신이 많아 기준선 없이는 이상 이동을 구분할 수 없습니다. 기준선 밖 출발지·경로·시점이 측면 이동 후보입니다. 9. 탐지 규칙 <!-- 실습 예시 룰: 적용 전 wazuh-logtest 및 테스트 환경 검증 필요 --> <group name="local,syssec_g,lateral,"> <!-- 비관리 호스트(서비스 서버)에서 내부 SSH 출발(기준선 이탈) --> <rule id="106030" level="11"> <if_sid>106000</if_sid> <field name="srcip" type="pcre2">^10\.0\.0\.(5[0-9]|[6-9][0-9])$</field> <description>서비스 서버 발 내부 SSH(인증 출발지 기준선 이탈)</description> <mitre><id>T1021.004</id></mitre> </rule> </group> 정상 관리·백업 호스트는 화이트리스트로 예외 처리합니다. 기준선은 네트워크 구조에 맞게 조정합니다. 10. 대응 방법 초기 확인 — 내부 연결 맵·인증 출발지·포트·시점의 정상 기준선을 수립합니다. 범위 확인 — 관측된 이동이 어떤 축(출발지·경로·시점)을 이탈했는지 확인합니다. 증거 확보 — 연결 맵 기준선과 이탈 이벤트를 보존합니다. 차단/조치 — 기준선 이탈 이동은 6축 판정(G08)으로 정탐/오탐을 가립니다. 재발 방지 — 연결 기준선 기반 측면 이동 탐지를 운영합니다. 11. 핵심 정리 요소 정상 vs 이상 연결 맵 web→db vs 새 경로 인증 출발지 mgmt vs 서비스 서버 포트·시점 정해진·업무 vs 비표준·새벽 계정 호스트별 vs 교차·공유 면접 포인트 "내부망은 정상 통신 많음 — 연결 맵 기준선 이탈이 측면 이동 후보" 12. 다음 편 예고 다음 편 305. 내부 확장·측면 이동 — 동-서 트래픽(east-west)과 이상 탐지 원리 에서는 동-서 트래픽을 다루는 동-서 트래픽(east-west)과 이상 탐지 원리 를 다룹니다. 이전 편: 303. 내부 확장·측면 이동 — 측면 이동 감사의 기반 구성(네트워크·인증 로그) 📚 시리즈 전체 보기: 시스템 보안 · 취약점
velog
1. 게임 개발자들은 완전히 멘탈이 나가버렸습니다 지난 편에서 빅오를 배웠습니다. 반복문 하나면 O(N), 다 비슷하다고 생각했죠. 그런데 이상한 일이 벌어졌습니다. 적 100만 마리의 체력을 한 번씩 더하는 코드를 두 가지로 짰는데요. 배열 에 담아서 한 번씩 훑기 → 원소 하나당 1.6ns 연결 리스트 에 담아서 한 번씩 훑기 → 원소 하나당 202ns 둘 다 "N개를 한 번씩 본다"는 똑같은 O(N) 인데, 120배 넘게 차이가 났거든요. 빅오로는 도저히 설명이 안 됐습니다. 이 글의 측정값은 모두 직접 돌려본 결과입니다. (클라우드 VM · Intel Xeon 2.1GHz · g++ -O2 · 단일 스레드 · 여러 번 실행한 중 최솟값) 숫자 자체는 PC마다 다르지만, 차이의 크기와 방향 은 거의 모든 PC에서 비슷하게 나옵니다. 2. CPU는 빠른데, 메모리가 너무 느렸거든요 CPU는 1초에 수십억 번 계산할 수 있습니다. 그런데 막상 계산할 데이터를 메모리(RAM)에서 가져오려니 , 그 한 번에 CPU가 수백 번 계산할 시간을 기다려야 했거든요. ▲ CPU와 메모리 사이의 캐시 계층 오른쪽으로 갈수록 크지만 느리고, 왼쪽으로 갈수록 작지만 빠릅니다. CPU는 필요한 데이터를 L1부터 찾고, 없으면 L2, L3, 마지막으로 RAM까지 갑니다. 대략적인 거리 감각은 이렇습니다. (CPU마다 다르니 "몇 배 차이"만 기억하세요.) 위치 대략적인 크기 대략적인 접근 시간 비유 L1 캐시 수십 KB 약 1ns 책상 위 L2 캐시 수백 KB ~ 수 MB 수 ns 책상 서랍 L3 캐시 수 MB ~ 수백 MB 십수 ~ 수십 ns 방 안 책장 RAM 수 GB 이상 수십 ~ 100ns 이상 동네 도서관 계산은 책상 위에서 1초 만에 끝나는데, 재료를 가지러 매번 도서관까지 다녀와야 했던 거죠. CPU는 대부분의 시간을 기다리면서 놀았습니다. 3. 그래서 똑똑한 개발자들이 기발한 생각을 했는데요 "어차피 도서관까지 간 김에, 옆에 꽂힌 책도 한 아름 같이 가져오자." CPU는 메모리에서 데이터 하나만 가져오지 않습니다. 그 주변까지 한 덩어리를 통째로 가져옵니다. 이 덩어리를 캐시 라인(Cache Line) 이라고 하고, 대부분의 x86 CPU에서 64바이트 입니다. ▲ 캐시 라인: 하나를 읽으면 64바이트가 통째로 온다 아래는 메모리, 굵은 세로선 사이가 캐시 라인 하나(float 16개 = 64바이트)입니다. 빨간 점 하나만 필요했지만, CPU는 그 캐시 라인 전체(주황)를 캐시(위 초록 상자)로 가져옵니다. 그래서 바로 다음 칸을 읽을 땐 도서관에 갈 필요가 없습니다. 이미 책상 위에 있거든요. 여기서 두 가지 규칙이 나옵니다. 이걸 지역성(Locality) 이라고 부릅니다. 공간 지역성(Spatial Locality): 방금 읽은 데이터의 바로 옆 을 곧 읽을 가능성이 높다 → 메모리에 나란히 붙여두면 빠르다 시간 지역성(Temporal Locality): 방금 읽은 데이터를 곧 다시 읽을 가능성이 높다 → 같은 데이터를 쓰는 작업은 몰아서 하면 빠르다 게다가 CPU는 "순서대로 읽고 있네?"를 눈치채면, 다음 캐시 라인을 미리 가져오기(Prefetch) 까지 합니다. 데이터가 나란히만 있으면 CPU는 거의 기다리지 않게 되죠. 4. 그런데 진짜 문제는 여기서부터였습니다 이 규칙을 몰랐던 코드는 캐시를 정면으로 배신 하고 있었거든요. 처음의 연결 리스트가 바로 그랬습니다. 1편에서 봤던 그림을 다시 떠올려 보세요. 배열은 원소가 나란히 붙어 있지만, 연결 리스트는 원소들이 메모리 여기저기 흩어져 있고 "다음 원소의 주소"를 따라 점프해야 합니다. 점프할 때마다 새 캐시 라인 을 가져와야 하고 다음 주소는 지금 원소를 읽어야만 알 수 있으니 미리 가져오기도 불가능 하죠 정말 그런지 데이터 크기를 바꿔가며 재봤습니다. ▲ 배열 vs 연결 리스트: 원소 하나당 걸린 시간 (로그 스케일, 실측) 가로축은 전체 데이터 크기, 세로축은 원소 하나를 처리하는 데 걸린 시간(ns)입니다. 세 방식 모두 원소를 한 번씩만 보는 O(N)입니다. 초록(배열) 은 데이터가 커져도 거의 평평합니다. 파랑(리스트지만 메모리에 순서대로 할당) 은 배열의 약 2배. 빨강(리스트, 메모리에 흩어져 할당) 은 데이터가 캐시를 넘어서는 순간 폭발해서, 384MB에서는 배열보다 120배 이상 느립니다. 데이터 크기 배열 리스트 (순서대로) 리스트 (흩어짐) 흩어짐 ÷ 배열 0.1MB 0.6ns 1.5ns 3.8ns 약 6배 1.5MB 0.6ns 1.5ns 7.7ns 약 13배 24MB 1.1ns 2.1ns 77.9ns 약 70배 384MB 1.6ns 3.5ns 202.1ns 약 120배 데이터가 작아서 캐시 안에 다 들어갈 때는 차이가 크지 않았는데요. 데이터가 커지자 흩어진 리스트는 원소마다 도서관을 다녀오기 시작했죠. 그리고 이건 연결 리스트만의 이야기가 아니었습니다. 게임 코드에서 아주 흔한 이 모양도 똑같거든요. TArray<AEnemy*> Enemies; // 배열이긴 하지만, 안에 든 건 "포인터" for (AEnemy* Enemy : Enemies) { Enemy->UpdateMovement(DeltaTime); // 포인터를 따라 액터마다 메모리 여기저기로 점프 } 배열에 담긴 건 주소 뿐이고, 액터 본체는 각자 따로 할당되어 메모리에 흩어져 있습니다. 결국 흩어진 리스트를 도는 것과 비슷한 일이 벌어지죠. 5. 바로 그 순간 또 다른 개발자가 천재적인 아이디어를 냈습니다 "객체 단위로 묶지 말고, 같이 쓰는 데이터끼리 나란히 묶자." 적을 이동시킬 때 필요한 건 위치와 속도 뿐입니다. 그런데 적 구조체에는 이름, 스탯, 인벤토리까지 잔뜩 들어 있었죠. // AoS (Array of Structures): 적 하나에 모든 걸 담고, 그걸 배열로 struct FFatEnemy { FVector3f Position; // 이동에 필요 (12바이트) FVector3f Velocity; // 이동에 필요 (12바이트) char Name[32]; // 이동엔 필요 없음 float Stats[16]; // 이동엔 필요 없음 int32 Inventory[8]; // 이동엔 필요 없음 }; // 총 152바이트 중 이동에 쓰는 건 24바이트 TArray<FFatEnemy> Enemies; ▲ AoS vs SoA: 캐시 라인 하나에 실려 오는 데이터 위 (AoS): 적 한 마리(152바이트)를 그린 것입니다. 주황이 이동에 쓰는 위치·속도, 회색이 이동엔 안 쓰는 나머지입니다. 초록 상자(캐시 라인 64바이트)를 가져와도 대부분이 쓸모없는 회색입니다. 아래 (SoA): 위치 X, Y, Z와 속도 X, Y, Z를 각각 따로 배열로 모았습니다. 초록 상자를 가져오면 16마리 분량의 쓸모 있는 데이터 가 꽉 차서 옵니다. 그래서 이렇게 바꿨습니다. 이게 SoA(Structure of Arrays) 입니다. // SoA (Structure of Arrays): 필드별로 배열을 따로 struct FEnemyMovementData { TArray<float> PosX, PosY, PosZ; TArray<float> VelX, VelY, VelZ; }; for (int32 i = 0; i < Num; ++i) { Data.PosX[i] += Data.VelX[i] * DeltaTime; Data.PosY[i] += Data.VelY[i] * DeltaTime; Data.PosZ[i] += Data.VelZ[i] * DeltaTime; } 결과는 이랬습니다. ▲ 왼쪽: 적 100만 마리 위치 갱신 (AoS vs SoA) / 오른쪽: 4096×4096 격자 전체 순회 (행 우선 vs 열 우선), 실측 왼쪽: 하는 계산은 똑같은데 SoA가 약 3.4배 빨랐습니다(7.9ms → 2.3ms). 오른쪽은 아래 6절에서 이어집니다. 계산은 한 줄도 줄지 않았습니다. CPU가 기다리는 시간 만 줄었을 뿐이죠. 이런 식으로 "객체가 아니라 데이터가 어떻게 흐르는지"를 기준으로 설계하는 걸 데이터 지향 설계(DOD, Data-Oriented Design) 라고 부릅니다. 실무에서는 전부 SoA로 쪼개기보다 자주 쓰는 데이터(Hot)와 가끔 쓰는 데이터(Cold)를 나누는 것 부터 시작합니다. 매 프레임 쓰는 위치·속도·체력은 작은 구조체 배열로, 이름·인벤토리는 다른 곳에 두는 것만으로도 효과가 큽니다. 6. 하지만 또 골칫거리가 생겼습니다 데이터를 나란히 붙여놨는데도 느린 코드가 또 나타났거든요. 공간 탐색 시리즈에서 많이 썼던 2D 격자 였습니다. // 4096 × 4096 격자: Grid[Y * Width + X] for (int32 X = 0; X < Width; ++X) // 바깥이 X { for (int32 Y = 0; Y < Height; ++Y) // 안쪽이 Y { Sum += Grid[Y * Width + X]; // 한 번에 Width칸(16KB)씩 점프! } } 격자는 메모리에 한 줄(행)씩 이어져 있습니다. 그런데 안쪽 반복문이 Y를 돌면, 매번 다음 줄로 점프 하게 되죠. 캐시 라인을 통째로 가져와 놓고 그중 4바이트만 쓰고 버리는 셈입니다. ▲ 행 우선 순회 vs 열 우선 순회 왼쪽 (행 우선): 한 줄씩 옆으로 훑습니다. 아래 메모리 띠에서도 한 방향으로 쭉 읽어서, 가져온 캐시 라인을 남김없이 씁니다. 오른쪽 (열 우선): 세로로 내려갑니다. 메모리에서는 한 줄 길이만큼 계속 점프해서, 캐시 라인마다 한 칸(주황)만 쓰고 버립니다. 앞의 실측 그래프 오른쪽이 바로 이 결과입니다. 64MB 격자를 한 번 훑는 데 행 우선은 3.5ms, 열 우선은 140ms . 반복문 두 줄의 순서만 바꿨을 뿐인데 40배 였습니다. 7. 바로 그 순간, 누군가 이렇게 말했습니다 "코드를 짜기 전에, 데이터가 메모리에 어떻게 놓여 있는지부터 그려보자." 캐시 지역성을 지키는 규칙은 결국 몇 줄로 정리됩니다. 나란히 놓는다: 포인터 배열보다 값 배열( TArray<FMyData> ) 같이 쓰는 것끼리 묶는다: 자주 쓰는 데이터(Hot)와 가끔 쓰는 데이터(Cold)를 분리, 필요하면 SoA 메모리 순서대로 돈다: 2D 배열은 바깥 Y, 안쪽 X 같은 데이터는 몰아서 처리한다: 적마다 "이동 → 공격 → 애니메이션"을 번갈아 하기보다, 모든 적 이동 → 모든 적 공격 순으로 (시간 지역성) // 메모리 순서대로: 안쪽 반복문이 연속된 주소를 돈다 for (int32 Y = 0; Y < Height; ++Y) { for (int32 X = 0; X < Width; ++X) { Sum += Grid[Y * Width + X]; } } 8. 그리고 지금, 언리얼 엔진에는 개발자들이 고생하며 깨달은 데이터 지향 설계는, 오늘날 언리얼의 Mass Entity 프레임워크로 들어가 있습니다. ▲ Mass의 아키타입 청크: 같은 종류의 데이터끼리 나란히 같은 프래그먼트 조합(여기서는 Transform, Velocity, Health)을 가진 엔티티들이 청크(큰 사각형) 단위로 모입니다. 청크 안에서는 Transform은 Transform끼리, Velocity는 Velocity끼리 나란히 붙어 있습니다. 위에서 본 SoA 구조를 엔진이 알아서 만들어 주는 셈입니다. 이동 처리는 이렇게 청크 단위로 돕니다. void UMyMovementProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) { EntityQuery.ForEachEntityChunk(Context, [](FMassExecutionContext& Context) { // 청크 안에 나란히 붙어 있는 프래그먼트 배열을 그대로 받는다 const TArrayView<FTransformFragment> Transforms = Context.GetMutableFragmentView<FTransformFragment>(); const TConstArrayView<FMassVelocityFragment> Velocities = Context.GetFragmentView<FMassVelocityFragment>(); const float DeltaTime = Context.GetDeltaTimeSeconds(); for (int32 i = 0; i < Context.GetNumEntities(); ++i) // 메모리 순서대로 쭉 { Transforms[i].GetMutableTransform().AddToTranslation(Velocities[i].Value * DeltaTime); } }); } ForEachEntityChunk 의 인자는 엔진 버전에 따라 다릅니다(구버전은 EntityManager 를 첫 인자로 받습니다). 사용 중인 버전의 Mass 샘플이나 엔진 소스를 꼭 확인하세요. 알아두면 좋은 포인트 TArray<UObject*> 를 매 프레임 도는 코드는 의심해 보세요. 액터 수가 수천 단위로 늘어날 로직이라면, 매 프레임 쓰는 데이터만 TArray<FMyHotData> 로 따로 모아 관리하는 매니저를 두는 것만으로 크게 빨라집니다. TInlineAllocator : TArray<int32, TInlineAllocator<8>> 처럼 쓰면 원소 8개까지는 힙 할당 없이 구조체 안에 바로 들어갑니다. 작은 배열이 여기저기 흩어지는 걸 막아줍니다. 구조체 필드 순서: 큰 타입부터 작은 타입 순으로 배치하면 정렬 패딩이 줄어서, 캐시 라인 하나에 더 많은 원소가 들어갑니다. 멀티스레드의 함정, False Sharing: ParallelFor 로 여러 스레드가 같은 캐시 라인 안의 서로 다른 변수 를 쓰면, 서로의 캐시를 계속 무효화해서 느려집니다. 스레드별 결과는 alignas(PLATFORM_CACHE_LINE_SIZE) 로 캐시 라인을 분리해 두세요. 최종 확인은 역시 측정: 캐시 문제는 코드만 봐서는 잘 안 보입니다. Unreal Insights로 구간 시간을 비교하고, 필요하면 VTune 같은 CPU 프로파일러로 캐시 미스를 직접 확인하세요. 정리 구분 내용 핵심 원리 CPU는 빠르고 메모리는 느리다. CPU는 64바이트 캐시 라인 단위로 가져온다 공간 지역성 옆에 있는 데이터를 곧 쓴다 → 나란히 붙여둔다 시간 지역성 방금 쓴 데이터를 곧 또 쓴다 → 같은 작업은 몰아서 한다 배열 vs 흩어진 리스트 같은 O(N)인데 384MB에서 약 120배 차이 (실측) AoS vs SoA 같은 계산인데 약 3.4배 차이 (실측) 행 우선 vs 열 우선 반복문 순서만 바꿨는데 약 40배 차이 (실측) 언리얼 실무 값 배열, Hot/Cold 분리, TInlineAllocator , Mass Entity, False Sharing 주의 빅오는 "일을 몇 번 하느냐" 를 알려주고, 캐시 지역성은 "한 번 할 때 얼마나 기다리느냐" 를 알려줍니다. 둘 다 봐야 진짜 빠른 코드가 나옵니다.
velog
시스템 보안 · 취약점 › G. 내부 확장 · 측면 이동 징후 · 5/50편 (전체 305/450) 학습 단계: 측면 이동 기본 개념 실습 표기: 이 글의 명령어·출력·로그는 로컬 VMware 테스트 VM(Rocky Linux 9 / Ubuntu 22.04) 기준의 「실습 예시」 이며, IP·계정·호스트명은 가상의 값입니다. 선행 학습 304. 내부 확장·측면 이동 — 측면 이동 정상 기준선 만들기 303. 측면 이동 감사의 기반 구성 1. 개념 네트워크 트래픽은 방향으로 구분됩니다. 남-북(north-south) 은 내부↔외부(경계)이고, 동-서(east-west) 는 내부 호스트 간 통신입니다. 측면 이동은 동-서 트래픽에서 일어나므로, 경계(남-북)만 보는 방어는 측면 이동을 놓칩니다. 트래픽 방향과 측면 이동 남-북(north-south): 내부 ↔ 외부 (방화벽·IPS가 봄) 동-서(east-west): 내부 ↔ 내부 (대개 감시 사각지대) → 측면 이동이 여기서 발생 전통적 경계 방어: 남-북만 감시 → 내부 확산 못 봄 필요: 동-서 가시성(내부 세그먼트·마이크로세그멘테이션) 대부분의 방어는 경계(남-북)에 집중돼 있어, 일단 내부에 들어온 공격자의 동-서 이동은 감시 사각지대입니다. 측면 이동 탐지는 동-서 가시성 확보가 핵심입니다. 2. 왜 중요한가 경계 방화벽·IPS는 남-북만 보고, 내부 동-서 트래픽은 대개 감시하지 않습니다. 측면 이동은 전부 동-서에서 일어나므로 동-서 가시성 없이는 탐지 불가입니다. 마이크로세그멘테이션·내부 NDR·호스트 로그 집계로 동-서를 봐야 합니다. 3. 핵심 명령어 / 설정 방향 경로 감시 남-북 내부↔외부 방화벽·IPS(보통 있음) 동-서 내부↔내부 사각지대(보강 필요) 측면 이동 동-서 동-서 가시성 필수 세그먼트 간 VLAN·존 내부 방화벽 세그먼트 내 같은 대역 호스트 로그·NDR 4. 실습 (실습 예시) # 동-서 트래픽 이상 탐지 (분석 방법) # 내부 대역 간 연결(동-서)만 필터 ss -tnp 2>/dev/null | grep -E '10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.' | grep -vE '127.0.0.1' # 세그먼트 간 비정상 연결(예: web 세그먼트 → db 세그먼트 신규) sudo ausearch -k lateral -i --start recent 2>/dev/null | grep -oE 'addr=10\.0\.[0-9]+\.[0-9]+' | sort | uniq -c | tail 5. 정상 상태 $ (동-서 연결) web01 → db01:3306 (정상 서비스 의존) app01 → cache01:6379 (정상) 동-서 연결이 정상 서비스 의존 관계(web→db, app→cache)에만 발생하는 상태가 정상입니다. 6. 이상 상태 $ (동-서 연결) web01 → db01:22, app01:22, file01:22 (SSH, 서비스 포트 아님) web01 → 10.0.1.0/24 (다른 세그먼트 신규 접근) web01이 내부 호스트들에 SSH(22) → 서비스 의존이 아닌 관리성 이동 다른 세그먼트(10.0.1.0/24) 신규 접근 → 세그먼트 경계 넘는 확산 동-서 비정상 연결(SSH·세그먼트 교차) → 측면 이동 → 정탐 7. 로그 분석 (분석 방법) 동-서 트래픽 이상 분석입니다(가상의 예시). [남-북] 외부 → web01:443 (경계 방화벽이 감시) ↓ (내부 침투 후) [동-서] web01 → db01:22 (경계가 못 봄) web01 → app01,file01 (확산) web01 → 10.0.1.x (세그먼트 교차) ↓ [탐지] 동-서 가시성으로만 포착 가능 = 경계만 보면 측면 이동 전부 놓침 방향 가시성 측면 이동 남-북 있음(경계) 안 보임 동-서 보강 필요 여기서 발생 동-서 가시성이 측면 이동 탐지의 전제입니다. 8. SOC 관제 포인트 측면 이동은 동-서(east-west, 내부 호스트 간) 트래픽에서 일어납니다. 경계(남-북) 방어만으로는 내부 확산을 놓치므로 동-서 가시성이 필수입니다. 마이크로세그멘테이션·내부 NDR·호스트 로그 집계로 동-서를 감시합니다. 9. 탐지 규칙 <!-- 실습 예시 룰: 적용 전 wazuh-logtest 및 테스트 환경 검증 필요 --> <group name="local,syssec_g,lateral,"> <!-- 서비스 서버 간 관리 포트(SSH) 동-서 연결 --> <rule id="106040" level="11"> <if_sid>106000</if_sid> <field name="audit.addr" type="pcre2">^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)</field> <field name="audit.dport" type="pcre2">^(22|3389|5985|445)$</field> <description>내부 호스트 간 관리 포트 연결(동-서 측면 이동)</description> <mitre><id>T1021</id></mitre> </rule> </group> 정상 서비스 의존(web→db:3306 등)은 기준선 예외로 두고, 관리 포트·세그먼트 교차 동-서 연결을 탐지합니다. 10. 대응 방법 초기 확인 — 내부 동-서 연결(출발지·목적지·포트·세그먼트)을 확인합니다. 범위 확인 — 서비스 의존이 아닌 관리 포트·세그먼트 교차 연결을 확인합니다. 증거 확보 — 동-서 연결 로그와 세그먼트 경로를 보존합니다. 차단/조치 — 비정상 동-서 이동 정탐이면 세그먼트 격리·연결 차단을 진행합니다. 재발 방지 — 동-서 가시성 기반 측면 이동 탐지를 운영합니다. 11. 핵심 정리 방향 감시 남-북(내부↔외부) 방화벽·IPS 동-서(내부↔내부) 사각지대·보강 필요 측면 이동 동-서에서 발생 세그먼트 교차 내부 방화벽 면접 포인트 "측면 이동은 동-서에서 발생 — 경계(남-북)만 보면 전부 놓침" 12. 다음 편 예고 다음 편 306. 내부 확장·측면 이동 — 출발지-목적지-계정-시점의 분석 축 에서는 분석 축을 다루는 출발지-목적지-계정-시점의 분석 축 을 다룹니다. 이전 편: 304. 내부 확장·측면 이동 — 측면 이동 정상 기준선 만들기 📚 시리즈 전체 보기: 시스템 보안 · 취약점
velog
시스템 보안 · 취약점 › G. 내부 확장 · 측면 이동 징후 · 6/50편 (전체 306/450) 학습 단계: 측면 이동 기본 개념 실습 표기: 이 글의 명령어·출력·로그는 로컬 VMware 테스트 VM(Rocky Linux 9 / Ubuntu 22.04) 기준의 「실습 예시」 이며, IP·계정·호스트명은 가상의 값입니다. 선행 학습 305. 내부 확장·측면 이동 — 동-서 트래픽(east-west)과 이상 탐지 원리 005. 접근 주체·경로·시점의 분석 축(C영역) 1. 개념 측면 이동 이벤트를 판정하려면 여러 축으로 평가해야 합니다. 내부 이동의 5개 분석 축(출발지·목적지·계정·시점·방법) 을 정리합니다. 각 축을 기준선(G04)과 비교해 이상을 평가합니다. 측면 이동 분석 5축 1 출발지(From): 어느 호스트에서(거점?) 2 목적지(To): 어느 호스트로(핵심 자산?) 3 계정(Who): 어떤 계정으로(탈취·공유?) 4 시점(When): 언제(업무/새벽, 정기/비정기) 5 방법(How): 어떻게(SSH·원격실행·재사용) 각 축을 기준선(G04)·연결 맵과 비교 → 이탈 평가 5축은 G08(판정 기준)의 기초이며, 모든 개별 이동 탐지(G09~)가 이 축을 구체화한 것입니다. 특히 출발지(거점)·목적지(핵심 자산)가 중요합니다. 2. 왜 중요한가 단일 축(연결 발생)만으로는 정상 통신과 구분할 수 없어 다축 평가가 필요합니다. 출발지(거점 여부)·목적지(핵심 자산 여부)가 위험도를 크게 좌우합니다. 각 축을 연결 맵·기준선과 비교해 이탈을 평가합니다. 3. 핵심 명령어 / 설정 축 질문 이탈 신호 출발지 어디서 거점·서비스 서버 발 목적지 어디로 핵심 자산·다수 계정 누구로 탈취·공유·교차 시점 언제 새벽·비정기 방법 어떻게 SSH·재사용·원격실행 4. 실습 (실습 예시) # 측면 이동 5축 분석 (분석 방법) — 한 이동을 5축으로 # 예: web01 → db01 SSH by root at 04:00 echo "1 출발지: web01(거점, 웹 서버) → 이탈" echo "2 목적지: db01(핵심 자산 DB) → 이탈" echo "3 계정: root(평소 미사용) → 이탈" echo "4 시점: 04:00(새벽) → 이탈" echo "5 방법: SSH(연결 맵에 없음) → 이탈" echo "=> 5축 전부 이탈 → 정탐" 5. 정상 상태 정상 이동 5축: 1 mgmt01(관리) 2 app01(정상 대상) 3 deploy(배포 계정) 4 14:00(업무) 5 정해진 배포 경로 => 5축 정상 → 정상 관리 이동 관리 호스트가 배포 계정으로 업무 시간에 정해진 경로로 이동하는, 5축이 모두 정상인 상태입니다. 6. 이상 상태 이상 이동 5축: 1 web01(거점) 2 db01(핵심 자산) 3 root(탈취) 4 04:00(새벽) 5 SSH(맵 외) => 5축 전부 이탈 → 정탐(측면 이동) 5축이 모두 기준선을 이탈 → 명백한 측면 이동 특히 출발지(거점)+목적지(핵심 자산) 조합이 결정적 정상(관리)과 정반대 → 정탐 7. 로그 분석 (분석 방법) 측면 이동 5축 분석 흐름입니다(가상의 예시). [이동] web01 → db01 SSH (root, 04:00) ↓ (5축 평가) 1 출발지 web01(거점) → 이탈 2 목적지 db01(핵심 자산) → 이탈 3 계정 root(탈취) → 이탈 4 시점 04:00 → 이탈 5 방법 SSH(맵 외) → 이탈 ↓ [종합] 이탈 축 수 → 정탐/오탐(G08) 축 가중치 출발지·목적지 높음(거점·자산) 계정 높음(탈취) 시점·방법 보조 출발지(거점)+목적지(핵심 자산) 조합이 가장 결정적인 판정 근거입니다. 8. SOC 관제 포인트 측면 이동은 5축(출발지·목적지·계정·시점·방법) 으로 분석합니다. 각 축을 연결 맵·기준선(G04)과 비교해 이탈을 평가합니다. 출발지(거점)+목적지(핵심 자산) 조합이 위험 판정의 핵심입니다. 9. 탐지 규칙 <!-- 실습 예시 룰: 적용 전 wazuh-logtest 및 테스트 환경 검증 필요 --> <group name="local,syssec_g,lateral,"> <!-- 거점(서비스 서버)에서 핵심 자산(DB)으로 이동(출발지+목적지 축) --> <rule id="106050" level="13"> <if_sid>106040</if_sid> <field name="audit.addr" type="pcre2">^10\.0\.0\.(10[0-9]|dbhost)$</field> <description>거점에서 핵심 자산으로 측면 이동(출발지+목적지 이탈)</description> <mitre><id>T1021</id></mitre> </rule> </group> 핵심 자산 IP 목록을 정의해 목적지 축을 구체화합니다. 계정·시점·방법 축은 상관 룰(G42)로 보완합니다. 10. 대응 방법 초기 확인 — 이동 이벤트를 5축(출발지·목적지·계정·시점·방법)으로 평가합니다. 범위 확인 — 거점 발·핵심 자산 행·탈취 계정·새벽·맵 외 방법을 확인합니다. 증거 확보 — 5축 평가 근거와 이동 로그를 보존합니다. 차단/조치 — 다축 이탈이면 경로 차단·확산 추적·대응을 진행합니다. 재발 방지 — 측면 이동 5축 분석을 개별 탐지의 공통 틀로 운영합니다. 11. 핵심 정리 축 이탈 신호 출발지 거점·서비스 서버 발 목적지 핵심 자산·다수 계정 탈취·공유·교차 시점·방법 새벽 / SSH·재사용 면접 포인트 "측면 이동은 5축 평가 — 출발지(거점)+목적지(핵심 자산)가 결정적" 12. 다음 편 예고 다음 편 307. 내부 확장·측면 이동 — 피벗(pivot)·터널링의 개념 에서는 피벗·터널링을 다루는 피벗(pivot)·터널링의 개념 을 다룹니다. 이전 편: 305. 내부 확장·측면 이동 — 동-서 트래픽(east-west)과 이상 탐지 원리 📚 시리즈 전체 보기: 시스템 보안 · 취약점
velog
MSA에서는 사용자, 상품, 주문 서비스가 게이트웨이의 경로로 구분되어 호출되며, 각 서비스는 자신의 데이터베이스만 조회한다. 따라서 서비스끼리 연동하기 전에는 주문이 저장되어 있어도 사용자 상세 조회의 주문 내역이 빈 리스트로 응답된다. 📑 목차 🔑 상황별 문법 찾기 📖 오늘의 주요 학습 내용 🚨 막혔던 지점 🎯 다음에 할 일 🌱 오늘 성장 기록 🔑 상황별 문법 찾기 이런 상황 쓰는 문법 이해도 게이트웨이에서 경로로 서비스를 나눌 때 uri: lb://USER-SERVICE + Path=/user-service/** ✅ 요청 객체의 값을 다른 객체로 옮겨 담을 때 mapper.map(user, UserDto.class) ✅ 필드 이름이 정확히 같을 때만 옮기게 할 때 setMatchingStrategy(MatchingStrategies.STRICT) 🔺 외부에 보여 줄 식별자를 만들 때 UUID.randomUUID().toString() ✅ 비밀번호를 암호화해서 저장할 때 passwordEncoder.encode(userDto.getPwd()) ✅ 등록 성공을 201로 응답할 때 ResponseEntity.status(HttpStatus.CREATED).body(responseUser) ✅ 값이 없는 필드를 응답 JSON에서 뺄 때 @JsonInclude(JsonInclude.Include.NON_NULL) ✅ 컬럼 값으로 조회하는 메서드를 만들 때 UserEntity findByUserId(String userId); ✅ 경로에 들어 있는 값을 인자로 받을 때 @PathVariable("userId") String userId ✅ 특정 경로만 인증 없이 열어 둘 때 .requestMatchers("/h2-console/**").permitAll() 🔺 나머지 요청에 Basic 인증을 요구할 때 .anyRequest().authenticated() + .httpBasic(Customizer.withDefaults()) ✅ 서버가 기동할 때 초기 데이터를 넣을 때 data.sql + defer-datasource-initialization: true ✅ 생성 시각을 DB 기본값으로 채울 때 @ColumnDefault(value = "CURRENT_TIMESTAMP") + insertable = false 🔺 브라우저에서 H2 DB를 직접 조회할 때 spring.h2.console.enabled: true ✅ ✅ 이해함 🔺 애매함 ❌ 아직 📖 오늘의 주요 학습 내용 1. 게이트웨이 라우트: 경로의 첫 마디로 세 서비스를 구분한다 🍞 개념 비유 우체국에서 우편번호 앞자리만 보고 지역별 함에 나눠 넣는 것과 비슷하다. 분류하는 사람은 받는 사람의 집 위치를 몰라도 된다. 우편번호 앞자리가 경로의 첫 마디( /user-service ), 지역별 함이 서비스, 분류하는 사람이 게이트웨이에 해당한다. 지난 실습의 first-service , second-service 에 더해 user-service , catalog-service , order-service 세 모듈이 추가되었다. 게이트웨이에는 서비스마다 라우트를 하나씩 등록하며, 경로가 /user-service/ 로 시작하면 USER-SERVICE 로 넘기는 방식이다. routes: - id: user-service uri: lb://USER-SERVICE predicates: - Path=/user-service/** - id: catalog-service uri: lb://CATALOG-SERVICE predicates: - Path=/catalog-service/** - id: order-service uri: lb://ORDER-SERVICE predicates: - Path=/order-service/** 게이트웨이는 경로를 바꾸지 않고 그대로 전달하므로, 각 서비스의 컨트롤러도 같은 경로로 시작해야 한다. @RestController //@RequestMapping("/") @RequestMapping("/user-service") @Slf4j public class UserController { 다섯 서비스를 모두 띄운 뒤 Eureka에 등록된 목록이다. 서비스 이름, 포트, 상태만 추렸다. CATALOG-SERVICE 60571 UP ORDER-SERVICE 58567 UP APIGATEWAY-SERVICE 8000 UP USER-SERVICE 60000 UP 8000번 포트 하나로 세 서비스의 health-check 를 호출한 결과다. == http://localhost:8000/user-service/health-check It's Working in User Service, port(local.server.port)=60000, port(server.port)=60000 [HTTP 200] == http://localhost:8000/catalog-service/health-check It's Working in Catalog Service on LOCAL PORT 60571 (SERVER PORT 0) [HTTP 200] == http://localhost:8000/order-service/health-check It's Working in Order Service on LOCAL PORT 58567 (SERVER PORT 0) [HTTP 200] ⚠️ server.port 는 설정 파일에 적은 값이고 local.server.port 는 실제로 열린 포트다. catalog-service 와 order-service 는 포트를 0으로 설정했으므로 SERVER PORT 0 으로 출력되며, 실제 포트는 기동할 때마다 달라진다. user-service 만 실습 편의상 60000으로 고정되어 있다. 2. 사용자 등록: 요청 객체를 DTO와 엔티티로 옮겨 담고 비밀번호를 암호화해 저장한다 🍞 개념 비유 병원에서 같은 환자에 대해 접수 서류, 진료 기록, 처방전의 양식이 각각 다른 것과 비슷하다. 접수 서류에는 환자가 적은 내용만 있고, 진료 기록에는 병원이 부여한 환자 번호가 추가되며, 처방전에는 약을 받는 데 필요한 내용만 적힌다. 접수 서류가 RequestUser , 진료 기록이 UserEntity , 처방전이 ResponseUser 에 해당한다. 회원 한 명의 정보는 네 개의 객체를 거친다. RequestUser 는 요청 본문을 받는 객체, UserDto 는 컨트롤러와 서비스 사이에서 전달하는 객체, UserEntity 는 users 테이블과 매핑되는 객체, ResponseUser 는 응답 본문으로 나가는 객체다. 객체 사이의 값 복사는 ModelMapper 가 담당한다. 이름이 같은 필드끼리 값을 옮겨 주며, MatchingStrategies.STRICT 로 설정하면 이름이 정확히 일치하는 필드만 옮긴다. @PostMapping("/users") public ResponseEntity<ResponseUser> createUser(@RequestBody RequestUser user) { ModelMapper mapper = new ModelMapper(); mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); // 요청 객체 -> DTO UserDto userDto = mapper.map(user, UserDto.class); userService.createUser(userDto); // DTO -> 응답 객체 (pwd, encryptedPwd 필드가 없으므로 비밀번호는 응답에서 빠진다) ResponseUser responseUser = mapper.map(userDto, ResponseUser.class); return ResponseEntity.status(HttpStatus.CREATED).body(responseUser); } 서비스에서는 외부에 노출할 식별자인 userId 를 UUID로 발급하고, 비밀번호를 BCryptPasswordEncoder 로 암호화한 뒤 저장한다. @Override public UserDto createUser(UserDto userDto) { // DB의 id(1, 2, 3...) 대신 외부에 보여 줄 식별자 userDto.setUserId(UUID.randomUUID().toString()); ModelMapper mapper = new ModelMapper(); mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); UserEntity userEntity = mapper.map(userDto, UserEntity.class); // userEntity.setEncryptedPwd("encrypted_password"); // 원문 pwd는 엔티티에 필드가 없어 옮겨지지 않고, 암호화한 값만 담는다 userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPwd())); userRepository.save(userEntity); UserDto returnUserDto = mapper.map(userEntity, UserDto.class); return returnUserDto; } BCryptPasswordEncoder 는 UserServiceApplication 에서 빈으로 등록하고, UserServiceImpl 이 생성자로 주입받는다. @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } users 테이블과 매핑되는 엔티티다. id 는 DB가 자동으로 증가시키는 값이고, userId 는 서비스에서 발급한 UUID다. @Data @Entity @Table(name = "users") public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 50, unique = true) private String email; @Column(nullable = false, length = 50) private String name; @Column(nullable = false, unique = true) private String userId; @Column(nullable = false, unique = true) private String encryptedPwd; } 등록 요청의 본문은 RequestUser 의 필드 세 개로 구성한다. email 과 name 은 실습에서 등록한 값이다. POST /user-service/users Content-Type: application/json { "email": "test@naver.com", "name": "이상혁", "pwd": "(직접 정한 비밀번호)" } 등록한 뒤 전체 사용자를 조회한 결과다. userId 에 UUID가 발급되어 있고, 비밀번호와 관련된 항목은 응답에 없다. orders 는 값이 null 이므로 @JsonInclude(JsonInclude.Include.NON_NULL) 에 의해 생략되었다. == http://localhost:8000/user-service/users [{"email":"test@naver.com","name":"이상혁","userId":"cb943bbd-46da-4ddc-974b-bd4533acecf5"}] [HTTP 200] ⚠️ 컨트롤러는 userService.createUser(userDto) 의 반환값을 받지 않고, 처음에 만든 userDto 로 응답 객체를 만든다. 서비스가 같은 객체에 setUserId() 를 호출했기 때문에 응답에 userId 가 포함된다. ⚠️ RequestUser 에 @NotNull , @Size(min = 8) , @Email 이 붙어 있지만, 컨트롤러의 인자에 @Valid 가 없으므로 현재 코드에서는 검증이 실행되지 않는다. 3. SecurityFilterChain: H2 콘솔만 열어 두고 나머지 요청에는 Basic 인증을 요구한다 🍞 개념 비유 아파트 단지의 놀이터와 동 출입문의 차이와 비슷하다. 놀이터는 누구나 들어갈 수 있지만, 동 출입문은 비밀번호를 눌러야 열린다. 놀이터가 permitAll() 로 열어 둔 경로, 동 출입문이 그 밖의 모든 요청, 출입문 비밀번호가 Basic 인증 정보에 해당한다. user-service 에는 spring-boot-starter-security 가 들어 있으며, 어떤 요청을 허용하고 어떤 요청에 인증을 요구할지를 SecurityFilterChain 빈으로 정한다. @Bean protected SecurityFilterChain configure(HttpSecurity http) throws Exception { http.csrf( (csrf) -> csrf.disable()) .authorizeHttpRequests(auth -> auth // .requestMatchers("/**").permitAll() // 모든 경로 허용 .requestMatchers("/h2-console/**").permitAll() // 특정 경로 허용 .anyRequest().authenticated() // 그 외는 인증 필요 ) .httpBasic(Customizer.withDefaults()) // ← Basic 인증 추가 .headers((headers) -> headers .frameOptions((frameOptions) -> frameOptions.sameOrigin())); return http.build(); } 각 줄의 역할은 다음과 같다. csrf.disable() : 브라우저 폼이 아닌 API 호출이므로 CSRF 토큰 검사를 끈다. 켜 두면 POST 요청이 거절된다. requestMatchers("/h2-console/**").permitAll() : H2 콘솔은 인증 없이 접속할 수 있다. anyRequest().authenticated() : 그 밖의 모든 요청은 인증이 필요하다. frameOptions.sameOrigin() : H2 콘솔 화면은 프레임으로 구성되어 있으므로, 같은 출처의 프레임은 허용한다. Basic 인증에 사용하는 계정은 application.yml 의 spring.security.user 에 적는다. BCryptPasswordEncoder 가 빈으로 등록되어 있으므로 비밀번호는 원문이 아니라 BCrypt로 암호화한 값을 적어야 한다. security: user: name: user # password: password password: $2a$10$g70r... # BCrypt로 암호화한 값 (뒷부분 생략) 암호화한 값은 Utils 의 main() 을 실행해서 얻는다. public class Utils { public static void main(String[] args) { System.out.println(new BCryptPasswordEncoder().encode("password")); } } 인증 정보 없이 호출하면 서비스의 컨트롤러까지 가지 않고 401이 응답된다. == http://localhost:8000/user-service/users HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="Realm" X-Frame-Options: SAMEORIGIN Content-Length: 0 Basic 인증 정보를 함께 보내면 200이 응답된다. == http://localhost:8000/user-service/welcome Welcome to the Simple E-commerce. [HTTP 200] ⚠️ 인증은 게이트웨이가 아니라 user-service 가 처리한다. 게이트웨이는 Authorization 헤더를 그대로 전달하며, catalog-service 와 order-service 에는 Security 의존성이 없으므로 인증 없이 호출된다. ⚠️ 존재하지 않는 userId 로 상세 조회를 하면 404가 아니라 401이 응답된다. getUserByUserId() 가 던지는 UsernameNotFoundException 이 인증 예외의 한 종류이므로, Spring Security가 인증 실패로 처리하기 때문이다. == http://localhost:8000/user-service/users/no-such-id {"timestamp":"2026-10-07T08:16:42.103+00:00","status":401,"error":"Unauthorized","trace":"org.springframework.security.core.userdetails.UsernameNotFoundException: User not found\r\n\tat com.example.userservice.service.UserServiceImpl.getUserByUserId(UserServiceImpl.java:56) ...","message":"Unauthorized","path":"/user-service/users/no-such-id"} [HTTP 401] 4. data.sql: 서버가 기동할 때 상품 초기 데이터를 넣는다 🍞 개념 비유 편의점이 문을 열기 전에 진열대에 상품을 채워 두는 것과 비슷하다. 진열대가 먼저 설치되어 있어야 상품을 올릴 수 있다. 진열대 설치가 Hibernate의 테이블 생성, 상품을 채우는 일이 data.sql 실행, 설치가 끝난 뒤에 채우도록 순서를 정하는 것이 defer-datasource-initialization 에 해당한다. catalog-service 에는 상품을 등록하는 API가 없다. 대신 src/main/resources/data.sql 에 적은 INSERT 문이 서버가 기동할 때 실행된다. insert into catalog(product_id, product_name, stock, unit_price) values('CATALOG-001', 'Berlin', 100, 1500); insert into catalog(product_id, product_name, stock, unit_price) values('CATALOG-002', 'Tokyo', 110, 1000); insert into catalog(product_id, product_name, stock, unit_price) values('CATALOG-003', 'Stockholm', 120, 2000); data.sql 은 기본적으로 Hibernate가 테이블을 만들기 전에 실행된다. 테이블이 없는 상태에서 INSERT 가 먼저 실행되지 않도록 defer-datasource-initialization: true 로 실행 시점을 테이블 생성 뒤로 미룬다. jpa: hibernate: ddl-auto: create-drop show-sql: true generate-ddl: true database: h2 defer-datasource-initialization: true # data.sql / schema.sql 실행을 Hibernate가 테이블을 만든 뒤로 미루는 설정 data.sql 에는 created_at 을 적지 않았다. 엔티티에서 insertable = false 로 INSERT 문에서 이 컬럼을 빼고, @ColumnDefault 로 DB의 기본값이 들어가도록 했기 때문이다. @Data @Entity @Table(name = "catalog") public class CatalogEntity { // implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 120, unique = true) private String productId; @Column(nullable = false) private String productName; @Column(nullable = false) private Integer stock; @Column(nullable = false) private Integer unitP
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%