fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • claude-code 페이지로 이동
    • harness 페이지로 이동
    • llm 페이지로 이동
    • ops 페이지로 이동
    • practice 페이지로 이동
  • algorithm 페이지로 이동
    • Alias Method: 가중치 랜덤 선택을 전처리 O(n), 추출 O(1)로 바꾸기
    • Welford's Online Algorithm: 값을 저장하지 않고 평균과 분산 계산하기
  • architecture 페이지로 이동
    • distributed-systems 페이지로 이동
    • domain 페이지로 이동
    • evolution 페이지로 이동
    • patterns 페이지로 이동
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • postgresql 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • gaps and islands: SQL 창 함수로 이어진 구간 찾기
    • MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 정규화와 역정규화: 무결성과 조회 비용 사이의 선택
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
    • 종료 신호는 어디서 멈추는가
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS와 TLS: 핸드셰이크, 인증서, 종료 지점
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • Alpine 이미지의 Java 에서 링크 바꿔치기(TOCTOU)를 막지 못하는 이유
    • Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • 로그에 traceId 남기기 — MDC 부터 OpenTelemetry 까지
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 클러스터 아키텍처: Broker, KRaft Controller와 복제
    • Kafka 기본 개념: Topic, Partition, Offset과 Segment
    • Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • Kafka 파티션·리밸런스·컨슈머 지연 운영
    • Kafka를 로그로 이해하기: 복제, 재처리, 상태와 데이터 통합
    • Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • SSH forced command 로 배포 키가 실행할 수 있는 명령 제한하기
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • model-router 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • K8s 위 Spring Boot 앱의 메트릭 수집
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS가 줄지 않는 이유: CPython, gc.collect(), malloc_trim
  • rabbitmq 페이지로 이동
    • RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • 경험 및 경력기술서
    • 김병태 경력기술서
    • 김병태 포트폴리오
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • personal-projects 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • thinking 페이지로 이동
    • 자기주도 학습: 질문에서 시작해 글로 검증하기
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/devops/컨테이너와 쿠버네티스가 필요한 이유 — ja…
devops

컨테이너와 쿠버네티스가 필요한 이유 — jar 를 서버에 올리던 사람의 관점에서

나는 오랫동안 Spring Boot 를 jar 로 말아서 서버에 올리는 방식으로 일했다. 배포는 "서버에 접속해서 기존 프로세스를 죽이고 새 jar 를 띄운다"였고, 그걸로 충분했다. 그러다 쿠버네티스 위에서 도는 서비스를 맡게 됐는데, ArgoCD 화면은 매일 보면서도 그 안에서 무슨 일이 벌어지는지는 몰랐다. Pod 가 왜 이름이 매번 바뀌는지, 왜 서...

2026.07.27·6 min read·80 views·SERIES · 백엔드 개발자를 위한 쿠버네티스 기본기 · 1/8

나는 오랫동안 Spring Boot 를 jar 로 말아서 서버에 올리는 방식으로 일했다. 배포는 "서버에 접속해서 기존 프로세스를 죽이고 새 jar 를 띄운다"였고, 그걸로 충분했다. 그러다 쿠버네티스 위에서 도는 서비스를 맡게 됐는데, ArgoCD 화면은 매일 보면서도 그 안에서 무슨 일이 벌어지는지는 몰랐다. Pod 가 왜 이름이 매번 바뀌는지, 왜 서버에 SSH 로 들어갈 생각을 안 하는지.

이 시리즈는 그때부터 하나씩 밟아나간 기록이다. 첫 글에서 답할 질문은 하나다 — jar 를 서버에 올리는 방식은 정확히 어디서 무너지고, 쿠버네티스는 그중 무엇을 대신해 주는가.

컨테이너가 먼저 푸는 문제 — "내 로컬에선 됐는데"

쿠버네티스 얘기를 하려면 컨테이너부터다. 컨테이너는 애플리케이션을 어디서 돌리든 똑같이 돌도록 만든 격리된 실행 환경이다.

jar 배포에서 겪던 문제를 떠올리면 필요성이 바로 보인다. 로컬은 JDK 17, 개발 서버는 11 이 깔려 있어서 안 뜬다. 서버마다 타임존이 다르다. 누군가 서버에 직접 설치한 라이브러리에 의존하고 있었는데 그 서버를 재구축하니 안 뜬다. 전부 실행 환경이 코드 밖에 있어서 생기는 문제다.

