3개월차 교육을 시작한 지도 벌써 3개월이 지났다. 이번 달에는 크게 CI/CD 와 Multi-Agent System 에 대해서 배웠다. CI/CD에서는 Docker, GitHub Actions, AWS를 이용해 코드가 실제 환경까지 배포되는 과정과 반복 작업을 자동화하는 방법을 배웠고, Multi-Agent에서는 하나의 목표를 여러 Task로 나누고 여러 Agent가 역할을 나눠 작업하도록 만드는 방법을 배웠다. 처음에는 두 주제가 크게 관련 없어 보였다. 하지만 한 달 동안 배운 내용을 다시 정리해보니 결국 둘 다 작업을 더 잘하기 위해 필요한 구조를 명시적으로 만드는 방법 이라는 공통점이 있었다. 사람이 일을 하든, 소프트웨어가 실행하든, AI가 작업하든 결국 비슷한 질문이 필요하다. 왜 하는가? ↓ 무엇을 해야 하는가? ↓ 어떻게 나눌 것인가? ↓ 누가 할 것인가? ↓ 어떤 순서와 관계로 실행할 것인가? ↓ 무엇을 주고받을 것인가? ↓ 현재 어디까지 진행됐는가? ↓ 실행 ↓ 잘했는지 확인 ↓ 다음 작업에 반영 이번 달에는 이런 질문들이 실제 기술에서는 어떤 개념과 구조로 표현되는지를 많이 확인했던 것 같다. 1. 배운 내용 CI/CD CI와 CD CI(Continuous Integration) 는 코드 변경을 지속적으로 통합하면서 Build와 Test를 통해 문제가 있는지를 빠르게 확인하는 과정이다. Code Change ↓ Integration ↓ Build ↓ Test ↓ Feedback CD 는 CI를 통과한 결과물을 배포 가능한 상태로 유지하거나 실제 실행 환경까지 전달하는 과정이다. Continuous Delivery → 언제든 배포할 수 있는 상태까지 자동화 Continuous Deployment → 검증된 변경을 실제 환경까지 자동 배포 결국 CI/CD는 코드 변경 ↓ 검증 ↓ Build ↓ 배포 가능한 결과물 ↓ 실제 실행 환경 이라는 과정을 반복 가능하게 만드는 것이라고 볼 수 있다. GitHub Actions CI/CD를 실제로 구성할 때는 GitHub Actions를 이용했다. 기본 구조는 Workflow → 전체 자동화 흐름 Event / Trigger → 언제 실행할 것인가 Job → 어떤 작업 묶음을 실행할 것인가 Step → 실제 실행 단위 Runner → 어디에서 실행할 것인가 정도로 나눌 수 있다. 예를 들어 코드가 Push되면 Push ↓ GitHub Actions ↓ Test ↓ Docker Image Build ↓ Registry Push ↓ Deploy 같은 작업을 자동으로 실행할 수 있다. 직접 배포할 때는 사람이 알고 있던 순서대로 명령을 실행하면 됐지만 자동화하려면 무엇을 언제 어떤 조건에서 실행할지를 명시적인 Workflow로 만들어야 한다. 그래서 CI/CD는 단순히 명령을 대신 실행하는 것보다 사람이 반복해서 하던 작업 절차를 시스템이 실행할 수 있는 형태로 바꾸는 것 에 가까운 것 같다. Docker와 AWS 배포 Docker를 이용하면 애플리케이션과 실행에 필요한 Runtime, Dependency 등을 이미지 형태로 묶을 수 있다. Application + Runtime + Dependency ↓ Docker Image 만들어진 이미지는 Registry에 저장하고 실제 서버에서 가져와 실행할 수 있다. 이번에는 AWS의 여러 서비스를 연결해 실제 배포 과정도 경험했다. OIDC / IAM → GitHub Actions가 AWS에서 사용할 권한 ECR → Docker Image 저장 EC2 → 애플리케이션 실행 RDS → 영속 데이터 저장 ElastiCache → Redis와 같은 인메모리 저장소 SSM → 서버에 명령 전달 및 관리 전체 흐름은 대략 GitHub ↓ GitHub Actions ↓ AWS 인증 ↓ Build / Test ↓ Docker Image ↓ ECR ↓ EC2 ↓ Container 실행 ↓ RDS / Redis 등과 연결 ↓ Service 로 볼 수 있었다. 각 서비스를 따로 배우는 것보다 실제 배포 흐름에서 어떤 역할을 담당하는지 보니 훨씬 단순하게 이해할 수 있었다. Kubernetes와 GitOps도 개념적으로 접했지만 이번 달에는 실제 활용보다는 이런 방식으로 실행 상태를 관리할 수 있다는 정도로 이해했다. Multi-Agent System Multi-Agent와 Orchestration Multi-Agent 자체는 크게 낯설지 않았다. AI를 어느 정도 일을 빠르게 처리할 수 있는 작업자라고 생각하면 Single Agent → 한 명에게 일을 맡긴다. Multi-Agent → 여러 명에게 일을 나눠 맡긴다. Orchestration → 여러 작업자를 하나의 목표에 맞게 조율한다. 정도로 생각할 수 있다. 실제로는 Goal ↓ Task Decomposition ↓ Task 관계 정의 ↓ Agent Assignment ↓ Execution ↓ Communication ↓ Result Merge ↓ Evaluation 같은 구조가 필요하다. 결국 Multi-Agent에서 중요한 것은 Agent의 숫자보다 작업을 어떻게 나누고 여러 실행 주체를 어떻게 연결할 것인가 라고 생각한다. Role과 Task Agent를 여러 개 사용할 때는 Role 과 Task 를 구분할 수 있다. Role → 이 Agent가 어떤 책임을 가지는가 Task → 지금 실제로 수행해야 하는 일 예를 들면 Role: Researcher Task: 특정 기술 조사 Role: Reviewer Task: 조사 결과 검증 처럼 볼 수 있다. 사람이 팀에서 맡고 있는 역할과 지금 처리해야 하는 일이 다른 것과 크게 다르지 않다. Task Decomposition과 Dependency 큰 일을 여러 Agent에게 맡기려면 먼저 Task를 나눠야 한다. Goal ↓ Task Decomposition ↓ Task A Task B Task C 하지만 Task를 나눴다고 모두 동시에 실행할 수 있는 것은 아니다. A → B → C 처럼 앞의 결과가 필요하다면 순차적으로 실행해야 한다. 반대로 → A → Input → B → Merge → C → 처럼 서로 독립적이라면 병렬로 처리할 수 있다. 따라서 Multi-Agent에서 먼저 봐야 하는 것은 Agent의 수보다 Task 사이의 Dependency 다. Goal ↓ Task Decomposition ↓ Dependency ↓ Execution Strategy ↓ Agent Assignment 작업 관계는 Graph로 표현할 수 있고, 순환이 없는 의존 관계라면 DAG(Directed Acyclic Graph) 형태로 볼 수도 있다. Workflow Pattern Task를 어떤 관계로 실행할지에도 반복되는 Pattern이 있다. Sequential / Chain A → B → C 앞 작업의 결과를 다음 작업이 사용한다. Parallel / Fan-out, Fan-in → A → Input → → B → Merge → C → 독립적인 작업을 동시에 수행하고 마지막에 합친다. Router → Agent A Input → Router → Agent B → Agent C 입력이나 상태에 따라 적절한 실행 경로를 선택한다. Evaluator–Optimizer Generate ↓ Evaluate ↓ Pass? ─ YES → Result │ NO ↓ Improve ↺ 결과를 평가하고 기준을 만족하지 못하면 다시 개선한다. 처음부터 특별히 새로운 개념이라기보다 작업 사이의 관계에 이미 존재하던 구조들에 이름이 붙어 있는 느낌 에 가까웠다. Contract와 Interface Agent들이 독립적으로 동작하려면 서로 무엇을 주고받을지에 대한 약속이 필요하다. Task Input Constraints Expected Output Status Error Metadata 같은 정보를 정해진 형태로 전달할 수 있다. Agent A ↓ Contract / Interface ↓ Agent B 이 부분 역시 기존 소프트웨어의 Interface나 Contract 개념과 크게 다르지 않았다. 내부 구현이 달라져도 Contract가 유지된다면 다른 구성요소에 주는 영향을 줄일 수 있다. 결국 Agent를 독립적으로 나눈다는 것도 경계를 명확하게 정의하는 문제 라고 생각할 수 있다. Router, Supervisor, Handoff 여러 Agent 사이에서 작업을 전달하거나 관리하는 방법도 나눌 수 있다. Router "누구에게 보낼 것인가?" 입력이나 현재 상태를 보고 적절한 Agent를 선택한다. → Research Input → Router → Coding → Review Supervisor "전체 작업을 어떻게 조율할 것인가?" 전체 목표와 진행 상태를 보고 여러 Agent에게 Task를 배분하거나 다음 행동을 결정한다. ┌→ Agent A Goal → Supervisor → Agent B └→ Agent C Handoff "현재 하던 작업을 누구에게 넘길 것인가?" 현재 Agent가 다른 Agent가 더 적합하다고 판단하면 필요한 Context와 함께 작업의 제어권을 넘긴다. Agent A ↓ 작업 수행 ↓ 다른 Agent가 더 적합 ↓ Handoff ↓ Agent B 정리하면 Router → 선택 Supervisor → 전체 조율 Handoff → 작업과 제어권 이전 정도로 볼 수 있다. 사람이 여러 명 모여서 일할 때도 자연스럽게 생기는 역할들이라 개념 자체가 어렵게 느껴지지는 않았다. State와 Context 여러 Agent가 작업하면 전체 Workflow가 현재 어떤 상태인지 관리해야 한다. Global State 현재 Task 완료된 Task Agent별 Result Error 다음 실행 대상 하지만 모든 정보를 모든 Agent에게 전달할 필요는 없다. Global State ↓ 필요한 정보 선택 ↓ Local Context ↓ Agent 각 Agent가 현재 작업을 처리하는 데 필요한 정보만 전달할 수 있다. 단일 Agent에서도 생각했던 문제지만 Multi-Agent에서는 실행 주체가 늘어나면서 누가 어떤 정보를 알아야 하는가 가 더 명시적인 문제가 되는 것 같다. A2A와 MCP 여러 Agent와 Tool을 연결하면서 A2A 와 MCP 의 역할도 볼 수 있었다. A2A → Agent ↔ Agent MCP → Agent ↔ Tool / Resource 하나의 시스템에서는 Agent A │ A2A ↓ Agent B │ MCP ↓ Tool / API / Data 처럼 함께 사용할 수도 있다. 서로 경쟁하는 기술이라기보다 연결하는 대상이 다른 도구 로 보는 편이 자연스럽다. Trace와 Evaluation Agent가 여러 개가 되면 최종 결과만 봐서는 내부에서 어떤 일이 있었는지 알기 어렵다. 그래서 Trace를 통해 어떤 Agent가 실행됐는가 어떤 Task를 수행했는가 어떤 Tool을 사용했는가 어디에서 실패했는가 얼마나 많은 비용과 시간이 들었는가 같은 실행 과정을 확인할 수 있다. Evaluation도 결과 하나만 보는 것이 아니라 Result Quality → 최종 결과가 좋은가 Task Quality → 각 Agent가 맡은 일을 잘했는가 Trajectory → 실행 경로가 적절했는가 Latency → 시간이 적절한가 Cost → 자원을 얼마나 사용했는가 Reliability → 반복해도 안정적인가 처럼 여러 기준을 사용할 수 있다. Agent가 늘어나면 능력뿐 아니라 관찰하고 평가해야 하는 범위도 같이 늘어난다. 결국 모두 작업을 잘하기 위한 도구 이번 달에 배운 내용을 다시 생각해보면 CI/CD와 Multi-Agent는 구체적으로 전혀 다른 기술이지만 더 위에서는 비슷한 질문에 답하고 있었다. 사람이든 AI든 소프트웨어든 일을 하려면 결국 왜 하는가? ↓ 무엇을 해야 하는가? ↓ 어떻게 나눌 것인가? ↓ 누가 할 것인가? ↓ 어떤 순서와 관계로 실행할 것인가? ↓ 어떤 정보를 주고받을 것인가? ↓ 현재 상태는 무엇인가? ↓ 실행 ↓ 잘했는지 확인 ↓ 다음 작업에 반영 이 필요하다. 이번 달에 배운 개념들을 여기에 대응하면 Goal / Intent → 왜 하는가 Task → 무엇을 해야 하는가 Task Decomposition → 어떻게 나눌 것인가 Dependency / DAG → 작업들은 어떤 관계인가 Role / Agent → 누가 할 것인가 Workflow Pattern → 어떤 흐름으로 실행할 것인가 Contract / Interface → 무엇을 어떤 형태로 주고받을 것인가 State → 지금 어디까지 진행됐는가 Context → 현재 작업에 무엇을 알아야 하는가 Router → 누구에게 보낼 것인가 Supervisor → 전체를 어떻게 조율할 것인가 Handoff → 다른 실행 주체에게 어떻게 넘길 것인가 A2A → Agent와 Agent가 어떻게 협력할 것인가 MCP → 외부 Tool과 Resource를 어떻게 사용할 것인가 Trace → 실제로 무엇이 일어났는가 Evaluation → 잘했는가 정도로 볼 수 있다. CI/CD는 이 중에서 특히 이미 방법이 정해진 반복 작업을 어떻게 안정적으로 실행할 것인가 에 대한 도구라고 생각할 수 있다. 사람이 반복하던 작업 ↓ 명시적인 Workflow ↓ 자동 실행 Multi-Agent는 한 주체가 모두 처리하기 어려운 작업을 어떻게 나누고 여러 실행 주체가 협력하게 할 것인가 에 더 가깝다. 복잡한 Goal ↓ Task 분리 ↓ 역할 분배 ↓ 관계 정의 ↓ 협력 ↓ 결과 결국 이번 달에 배운 기술들은 새로운 작업 원리를 만든다기보다 사람이 일을 잘하기 위해 원래 필요했던 구조들을 소프트웨어와 AI가 실행할 수 있는 형태로 명시한 것 처럼 보인다. AI가 특별해서 완전히 새로운 질문이 생긴 것이 아니라, 왜 하는가? 무엇을 해야 하는가? 누가 하는 것이 좋은가? 어떤 정보를 알아야 하는가? 어떻게 협력할 것인가? 잘했는지는 어떻게 알 것인가? 같은 질문에 답하는 방식이 조금씩 달라지고 있는 것 같다. 2. 소감 이번 달에는 새로운 개념을 이해하는 데 어렵거나 헷갈린다는 느낌은 거의 없었다. 단일 Agent를 배우면서 이미 역할, Task, Context, State, Tool, Trace, Evaluation 등에 대해서 많이 생각했고, AI를 어느 정도 똑똑한 사람이 일을 빠르게 처리해주는 것처럼 생각하고 있었기 때문에 Multi-Agent의 개념들도 대부분 바로 이해할 수 있었다. Supervisor는 여러 작업자를 관리하는 사람, Handoff는 더 적절한 사람에게 일을 넘기는 것, Contract는 서로 일을 주고받기 위한 약속이라고 생각하면 특별히 새롭게 느껴질 부분이 많지 않았다. 처음에는 내가 이전에 비슷한 생각을 많이 해봤기 때문에 쉬운 것이라고 생각했다. 그런데 이번 달 내용을 다시 정리하면서 꼭 그것만은 아닌 것 같다는 생각도 들었다. Role, Task, Workflow, Supervisor, Handoff, Contract 같은 개념은 결국 사람이 여러 명이서 일을 잘하려고 해도 자연스럽게 필요한 것들 이다. 누가 어떤 일을 할지 정하고, 일을 작은 단위로 나누고, 순서를 정하고, 관리자가 전체 상황을 보고, 더 적절한 사람이 있으면 일을 넘기고, 서로 결과를 주고받기 위한 약속을 만드는 것은 AI가 없어도 원래 존재했던 문제다. 소프트웨어도 마찬가지다. 모듈 사이에는 Interface가 필요하고, 여러 작업에는 Dependency가 생기고, 반복 작업에는 Workflow가 필요하다. AI가 들어오면서 완전히 새로운 방식으로 일이 바뀐다기보다 기존 작업 구조 안에 스스로 판단할 수 있는 새로운 실행 주체가 추가되고 있다는 느낌 이 더 강했다. CI/CD도 비슷했다. Test, Build, Image 생성, 배포를 사람이 직접 할 수도 있지만 방법이 충분히 명확하고 반복된다면 사람이 매번 할 이유가 없다. 사람이 판단하며 반복 ↓ 과정을 명시 ↓ 자동화 라는 흐름이다. 어떻게 보면 Multi-Agent도 같은 방향에 있다. 사람이 직접 여러 AI에게 하나씩 일을 나눠주고 결과를 확인할 수도 있지만 역할과 작업 흐름이 어느 정도 반복된다면 그 관계 역시 시스템으로 만들 수 있다. 이렇게 생각하니 이번 달에 배운 여러 기술들이 모두 사람이 직접 하던 판단과 작업 중 어떤 부분을 명시적으로 만들고 시스템에 맡길 수 있는가 라는 문제로도 보였다. 그래서 새로운 기술을 볼 때 이름이나 사용법을 외우는 것보다 이 기술은 일을 하는 과정에서 어떤 질문에 답하기 위해 필요한가? 를 먼저 생각하는 방식이 나에게는 더 잘 맞는 것 같다. 기술이 달라져도 해결하려는 질문이 같으면 전체 구조에서 어디에 들어가는지도 금방 이해할 수 있다. 앞으로는 새로운 개념을 더 많이 아는 것보다 실제 작업을 하나 선택해서 이런 구조가 어디까지 도움이 되는지 직접 확인해보는 것이 더 중요할 것 같다. 생각으로는 자연스럽게 연결되지만 실제로 사용했을 때도 항상 효과적인지는 별개의 문제이기 때문이다. 3. KPT Keep 기술보다 먼저 "왜 필요한가"를 생각하기 새로운 개념을 배울 때 무엇인가? 만 보는 것보다 왜 필요한가? 어떤 문제를 해결하는가? 를 먼저 생각하는 방식은 계속 유지하고 싶다. 이번 달에도 Router → 적절한 실행 주체 선택 Handoff → 작업 이전 Contract → 협업을 위한 약속 CI/CD → 반복 실행의 자동화 처럼 목적을 기준으로 보니 새로운 개념을 빠르게 이해할 수 있었다. 이미 알고 있는 작업 구조와 연결하기 AI의 개념을 AI 안에서만 생각하지 않고 사람이나 기존 소프트웨어가 일하는 방식과 비교하는 것이 도움이 됐다. 구체적인 구현은 달라도 어떤 문제가 반복되는지를 찾으면 전체 구조를 이해하기 쉬웠다. 직접 실행해서 확인하기 AWS 배포와 GitHub Actions처럼 실제로 해봤을 때 각 기술의 역할이 더 명확해지는 경우가 많았다. 생각으로 이해한 것과 실제로 효과가 있는 것은 다를 수 있기 때문에 작은 범위라도 직접 실행하는 것은 계속 유지하고 싶다. Problem 생각보다 구현과 검증에 훨씬 많은 시간이 든다 구조 자체는 비교적 빠르게 이해하고 설계할 수 있지만 실제 코드로 만들고 여러 상황에서 정상적으로 동작하는지 확인하려면 훨씬 많은 시간이 필요하다. AI를 사용하면 만드는 속도는 빠르지만 사람이 결과를 이해하고 검증하는 속도는 그만큼 빠르게 늘어나지 않는다. 사용할 수 있는 도구가 너무 많다 Framework, A2A, MCP, Observability, Evaluation 등 사용할 수 있는 기술이 굉장히 많다. 문제는 각각 좋은 기술이라는 이유만으로 추가하면 시스템이 실제 필요 이상으로 복잡해질 수 있다는 것이다. 어디까지 기존 기술을 사용하고 어디부터 직접 만들지 결정하기 어렵다 이미 잘 만들어진 도구를 사용하면 빠르지만 내가 직접 보고 싶은 구조가 내부에 숨겨질 수 있고, 전부 직접 구현하면 이미 해결된 문제에 많은 시간을 쓰게 된다. 현재 목적에서 무엇을 확인하려는지를 기준으로 선택할 필요가 있다. 생각하는 범위가 계속 커진다 하나의 문제를 생각하면 자연스럽게 다음 문제도 보인다. Agent ↓ State ↓ Trace ↓ Evaluation ↓ Policy ↓ Memory ... 계속 연결해서 생각하는 것은 도움이 되지만 실제 실험 전에 범위가 지나치게 커지지 않게 조절할 필요가 있다. Try 1. 작업의 "왜"부터 적어보기 새로운 기술이나 구조를 선택하기 전에 왜 이 작업을 하는가? ↓ 무엇이 성공인가? ↓ 무엇을 해야 하는가? 부터 정리해보려고 한다. 목적이 명확해야 이후의 Task, Role, Workflow도 결정할 수 있다. 2. Task를 먼저 구조화하고 기술을 나중에 선택하기 Goal ↓ Task ↓ Dependency ↓ Workflow ↓ Role ↓ Executor ↓ Technology 순서로 보는 습관을 만들고 싶다. 특히 Multi-Agent 자체를 목적으로 사용하지 않고 Task 구조상 필요할 때 선택하려고 한다. 3. 작은 구조부터 실제로 비교하기 Single Agent ↓ 작은 Workflow ↓ Multi-Agent 로 단계적으로 늘리면서 품질 시간 비용 안정성 이 실제로 어떻게 달라지는지 비교해보고 싶다. 4. 기존 기술은 해결하려는 문제를 기준으로 선택하기 어떤 문제가 있는가? ↓ 이미 해결한 기술이 있는가? ↓ 현재 목적에 맞는가? ↓
Make your Jaipur journey more memorable with a visit to the magnificent Hawa Mahal. Known as the Palace of Winds, this historic monument showcases beautiful Rajput architecture, delicate screens, arched openings, and an unforgettable pink sandstone façade. Whether you love history, architecture, photography, or Indian culture, Hawa Mahal offers something special. Before visiting, check the latest timings, ticket price, entrance information, and travel tips. A little planning can help you enjoy the monument without rushing. Explore Jaipur’s old city and create lasting memories around one of Rajasthan’s most celebrated attractions. https://coloursindiatours.com/hawa-mahal-jaipur-timings-ticket-price
출근 첫 주가 끝나고 주말 일을 하다가 복기하면서 문득 궁금해진 것들을 정리했다. 그런데 세 개를 다 확인해보니 내가 알던 게 전부 틀렸거나 반쯤 틀렸다. 그리고 공통점이 있었다. 1. JSP는 HTML에 JS를 끼워넣는 것인가 내가 알던 것 "JSP는 HTML 파일에 <script> 태그 사이로 자바스크립트 기능을 동적으로 끼워넣는 것" 뭔가 기능은 맞는 것 같은데 핵심을 놓친 느낌이 계속 들었다. 그럴 만했다. 실제 — 자바스크립트가 아니라 자바다 <%@ page contentType="text/html; charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> <c:forEach var="dto" items="${freeBoardList}"> <tr><td>${dto.title}</td></tr> </c:forEach> </body> </html> 이 코드는 서버에서 실행된다. 브라우저는 이걸 본 적이 없다. [서버] JSP 를 읽는다 → ${freeBoardList} 를 실제 데이터로 치환 → <c:forEach> 를 돌려서 <tr> 을 여러 개 만든다 → 완성된 HTML 을 만든다 ↓ [브라우저] <tr><td>안녕하세요</td></tr> <tr><td>반갑습니다</td></tr> ← 이것만 받는다 브라우저가 받는 HTML 에는 <c:forEach> 도 ${...} 도 없다. 서버가 다 처리하고 결과만 보낸다. 반대로 브라우저의 소스 보기에 <c:forEach> 나 ${...} 가 그대로 보이면 서버가 처리하지 않은 것이다. 맨 위 taglib 선언이 빠지면 <c:...> 태그가 글자 그대로 나가고, web.xml 이 2.4 미만 버전이면 EL 이 꺼져서 ${...} 가 그대로 나간다. 한 줄 정의 HTML 안에 자바 코드를 섞어서, 서버가 HTML을 만들어내는 기술. "동적"의 주체가 서버다. 요청할 때마다 다른 HTML이 만들어진다. 값이 HTML 의 일부가 된다는 뜻이기도 하다. ${dto.title} 은 값을 escape 없이 그대로 넣는다. 게시판 제목처럼 사용자가 쓴 값은 <c:out value="${dto.title}"/> 로 찍어야 < , > 가 태그로 해석되지 않는다. JS 로 목록을 그릴 때도 같다. 문자열로 이어 붙여 .html() 에 넣는다면 escape 를 거치거나 .text() 로 넣는다. 그럼 내가 말한 건 뭐였나 <script> $("#tb").html(html); </script> 이건 브라우저에서 실행되는 자바스크립트다. JSP와 아무 상관이 없다. JSP 파일 안에 <script> 를 쓸 수 있어서 헷갈린 것이다. <%-- JSP 파일 --%> <c:forEach var="dto" items="${list}"> ← 서버가 실행 <tr><td>${dto.title}</td></tr> </c:forEach> <script> function searchFn() { ... } ← 브라우저가 실행 </script> 한 파일 안에 두 세계가 공존한다. 그게 JSP 를 처음 볼 때 혼란스러운 이유였다. 실행 시점이 다르다 JSP/EL/JSTL 서버가 HTML 을 만들 때 한 번 JavaScript 브라우저가 HTML 을 받은 뒤, 계속 이 차이 때문에 실제로 막힌 적이 있다. 검색 조건 <select> 를 바꿀 때마다 다른 입력창을 띄워야 했는데, <c:choose> 로 하려다 실패했다. <!-- [오개념] 이건 "페이지가 처음 뜰 때" 만 판단한다 --> <c:choose> <c:when test="${searchType eq '6'}"> <input type="text" name="startDate"> </c:when> </c:choose> 사용자가 select 를 바꾸는 건 브라우저에서 일어나는 일이라, 이미 끝난 JSTL 은 반응할 수 없다. // [정답] 미리 다 만들고 JS 로 숨긴다 function changeFn() { $("#case1, #case2, #case3").hide(); var v = $("#selection").val(); if (v == "6") $("#case3").show(); } 판단 기준 서버가 아는 값을 그린다 → JSTL / EL 사용자 조작에 반응한다 → JavaScript "누가 그 값을 알고 있느냐." 이 기준 하나로 정리된다. 검색 후 "아까 고른 옵션을 유지"하는 건 서버가 아는 값이니 EL 이 맞다. ${paramMap.searchType eq '4' ? 'selected' : ''} 같은 식으로. paramMap 은 컨트롤러가 model 에 담아 줘야 보이고, 안 담았다면 EL 기본 객체로 ${param.searchType ...} 이라 쓴다. 페이지를 서버가 다시 그릴 때 얘기고, AJAX 로 목록만 바꾸면 select 는 고른 그대로 남는다. 결정적 사실 — JSP는 서블릿이다 톰캣이 .jsp 파일을 자바 소스로 변환해서 컴파일한다. // 톰캣이 자동 생성한 freeBoardMain_jsp.java (패키지·implements·throws 는 줄였다) public final class freeBoardMain_jsp extends HttpJspBase { public void _jspService(HttpServletRequest request, HttpServletResponse response) { out.write("<html>\n<body>\n"); // ... } } 톰캣의 work/ 폴더에 이 파일이 실제로 생긴다. 열어보면 out.write("<html>") 같은 코드가 줄줄이 나온다. 이클립스에서 띄웠다면 워크스페이스의 .metadata/.plugins/org.eclipse.wst.server.core/tmp0/work/Catalina/localhost/프로젝트명/org/apache/jsp/ 아래다. WEB-INF 폴더는 WEB_002dINF 로 바뀌어 있다. 서블릿 자바 코드 안에 HTML 을 문자열로 섞는다 JSP HTML 안에 자바 코드를 섞는다 방향이 반대일 뿐 같은 것이다. 화면을 만들 때는 후자가 편해서 JSP 가 나왔다. 정리해서 말하면 JSP는 HTML 안에 자바 코드를 넣어 서버에서 동적으로 HTML을 생성하는 기술입니다. 톰캣이 JSP를 서블릿 자바 코드로 변환하고 컴파일해서 실행하기 때문에, 실제로는 서블릿과 같은 것입니다. 브라우저는 JSP 코드를 받지 않고 완성된 HTML만 받습니다. 2. SqlSessionTemplate 과 @Mapper 인터페이스 두 방식을 다 겪었다 // 개인 프로젝트에서 쓰던 방식 mapper.selectBoardList(boardCode, rowBounds); // 회사에서 보는 방식 sqlSessionTemplate.selectList("freeBoardGetList", param); 둘 다 XML 의 쿼리를 불러와 SQL 을 실행하는 건 같은데, 왜 이름과 형태가 다른지 궁금했다. 답 — 같은 것의 두 가지 표현이다 @Mapper 인터페이스 ↓ MyBatis 가 실행 시점에 구현체를 자동 생성 (동적 프록시) ↓ 메서드명 → XML 의 id 로 매칭 ↓ 내부적으로 SqlSession.selectList("...selectBoardList", param) 호출 인터페이스 방식이 결국 SqlSessionTemplate 방식을 대신 해주는 것이다. @Mapper public interface NeighborhoodMapper { List<Neighborhood> selectBoardList(int boardCode, RowBounds rowBounds); } 구현 클래스가 없는데 동작하는 게 신기했는데 , MyBatis 가 실행 시점에 만들어주는 것이었다. <mapper namespace="com.zipinfo.project.neighborhood.model.mapper.NeighborhoodMapper"> ↑ 인터페이스의 전체 경로와 일치해야 한다 <select id="selectBoardList"> ← 메서드명과 일치 namespace + id 를 합치면 SqlSessionTemplate 에 넘기는 문자열이 된다. 회사 코드처럼 "freeBoardGetList" 만 넘겨도 되는 건 MyBatis 가 짧은 id 도 같이 등록하기 때문이다. 다른 매퍼에 같은 id 가 생기면 짧은 id 호출은 is ambiguous in Mapped Statements collection 으로 터진다. 이것도 실행해봐야 안다. 차이점 SqlSessionTemplate Mapper 인터페이스 쿼리 지정 문자열 메서드 호출 오타 발견 런타임 메서드는 컴파일, XML id 는 런타임 파라미터 타입 Object 타입 지정 반환 타입 캐스팅 필요할 때 있음 선언된 타입 IDE 자동완성 안 됨 됨 여러 파라미터 Map 으로 묶어야 @Param 사용 가능 실제로 이 차이를 겪었다 Mapper 에서 쿼리 두 개를 하나로 합치면서 freeBoardSearchList 를 지웠는데, Service 가 아직 그 id 를 부르고 있었다. Mapped Statements collection does not contain value for freeBoardSearchList sqlSessionTemplate.selectList("freeBoardSearchList", paramMap); // ↑ 오타든 삭제된 id든 실행해봐야 안다 인터페이스 방식이었으면 메서드를 지우는 순간 컴파일 단계에서 "그런 메서드 없다"고 잡혔을 것이다. 단 XML 의 <select> 만 지우고 메서드를 남기면 컴파일은 통과하고, 처음 호출할 때 Invalid bound statement (not found) 로 터진다. 그리고 문제가 하나 더 있었다. return 문은 고쳤는데 위쪽 로그 줄이 옛 id 를 그대로 쓰고 있었다. System.out.println(sqlSessionTemplate.selectList("freeBoardSearchList", paramMap)); // ← 여기서 터짐 return sqlSessionTemplate.selectList("freeBoardGetList", paramMap); // ← 도달 못 함 문자열이라 Ctrl+F 로 찾지 않으면 놓친다. 메서드였으면 지우는 순간 IDE 가 부르는 곳을 전부 빨갛게 표시했을 것이다. 이 코드에는 다른 문제도 있다. 로그를 찍으려고 쿼리를 한 번 더 실행하고 있다. 결과를 변수에 담아 쓰는 게 맞다. 왜 회사는 옛날 방식인가 iBatis 2.x (~2010) 문자열 id 방식뿐. Spring 에서는 SqlMapClientTemplate MyBatis 3.0 (2010) SqlSession 문자열 방식과 Mapper 인터페이스가 처음부터 같이 있었다 mybatis-spring 1.0 (2010) SqlSessionTemplate 과 MapperFactoryBean 이 같이 나왔다 MyBatis 3.4 (2016) @Mapper 어노테이션 추가 현재 인터페이스 방식이 권장 SqlSessionTemplate 이 옛날 방식인 게 아니라, 문자열 id 로 부르는 습관이 iBatis 시절에서 온 것이다. MyBatis 3 와 mybatis-spring 은 처음부터 두 방식을 같이 줬다. 레거시 프로젝트는 만들 당시의 방식이 그대로 남아 있다. 그리고 한번 정한 방식을 중간에 바꾸지 않는다. 같은 맥락 — Service / ServiceImpl 개인 프로젝트 NeighborhoodService (인터페이스) + NeighborhoodServiceImpl (구현) 회사 FreeBoardService (클래스 하나) 인터페이스 분리는 "구현을 갈아끼울 수 있게" 하려는 것인데, 실제로 갈아끼우는 일이 거의 없어서 요즘은 생략하기도 한다. 옛 Spring 에서는 인터페이스가 있느냐가 AOP 프록시 방식을 갈랐다. Spring 3.2 전에는 인터페이스가 없는 클래스에 트랜잭션 같은 AOP 를 걸려면 CGLIB 라이브러리를 따로 넣어야 했고, 인터페이스가 있으면 JDK 동적 프록시로 끝났다. 3.2부터는 CGLIB 가 Spring 안에 들어 있어서 라이브러리를 따로 넣을 일이 없다. 둘 다 흔한 방식이다. 프로젝트 관례를 따르면 된다. 정리 원리 같다. 인터페이스 방식이 내부적으로 SqlSession 을 호출한다 차이 타입 안전성과 IDE 지원 현대적 인터페이스 방식 현장 둘 다 본다. 레거시일수록 SqlSessionTemplate 두 방식을 다 해본 게 오히려 유리하다. 어느 프로젝트에 가도 읽을 수 있다. 3. serialize() 없으면 여러 개를 못 보내나 내가 알던 것 "데이터를 둘 이상 백엔드로 넘긴다면 serialize() 를 한다" 검색 기능을 만들 때 기간 검색만 입력칸이 두 개였다. 통일성을 위해 전부 serialize() 로 보내야 한다고 생각했다. 그런데 최종 코드에는 serialize() 가 없었다. 왜 그랬는지 정리해봤다. 먼저 — 전제가 틀렸다 data : { searchType : "6", startDate : "20260101", endDate : "20261231" } $.ajax 의 data 에 객체를 주면 jQuery 가 알아서 직렬화한다. searchType=6&startDate=20260101&endDate=20261231 serialize() 를 쓴 것과 결과가 똑같다. 둘 다 key=value&key=value 형태로 나간다. serialize() 폼 안의 name 붙은 것을 자동 수집 객체 직접 작성 내가 고른 것만 담는다 serialize() 는 "폼 요소를 자동으로 수집"하는 편의 기능일 뿐이다. 여러 개를 보내는 것과는 별개 문제다. 그리고 검색 화면은 조건이 안 맞았다 serialize() 가 무엇을 담는지는 세 가지로 정해진다. 대상 <form> 에 부르면 안의 입력 요소를 모은다 <div> 에 부르면 에러 없이 빈 문자열이다. 안의 입력 요소를 직접 골라야 한다 name name 속성이 있는 요소만 담긴다 제외 disabled, 버튼류, type="file", 체크 안 된 체크박스·라디오는 빠진다. 숨긴 요소는 안 빠진다 문제 1 — <form> 이 없었다 <div> <select id="selection" onchange="changeFn()">...</select> <span id="case1" style="display:none">...</span> <span id="case2" style="display:none">...</span> <span id="case3" style="display:none">...</span> <button onclick="searchFn()">검색</button> </div> <div> 로만 감싸져 있었다. 여기에 바로 serialize() 를 부르면 빈 문자열이 나온다. 폼으로 감싸거나, div 에 id 를 붙여 $("#searchArea :input").serialize() 처럼 입력 요소를 직접 골라야 했다. select 에는 name 도 없어서 그대로는 searchType 부터 빠진다. 문제 2 — 숨긴 요소도 전송된다 이게 더 큰 문제다. display:none 이어도 disabled 가 아니면 값이 나간다 name 을 칸마다 따로 붙였다면( searchType , typeQuery , textQuery , startDate , endDate ), "제목" 검색을 골라도 서버가 받는 건 이렇게 된다. {searchType=4, typeQuery=01, textQuery=카페, startDate=, endDate=} ↑ 안 보이는데 나간다 어떤 값을 써야 하는지 서버가 판단해야 한다. 막으려면 hide() 할 때마다 disabled 를 걸고, show() 할 때 다시 풀어야 한다. $("#case1, #case2, #case3").hide() .find("input, select").prop("disabled", true); $("#case3").show() // 6번일 때 .find("input, select").prop("disabled", false); // 보여줄 칸은 다시 푼다 코드가 늘어난다. $("#searchArea :input:visible").serialize() 처럼 보이는 것만 고르는 방법도 있다. 대신 type="hidden" 도 안 보이는 요소로 쳐서 같이 빠진다. 문제 3 — name 충돌 이게 결정적이었다. 서버가 기대하는 것 searchType=4 & query=카페 ← 2~5번 searchType=6 & startDate=... & endDate=... ← 6번 query 라는 이름으로 보내야 하는데 입력칸이 두 개다. <select name="query" id="typeQuery"> <!-- 1번용 --> <input name="query" id="textQuery"> <!-- 2~5번용 --> name 이 같으면 serialize() 가 둘 다 담는다. query=01&query=카페 서버에서 Map<String,String> 으로 받으면 첫 번째 값 하나만 들어온다. 마크업에서 select 가 먼저라, "카페"를 입력해도 서버는 01 을 받는다. String 하나로 받으면 01,카페 로 붙어서 온다. 그래서 택한 방식 var value = $("#selection").val(); var data = { searchType : value }; if (value == "1") { data.query = $("#typeQuery").val(); } else if (value == "2" || value == "3" || value == "4" || value == "5") { data.query = $("#textQuery").val(); } else if (value == "6") { data.startDate = $("#startDate").val(); data.endDate = $("#endDate").val(); } 필요한 것만 골라 담는다. 1~5번 { searchType, query } 6번 { searchType, startDate, endDate } 보내는 방식은 하나고 담기는 키만 다르다. 원래 원했던 "통일성"이 이 부분이다. 나중에 페이지네이션을 붙일 때 data.page = page 한 줄만 추가하면 됐다. 조건부로 담는 구조라 확장이 쉬웠다. 그럼 serialize() 는 언제 쓰나 상세/수정 화면에서는 썼다. data : $("#detailForm").serialize() <form id="detailForm"> <input type="hidden" name="num" value="${freeBoardDto.num}"/> <select name="codeType">...</select> <input type="text" name="name" value="..."/> <input type="text" name="title" value="..."/> <textarea name="content">...</textarea> </form> 여기는 조건이 다 맞았다. <form> 으로 감싸져 있다 모든 요소에 name 이 있다 숨기거나 조건부로 보이는 게 없다 필드가 다섯 개라 손으로 담으면 길다 판단 기준 serialize() 가 좋을 때 폼 형태가 고정되어 있다 필드가 많다 전부 보내면 된다 직접 담는 게 좋을 때 조건에 따라 보낼 항목이 달라진다 숨겨진 요소가 있다 보내기 전에 가공이 필요하다 검색은 후자였다. "전체" 보기를 빼면 조건이 6개고 각각 다른 입력칸을 쓰니까. 정리 틀린 표현 "여러 개를 보내려면 serialize" 맞는 표현 "폼 전체를 통째로 보낼 때 serialize" 세 가지의 공통점 정리하고 보니 셋이 같은 종류의 오해였다. JSP "브라우저에서 JS 를 실행하는 것" → 서버에서 HTML 을 만드는 것 Mapper "다른 방식" → 같은 것을 대신 해주는 것 serialize "여러 개를 보내는 방법" → 폼을 수집해주는 것 전부 "어디서 실행되는가" 혹은 "무엇을 대신해주는가" 의 문제였다. 도구가 대신해주는 걸 모르면 Mapper 인터페이스 SqlSession 호출을 대신한다 serialize() 폼 요소 수집을 대신한다 jQuery DOM API 호출을 대신한다 Spring MVC URL 마다 서블릿을 만드는 일을 대신한다
솔직히 말하면, 처음엔 "또 클라우드 회사네" 하고 넘겼습니다. 그런데 최근 고객사에서 가상화 라이선스 비용 이슈가 터지면서 제대로 뜯어볼 기회가 생겼고, 파고들수록 "이거 꽤 다르다"는 생각이 들었습니다. 왜 지금 뉴타닉스인가 요즘 기업 IT 담당자들 사이에서 공통적으로 나오는 한숨이 있습니다. "VMware 라이선스 비용이 너무 올랐다"는 거죠. 브로드컴 인수 이후 구독형 전환이 강제되면서 TCO가 단기간에 4배 이상 뛰었다는 얘기가 실제로 나오고 있고, 여기에 예기치 않은 컴플라이언스 패널티 위험까지 더해졌습니다. 비용 문제만이 아닙니다. AI를 도입하려면 인프라부터 뜯어고쳐야 한다는 인식도 확산되고 있어요. 최근 설문에서 기업 응답자의 96%가 생성형 AI가 조직 전략을 재편하고 있다고 답했는데, 정작 현재 인프라로 그 워크로드를 제대로 감당할 수 있다고 자신하는 곳은 많지 않습니다. 이 틈새를 파고드는 게 뉴타닉스입니다. IDC 분석 기준 — 뉴타닉스 도입 기업의 3년 평균 ROI는 391%, 초기 투자 회수 기간은 7개월. 5년 기준 TCO 절감은 평균 62%로 집계됩니다. 처음엔 반신반의했는데, 구조를 뜯어보니 어느 정도 납득이 됩니다. VMware에서 빠져나오는 현실적인 방법 저도 예전에 수백 대 VM을 다른 하이퍼바이저로 옮기는 프로젝트를 옆에서 지켜본 적이 있는데, 그때 제일 힘들었던 게 드라이버 충돌과 네트워크 재설정이었습니다. 마이그레이션이 끝났는데도 하드코딩된 IP 때문에 여기저기 앱이 터지는 사태가 벌어졌거든요. Nutanix Move는 이 부분을 꽤 영리하게 풀었습니다. 무료로 제공되는 도구인데, 핵심은 세 가지입니다. 서비스 중단 없는 데이터 동기화 — VM을 끄지 않고 백그라운드에서 먼저 복제합니다. 변경된 블록만 추적해서 실제 컷오버 중단 시간은 수 분 이내로 줄어듭니다. 드라이버 자동 주입 — AHV 환경에서 부팅 시 블루스크린이 뜨는 상황을 사전에 차단합니다. VirtIO 드라이버를 자동으로 넣어주기 때문에 애플리케이션 리팩토링이 필요 없습니다. IP·MAC 주소 유지 — 이게 제일 중요한 포인트입니다. 기존 방화벽 정책이나 앱 간 통신 설정을 건드릴 필요가 없습니다. 마이그레이션 후유증을 원천에서 막아줍니다. 결과적으로 고가의 가상화 라이선스를 폐기하고 AHV로 전환하면, Prism 단일 콘솔로 전체 인프라를 관리하면서 TCO를 53% 이상 추가로 낮출 수 있다고 합니다. 온프레미스와 AWS를 하나처럼 — NC2 이야기 기존 앱을 AWS로 올릴 때 왜 이렇게 복잡하냐면, 보통 클라우드 전용 이미지로 변환하거나 오버레이 네트워크를 얹어야 하는데, 그 과정에서 성능이 떨어지고 운영 복잡도가 올라가기 때문입니다. NC2(Nutanix Cloud Clusters) on AWS 는 접근 방식이 다릅니다. AWS 베어메탈 인스턴스 위에 뉴타닉스 AHV를 직접 올려서, 온프레미스와 완전히 동일한 환경을 퍼블릭 클라우드에서 구현합니다. 중첩 가상화를 쓰지 않으니 자원 낭비가 없고, AWS의 ENI와 직접 연결되어 S3, RDS, ELB 같은 200여 개의 AWS 서비스와 속도 저하 없이 통신할 수 있습니다. 개인적으로 흥미로웠던 건 하이버네이트 & 재개 기능입니다. 야간이나 주말에 쓰지 않는 개발·테스트 환경을 통째로 S3에 오프로드해두고, 필요할 때만 다시 깨우는 방식이에요. 비싼 베어메탈 인스턴스를 실제 쓴 시간만큼만 과금하니 비용이 크게 줄어듭니다. TCO 53% 절감이라는 숫자도 이런 구조에서 나오는 겁니다. 쿠버네티스를 프로덕션에서 쓰는 게 왜 이렇게 힘드냐 하면 실제로 쿠버네티스를 운영해본 팀들이 공통적으로 하는 말이 있습니다. "클러스터 띄우는 건 쉬운데, 로깅·모니터링·백업·보안 정책 다 붙이고 나면 반년이 지나 있더라." 파편화된 오픈소스 에코시스템을 직접 조립하는 피로감이 실제로 큽니다. NKP(Nutanix Kubernetes Platform) 는 이 복잡성을 하나로 묶어줍니다. 마음에 들었던 건 순수 CNCF 업스트림 기반이라 특정 벤더에 묶이지 않는다는 점입니다. 오늘 NKP에서 돌리는 앱을 내일 EKS나 AKS로 그대로 옮길 수 있습니다. 보안 스택도 실용적입니다. eBPF 기반 Cilium으로 마이크로서비스 간 통신을 격리하고, Trivy가 컨테이너 이미지 취약점을 자동 스캔하며, Gatekeeper가 규정 위반 배포를 원천 차단합니다. 인터넷이 아예 안 되는 망 분리 환경(국방·공공기관)에서도 오프라인 패치가 가능하도록 설계된 점은 국내 공공 시장에서 꽤 유효한 포인트라고 봅니다. 기업용 AI, 외부 API에만 의존하면 생기는 문제 ChatGPT API를 내부 서비스에 연동하면 편하긴 한데, 민감한 데이터를 외부 서버로 보내야 한다는 걱정과 예측 불가능한 토큰 과금이 항상 따라옵니다. 의료나 금융은 규제 이슈까지 있고요. Nutanix Enterprise AI(NAI) 는 자체 데이터센터 안에서 언어 모델을 구동하는 프라이빗 AI 플랫폼입니다. NVIDIA와의 파트너십을 통해 BlueField DPU로 네트워크·보안 처리를 오프로드하는 구조인데, 덕분에 GPU가 AI 연산에만 집중할 수 있게 됩니다. 더 눈길을 끈 건 KV 캐시 오프로딩입니다. LLM이 긴 문서나 대화를 처리할 때 GPU 메모리에 임시 데이터가 쌓이는데, 이게 꽉 차면 시스템이 멈추거나 GPU를 더 사야 합니다. NAI는 이 데이터를 NVMe 스토리지로 내보내고 RDMA 프로토콜로 초저지연 연결을 유지합니다. GPU 메모리를 적게 쓰면서도 긴 컨텍스트를 처리할 수 있으니, 토큰당 비용을 낮추는 실용적인 구조입니다. GPT-in-a-Box 2.0 — Cisco UCS 서버, NVIDIA GPU, 뉴타닉스 소프트웨어(NCI + NKP + NAI)가 공장 검증된 상태로 배송됩니다. Hugging Face 모델이나 NVIDIA NIM을 몇 번의 클릭으로 바로 배포할 수 있고, 부서별 토큰 사용량 제한과 과금 거버넌스 기능도 내장되어 있습니다. 재해 복구, 보조 데이터센터 없이 가능할까 DR을 위해 동일한 규모의 보조 데이터센터를 별도 운영하는 전통적인 방식은 비용이 너무 큽니다. 평소엔 아무것도 안 하는 인프라에 돈을 계속 쏟아붓는 격이니까요. MST(Multi-Cloud Snapshot Technology)는 이 구조를 바꿉니다. 온프레미스 스냅샷을 Amazon S3에 저장해두고, 실제 재해가 날 때만 NC2 on AWS를 긴급 프로비저닝해서 복원하는 방식입니다. 평소에는 저렴한 스토리지 비용만 내다가 필요할 때 컴퓨팅을 켜는 'Just-in-Time DR'이라고 볼 수 있습니다. 가장 경제적이면서도 강력한 RTO/RPO를 충족하는 현대적 아키텍처라고 생각합니다. 정책 기반 자동화도 실용적입니다. VM에 'DB', 'WEB' 같은 태그만 붙여두면 그 기준으로 복제 정책이 자동 적용되고, 재해 시에는 런북이 자동 실행되어 DB → 미들웨어 → 웹 순으로 순차 부팅됩니다. 실제 운영 환경에 영향 없이 격리망에서 DR 훈련을 해볼 수 있다는 것도 실무에서 중요한 포인트입니다. 보안 측면에서는 AccuKnox와의 파트너십이 눈에 띕니다. 뉴타닉스 Flow가 VM 간 통신을 네트워크 레벨에서 통제하고, AccuKnox의 eBPF 에이전트가 프로세스 레벨에서 이상 행동을 실시간 감지합니다. 랜섬웨어가 VM 하나를 뚫어도 내부 전파를 입체적으로 차단하는 구조입니다. 정리하면서 인프라 전환이 부담스러운 건 당연합니다. 잘못되면 서비스가 통째로 멈추니까요. 그런데 가상화 라이선스 비용은 계속 올라가고, AI 워크로드도 감당해야 하는 현실에서 "지금 쓰던 것 계속 쓰자"는 선택도 점점 더 위험해지고 있습니다. 뉴타닉스가 흥미로운 이유는 기술적 완성도뿐 아니라, 실제 마이그레이션 리스크를 줄이는 자동화 도구들이 잘 갖춰져 있기 때문입니다. Nutanix Move, 하이버네이트 기능, MST 기반 DR, GPT-in-a-Box가 각각 독립적으로 설계된 게 아니라 하나의 흐름 안에서 맞물려 있다는 인상을 받았습니다. 다음에는 실제 마이그레이션 PoC를 시작하는 방법과 TCO 계산 시 놓치기 쉬운 항목들을 정리해보겠습니다. 인프라 전환을 고민 중이시라면, 댓글로 어떤 부분이 가장 걸림돌인지 알려주세요.
스위치를 이중으로 연결해두면 한쪽 링크가 죽어도 통신이 이어져서 좋지만, L2에서는 이 이중화가 오히려 문제를 만듭니다. 바로 루프 예요. STP(Spanning Tree Protocol)는 브로드캐스트 도메인에서 발생하는 이 루프를 막아주는 프로토콜입니다. 그림처럼 스위치 4대를 사각형으로 연결하면 프레임이 빨간 화살표를 따라 계속 돌게 됩니다. L2 프레임에는 TTL이 없어서 한 번 루프에 빠지면 스스로 사라지지 않거든요. 그래서 두 가지 문제가 생겨요. 브로드캐스트 스톰 : 브로드캐스트 프레임이 루프를 돌며 계속 늘어나서 대역폭과 장비 CPU를 잡아먹습니다. MAC 러닝 중복 : 같은 출발지 MAC이 여러 포트에서 번갈아 학습되어 MAC 테이블이 계속 흔들립니다. STP는 링크를 물리적으로 끊는 대신 일부 포트를 논리적으로 막아서 루프 없는 트리 구조를 만듭니다. 평소엔 막아두고, 활성 링크가 죽으면 그 포트를 열어서 백업으로 씁니다. 스패닝 트리의 필수 조건 트리가 만들어지려면 아래 세 가지가 성립해야 합니다. 네트워크당 Root bridge 가 하나 있다. Root bridge가 아닌 모든 bridge는 Root port 를 하나씩 가진다. 세그먼트(링크)마다 Designated port 가 하나씩 있다. Root port는 Root bridge로 가는 가장 좋은 길 쪽 포트이고, Designated port는 각 링크에서 Root bridge 방향으로 프레임을 전달하는 포트입니다. 둘 중 어느 쪽도 못 된 포트는 막힙니다. Root bridge, Root port, Designated port는 어떻게 정해질까 스위치들은 기본값으로 2초마다 BPDU 를 주고받으면서 아래 순서로 우선순위를 비교합니다. 위 기준에서 결판이 나면 아래 기준은 보지 않아요. 누가 더 작은 Root BID 를 가지는가 Root bridge까지의 Path cost 가 누가 더 작은가 누구의 BID 가 더 낮은가 누구의 포트 ID 가 더 낮은가 BPDU는 이 정보를 주고받기 위한 프레임으로 Root BID, Root Path Cost, Sender BID, Port ID를 담고 있습니다. BID는 priority + MAC 주소 로 만들어집니다. priority가 낮은 쪽이 이기고, priority가 같으면 MAC이 낮은 쪽이 이깁니다. EOS의 기본 priority는 32768이고, Path cost는 링크 속도가 빠를수록 낮아요. 실습 vEOS13의 priority를 28672로 설정했습니다. 기본값(32768)보다 낮으니 vEOS13이 Root bridge로 선출됩니다. spanning-tree priority 28672 그림의 포트 역할(R은 Root port, D는 Designated port, A는 Alternate port)을 보면 이렇습니다. vEOS13 : Root bridge라서 모든 포트가 D입니다. vEOS14, vEOS15 : Root bridge와 직접 연결된 포트가 R이고, 반대쪽 포트는 D입니다. 오른쪽 아래 vEOS : Root bridge로 가는 경로가 vEOS14 쪽과 vEOS15 쪽, 두 개입니다. Root port는 하나만 가질 수 있어서 한 경로(Eth1)가 R이 되고, 나머지(Eth2)는 A로 막힙니다. 그림의 빨간 X가 막힌 지점이에요. 포트 상태 변화 STP 포트는 5가지 상태를 거칩니다. Disabled(비활성) : 포트에 문제가 있거나 shutdown된 상태입니다. Blocking(차단) : BPDU 수신만 하고 데이터 전송과 MAC 학습은 하지 않습니다. BPDU로 상태를 확인하면서 토폴로지 변화에 대비해요. Listening(청취) : Blocking 포트가 Root port나 Designated port로 선출되고 20초(max age)가 지나면 전환됩니다. 간접 링크 장애 기준이고, 직접 링크 장애라면 20초는 생략할 수 있어요. 아직 데이터 전송과 MAC 학습은 안 합니다. Learning(학습) : Listening 상태를 15초 유지하면 전환됩니다. 이때부터 MAC 정보를 받아서 MAC 테이블을 갱신하지만 데이터 전송은 아직입니다. Forwarding(전송) : Learning 상태를 15초 유지하면 전환됩니다. 이제부터 데이터 송수신이 가능해요. 어느 단계에 있든 차단 포트로 선정되면 바로 Blocking으로 가고, 포트가 shutdown되거나 문제가 생기면 Disabled로 넘어갑니다. 전부 거치면 20 + 15 + 15 = 50초 , 직접 링크 장애라서 max age 대기를 생략하면 30초 입니다. 그래서 총 convergence time이 30~50초예요. 알아두면 좋은 점 1. 위 타이머는 고전 STP(802.1D) 기준입니다. 장애가 나고 30~50초 동안 통신이 안 된다는 건 실무에서 꽤 깁니다. EOS는 rstp , mstp , rapid-pvst 모드를 지원하는데 모두 RSTP를 기반으로 해서 훨씬 빠르게 수렴해요. 그림에 나온 A(Alternate) 포트도 RSTP에서 쓰는 역할 이름입니다. 2. Root bridge는 직접 정해두는 게 좋습니다. priority를 건드리지 않으면 MAC이 가장 낮은 장비가 Root bridge가 되는데, 오래된 액세스 스위치가 될 수도 있어요. 이번 실습처럼 백본(코어) 스위치에 낮은 priority를 주는 게 일반적입니다. EOS에는 spanning-tree root primary (priority 8192), secondary (16384)처럼 편하게 지정하는 명령도 있습니다. 3. 확인 명령어 show spanning-tree show spanning-tree instance detail 앞의 명령은 Root bridge와 포트 역할을, 뒤의 명령은 Hello time(2초), Max Age(20초), Forward Delay(15초) 같은 타이머를 확인할 때 씁니다. 정리 STP는 L2 루프를 막기 위해 Root bridge를 하나 정하고, 나머지 스위치마다 Root port, 세그먼트마다 Designated port를 정해서 나머지 포트를 막는 프로토콜입니다. BPDU를 주고받으며 Root BID, Path cost, BID, 포트 ID 순으로 비교하고, 포트는 Blocking에서 Forwarding까지 최대 50초에 걸쳐 올라옵니다.
Simple Ways to Buy Telegram Accounts Telegram is a popular messaging platform used by individuals, businesses, creators, and online communities. Some people look for established Telegram accounts because they want an existing username, profile, or online presence. We’re Always Online — 24/7 Reply/Contact 📲▶ Telegram: @usanovapro 📞▶ WhatsApp: ▶+1(208) 402-6908 🌐Visit Now Link ; infousanovapro@gmail.com However, buying an account from another person can involve important security, privacy, ownership, and fraud risks. Before considering an account purchase, it is important to understand the safer options and what to watch for. Why Do People Want Existing Telegram Accounts? There are several reasons someone might be interested in an existing account. An account may already have a username that a buyer wants. A business or community manager may also believe that an established account will save time compared with starting from scratch. However, account age or an existing profile does not automatically make an account more trustworthy or valuable. The Simplest Option: Create Your Own Account For most users, creating a new Telegram account is the safest approach. When you create your own account, you control the registration information, security settings, profile, and account history from the beginning. You also avoid many problems associated with accounts previously controlled by unknown users. If You Are Considering an Existing Account If you have a legitimate reason to evaluate an existing account, research carefully before making any transaction. Verify Ownership Make sure the person offering the account has legitimate authority to transfer it. If ownership cannot be clearly established, consider that a major warning sign. Understand the Account's History Ask what you can legitimately learn about the account's previous use. An account with an unknown history may carry security or reputation risks. Review Telegram's Current Policies Telegram's rules and policies may change. Review the current requirements before considering an account transfer and ensure that your intended use complies with them. Be Careful With Sellers Avoid sellers who pressure you to pay immediately or refuse to provide basic information. Promises such as "guaranteed permanent access" or "never banned" should be treated skeptically. We’re Always Online — 24/7 Reply/Contact 📲▶ Telegram: @usanovapro 📞▶ WhatsApp: ▶+1(208) 402-6908 🌐Visit Now Link ; infousanovapro@gmail.com Avoid Suspicious Account Offers Some advertisements may claim to offer special or "verified" Telegram accounts. Be cautious if an offer includes claims such as: "100% unbannable" "Permanent verification" "Zero risk" "Impossible to recover" "Complete anonymity" These claims cannot reliably guarantee the long-term security of an account. Protect Your Telegram Credentials Never give your Telegram verification code or password to a seller or stranger. A verification code is sensitive authentication information. Sharing it can allow someone else to gain access to an account. Also avoid entering Telegram credentials into unfamiliar websites or links. Check for Account Security Issues An existing account may have active sessions on devices belonging to previous users. Security should therefore be considered carefully before relying on any account previously controlled by someone else. For accounts you legitimately control, regularly review active sessions, enable two-step verification, and remove devices you no longer recognize. Watch Out for Payment Scams Online transactions involving digital accounts can attract fraudulent sellers. Be cautious when someone: Demands immediate payment Refuses to explain ownership Offers an unusually cheap account Provides only screenshots as proof Disappears after receiving payment Requests sensitive login information Do not let urgency pressure you into making a decision. Why a New Account May Be Better Creating your own Telegram account offers several advantages. You have direct control over: Registration Security settings Two-step verification Profile information Active sessions Account history This makes it easier to establish clear ownership and maintain account security. Frequently Asked Questions Is it safe to buy a Telegram account? Buying an account from an unknown seller can involve significant risks, including fraud, ownership disputes, privacy problems, and unknown account history. Is an old account more valuable? Not necessarily. Age alone does not prove that an account is legitimate, secure, or useful. Should I share my Telegram login code with a seller? No. Keep all authentication codes private. Can I create my own Telegram account instead? Yes. For most legitimate purposes, creating your own account is the simplest and safest option. Conclusion There may be reasons to consider an existing Telegram account, but purchasing one can introduce risks that are easy to overlook. If you are evaluating an account, verify legitimate ownership, understand its history, review Telegram's current policies, and avoid suspicious sellers. Never share authentication codes or passwords. For most people, creating and securing their own Telegram account remains the most straightforward way to establish a reliable presence on the platform. We’re Always Online — 24/7 Reply/Contact 📲▶ Telegram: @usanovapro 📞▶ WhatsApp: ▶+1(208) 402-6908 🌐Visit Now Link ; infousanovapro@gmail.com
AI가 정말 빠르게 변화하고 있다고 느끼는 요즘입니다. 2달 전까지만 해도 최신 개념인 Loop Engineering에 대해서 블로그를 쓰려고 했는데 벌써 Loop Engineering을 넘어서 Graph Engineering이라는 이름이 나왔다고 하네요. 그래도 하던 것부터 마무리 하고 다음으로 넘어가겠습니다. Intro 말을 안해도 다들 AI가 정말 빠르게 발전하고 있다는 것을 느끼고 계실겁니다. 제가 AI를 처음 사용했을 때를 생각해 봅니다 24년 하반기까지만 해도 제가 느끼는 AI의 성능은 썩 좋지 않았습니다. md파일 먹이기 이런 개념도 없었을 뿐더러 뭐하나 물어보면 이상한 답변을 줄 때도 많았습니다. 저만 그런게 아니었나 봅니다. 한층 한층 문제들이 개선되더니 정말 기하급수적인 속도로 지금까지 와버렸습니다. 먼저 어떻게 여기까지 왔나 살펴보겠습니다. Prompt Engineering 프롬프트를 입력하여 AI에게 답변을 듣는 방식입니다. 아직까지 대부분의 사람들이 이 방식으로 AI를 사용하고 있을 것이라 생각합니다. 간단한 질문 (요리 레시피 알려줘, 사주 봐줘) 들은 프롬프트 입력만으로 충분히 해결됩니다. 더 나아가 "너는 10년차 컨설턴트야", "너는 운동 코치야" 라면서 AI에게 페르소나를 입혀주기도 합니다. 그래서 Persona Engineering 이라는 단어가 등장하기도 합니다. 하지만 규모가 큰 작업을 하고 있다면? 회사 업무나 프로젝트 등 장기간 업무를 진행해야 하는 상황입니다. AI가 이전 내용을 기억하지 못하는 상황이 발생합니다. 처음부터 다시 알려줘야 하는 상황이 반복됩니다. Context Engineering 프롬프트 입력 전에 AI가 알아야 할 사전 지식을 미리 각인시켜주고, 매 턴 모델이 뭘 볼지, 뭘 뺄지 정하는 방식입니다. AI에게 작업을 맡기기 전에 미리 필요한 지식을 입력해주고, 실질적으로 업무에 도움이 되는 지식을 AI가 선별해 유용한 답변을 받을 수 있습니다. 하지만 여기서 만족하지 않습니다. AI는 행동하지 않습니다. 결국 답변을 받은 사람이 행동으로 연결해야 한다는 귀찮음이 존재합니다. Harness Engineering AI에게 손발을 달아주고, 규제를 걸어줌으로써 안정적이고 정확하게 AI가 스스로 동작하는 방식입니다. MCP, Tool, SubAgent 등 기존에 등장한 도구들을 AI에게 쥐어줌으로써 사고만 하던 AI에서 행동까지 연결하는 AI로 만들어 줍니다. 하지만 아무런 규제 없이 AI에게 과한 권한을 부여해준다면, AI가 무슨 일을 벌일지 예측할 수 없습니다. 그래서 도구와 더불어, 컨텍스트 관리, 권한 규제 등을 같이 입력해 어느 정도 사람이 통제할 수 있는 AI로 만듭니다. 충분히 쓸만해 보입니다. 그렇지만 한번 행동을 하면 결과가 좋든 나쁘든 사람이 확인을 해야합니다. 결과를 확인하고, 수정하고, 다시 실행하고. 상당히 귀찮습니다. Loop Engineering 사람이 매번 띄우는 대신, 띄우고 검사하고 멈추는 일까지 시스템에 맡기는 방식입니다. 인간의 판단 기준을 검증기와 정지 규칙에 한 번 입력해 놓으면, 루프는 검증기 기준과 정지 규칙을 모두 통과할 때까지 반복하여 돕니다. 결국 루프의 판단은 모델의 성능이 아니라 인간이 박아둔 판단에 기반합니다. Loop의 구성요소 루프라고 하면 while 문 한 줄이 떠오르지만, 실제로 굴러가는 루프에는 조각이 몇 개 더 붙습니다. Addy Osmani는 이를 여섯 조각으로 분류했습니다. Automations - 시작 트리거 '언제 무슨 일을 해줘' 요청입니다. 사람이 "이것 좀 해줘"라고 직접 말하는 단계가 사라집니다. Claude Code 의 /loop(주기 실행) 와 /goal(조건 충족까지 실행) 이 여기에 해당합니다. Worktrees - 병렬 작업 공간 격리 여러 에이전트를 동시에 돌리면 같은 파일을 서로 덮어씁니다. git worktree 로 작업 공간을 분리해야 단일 스레드를 벗어날 수 있습니다. Skills - 프로젝트 지식 입력 매 사이클마다 "우리 프로젝트는 이렇게 돌아갑니다"를 다시 설명할 수는 없습니다. 처음으로 작업을 시작(콜드 스타트)한 모델은 모르는 부분을 추측합니다. 저희는 왜 모델이 그렇게 추측했는지 알 수가 없습니다. 이를 방지하기 위해 사전 지식을 파일로 박아둡니다. Connectors - 바깥 세상과의 연결 MCP로 이슈 트래커, DB, CI를 붙입니다. 여기서 "여기 수정안입니다"가 "PR 열었고 티켓 걸어뒀습니다"로 바뀝니다. Sub-agents - 작업 모델, 검사 모델 분리 작업을 수행하는 모델은 자신이 한 작업을 후하게 채점합니다. 그래서 작업 수행 에이전트와 작업 검사 에이전트를 나눕니다. 모델이나 추론 강도를 다르게 주기도 합니다. State / Memory - 기억 저장소 마크다운 파일이든 보드든, 밖에 적어둬야 다음 사이클이 이어집니다. "에이전트는 잊지만 레포는 잊지 않는다." Osmani의 표현입니다. 여섯 개를 다 갖춰도 빠진 게 하나 있습니다. 정지 규칙 입니다. 언제 멈출지를 정하지 않은 루프는 언제 어디서 끝날지 아무도 모릅니다. 밤새 돌려 수백 달러를 태웠다는 후기는 대부분 정지 규칙이 비어 있던 경우입니다. CodeReferee 사실 Loop Engineering을 알고 있기 전부터, 진행하고 있던 프로젝트가 있습니다. 깃허브 레포의 안정성을 검사하고, 개선사항을 알려주는 서비스입니다. Input을 깃허브 레포로 받으면 검증, 피드백, 수정을 거쳐 안정적인 코드로 재탄생시켜주는 서비스입니다. 알게 모르게 이미 Loop Engineering 개념을 적용하고 있었네요. 자세히 한번 살펴보겠습니다. CodeReferee 루프 Repository URL입력 → Preflight → Sandbox 실행 → Judge → Critic → Refiner → 리포트 먼저 Repository URL을 입력으로 받으면 실행 가능한 유효한 주소인지 검사하는 단계( Preflight )를 거칩니다. Preflight가 통과하면 샌드박스에서 Chaos Engineering 을 통해 장애를 주입하고 평가 결과를 도출합니다 결과를 보고 Judge가 Fail을 주면 Critic이 원인을 분석하고 Refiner가 수정안( diff )을 냅니다. 그 수정안을 샌드박스에 적용해 다시 돌립니다. 여기서 봐야할 점은 Loop 구조가 _run_refinement_rounds 함수 하나에 들어 있고, 70줄이 안 된다는 것입니다. 또한 이 70줄에서 대부분이 "반복"이 아니라 "중단"이라는 점입니다. 검증기 검증기는 Judge입니다. 제가 미리 적어둔 판정 기준표를 보고 Pass와 Fail만 냅니다. LLM이 혼자 판단하지 않게 하려는 목적입니다. 인간이 설정한 Chaos Engineering 통과 기준을 넘은 레포에만 '안정' 판정을 주고, 검사 보고서를 함께 냅니다. 정지 규칙 라운드 상한 3회 - 비용적 측면에서 Trade-off 고려 Refiner가 수정안을 못 내면 즉시 중단 - 고칠 수단이 없는데 반복해봐야 같은 결과입니다. 수정안이 1MB를 넘으면 중단 - 수정 범위가 그 정도면 판정 자체를 믿을 수 없습니다. 인프라 오류면 판정을 건너뛰고 중단 - Docker가 죽은 걸 사용자 코드 실패로 보고하면 안 됩니다. 아직 없는 두 조각 구성요소 여섯 개 중 둘이 제 루프에 없습니다. Automations 가 없습니다. 아직 사람이 띄웁니다. 검증과 중단은 시스템이 하는데, 레포 입력은 사용자가 진행합니다. 그래서 엄밀히 말하면 완전히 도는 루프가 아니라, 띄우면 끝까지 알아서 가는 루프입니다. Worktrees 도 없습니다. 이건 필요가 없어서 안 넣었습니다. 레포 하나를 받아 검증하는 구조라 에이전트 여러 개가 같은 파일을 다툴 일이 없습니다. 병렬은 레포 여러 개를 동시에 검증할 때 필요할 것이라 그때 붙일 계획입니다. 느낀점 구성요소를 다 채우는 게 목표가 아니라는 걸 여기서 배웠습니다. Worktrees는 안 넣은 게 맞고, Automations는 사용자가 레포 입력을 하지 않고 자동화할 방법이 있는지 고민 중에 있습니다. 정지 규칙이 본체였습니다. 제 루프의 정지 조건은 설계가 멋져서 넣은 게 아닙니다. 반복 상한이 없으면 같은 실패를 세 번 더 겪습니다. 수정안 크기 상한이 없으면 레포를 새로 쓴 걸 "수정"으로 받습니다. 인프라 오류와 코드 실패를 구분하지 않으면 Docker가 죽은 걸 사용자 잘못으로 보고합니다. 하나하나 당해보고 박아둔 규칙입니다. 루프는 제 판단의 증폭기입니다. 좋은 기준을 넣으면 좋은 판단이 빠른 속도로 반복되고, 나쁜 기준을 넣으면 자신만만한 오답이 빠른 속도로 반복됩니다. 객관적 숫자를 기반으로 기준을 세우려고 했습니다. 프롬프트를 더 잘 쓰는 일에서 멈추는 조건을 더 잘 쓰는 일로. 재미있는 쪽은 전자지만 결과를 정하는 건 후자였습니다.
HTTP란? HyperText Transfer Protocal 웹에서 브라우저와 서버가 통신하기 위한 포로토콜(규약) → 브라우저가 사이트에 접속할 때 서버에서 웹페이지를 구성하는 데이터 (HTML, CSS, JavaScript 등)을 주고받는 규칙 요청(Request)와 응답(Response)의 형태로 진행된다. HTTP의 특징 커넥션당 하나의 요청과 응답만 처리 즉, 여러개의 요청을 한번에 전송/응답할 수 없다. 기본 80번 포트 사용 무상태성 서버가 클라이언트의 상태를 보존하지 않기에 로그인과 같이 유저의 상태를 유지해야하는 서비스일 경우 브라우저 쿠키, 서버 세션, 토큰 등을 이용해 상태를 유지해야 한다. 비연결성 기본적으로 연결을 유지하지 않기에 요청을 보낼 때마다 연결했다 끊는 작업을 반복한다. 각각의 자원을 다운로드하기 위해 연결과 종료를 반복하는 비효율이 생긴다. 트래픽이 많지 않고 빠른 응답을 제공할 수 있는 경우 효율적이지만 트래픽이 많고 큰 규모의 운영을 할 때는 한계를 가진다. HTTP의 한계 데이터가 전혀 암호화되어 있지 않은 평문 통신이다. 중간에서 통신 내용을 엿보거나 가로채는 공격이 가능하다. 스니핑 네트워크상에서 다른 사용자들이 주고받는 패킷(데이터 조각)을 몰래 엿보거나 도청하는 해킹 기법 중간자 공격 두 통신 당사자 사이에 공격자가 몰래 끼어들어 중계하는 공격 HTTP 흐름 브라우저 주소창에 URL을 입력한다. DNS 조회 도메인 주소(예: www.naver.com)를 실제 서버의 IP 주소로 바꾼다. TCP 연결 3-way handshake로 브라우저와 서버가 연결을 맺는다. HTTPS라면 이후 TLS 핸드셰이크까지 진행한다. HTTP 요청 브라우저가 서버에 요청 메시지를 보낸다. 요청 메시지는 요청 라인(메서드, 경로, 버전), 헤더, 바디로 구성된다. HTTP 응답 서버가 요청을 처리하고 응답 메시지를 보낸다. 응답 메시지는 상태 라인(버전, 상태 코드), 헤더, 바디로 구성된다. 렌더링 브라우저가 받은 HTML, CSS, JS를 해석해 화면에 그린다. 이후 연결은 버전에 따라 유지되거나 종료된다. HTTP 요청 메서드 클라이언트가 서버에게 어떤 동작을 원하는지 알려주는 방법이다. 주요 메서드 GET 리소스를 조회한다. 데이터를 URL의 쿼리 스트링에 담아 보내기 때문에 주소창에 노출된다. POST 데이터를 서버에 보내 새로운 리소스를 생성한다. 데이터를 바디에 담아 보낸다. PUT 리소스 전체를 교체한다. 리소스가 없으면 새로 생성한다. PATCH 리소스의 일부만 수정한다. DELETE 리소스를 삭제한다. OPTIONS 서버가 지원하는 메서드를 확인한다. CORS의 사전 요청(Preflight)에 사용된다. 멱등성 같은 요청을 여러 번 보내도 결과가 같은 성질이다. GET, PUT, DELETE는 멱등하고 POST는 멱등하지 않다. 예) 게시글 작성 POST를 3번 보내면 게시글이 3개 생긴다. HTTP 응답 상태 코드 상태 코드는 클라이언트가 보낸 요청의 처리 상태를 응답에서 알려주는 기능으로서, 3자리 숫자로 만들어져 있으며, 100 ~ 500 번대 숫자로 이루어져 있다. 1xx (정보) 요청을 받았고 처리 중이라는 의미다. 실무에서 직접 볼 일은 거의 없다. 2xx (성공) 200 OK: 요청 성공 201 Created: 요청이 성공해 새로운 리소스가 생성됨 (주로 POST) 204 No Content: 요청은 성공했지만 돌려줄 데이터가 없음 (주로 DELETE) 3xx (리다이렉션) 301 Moved Permanently: 리소스가 영구적으로 다른 주소로 이동함 302 Found: 리소스가 일시적으로 다른 주소로 이동함 304 Not Modified: 리소스가 변경되지 않았으니 브라우저 캐시를 사용하라는 의미 4xx (클라이언트 오류) 400 Bad Request: 요청 형식이 잘못됨 401 Unauthorized: 인증이 필요함 (로그인 안 된 상태) 403 Forbidden: 인증은 됐지만 권한이 없음 404 Not Found: 요청한 리소스가 없음 5xx (서버 오류) 500 Internal Server Error: 서버 내부에서 오류가 발생함 502 Bad Gateway: 중간 서버(게이트웨이)가 잘못된 응답을 받음 503 Service Unavailable: 서버 과부하나 점검으로 일시적으로 요청을 처리할 수 없음 HTTP의 버전별 비교 HTTP/1.1 지속 연결 keepalive 라는 기능이 추가되어 연결이 이루어지고 난 뒤 각각의 자원들을 요청하고, 모든 자원에 대한 응답이 돌아온 후에 연결을 종료한다. HOL(Head-of-Line) Blocking 앞 요청의 응답이 늦으면 뒤 요청도 줄줄이 대기하는 문제 발생 HTTP/2.0 다중 요청/응답 가능 커넥션당 여러개의 요청과 응답을 주고받을 수 있다. HTTP/2.0은 여러 리소스의 동시 전송이 가능하므로 HTTP/1.1에 비해 페이지 로드 속도가 약 50% 정도 빠르다고 알려져 있다. HTTP/3.0 QUIC 프로토콜 기반: TCP 대신 UDP 위에서 동작 TCP 보낸 데이터를 확실히 받았는지 확인하면서 보내는 방식 데이터가 빠짐없이, 순서대로 도착 확인 절차가 많아서 느림 UDP 데이터를 받았는지 확인 없이 되는대로 응답을 보내는 방식 확인 절차가 없어서 매우 빠름 데이터가 빠지거나 순서가 뒤섞일 수 있음 QUIC은 UDP 위에서 재전송·순서 보장을 직접 해준다 TCP HOL Blocking 해결: 스트림이 독립적이라 한 스트림의 패킷이 유실돼도 다른 스트림은 영향을 받지 않음 HTTPS란? 보안이 강화된 HTTP 프로토콜로 HTTPS 의 S는 Secure를 의미한다. HTTPS의 특징 HTTP 통신 내용을 SSL 또는 그 후속 기술인 TLS 프로토콜을 이용해 암호화 한다. 기본 443번 포트 사용 아래 3가지 핵심 목표를 가진다 기밀성 데이터를 암호화해서 제 3자가 데이터를 훔쳐보더라도 내용을 해석할 수 없도록 한다. 무결성 전송 중에 데이터가 변조되지 않았음을 보장 누군가 내용을 바꾸면 받는 쪽에서 알아챌 수 있다. 인증 인증서를 통해 지금 접속한 서버가 위조되지 않은 진짜 서버임을 보장한다. SSL/TLS 핸드셰이크 브라우저와 서버가 본격적으로 암호화 통신을 시작하기 전에 서로를 확인하고 데이터 암호화 규칙을 정하는 과정 SSL/TLS 핸드셰이크 과정 Client Hello 클라이언트가 서버에 연결을 요청하며 아래 정보를 보낸다. 지원하는 TLS 버전 사용 가능한 암호화 방식 목록 클라이언트 랜덤 값 Server Hello 서버가 응답하며 아래 정보를 보낸다. 클라이언트가 보낸 목록 중 서버가 선택한 TLS 버전과 암호화 방식 서버 랜덤 값 Certificate 서버가 자신의 SSL 인증서를 보낸다. 인증서 안에는 서버의 공개키와 도메인 정보, CA의 전자서명이 들어 있다. 이후 Server Hello Done 메시지로 서버 쪽 전송이 끝났음을 알린다. 인증서 검증 클라이언트는 받은 인증서가 믿을 수 있는지 확인한다. 브라우저에 내장된 신뢰할 수 있는 CA가 서명했는지 유효 기간이 지나지 않았는지 인증서의 도메인이 접속한 주소와 일치하는지 검증에 실패하면 브라우저가 경고 화면을 띄운다. Client Key Exchange 클라이언트가 Pre-Master Secret이라는 랜덤 값을 새로 만든다. 이 값을 인증서에 들어 있던 서버의 공개키로 암호화해서 서버에 보낸다. 공개키로 암호화한 값은 서버의 개인키로만 풀 수 있기 때문에, 중간에서 가로채도 내용을 알 수 없다. 세션키 생성 서버는 자신의 개인키로 Pre-Master Secret을 복호화한다. 이제 클라이언트와 서버는 같은 재료 3개를 갖게 된다. Client Random + Server Random + Pre-Master Secret 양쪽이 이 재료로 각자 같은 계산을 해서 동일한 세션키(대칭키)를 만든다. 세션키 자체는 네트워크로 전송되지 않는다. Finished 양쪽이 "이제부터 세션키로 암호화한다"는 신호를 보낸다. 그동안 주고받은 핸드셰이크 내용을 세션키로 암호화한 Finished 메시지를 서로 보내 확인한다. 양쪽이 정상적으로 복호화되면 핸드셰이크가 완료된다. 암호화 통신 시작 이후 모든 HTTP 데이터는 세션키(대칭키)로 암호화해서 주고받는다. SSL 인증서의 중요성 인증서 기반 통신이 안전한 이유는 신뢰할 수 있는 제3자, 즉 인증 기관이 존재하기 때문 브라우저는 이 SSL 인증서를 확인할 때 코모도/고대디 같은 공신력 있는 인증 기관에서 발급한 것이 맞는지 브라우저에 내장된 인증 기관 목록과 비교해 철저히 검사한다. 만약 공격자가 만든 가짜 인증서라면 브라우저는 즉시 ‘이 사이트는 안전하지 않습니다’라는 경고창을 띄우고 접속을 차단한다. 검증 과정을 통과했다는 것은 현재 접속한 웹 사이트는 사칭된 웹 사이트가 아니라 진짜 해당 회사나 기관이 운영하는 진짜 서버라는 뜻이 된다. SSL 인증서는 암호화에 사용되는 공개키를 안전하게 전달하는 역할과 동시에, 접속하려는 서버의 신원을 확실하게 증명하는 역할을 한다. 이 2가지가 결합되어 HTTPS 통신은 높은 수준의 보안을 유지할 수 있다. 비대칭키와 대칭키를 섞어 쓰는 이유 비대칭키(공개키/개인키) 키를 안전하게 전달할 수 있지만 연산이 느림 대칭키(세션키) 연산이 빠르지만 같은 키를 양쪽이 안전하게 나눠 갖기 어려움 그래서 핸드셰이크에서만 비대칭키를 써서 대칭키 재료를 안전하게 전달하고, 실제 데이터는 빠른 대칭키로 암호화 Mixed Content HTTPS 페이지에서 HTTP 리소스를 불러오면 브라우저가 차단하거나 경고 HSTS 브라우저가 해당 도메인에 항상 HTTPS로만 접속하도록 강제하는 헤더
이게 뭐예요? Codex Gauge 는 맥 상단 메뉴바에서 Codex의 남은 사용량을 확인하는 앱 이에요. 남은 사용량을 고양이와 밥그릇, 게이지, 퍼센트로 보여줘요. 사용량이 넉넉하면 통통한 고양이 옆에 사료가 가득 있어요. 사용량이 줄어들면 고양이도 홀쭉해지고, 밥그릇도 비어가요. 밥이 없어질수록 표정도 짜증 나요. 제가 쓰려고 만들었는데 다른 분들도 쓸 수 있게 올려봤어요. 👉 GitHub 구경하기 👉 앱 다운로드 이렇게 보여요 메뉴바 구성은 이렇게 되어 있어요. [고양이와 밥그릇] [남은 사용량 게이지] [퍼센트] 게이지는 100%에서 시작해서 남은 사용량에 따라 줄어들어요. 고양이는 가만히 앉아서 작은 움직임만 보여줘요. 맥 전용이고 M1 이상만 되어요 macOS 13 이상 Apple Silicon, M1 이상 ChatGPT 계정으로 Codex 로그인 필요 Codex CLI 별도 설치 필요 윈도우는 없어서 못 만들었어요. CPU 폭주도 잡았어요 🐈 만들고 보니까 메뉴바 앱이 CPU 코어 하나를 계속 쓰고 있었어요. 고양이 밥보다 제 맥 배터리를 더 많이 먹고 있었어요. 메뉴바 갱신이 반복되는 문제를 수정하고, 이미지 처리와 캐시도 정리했어요. 항목 이전 개선 후 앱 CPU 사용률 약 99.8% 약 4.0% 메모리 부담 약 248MB 약 47MB 조회당 서버 실행 2회 1회 개발 맥에서 측정한 결과예요. 개선 후 10분간 CPU 평균과 마지막 메모리 부담(physical footprint)을 비교했어요. 환경에 따라 달라질 수 있어요. 화면이 꺼지거나 세션이 비활성화되면 애니메이션과 자동 조회도 쉬어요. 절전 모드와 동작 줄이기에서는 고양이도 쉬어요. 고양이 32프레임은 그대로예요. 테스트 61개도 통과했어요. 설치는 이렇게 하셔요 Releases 에서 앱 ZIP을 받아요. 압축을 풀고 Codex Gauge.app 을 응용 프로그램 폴더에 넣어요. Codex CLI가 없으면 터미널에서 아래 명령어를 한 번 실행해요. curl -fsSL https://chatgpt.com/codex/install.sh | sh Codex CLI 공식 설치 안내 앱을 더블 클릭하고, 로그인이 필요하면 브라우저에서 내 ChatGPT 계정으로 로그인해요. 위에 고양이 뜨면 된 거예요. 기존 버전을 쓰고 있다면 앱을 종료한 뒤 새 앱으로 교체해 주세요. 인증되지 않은 앱 이어요 아직 애플 개발자 서명과 공증을 받지 않은 앱이에요. 실행이 막히면 출처를 확인하고 시스템 설정 → 개인정보 보호 및 보안 → 그래도 열기 로 열어요. 마음껏 수정하세요 소스도 같이 올려뒀어요. 제가 쓰고 싶어서 만든 작은 앱이라 부족한 부분이 있을 수 있어요. 문제가 생기면 GitHub Issues 에 남겨주세요. 고양이가 마음에 들면 GitHub 스타도 하나 주셔요. ⭐