컨테이너는 실행 환경을 코드와 같이 묶어서 이 문제를 없앤다.

  • 이미지(Image) — 베이스 OS · JDK · 내 jar · 설정까지 통째로 담은 스냅샷. eclipse-temurin:21-jre 위에 app.jar 를 얹은 것이 하나의 이미지다.
  • 컨테이너(Container) — 그 이미지를 실행한 상태. 즉 프로세스다.
  • 불변성 — 한 번 빌드한 이미지는 어디서 실행하든 내용이 같다. 그래서 "개발 서버에서 검증한 그 이미지"를 운영에 그대로 올린다. 운영에서 다시 빌드하지 않는다.

VM 과 헷갈리기 쉬운데, 결정적 차이는 OS 커널을 새로 띄우느냐다. VM 은 게스트 OS 를 통째로 부팅하므로 수십 초가 걸리고 메모리도 GB 단위로 먹는다. 컨테이너는 호스트 커널을 공유하고 프로세스만 격리하므로 수 초 안에 뜨고 오버헤드가 훨씬 작다. 배포 한 번에 컨테이너 수십 개를 새로 띄우는 롤링 업데이트가 현실적인 건 이 가벼움 덕분이다.

도커만으로는 어디서 막히는가

컨테이너로 실행 환경 문제는 풀렸다. 그런데 서비스가 커지면 도커 단독으로는 감당이 안 되는 지점이 순서대로 나타난다.

막히는 지점도커 단독쿠버네티스가 대신하는 것
컨테이너가 죽음사람이 알아채고 다시 띄움죽으면 자동으로 새로 띄움
트래픽 증가사람이 서버에 들어가 추가 실행replica 수를 늘리면 알아서 배치
무중단 배포스크립트로 직접 구현롤링 업데이트가 기본 기능
서버 여러 대 분산어느 서버에 뭘 띄울지 사람이 관리스케줄러가 여유 있는 노드에 배치
컨테이너 주소 찾기IP·포트를 사람이 관리Service 이름으로 호출

표를 세로로 읽으면 공통점이 보인다. 왼쪽 열은 전부 사람이 알아채고 사람이 조치하는 일이고, 오른쪽 열은 전부 시스템이 알아채고 시스템이 조치하는 일이다. 쿠버네티스가 파는 것은 기능 목록이 아니라 이 전환이다.

그래서 쿠버네티스를 한 문장으로 정의하면 이렇게 된다 — 컨테이너를 자동으로 배치하고, 유지하고, 스스로 복구하는 오케스트레이션 시스템.

클러스터는 어떻게 생겼나

쿠버네티스는 서버 여러 대를 하나의 자원 풀로 묶는다. 그 묶음이 클러스터(Cluster) 고, 안에는 역할이 다른 두 종류의 서버가 있다.

각 조각을 백엔드 개념에 붙여 보면 이해가 빠르다.

  • kube-apiserver — 클러스터의 유일한 REST API 서버다. kubectl 도, 각 노드의 kubelet 도, 컨트롤러도 전부 여기를 통해서만 대화한다. 컴포넌트끼리 직접 연결되지 않고 API 서버 하나를 허브로 두는 구조라, 인증·권한·검증을 한 곳에서 건다.
  • etcd — 클러스터의 모든 상태가 저장되는 데이터베이스다. "Pod 를 2개 띄워라"는 선언도, 지금 몇 개가 떠 있는지도 전부 여기 있다. etcd 가 죽으면 클러스터의 기억이 사라진다.
  • kube-scheduler — 새로 만들어야 할 Pod 를 보고 어느 노드에 놓을지 정한다. CPU·메모리 여유, 노드 라벨, 배치 제약을 따진다. 놓기만 하고 실행은 하지 않는다.
  • controller-manager — "선언된 상태"와 "실제 상태"를 계속 비교해 맞추는 루프들의 집합이다. 이 루프가 쿠버네티스의 핵심 원리인데, 다음 글에서 따로 다룬다.
  • kubelet — 각 워커 노드에 상주하는 에이전트다. API 서버에서 "네 노드에 이 Pod 를 띄워라"를 받아 실제로 컨테이너를 실행하고, 상태를 다시 보고한다.

여기서 감각이 하나 바뀐다. 내가 특정 서버를 지목해서 앱을 올리는 게 아니다. 나는 "이 앱을 2개 띄워둬"라고 API 서버에 말할 뿐이고, 어느 노드에 놓을지는 스케줄러가 정한다. 그래서 Pod 가 어느 서버에 떠 있는지 평소에 신경 쓰지 않고, SSH 로 들어갈 생각도 하지 않게 된다.

배포의 의미가 바뀐다

정리하면 쿠버네티스로 넘어오면서 바뀌는 건 도구가 아니라 배포라는 행위의 정의다.

  • 기존: 서버에 접속해 절차를 실행한다. 프로세스를 죽이고, 새 jar 를 올리고, 다시 띄운다.
  • 쿠버네티스: 원하는 상태를 선언한다. "이 이미지로 2개 떠 있어야 한다"고 적어두면, 그 상태를 유지하는 건 시스템 몫이다.

이 차이 때문에 장애 대응 방식도 달라진다. Pod 가 죽었을 때 되살리는 게 아니라 폐기하고 새로 만든다. 그래서 Pod 안에 중요한 걸 쌓아두면 안 되고, 로그도 파일로 남기지 않고 표준 출력으로 내보내 외부 수집기로 보낸다. 서버를 "고쳐 쓰는 가축이 아니라 갈아 끼우는 부품"으로 보는 전제가 깔려 있다.

쿠버네티스가 해결해 주지 않는 것

도입하면 다 되는 것처럼 보이지만, 실제로 겪어보니 그렇지 않았다. 미리 알아두면 좋은 한계가 있다.

  • 애플리케이션이 준비돼 있어야 무중단이 된다. 롤링 업데이트 기능이 있다고 무중단이 되는 게 아니다. 종료 신호를 받았을 때 처리 중인 요청을 마저 끝내는 코드가 없으면, 배포할 때마다 사용자 쪽에 에러가 나간다. 실제로 이 준비가 안 된 서비스에서 배포·스케일인 때마다 503 이 튀는 걸 겪었다.
  • 상태를 가진 것은 여전히 어렵다. DB 처럼 데이터를 들고 있는 컴포넌트는 "죽으면 새로 만든다"는 전제와 충돌한다. 방법은 있지만(StatefulSet, PersistentVolume) 난이도가 다르고, 관리형 DB 를 쓰는 선택이 흔하다.
  • 네트워크를 모르면 결국 막힌다. 외부 요청이 Pod 까지 닿는 경로에는 로드밸런서·Ingress·Service 가 겹겹이 끼어 있다. 이 계층을 모르면 "왜 접속이 안 되는지"를 추적할 수 없다. 이 시리즈와 별개로 실전 연재를 따로 둔 이유다.
  • 추상화가 두꺼워 디버깅이 어렵다. 선언만 하고 실행은 시스템이 하므로, 안 될 때 "어디서 멈췄는지"가 안 보인다. 이걸 읽는 법이 다음 글의 주제다.

이 시리즈에서 다룰 것

앞으로의 순서는 이렇다. 아래로 갈수록 구체적이다.

  1. 컨테이너와 쿠버네티스가 필요한 이유 — 지금 이 글
  2. 선언형 API 와 reconcile loop — 쿠버네티스를 관통하는 단 하나의 원리
  3. 핵심 객체 4종 — Pod · Service · Ingress · Namespace 의 관계
  4. Deployment · ReplicaSet · Pod — 배포가 실제로 굴러가는 3층 구조
  5. admission — etcd 에 저장되기 직전 — 요청이 거부되는 지점
  6. Helm — YAML 을 템플릿으로 관리하기
  7. Argo CD — git 을 정답으로 삼는 배포
  8. Helm 과 ArgoCD 로 GitOps 하기 — 둘을 합친 실전 흐름

다음 글에서는 지금까지 계속 미뤄 둔 것 — "원하는 상태를 선언하면 시스템이 맞춘다"가 내부적으로 어떤 구조인지 — 를 본다. 이 원리 하나를 잡아두면 뒤의 Deployment · admission · ArgoCD 가 전부 같은 패턴의 변주로 읽힌다.

참고 링크

  • Kubernetes Components — 공식 문서
  • What is Kubernetes — 공식 문서
  • 컨테이너 격리 원리 — Linux 프로세스 격리
on this page
  • 01컨테이너가 먼저 푸는 문제 — "내 로컬에선 됐는데"
  • 02도커만으로는 어디서 막히는가
  • 03클러스터는 어떻게 생겼나
  • 04배포의 의미가 바뀐다
  • 05쿠버네티스가 해결해 주지 않는 것
  • 06이 시리즈에서 다룰 것
  • 07참고 링크
tags
#입문#study📚 백엔드 개발자를 위한 쿠버네티스 기본기
NEXT →선언형 API 와 reconcile loop — 쿠버네티스를 관통하는 단 하나의 원리

이런 글도

  • devops

    선언형 API 와 reconcile loop — 쿠버네티스를 관통하는 단 하나의 원리

    2026.07.27
  • devops

    종료 신호는 어디서 멈추는가

    2026.09.02
  • devops

    파드가 1개면 minAvailable 이 노드 교체를 막는다

    2026.08.20
  • devops

    ArgoCD는 초록불인데 서비스는 죽어 있었다 — 불변 필드와 재조정의 경계

    2026.08.11

댓글 (0)