<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>소소한 지식 저장소</title>
    <link>https://ch010104.tistory.com/</link>
    <description>소소한 지식들을 기록하는 공간</description>
    <language>ko</language>
    <pubDate>Wed, 17 Jun 2026 19:46:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ch010104</managingEditor>
    <item>
      <title>대규모 백엔드 인프라 아키텍처 및 배포 전략 - Ngnix, Load Balancer, 웹/앱</title>
      <link>https://ch010104.tistory.com/284</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 대규모 백엔드 인프라 아키텍처: 트래픽 분산과 동기화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 서비스는 단일 서버(Single Server)로 시작하지만, 트래픽 폭주 시 서버의 사양을 높이는 스케일 업(Scale-Up)에는 물리적/비용적 한계가 존재합니다. 이를 해결하기 위해 똑같은 애플리케이션 서버를 여러 대 복제하여 트래픽을 나누어 처리하는 &lt;b&gt;수평 확장(Scale-Out)&lt;/b&gt; 아키텍처를 도입하며, 인프라는 거대한 분산 시스템으로 진화합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 로드 밸런서 (Load Balancer)와 Nginx 심화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 하나의 공인 주소(Domain/IP)로 들어오는 수많은 사용자의 요청을 뒤에 대기 중인 여러 대의 백엔드 서버(K대)로 균등하게 분배해 주는 최전선 문지기이자 트래픽 지휘관입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L4 vs L7 로드 밸런서&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;L4 로드 밸런서 (네트워크/전송 계층):&lt;/b&gt; IP 주소와 포트(Port) 번호만 보고 트래픽을 분산합니다. 패킷의 내용물을 열어보지 않으므로 연산 속도가 매우 빠르고 효율적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;L7 로드 밸런서 (애플리케이션 계층):&lt;/b&gt; HTTP 헤더, 쿠키, URL 경로(Path), 페이로드 등을 분석하여 지능적으로 라우팅합니다. (예: /api/users는 유저 서버로, /api/payments는 결제 서버로 분산). Nginx, AWS ALB(Application Load Balancer)가 대표적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Nginx 로드 밸런싱 및 헬스 체크 설정 (nginx.conf)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx를 리버스 프록시(Reverse Proxy) 및 로드 밸런서로 사용할 때, 단순히 트래픽만 나누는 것이 아니라 장애가 발생한 서버를 격리(Health Check)하고 가중치(Weight)를 부여할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;http {
    # 백엔드 서버 그룹 (Upstream) 정의
    upstream my_backend_servers {
        # 1. 라운드 로빈 (기본값): 순서대로 분배
        # 2. least_conn: 현재 연결 수가 가장 적은 한가한 서버로 분배
        # 3. ip_hash: 클라이언트 IP를 해싱하여 항상 동일한 서버로 접속 보장 (세션 유지에 유리)
        least_conn;

        # weight: 서버 사양에 따라 가중치 부여 (8081 서버에 3배 더 많은 트래픽 할당)
        # max_fails / fail_timeout: 10초 동안 3번 응답이 없으면 해당 서버를 죽은 것으로 간주하고 트래픽 차단
        server 127.0.0.1:8081 weight=3 max_fails=3 fail_timeout=10s;
        server 127.0.0.1:8082 weight=1 max_fails=3 fail_timeout=10s;
        server 127.0.0.1:8083 backup; # 다른 서버가 모두 죽었을 때만 작동하는 예비 서버
    }

    server {
        listen 80;
        server_name api.myservice.com;

        location / {
            proxy_pass http://my_backend_servers;

            # 클라이언트의 진짜 IP와 프로토콜 정보를 백엔드 서버에 전달 (X-Forwarded 헤더)
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[무중단 배포 메커니즘]&lt;/b&gt; 8081 서버의 코드를 교체하기 위해 프로세스를 종료하면, Nginx의 max_fails 설정이 이를 즉각 감지하여 8082 서버로만 트래픽을 우회시킵니다. 업데이트가 완료되어 8081 서버가 다시 켜지면 트래픽 분산이 자동으로 재개되어 사용자 입장에서는 서비스가 단 1초도 끊기지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. Docker 기반 다중 컨테이너 배포 (Docker Compose)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 환경을 물리적 서버에 일일이 세팅하는 것은 비효율적입니다. Docker를 활용하면 Nginx와 수많은 백엔드 서버를 도커 브릿지 네트워크(Bridge Network)라는 격리된 가상 망으로 묶어, IP 대신 &lt;b&gt;컨테이너 이름&lt;/b&gt;으로 통신하는 깔끔한 아키텍처를 구축할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  실무형 docker-compose.yml 상세 예시&lt;/h3&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;version: '3.8'

services:
  # 1. 백엔드 서버 1번 (App Container)
  backend-app-1:
    image: my-spring-boot-app:latest
    container_name: web-app-1
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DB_HOST=master-db # 내부 DNS를 통해 DB 컨테이너 이름으로 맵핑
      - REDIS_HOST=redis-cluster
    networks:
      - my-network

  # 2. 백엔드 서버 2번 (App Container)
  backend-app-2:
    image: my-spring-boot-app:latest
    container_name: web-app-2
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DB_HOST=master-db
      - REDIS_HOST=redis-cluster
    networks:
      - my-network

  # 3. 최전선 Nginx 로드 밸런서
  nginx-loader:
    image: nginx:1.21-alpine
    container_name: nginx-gateway
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro # 호스트의 설정 파일을 읽기 전용으로 마운트
      - ./certs:/etc/nginx/certs # HTTPS 인증서 마운트
    depends_on:
      - backend-app-1
      - backend-app-2
    networks:
      - my-network

networks:
  # 컨테이너들끼리 통신할 가상 네트워크 정의
  my-network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 환경에서는 nginx.conf 내부의 upstream 블록을 server web-app-1:8080; 처럼 작성할 수 있습니다. 도커 내장 DNS가 컨테이너 이름을 해당 컨테이너의 내부 IP로 자동 변환해주기 때문입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3. 대용량 트래픽 처리의 정석: K : N : M 분산 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽을 견디기 위해 백엔드 서버 1개를 복사할 때, 묶여있는 DB와 Redis까지 통째로 세트로 늘리는 것은 데이터 동기화의 재앙을 초래합니다. 현업에서는 각 컴포넌트의 역할과 특징에 맞게 &lt;b&gt;독립적인 클러스터 그룹&lt;/b&gt;을 형성하여 수평 확장합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 백엔드 서버 그룹 (K대) : 무상태성(Stateless) 일꾼&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Stateless 구조:&lt;/b&gt; 서버 내부에 사용자 세션(로그인 상태)이나 중요 데이터를 저장하지 않습니다. 서버가 $K$대로 분산되어 있기 때문에, 1번 서버에 로그인한 유저가 다음 요청 시 2번 서버로 배정되면 로그아웃 처리되는 '세션 불일치' 문제가 발생하기 때문입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책:&lt;/b&gt; 세션 정보는 JWT(JSON Web Token)로 클라이언트에 맡기거나, 모든 서버가 공유하는 Redis 세션 스토어에 저장하여 서버 자신은 언제든 죽고 새로 태어나도 문제없는 순수한 연산 장치로 만듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) Redis 클러스터 그룹 (N대) : 동시성 제어 및 초고속 캐시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동시성 제어 (Distributed Lock):&lt;/b&gt; K대의 서버가 동시에 DB의 재고를 차감하려고 할 때 발생하는 경합(Race Condition)을 방지합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바의 synchronized는 단일 서버 메모리에서만 동작하므로, 분산 환경에서는 모든 서버가 공통으로 바라보는 Redis를 이용해 &lt;b&gt;분산 락&lt;/b&gt;을 구현합니다.&lt;/li&gt;
&lt;li&gt;구현 원리: Redis의 원자적 연산인 SETNX (Set if Not Exists)를 활용하거나, Redisson 라이브러리의 Pub/Sub 기능을 활용해 스레드 대기 부하 없이 락을 획득/반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐싱 (Caching):&lt;/b&gt; DB 디스크 I/O를 줄이기 위해 자주 조회되는 데이터(이벤트 배너, 상품 목록)를 메모리 단에 저장합니다. (Look-aside, Write-through 전략 등 활용)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis 고가용성 구성:&lt;/b&gt; 레디스 자체의 장애를 막기 위해 데이터가 동기화되는 &lt;b&gt;Master-Slave 구조&lt;/b&gt;를 띄우고, &lt;b&gt;Sentinel(감시자)&lt;/b&gt; 컨테이너를 배치하여 Master가 죽으면 즉각 Slave를 Master로 승격(Failover)시킵니다. 극단적인 대용량 데이터는 &lt;b&gt;Redis Cluster(샤딩)&lt;/b&gt; 구조로 쪼개어 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) Database 레플리카 그룹 (M대) : 최종 데이터의 보루&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Master-Slave 복제 (Replication):&lt;/b&gt; 데이터베이스를 여러 대 띄우고 역할을 철저히 분리합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Master DB (1대):&lt;/b&gt; 데이터의 삽입/수정/삭제(INSERT, UPDATE, DELETE) 등 '쓰기' 요청만을 전담합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Slave DB (여러 대):&lt;/b&gt; 전체 트래픽의 80~90%를 차지하는 '조회(SELECT)' 요청을 여러 대가 분산 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엔진 단위의 동기화:&lt;/b&gt; 쓰기 작업이 Master DB에서 발생하면, DB 엔진은 이를 바이너리 로그(Binary Log)에 기록합니다. Slave DB들은 백그라운드 스레드를 통해 이 로그를 실시간으로 가져와(Relay Log) 자신의 디스크에 반영하여 데이터를 동일하게 맞춥니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 단의 라우팅:&lt;/b&gt; Spring Boot 등의 백엔드 프레임워크에서는 @Transactional(readOnly = true) 어노테이션이 붙은 메서드는 DB 커넥션 풀에서 &lt;b&gt;Slave DB 주소&lt;/b&gt;를, 그렇지 않은 트랜잭션은 &lt;b&gt;Master DB 주소&lt;/b&gt;를 선택하도록 AbstractRoutingDataSource 등을 통해 지능적으로 라우팅합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프론트엔드와 모바일 앱의 배포 전략 (AWS 기반)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거대한 K:N:M 백엔드 API 클러스터가 구축된 위에서, 최종 사용자에게 UI/UX를 제공하는 '클라이언트'를 어떻게 배포하느냐에 따라 웹과 앱의 아키텍처가 확연히 달라집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. 웹 환경의 배포 (SPA vs SSR)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 브라우저는 빈 껍데기로 시작하여 접속할 때마다 화면을 그릴 리소스(HTML/CSS/JS)를 서버로부터 다운로드해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SPA (정적 파일 기반 - React, Vue):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트엔드 연산 서버를 따로 띄우지 않는 &lt;b&gt;Serverless&lt;/b&gt;에 가까운 구조입니다.&lt;/li&gt;
&lt;li&gt;빌드된 정적 리소스를 &lt;b&gt;AWS S3&lt;/b&gt;에 업로드하고, 전 세계 수백 곳의 엣지 로케이션에 캐싱해 주는 AWS CloudFront(CDN)를 덮어씌웁니다. 전 세계 어디서 접속하든 물리적으로 가장 가까운 엣지 서버에서 즉각적으로 파일을 응답하므로, 서버 관리 포인트 없이 대규모 트래픽을 완벽하게 소화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSR (서버 연산 기반 - Next.js 등):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SEO 최적화와 초기 로딩 속도를 위해 &lt;b&gt;서버에서 먼저 HTML을 동적으로 그려서(렌더링)&lt;/b&gt; 내려주는 방식입니다.&lt;/li&gt;
&lt;li&gt;정적 파일이 아니기 때문에, 백엔드 서버처럼 Docker 컨테이너 이미지로 빌드한 후 &lt;b&gt;AWS ALB(로드밸런서) 뒤에&lt;/b&gt; K&lt;b&gt;대의 프론트엔드 Node.js 서버를 수평 확장&lt;/b&gt;하여 배포해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. 모바일 앱 환경의 배포 특이성과 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 배포의 가장 큰 특징은 &lt;b&gt;수천만 명의 사용자 스마트폰(단말기) 하나하나가 화면을 그리는 개별 프론트엔드 서버 역할&lt;/b&gt;을 한다는 점입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클라이언트의 분산 (Zero-Frontend Server in AWS):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면을 구성하는 UI 코드(컴포넌트)는 구글 플레이스토어나 애플 앱스토어를 통해 스마트폰의 디스크에 100% 영구 설치됩니다.&lt;/li&gt;
&lt;li&gt;따라서 AWS 인프라에는 프론트엔드 화면을 주기 위한 서버(S3, CDN 등)가 일절 필요 없으며, 스마트폰이 화면을 그리기 위해 요청하는 &lt;b&gt;JSON 데이터만 내려주는 순수 API 서버 클러스터&lt;/b&gt;만 존재하면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점:&lt;/b&gt; 화면 에셋 전송이 사라지므로 네트워크 아웃바운드(Egress) 트래픽 비용이 극단적으로 절감되며, 렌더링을 폰의 CPU가 담당하므로 체감 성능이 매우 뛰어납니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;앱 환경의 SSR 대체 기술 (실시간 업데이트 딜레마 극복):&lt;/b&gt; 정석적인 SSR은 불가능하지만, &quot;스토어 심사(수일 소요)를 거치지 않고 실시간으로 화면을 변경하고 싶다&quot;는 니즈를 해결하기 위해 다음과 같은 아키텍처를 도입합니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;코드푸시 (CodePush / OTA 업데이트):&lt;/b&gt; React Native, Flutter 환경에서 사용되며, 앱 구동 시 클라우드(S3 등)를 찔러 최신 JS 번들 코드를 백그라운드에서 다운로드하여 화면 로직을 즉각 덮어씌웁니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 주도 UI (SDUI - Server Driven UI):&lt;/b&gt; 백엔드가 단순 텍스트 데이터를 넘어 &quot;어떤 컴포넌트를 어디에 무슨 색상으로 배치하라&quot;는 화면 설계도(Schema)를 JSON으로 내려줍니다. 단말기는 이를 파싱하여 레고 블록 조립하듯 즉석에서 네이티브 화면을 그려냅니다. (토스, 배달의민족 등에서 적극 활용)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;웹뷰 (WebView) 하이브리드:&lt;/b&gt; 앱 내의 특정 이벤트 탭 등에 보이지 않는 브라우저(WebView)를 띄워, 기존 웹 SSR 환경(AWS 배포 Next.js 등)의 URL을 호출해 렌더링된 화면을 그대로 투영합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타</category>
      <category>load balancer</category>
      <category>ngnix</category>
      <category>배포</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/284</guid>
      <comments>https://ch010104.tistory.com/284#entry284comment</comments>
      <pubDate>Fri, 12 Jun 2026 13:29:21 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] MAC 주소와 ARP</title>
      <link>https://ch010104.tistory.com/283</link>
      <description>&lt;h2 data-pm-slice=&quot;1 5 []&quot; data-ke-size=&quot;size26&quot;&gt;1. 두 가지 네트워크 주소 체계 비교 (IP vs MAC)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 기기들은 통신을 위해 두 가지 대표적인 주소(논리 주소와 물리 주소)를 가집니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kAfZF/dJMcabdi0tE/9Tpj3krnWOvwUAGaTx6VN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kAfZF/dJMcabdi0tE/9Tpj3krnWOvwUAGaTx6VN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kAfZF/dJMcabdi0tE/9Tpj3krnWOvwUAGaTx6VN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkAfZF%2FdJMcabdi0tE%2F9Tpj3krnWOvwUAGaTx6VN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;468&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;u&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/u&gt;&lt;/td&gt;
&lt;td&gt;&lt;u&gt;&lt;b&gt; 32-bit IP 주소 (Internet Protocol) &lt;/b&gt;&lt;/u&gt;&lt;/td&gt;
&lt;td&gt;&lt;u&gt;&lt;b&gt; 48-bit MAC 주소 (Media Access Control) &lt;/b&gt;&lt;/u&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;비유&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;우편 주소 (집 주소)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;주민등록번호 (기기 고유 번호)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;성격&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;네트워크 상에서 위치에 따라 변하는 &lt;b&gt;논리적 주소&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;하드웨어(랜카드 ROM)에 새겨진 &lt;b&gt;물리적 주소&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;주요 역할&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;다른 네트워크(서브넷) 간의 경로 배정(라우팅)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;동일 서브넷(LAN) 내에서 인접 기기 간 데이터 전달&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;고유성 범위&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Locally Unique&lt;/b&gt; (사설 IP의 경우 해당 서브넷 내 고유)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Globally Unique&lt;/b&gt; (IEEE 주관 하에 전 세계 유일 보장)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;표기법&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;10진수 4개 조합 (예: 128.119.40.136)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;16진수 12자리 조합 (예: 1A-2F-BB-76-09-AD)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이동성(Portability)의 차이:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MAC 주소:&lt;/b&gt; 이동성이 있습니다. 노트북을 들고 학교, 집, 카페로 이동해도 내 랜카드의 MAC 주소는 변하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IP 주소:&lt;/b&gt; 이동성이 없습니다. 접속하는 서브넷 대역에 따라 동적으로 새로운 IP를 할당받아야 통신이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로토콜 계층과 데이터 포장 (캡슐화/역캡슐화)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 전송될 때는 프로토콜 스택 상단에서 하단으로 내려오며 포장지(헤더)가 붙고, 수신할 때는 반대로 올라가며 해체됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;IP (Internet Protocol Layer / 3계층):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 알맹이에 출발지 IP / 목적지 IP 라벨을 붙여 패킷(Packet)을 생성합니다. 이 알맹이는 목적지에 도착할 때까지 &lt;b&gt;절대 변하지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Eth (Ethernet Layer / 2계층):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP 패킷을 다시 한번 출발지 MAC / 목적지 MAC 봉투로 감싸 프레임(Frame)을 만듭니다. 이 봉투는 &lt;b&gt;다음 목적지(인접 라우터 혹은 최종 기기)로 넘어갈 때마다 계속 교체&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Phy (Physical Layer / 1계층):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최종 완성된 프레임을 전기/광/무선 신호로 변환하여 물리적인 랜선이나 공기 중으로 쏘아 보냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;&quot;IP 주소만으로는 전송할 수 없는가?&quot;&lt;/b&gt; 랜카드(하드웨어)는 오직 2계층(Eth) 프레임 형태로 조립되어 목적지 MAC 주소가 찍혀 있어야만 물리 신호로 보낼 수 있습니다. 따라서 &lt;b&gt;최종 목적지 IP를 알고 있더라도, 최종 전송을 위해서는 반드시 목적지 MAC 주소를 알아내야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. ARP (Address Resolution Protocol)의 역할과 동작&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lErRe/dJMcahxTel1/rKswggDXEemjJkb2XT0hc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lErRe/dJMcahxTel1/rKswggDXEemjJkb2XT0hc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lErRe/dJMcahxTel1/rKswggDXEemjJkb2XT0hc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlErRe%2FdJMcahxTel1%2FrKswggDXEemjJkb2XT0hc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;598&quot; height=&quot;409&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;409&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ARP는 &lt;b&gt;&quot;상대방의 IP 주소는 알지만, MAC 주소를 모를 때&quot;&lt;/b&gt; 이를 해결해 주는 통역사 역할을 수행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 ARP 테이블 (ARP Table)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기기(호스트, 라우터)는 매번 주소를 물어보는 비효율을 줄이기 위해 &amp;lt; IP 주소 ; MAC 주소 ; TTL &amp;gt; 정보를 메모리에 표 형태로 저장해 둡니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TTL (Time To Live):&lt;/b&gt; 매핑 정보의 유효 시간(통상 20분)입니다. 기기의 이동이나 IP 변경이 빈번하기 때문에, 유효 시간이 지나면 잘못된 전송을 막기 위해 테이블에서 자동 삭제됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 ARP의 동작 과정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEWJaY/dJMcacwAnfs/oKzzVIJVdGbmW4CeGknvX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEWJaY/dJMcacwAnfs/oKzzVIJVdGbmW4CeGknvX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEWJaY/dJMcacwAnfs/oKzzVIJVdGbmW4CeGknvX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEWJaY%2FdJMcacwAnfs%2FoKzzVIJVdGbmW4CeGknvX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1277&quot; height=&quot;521&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;521&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;ARP Query (Request):&lt;br /&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A가 B의 MAC 주소를 모를 때, LAN 상의 모든 기기에게 브로드캐스트(FF-FF-FF-FF-FF-FF)로 질문을 던집니다.&lt;/li&gt;
&lt;li&gt;&quot;IP 137.196.7.14 쓰시는 분? MAC 주소 알려주세요!&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1355&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y1vwk/dJMcaaetsq2/1X6qEDKg5KaPWzMjv05zv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y1vwk/dJMcaaetsq2/1X6qEDKg5KaPWzMjv05zv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y1vwk/dJMcaaetsq2/1X6qEDKg5KaPWzMjv05zv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY1vwk%2FdJMcaaetsq2%2F1X6qEDKg5KaPWzMjv05zv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1355&quot; height=&quot;495&quot; data-origin-width=&quot;1355&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.패킷 필터링:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자신과 상관없는 IP를 받은 기기(C, D)는 패킷을 버립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tdpP5/dJMcad3f3kb/CIzJcmahAUmBrkLMIuYz5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tdpP5/dJMcad3f3kb/CIzJcmahAUmBrkLMIuYz5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tdpP5/dJMcad3f3kb/CIzJcmahAUmBrkLMIuYz5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtdpP5%2FdJMcad3f3kb%2FCIzJcmahAUmBrkLMIuYz5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1166&quot; height=&quot;461&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;3. &lt;/span&gt;ARP Reply:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 IP를 가진 B만 요청을 수락하고, A의 IP-MAC 정보를 자신의 ARP 테이블에 저장한 뒤, A에게만 콕 집어 응답(Unicast)을 보냅니다.&lt;/li&gt;
&lt;li&gt;&quot;저 여기 있습니다! 제 MAC 주소는 58-23-D7-FA-20-B0입니다.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 다른 서브넷(외부 네트워크)으로의 라우팅 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 인터넷을 통해 외부 서버와 통신할 때, 데이터는 릴레이 달리기(Hop-by-Hop) 방식으로 전달됩니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;[ 컴퓨터 A (서브넷 A) ] ───► [ 라우터 R ] ───► [ 컴퓨터 B (서브넷 B) ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단계별 주소 변화 과정 분석&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1단계: 컴퓨터 A에서 출발할 때&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oDbVT/dJMcaiXM8PL/buD5rg6aM5kvUtSNBryxxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oDbVT/dJMcaiXM8PL/buD5rg6aM5kvUtSNBryxxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oDbVT/dJMcaiXM8PL/buD5rg6aM5kvUtSNBryxxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoDbVT%2FdJMcaiXM8PL%2FbuD5rg6aM5kvUtSNBryxxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1012&quot; height=&quot;323&quot; data-origin-width=&quot;1012&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴퓨터 A는 목적지 IP가 자신과 다른 서브넷에 있음을 인지하고, 데이터를 기본 게이트웨이(라우터 R)로 보내기로 결정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IP 헤더:&lt;/b&gt; 출발지 111.111.111.111 (A) ➡️ 목적지 222.222.222.222 (B)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이더넷 헤더 (MAC):&lt;/b&gt; 출발지 A의 MAC ➡️ 목적지 &lt;b&gt;라우터 R의 왼쪽 팔 MAC (E6-E9-00-17-BB-4B)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2단계: 라우터 R에서의 중계 및 포장지 교체&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cD4PSh/dJMcaaFzdjj/QvxAuvXtcFvC5XLvBZtOn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cD4PSh/dJMcaaFzdjj/QvxAuvXtcFvC5XLvBZtOn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cD4PSh/dJMcaaFzdjj/QvxAuvXtcFvC5XLvBZtOn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcD4PSh%2FdJMcaaFzdjj%2FQvxAuvXtcFvC5XLvBZtOn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1045&quot; height=&quot;499&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라우터 R은 왼쪽 인터페이스로 들어온 프레임을 열어 목적지 IP(B)를 확인한 뒤, 오른쪽 인터페이스(서브넷 B 방향)로 내보내기로 결정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IP 헤더:&lt;/b&gt; 출발지 111.111.111.111 (A) ➡️ 목적지 222.222.222.222 (B) &lt;b&gt;(변하지 않음)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이더넷 헤더 (MAC):&lt;/b&gt; 출발지 &lt;b&gt;라우터 R의 오른쪽 팔 MAC (1A-23-F9-CD-06-9B)&lt;/b&gt; ➡️ 목적지 &lt;b&gt;B의 MAC (49-BD-D2-C7-56-2A)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;참고: 이때 라우터 R은 서브넷 B에서 ARP를 활용하여 B의 MAC 주소를 조회하거나 획득합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3단계: 컴퓨터 B에서의 수신 및 해체&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhoLkb/dJMcadvottp/66W3X1lOemgllYO8otyyL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhoLkb/dJMcadvottp/66W3X1lOemgllYO8otyyL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhoLkb/dJMcadvottp/66W3X1lOemgllYO8otyyL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhoLkb%2FdJMcadvottp%2F66W3X1lOemgllYO8otyyL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1023&quot; height=&quot;485&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴퓨터 B는 들어온 프레임의 목적지 MAC 주소가 자신인 것을 확인하고 이더넷(Eth) 헤더를 폐기합니다.&lt;/li&gt;
&lt;li&gt;남은 IP 데이터그램을 상위 계층으로 올리며 목적지 IP가 자신(222.222.222.222)임을 최종 확인하고 데이터 수신을 완료합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 핵심 질문 및 예외 상황 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MAC 주소와 IP 주소는 항상 &lt;/b&gt;1:1&lt;b&gt;&amp;nbsp;대응인가요?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 원칙:&lt;/b&gt; 일반적인 단일 인터페이스 PC 환경에서는 특정 시점에 $1:1$로 대응되며, IP가 겹치면 IP 충돌이 발생합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예외 상황 (&lt;/b&gt;1:多&lt;b&gt;):&lt;/b&gt; 하나의 웹 서버 랜카드(MAC)에 여러 개의 웹사이트 운영을 위해 다중 IP 주소를 바인딩(IP Aliasing)할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예외 상황 (&lt;/b&gt;多:1&lt;b&gt;):&lt;/b&gt; 이중화 구성(로드 밸런싱/HA) 환경에서는 외부로 드러난 가상 IP 하나를 백엔드의 여러 대 장비(여러 MAC)가 나누어 처리하기도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>NETWORK</category>
      <category>CS</category>
      <category>Network</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/283</guid>
      <comments>https://ch010104.tistory.com/283#entry283comment</comments>
      <pubDate>Wed, 3 Jun 2026 16:01:26 +0900</pubDate>
    </item>
    <item>
      <title>[AI AGENT] AI Agent 성능 평가</title>
      <link>https://ch010104.tistory.com/282</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AI Agent 평가의 핵심 레이어 및 메트릭&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent의 평가는 크게 생성 단계(Generation Layer)와 행동 단계(Action Layer)의 두 가지 관점으로 접근합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;① 생성 단계 (Generation Layer) - RAG 기반 할루시네이션 검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Agent가 외부 데이터(DB, 문서, API)를 참조해 답변할 때, 할루시네이션을 잡아내기 위해 &lt;b&gt;LLM-as-a-Judge(더 똑똑한 LLM을 판사로 쓰는 방식)&lt;/b&gt; 기법을 활용한 3대 메트릭을 주로 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;충실성 (Faithfulness / Groundedness)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개념:&lt;/b&gt; Agent가 내놓은 답변이 참조한 컨텍스트(문서나 API 결과)에 실제로 기반하고 있는가?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;측정법:&lt;/b&gt; 답변의 문장들을 개별적 사실(Statements) 단위로 쪼갠 뒤, 판사 LLM에게 *&quot;이 문장이 컨텍스트로부터 유출 가능한가?&quot;*를 물어 예/아니오로 판별한 뒤 비율을 점수화합니다. 컨텍스트에 없는 내용을 지어냈다면(할루시네이션) 이 점수가 낮아집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;답변 부합성 (Answer Relevance)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개념:&lt;/b&gt; Agent가 질문의 의도에 맞는 답변을 했는가? (엉뚱한 삼천포로 빠지지 않았는가?)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;측정법:&lt;/b&gt; Agent의 답변을 바탕으로 판사 LLM에게 역으로 질문을 역생성하게 만든 뒤, 그렇게 만들어진 역생성 질문이 최초 유저의 질문과 의미론적으로 얼마나 유사한지 비교하여 측정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨텍스트 정밀도 (Context Precision)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개념:&lt;/b&gt; Agent가 필요한 정보를 정확하게 잘 검색(Retrieval)해 왔는가?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;측정법:&lt;/b&gt; 검색된 정보 조각들 중 진짜 답변에 도움을 준 유용한 정보가 상위에 랭크되어 있는지를 판사 LLM이 평가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② 행동 단계 (Action Layer) - Agent 특화 행동 평가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Agent는 단순히 답변만 하는 게 아니라 행동($Action$)을 취하므로, 할루시네이션이 행동의 오류로 이어지는지 검증해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;도구 호출 정확도 (Tool Call Correctness)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개념:&lt;/b&gt; Agent가 엉뚱한 API를 호출하거나, 존재하지 않는 인자($Argument$)를 지어내서 채워 넣지 않는지(이것도 일종의 행동 수준 할루시네이션입니다) 검증합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;태스크 완수율 (Task Completion Rate)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개념:&lt;/b&gt; 여러 차례의 턴(Turn)을 거친 뒤 최종 목적지에 올바르게 잘 도달했는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;궤적 및 비용 효율성 (Trajectory &amp;amp; Step Efficiency)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개념:&lt;/b&gt; 목표를 달성하기 위해 무한 루프에 빠지거나 쓸데없이 20~30턴씩 돌지 않고, 최적의 단계(예: 3~5턴 이내)로 해결했는지 측정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 평가를 자동화하는 파이프라인 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Agent 성능 평가는 사람이 일일이 검사할 수 없기 때문에, 개발 단계(CI/CD)에서 '골든 데이터셋(Golden Dataset)'을 기반으로 자동 평가 파이프라인을 구축하여 성능 저하를 방지합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[테스트 입력 (User Query)]
       │
       ▼
 ┌───────────┐      [도구/DB 조회]      ┌────────────────┐
 │ AI Agent  │ ──────────────────────&amp;gt; │ Context / Tool │
 └───────────┘                         └────────────────┘
       │                                       │
       ├───────────────────────────────────────┘
       ▼ (중간 과정 Trace 및 최종 결과 산출)
 ┌──────────────────────────────────────────────────────┐
 │               LLM-as-a-Judge 평가                    │
 │  - Faithfulness (컨텍스트 기반 유추 여부)            │
 │  - Tool Accuracy (올바른 인자 사용 여부)             │
 └──────────────────────────────────────────────────────┘
       │
       ▼
 [Pass / Fail 결과 리포트 및 대시보드 시각화]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;골든 데이터셋 구축:&lt;/b&gt; 질문, 가이드라인(원하는 컨텍스트), 그리고 이상적인 정답 예시를 모아둔 100~200개의 평가 데이터셋을 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 실행:&lt;/b&gt; 코드가 수정될 때마다 Agent에게 이 질문들을 대량으로 던져 일제히 테스트를 수동/자동으로 기동합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트레이싱(Tracing):&lt;/b&gt; Agent가 어떤 생각을 하고 어떤 도구를 썼는지 내부 실행 흐름의 모든 과정을 추적 기록합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;판사 LLM 검사:&lt;/b&gt; 판사 LLM(주로 GPT-4o나 Claude 3.5 Sonnet 같은 고성능 모델)이 정교한 프롬프트 수식을 기반으로 점수(0.0 ~ 1.0)를 산출해 리포트를 발행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 실무에서 사용하는 3대 테스트 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 날씨 API, 예약 시스템 등 &lt;b&gt;실시간 데이터&lt;/b&gt;에 의존하는 AI Agent를 평가하기 위해 활용하는 전략들의 특성 비교입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 171px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 11px;&quot;&gt;
&lt;td style=&quot;height: 11px;&quot;&gt;&lt;b&gt;&lt;u&gt;전략&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 11px;&quot;&gt;&lt;b&gt;&lt;u&gt;핵심 동작 방식 &lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 11px;&quot;&gt;&lt;b&gt;&lt;u&gt;장점&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 11px;&quot;&gt;&lt;b&gt;&lt;u&gt;단점&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 11px;&quot;&gt;&lt;b&gt;&lt;u&gt;주요 활용 타이밍&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;전략 1: Tool Assertions&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;에이전트가 어떤 인자로 함수를 호출했는지만 확인&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;실시간 결과 데이터가 바뀌어도 강건하게 작동함&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;최종 답변의 문맥적 할루시네이션은 잡지 못함&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;API 호출 구조 유닛 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;&lt;b&gt;전략 2: Referenceless Eval&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;Expected Output 없이 Context와 Actual Output만 대조&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;정답 데이터셋이 불필요하며 실시간 모니터링 가능&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;가벼운 LLM-as-a-Judge를 쓰면 채점 신뢰도 떨어짐&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;프로덕션(운영) 환경 실시간 모니터링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;&lt;b&gt;전략 3: Mocking &amp;amp; Gold Set&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;외부 호출을 Mocking하여 항상 일정한 테스트 환경 구축&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;멱등성이 보장되어 CI/CD 파이프라인에서 완벽히 통제 가능&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;가짜 응답(Mock) 구조를 선언하고 유지보수해야 함&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;배포 전 회귀(Regression) 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 현업에서 주로 쓰는 에이전트 평가 툴 (Frameworks)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 AI 엔지니어들이 에이전틱 시스템의 할루시네이션과 에러를 예방하기 위해 사용하는 대표적인 도구들입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;① DeepEval &amp;amp; RAGAS (코드 기반 자동화 평가)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RAGAS (RAG Assessment):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초점:&lt;/b&gt; RAG 파이프라인 및 문서 활용 퀄리티 평가에 특화.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점:&lt;/b&gt; 학술적이고 정교한 수식 기반 메트릭 제공. 데이터프레임 단위의 실험에 최적화.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DeepEval:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초점:&lt;/b&gt; LLM 애플리케이션 및 AI 에이전트 종합 테스트 프레임워크.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점:&lt;/b&gt; pytest 스타일 인터페이스 제공, 에이전트용 행동 평가 메트릭 지원. CI/CD 자동화 구축에 압도적으로 편리함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② Promptfoo (프롬프트 A/B 테스트 및 보안 진단)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초점:&lt;/b&gt; 대규모 프롬프트 비교 실험 및 취약점 진단(Red-teaming).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점:&lt;/b&gt; 수많은 프롬프트 후보군과 모델 버전별 결과를 격자형(Matrix) UI 리포트로 한눈에 비교할 수 있어 최적의 프롬프트 튜닝에 특화.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;③ LangSmith &amp;amp; Langfuse (실행 과정 로깅 및 추적)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초점:&lt;/b&gt; Agent가 백그라운드에서 실행된 중간 궤적(Trace)을 시각적으로 투명하게 모니터링하는 플랫폼.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점:&lt;/b&gt; &quot;에이전트가 왜 이 시점에 이런 환각을 유발했는지&quot;, &quot;API 호출 속도가 왜 이렇게 느려졌는지&quot; 타임라인 별로 병목 구간을 상세히 파고들 수 있어 운영 환경 필수 도입 도구로 평가받음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. DeepEval 기반의 실제 평가 코드 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DeepEval을 활용하여 전략 3(Mocking)과 전략 1(Tool 검증) 및 전략 2(할루시네이션 검증)를 통합 적용한 실제 파이썬 구현체 예시입니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os
import unittest
from unittest.mock import patch

# 1. DeepEval 모듈 임포트
from deepeval import evaluate
from deepeval.test_case import LLMTestCase
from deepeval.metrics import HallucinationMetric, ToolCorrectnessMetric

# OpenAI API 키가 환경변수로 등록되어 있어야 판사 LLM이 평가를 수행할 수 있습니다.
# os.environ[&quot;OPENAI_API_KEY&quot;] = &quot;YOUR_OPENAI_API_KEY&quot;

# ==========================================
# [Target AI System] 대상 에이전트 코드 (시뮬레이션)
# ==========================================
class WeatherAgent:
    &quot;&quot;&quot;사용자 질문에 따라 날씨 API를 호출하고 응답을 재가공하는 단순 에이전트&quot;&quot;&quot;
    def __init__(self, api_client):
        self.api_client = api_client

    def run(self, user_query: str) -&amp;gt; dict:
        # 실제 환경에서는 LLM이 Tool Calling을 판정하지만,
        # 여기서는 LLM이 get_weather API를 호출하기로 결심하고 인자를 추출한 상황을 가정합니다.

        # 1. Tool Call Argument 추출 (예시: 서울, 내일)
        location = &quot;Seoul&quot;
        date = &quot;tomorrow&quot;

        # 2. 외부 API 호출 (우리는 테스트 환경에서 이 client를 Mocking할 것입니다)
        api_response = self.api_client.get_weather(location=location, date=date)

        # 3. API 결과를 종합하여 최종 답변 작성 (할루시네이션 시뮬레이션을 위해 일부 오류를 포함함)
        # 실제 API 결과에는 최고기온이 25도라고 되어있으나, 32도라고 답변을 왜곡함 (환각)
        actual_output = f&quot;내일 서울 날씨는 매우 덥겠으며, 최고 기온은 32도까지 치솟을 예정입니다.&quot;

        return {
            &quot;actual_output&quot;: actual_output,
            &quot;tool_called&quot;: &quot;get_weather&quot;,
            &quot;tool_arguments&quot;: {&quot;location&quot;: location, &quot;date&quot;: date},
            &quot;retrieved_context&quot;: [str(api_response)]  # 평가를 위해 로깅된 컨텍스트
        }

# ==========================================
# [Test Suite] DeepEval + Mocking 기반 평가
# ==========================================
class TestWeatherAgent(unittest.TestCase):

    @patch('unittest.mock.MagicMock') # 외부 API 호출 클라이언트를 격리하기 위한 Mock 선언
    def test_weather_agent_evaluation(self, mock_api_client):
        # 1. Mocking 응답 설정 (전략 3: 고정된 Mock 데이터 제공)
        # 외부 날씨가 실제로 몇 도이든 관계없이 API는 고정 값을 리턴합니다.
        mock_api_client.get_weather.return_value = {
            &quot;status&quot;: &quot;success&quot;,
            &quot;temp&quot;: 25,
            &quot;condition&quot;: &quot;mildly cloudy&quot;,
            &quot;warning&quot;: &quot;none&quot;
        }

        # 2. 에이전트 실행 및 추적 데이터 수집
        agent = WeatherAgent(api_client=mock_api_client)
        user_query = &quot;내일 서울 날씨 어때?&quot;

        agent_result = agent.run(user_query)

        # 3. DeepEval TestCase 구성
        # 수집된 정보들을 DeepEval 평가 데이터 객체로 포장합니다.
        test_case = LLMTestCase(
            input=user_query,
            actual_output=agent_result[&quot;actual_output&quot;],
            expected_output=&quot;내일 서울은 25도이며 대체로 온화합니다.&quot;, # 골든 데이터셋 기반 이상적 정답
            retrieval_context=agent_result[&quot;retrieved_context&quot;]   # 에이전트가 툴로부터 획득한 진짜 로우 데이터
        )

        # 4. 평가 메트릭 정의 (LLM-as-a-Judge 설정)
        # 가공된 실제 답변(32도)과 취득한 컨텍스트(25도) 대조를 통해 환각을 검사합니다.
        hallucination_metric = HallucinationMetric(threshold=0.5, model=&quot;gpt-4o&quot;)

        # 에이전트가 지정된 스펙대로 Tool을 알맞게 호출했는지 검증합니다.
        tool_correctness_metric = ToolCorrectnessMetric(
            threshold=0.7,
            model=&quot;gpt-4o&quot;,
            should_mock=False # 이미 앞에서 unittest.mock으로 처리했으므로 False
        )

        # 5. 실행 및 리포팅
        # 해당 테스트 케이스를 명시된 지표에 맞춰 채점합니다.
        results = evaluate(
            test_cases=[test_case],
            metrics=[hallucination_metric, tool_correctness_metric]
        )

        # 6. 검증 성공 여부 체크 (Optional)
        # 평가 점수가 정해진 임계치(Threshold)를 넘었는지 테스트 프레임워크 수준에서 단언(Assert)합니다.
        for result in results:
            self.assertTrue(result.success, f&quot;평가 지표 미달: {result.metric} - 점수: {result.score}&quot;)

if __name__ == &quot;__main__&quot;:
    unittest.main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 현업 도입 시 베스트 프랙티스 로드맵&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트 데이터셋 확보 (Synthesizer 활용):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직접 100개의 골든 세트를 손수 짜기 어렵다면, Ragas나 DeepEval의 Synthesizer 패키지를 사용하여 보유 중인 서비스 시나리오 텍스트 문서로부터 대량의 '질문-API 응답 모델-정답 답변'을 자동 생성해 테스트 세트를 초기에 선구축합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI/CD 파이프라인 통합:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 unittest 또는 pytest 형태로 만들어진 평가 코드를 깃허브 액션(GitHub Actions)과 연동합니다.&lt;/li&gt;
&lt;li&gt;개발자가 에이전트의 시스템 프롬프트를 교체하고 Push할 때마다, 50개의 골든 케이스에 대해 전체 평가 프로세스를 자동 가동하여 Hallucination 점수가 상승하거나 Tool Correctness가 하락하면 병합(Merge)을 차단하는 보호 장치로 활용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로덕션 런타임 모니터링 및 로깅:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유저의 모든 대화 흐름을 론칭 이후 실시간으로 수집하고, LangSmith 혹은 Langfuse와 같은 가시성(Observability) 도구를 연결하여 Agent가 실제로 무한 루프에 돌지 않는지, 응답 속도가 늘어나지 않는지 실시간 모니터링 대시보드를 연계 구축합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>AI(MCP)</category>
      <category>ai agent</category>
      <category>spring boot</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/282</guid>
      <comments>https://ch010104.tistory.com/282#entry282comment</comments>
      <pubDate>Wed, 3 Jun 2026 11:52:40 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] 링크 계층(CRC)과 다중 접속 프로토콜</title>
      <link>https://ch010104.tistory.com/281</link>
      <description>&lt;h2 data-pm-slice=&quot;1 5 []&quot; data-ke-size=&quot;size26&quot;&gt;1. 링크 계층의 개요 및 역할 (Introduction &amp;amp; Context)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 기본 용어 정리 (Terminology)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;노드 (Nodes):&lt;/b&gt; 네트워크에 연결되어 전송을 수행하는 호스트(PC, 노트북, 스마트폰 등)와 라우터(Routers).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;링크 (Links):&lt;/b&gt; 데이터 이동 경로 상에서 서로 인접한 노드들을 물리적으로 연결하는 통신 채널.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유선 링크 (Wired): 광케이블, 구리 동축 케이블 등.&lt;/li&gt;
&lt;li&gt;무선 링크 (Wireless): Wi-Fi, LTE/5G 대역 등.&lt;/li&gt;
&lt;li&gt;LAN (Local Area Network): 로컬 단위로 묶인 통신망.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프레임 (Frame):&lt;/b&gt; 링크 계층의 데이터 전송 단위. 3계층의 &lt;b&gt;데이터그램(Datagram)을 캡슐화&lt;/b&gt;하여 전송용 헤더와 트레일러를 붙인 형태.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 네트워크 계층 vs 링크 계층 (교통수단 비유)&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt;교통 시스템 비유&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt;네트워크 개념&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt;설명&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;관광객 (Tourist)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;데이터그램 (Datagram)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;이동하는 궁극적인 목적을 가진 알맹이 데이터&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;개별 이동 구간 (Transport segment)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;통신 링크 (Communication link)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;목적지까지 가기 위해 거쳐야 하는 각 도로/경로 구간&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;이동 수단 (Transportation mode)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;링크 계층 프로토콜 (Link-layer protocol)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;WiFi, 이더넷 등 각 구간 특성에 맞는 개별 전송 규약&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;여행사/가이드 (Travel agent)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;라우팅 알고리즘 (Routing algorithm)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;전체 여정(출발지부터 목적지까지)의 최적 경로를 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;핵심 메시지:&lt;/b&gt; 3계층(네트워크)이 출발지부터 목적지까지의 전체 경로(End-to-End)를 책임진다면, 2계층(링크)은 물리적으로 바로 맞닿아 있는 인접 노드(Hop-by-Hop)로 데이터를 안전하게 건네주는 구체적인 수송 책임을 집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 링크 계층의 주요 서비스 및 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 제공 서비스 (Link Layer Services)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;프레이밍 및 링크 접근 (Framing, Link Access):&lt;/b&gt; 데이터그램에 헤더와 트레일러를 추가하여 프레임을 구성합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프레임 헤더에는 장치 고유의 물리 주소인 MAC 주소(MAC Address)가 들어갑니다. (IP 주소와 무관한 하드웨어 고유 식별자)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인접 노드 간 신뢰성 전송 (Reliable Delivery):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;송수신 노드 간에 손실이나 오류 없이 데이터를 전달하는 기능입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유선 링크:&lt;/b&gt; 에러율이 극도로 낮으므로 불필요한 오버헤드를 막기 위해 링크 계층 수준의 신뢰성 전송을 &lt;b&gt;거의 사용하지 않음 (Seldom used)&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무선 링크:&lt;/b&gt; 간섭과 신호 감쇄로 에러 발생률이 매우 높으므로, 상위 계층(TCP)까지 깨진 패킷을 보내 태평양을 왕복하는 재전송 딜레이를 유발하지 않고 링크 레벨에서 즉시 에러를 감지해 재전송(Local Recovery)함으로써 효율성을 끌어올림.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;흐름 제어 (Flow Control):&lt;/b&gt; 인접한 송신 노드가 수신 노드의 처리 속도(버퍼 크기)보다 빠르게 데이터를 들이붓지 못하도록 속도를 조절하는 페이싱(Pacing) 기능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 검출 및 정정 (Error Detection &amp;amp; Correction):&lt;/b&gt; 신호 감쇄나 잡음으로 발생한 비트 에러를 처리하는 기술.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이중화 방식 제어 (Half-duplex / Full-duplex):&lt;/b&gt; * 반이중(Half-duplex): 양방향 송수신이 가능하지만 동시에 통신할 수는 없는 방식 (예: 무전기, 옛날 와이파이).
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전이중(Full-duplex): 충돌 없이 양방향에서 동시에 송수신이 가능한 방식 (예: 스위치 기반 이더넷).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 링크 계층의 구현 위치 (Implementation)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;구현 하드웨어:&lt;/b&gt; 링크 계층은 소프트웨어 중심의 상위 계층과 달리 주로 &lt;b&gt;네트워크 인터페이스 카드(NIC, 랜카드)&lt;/b&gt; 또는 메인보드 내 &lt;b&gt;통신 칩셋&lt;/b&gt;에 구현됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하이브리드 결합체:&lt;/b&gt; 비트를 하드웨어 신호로 쏘는 물리적 기능은 하드웨어(Hardware)가 담당.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;링크 접근 알고리즘과 제어 로직은 NIC의 펌웨어(Firmware)가 처리.&lt;/li&gt;
&lt;li&gt;운영체제(OS)의 커널에 탑재된 &lt;b&gt;네트워크 카드 드라이버 소프트웨어&lt;/b&gt;가 상위 IP 계층과의 다리 역할을 수행.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 에러 검출 및 정정 기술 (Error Detection &amp;amp; Correction)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 기본 논리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZYRUA/dJMcabqTw78/sri96pkiZayBE3Yy7tlVu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZYRUA/dJMcabqTw78/sri96pkiZayBE3Yy7tlVu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZYRUA/dJMcabqTw78/sri96pkiZayBE3Yy7tlVu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZYRUA%2FdJMcabqTw78%2Fsri96pkiZayBE3Yy7tlVu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;390&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;송신 측은 원본 데이터 D에 특정 알고리즘을 수행하여 구한 오류 검출용 잉여 비트 EDC (Error Detection &amp;amp; Correction)를 덧붙여 전송합니다.&lt;/li&gt;
&lt;li&gt;수신 측은 받은 데이터 D'를 기반으로 동일 알고리즘을 돌려 계산한 값과 받은 EDC'를 교차 비교합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주의점:&lt;/b&gt; 에러 검출 기술은 &lt;b&gt;100% 완벽할 수 없습니다.&lt;/b&gt; 극히 희박한 확률로 데이터는 깨졌는데 수신 알고리즘 계산 결과는 정상인 오류 필터링 실패(Missed error)가 생길 수 있습니다. (EDC 필드가 클수록 정밀도가 향상되나, 전송 오버헤드가 증가하는 트레이드오프 발생)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 패리티 검사 (Parity Checking)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 검출 기법으로, 비트 스트림 뒤에 1비트의 체크용 공간을 추가하는 방식입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;① 단일 비트 패리티 (Single Bit Parity)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;348&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cK2EUg/dJMcaicvPZG/RlQOsN6KJwSRij6SaST6Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cK2EUg/dJMcaicvPZG/RlQOsN6KJwSRij6SaST6Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cK2EUg/dJMcaicvPZG/RlQOsN6KJwSRij6SaST6Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcK2EUg%2FdJMcaicvPZG%2FRlQOsN6KJwSRij6SaST6Qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;348&quot; height=&quot;318&quot; data-origin-width=&quot;348&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;짝수 패리티 (Even Parity) 기준:&lt;/b&gt; 데이터 비트들과 패리티 비트 내부의 1의 총 개수가 &lt;b&gt;짝수&lt;/b&gt;가 되도록 패리티를 0 또는 1로 결정합니다.&lt;/li&gt;
&lt;li&gt;예시: 데이터가 0111000110101011 일 때, 원래 1이 9개(홀수)이므로 패리티 비트를 1로 두어 짝수 개를 맞춰 보냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한계:&lt;/b&gt; 오직 &lt;b&gt;1비트 에러만 감지&lt;/b&gt;할 수 있습니다. 만약 전송 중 절묘하게 &lt;b&gt;2개의 비트가 동시에 깨지면&lt;/b&gt; 전체 1의 개수 짝수 성질이 깨지지 않아 에러 감지에 &lt;b&gt;완전히 실패&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;② 2차원 비트 패리티 (Two-dimensional Bit Parity)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c50SEu/dJMcaijeaik/h3m24MDf0R0DyXL8hXC6M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c50SEu/dJMcaijeaik/h3m24MDf0R0DyXL8hXC6M1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c50SEu/dJMcaijeaik/h3m24MDf0R0DyXL8hXC6M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc50SEu%2FdJMcaijeaik%2Fh3m24MDf0R0DyXL8hXC6M1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;772&quot; height=&quot;473&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;473&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 가로 i행, 세로 j열의 매트릭스 형태로 정렬하고 가로행 끝(row parity)과 세로열 끝(column parity)에 각각 패리티 비트를 부여합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;강력한 장점:&lt;/b&gt; &lt;b&gt;1비트 에러에 대한 자동 정정(Correction) 가능!&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 비트가 하나 깨지면, 그 비트가 소속된 행의 패리티와 열의 패리티에 동시에 에러가 찍힙니다.&lt;/li&gt;
&lt;li&gt;가로 에러선과 세로 에러선이 만나는 교차점(Intersection)의 비트를 즉시 찾아내어 반대 값($0 \to 1$ 또는 $1 \to 0$)으로 뒤집어서 &lt;b&gt;재전송 요청 없이 그 자리에서 자가 치료&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 인터넷 체크섬 (Internet Checksum) - 비교 학습용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용 레이어:&lt;/b&gt; 주로 &lt;b&gt;3, 4계층(IP, TCP/UDP)&lt;/b&gt; 소프트웨어 단에서 계산합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;계산 방식:&lt;/b&gt; 데이터를 16비트 단위의 정수로 정렬하여 모두 더한 뒤, 캐리(자리올림)를 처리하고 1의 보수를 취해 체크섬 값을 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징:&lt;/b&gt; 소프트웨어 구현이 빠르고 가벼워 CPU 부담을 덜지만, &lt;b&gt;에러 정정은 불가능&lt;/b&gt;하고 감지만 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 순환 중복 검사 (CRC, Cyclic Redundancy Check)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LFRwI/dJMcagMwVLW/QuOyRnUFPZ4H76qXUE3f8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LFRwI/dJMcagMwVLW/QuOyRnUFPZ4H76qXUE3f8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LFRwI/dJMcagMwVLW/QuOyRnUFPZ4H76qXUE3f8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLFRwI%2FdJMcagMwVLW%2FQuOyRnUFPZ4H76qXUE3f8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;838&quot; height=&quot;200&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 유선 이더넷과 802.11 Wi-Fi 표준에서 사용하는 극도로 강력한 다항식 기반 에러 감지 방식입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;① 수학적 원리 및 수식&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주어진 값:&lt;/b&gt; 원본 데이터 D (이진수), 미리 약속한 생성 다항식 패턴 G (r+1 비트 크기).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목표:&lt;/b&gt; 데이터 D의 뒤에 r비트 크기의 나머지 값 R을 붙인 전체 프레임 &amp;lt; D,R &amp;gt;이 G로 나누어떨어지게 만드는 것 (Modulo-2 연산 기반).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; D,R &amp;gt; = D * 2^r&amp;nbsp; XOR&amp;nbsp; R&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;② Modulo-2 산술 규칙 (핵심)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Modulo-2 연산은 덧셈과 뺄셈을 내림(Borrow)이나 올림(Carry) 없이 오직 XOR(배타적 논리합)로만 연산합니다.&lt;/li&gt;
&lt;li&gt;즉, 두 비트가 같으면 0, 다르면 1이 됩니다. (예: 1 XOR 1 = 0, 1 XOR 0 = 1, 0 XOR 0 = 0)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;③ 연산 예시 및 가이드 (D = 101110, G = 1001, r = 3)&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;G가 4비트(r+1)이므로, r=3입니다.&lt;/li&gt;
&lt;li&gt;데이터 D 뒤에 r=3개만큼 0을 붙여 자릿수를 늘립니다: D * 2^r = 101110000&lt;/li&gt;
&lt;li&gt;이 값을 G=1001로 Modulo-2 나누기 연산을 수행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;            1 0 1 0 1 1  &amp;lt;-- 몫 (중요하지 않음)
       ----------------
1 0 0 1 | 1 0 1 1 1 0 0 0 0
          1 0 0 1
          -------
          0 0 1 0 1  &amp;lt;-- 1-1=0, 0-0=0, 1-0=1, 1-1=0 (XOR 연산 수행)
            0 0 0 0  &amp;lt;-- 앞자리가 0이므로 0000으로 연산
            -------
              1 0 1 0
              1 0 0 1
              -------
              0 0 1 1 0
                0 0 0 0
                -------
                1 1 0 0
                1 0 0 1
                -------
                0 1 0 1 0
                  1 0 0 1
                  -------
                  0 0 1 1  &amp;lt;-- 최종 나머지 3자리 (R = 011)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;결과 나머지 (&lt;/b&gt;R&lt;b&gt;):&lt;/b&gt; 011&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최종 송신 데이터 &lt;/b&gt;(&amp;lt; D,R &amp;gt;)&lt;b&gt;:&lt;/b&gt; 원래 데이터 뒤에 R을 붙인 &lt;b&gt;101110011&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;감지 방식:&lt;/b&gt; 수신 측은 101110011을 그대로 받아 G=1001로 나누었을 때 나머지가 정확히 000이 나오면 정상 수신으로 인정하고, 이외의 값이 나오면 오류 감지(Error Detected)로 취급해 드롭합니다. (에러 위치 특정 및 정정은 불가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 다중 접속 프로토콜 (Multiple Access Protocols)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 브로드캐스트 전송 매체(Shared line, Wireless air 등)를 여러 노드가 공유할 때, 신호가 겹쳐서 데이터가 폭파되는 충돌(Collision)을 통제하기 위해 설계된 분산 약속 시스템입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 이상적인 다중 접근 프로토콜의 4가지 요구조건 (Desiderata)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 대역폭 속도가 R bps인 채널 환경에서 바라는 이상향:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Full Rate 보장:&lt;/b&gt; 통신하는 노드가 단 &lt;b&gt;1개&lt;/b&gt;뿐일 때는 그 노드가 최대 전송 속도 R을 통째로 누릴 수 있어야 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fair Share 공평성:&lt;/b&gt; 통신하는 노드가 M&lt;b&gt;개&lt;/b&gt;로 늘어나면, 각 노드는 평균적으로 공평하게 R/M의 안정적인 속도를 가질 수 있어야 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;완전한 분산 제어 (Fully Decentralized):&lt;/b&gt; 전체 조율을 지휘하는 값비싼 중앙 통제 서버가 없어야 하며, 모든 노드 간의 클록(시간대) 동기화 요구가 없어야 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단순함 (Simple):&lt;/b&gt; 구조와 하드웨어 연산 비용이 단순해야 함.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) MAC 프로토콜 3대 카테고리 (Taxonomy)&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 핵심 프로토콜 상세 동작 메커니즘&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 채널 분할 방식: TDMA (Time Division Multiple Access)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9p8qJ/dJMcaffMGCJ/AwBzJHwssfKKUeer4ptPt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9p8qJ/dJMcaffMGCJ/AwBzJHwssfKKUeer4ptPt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9p8qJ/dJMcaffMGCJ/AwBzJHwssfKKUeer4ptPt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9p8qJ%2FdJMcaffMGCJ%2FAwBzJHwssfKKUeer4ptPt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;736&quot; height=&quot;124&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동작:&lt;/b&gt; 시간을 주기적인 '라운드'로 쪼개고, 각 라운드 내에 노드별로 전용 고정 시간 슬롯(Fixed-length slot)을 지정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장단점:&lt;/b&gt; * 장점: 자기 지정 시간대에는 혼자만 말하므로 &lt;b&gt;충돌이 절대 안 남.&lt;/b&gt; * 단점: 나 혼자 전송 중이어도 내 할당 슬롯 외에는 쓰지 못하고 노는 슬롯들(&lt;b&gt;Idle slots&lt;/b&gt;)을 보며 낭비해야 하므로 최대 속도(R$) 활용에 실패함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 랜덤 접속 방식: CSMA (Carrier Sense Multiple Access)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coGZA8/dJMcai4xkaL/xzfFhdMlRW5xQukqtIS8z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coGZA8/dJMcai4xkaL/xzfFhdMlRW5xQukqtIS8z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coGZA8/dJMcai4xkaL/xzfFhdMlRW5xQukqtIS8z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoGZA8%2FdJMcai4xkaL%2FxzfFhdMlRW5xQukqtIS8z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;487&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개념:&lt;/b&gt; &quot;말하기 전에 조용히 귀 기울여 들어보고(Carrier Sensing) 들어가자.&quot;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Idle 상태 감지 시: 전체 프레임 즉시 전송 시작.&lt;/li&gt;
&lt;li&gt;Busy 상태 감지 시: 끼어들지 않고 전송을 연기(Defer)한 후 대기.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  귀를 기울이는데도 충돌이 발생하는 이유: 전파 지연 (Propagation Delay)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신호가 전선이나 무선 공간을 지나 반대쪽 끝 노드까지 전달되는 데는 미세한 전파 물리적 지연 시차(t_prop)가 걸립니다.&lt;/li&gt;
&lt;li&gt;왼쪽 끝 노드가 t_0에 전송을 시작했으나 이 신호가 오른쪽 끝 노드에 닿기 직전 시점인 t_1에 오른쪽 끝 노드가 채널을 들어보면 조용한 것으로 인식(Idle 착각)합니다.&lt;/li&gt;
&lt;li&gt;결국 오른쪽 노드도 데이터를 동시에 쏘아 보내며 선로 중간에서 신호가 겹치는 충돌이 발생합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일반 CSMA의 비효율:&lt;/b&gt; 충돌을 감지하지 못해 데이터가 이미 도중에 다 뭉개졌음에도 불구하고 패킷이 끝날 때까지 꿋꿋이 붙잡고 전송을 진행하므로 심각한 채널 대역폭 낭비가 발생합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 랜덤 접속 방식: CSMA/CD (Collision Detection)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원리:&lt;/b&gt; 데이터를 송신하면서 동시에 귀를 계속 열어두어 &lt;b&gt;전송 중 충돌 발생 여부를 상시 감시&lt;/b&gt;하는 유선 이더넷의 표준 방식.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작동 규칙:&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터를 보내다가 전압 등의 변화로 충돌을 감지하면 즉시 전송을 중단(Abort)하여 채널 낭비를 차단합니다.&lt;/li&gt;
&lt;li&gt;수신 노드 및 전체 네트워크 노드들에게 충돌이 났음을 확산 전파하기 위한 경고용 잼 신호(Jam Signal)를 보냅니다.&lt;/li&gt;
&lt;li&gt;재전송 타이밍이 겹치지 않도록 &lt;b&gt;이진 지수 백오프(Binary Exponential Backoff)&lt;/b&gt; 알고리즘 영역으로 들어갑니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  이진 지수 백오프 (Binary Exponential Backoff)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;m번째 연쇄 충돌 발생 시, 노드는 다음 범위의 정수 집합에서 무작위로 K를 하나 뽑습니다.&lt;/li&gt;
&lt;li&gt;대기 시간은 K * 512&lt;b&gt;&amp;nbsp;비트 시간&lt;/b&gt;만큼 기다립니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Q. 왜 최댓값(&lt;/b&gt;2^m-1&lt;b&gt;)을 안 쓰고 &lt;/b&gt;0&lt;b&gt;부터 전체 범위 중 무작위(Random)로 뽑나요?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;A:&lt;/b&gt; 연쇄 충돌이 났을 때 두 노드가 똑같이 최댓값을 고르면, 대기가 끝나는 시점이 마이크로초 단위까지 완벽하게 동일해져서 &lt;b&gt;재전송하는 순간 100% 또 부딪치기 때문&lt;/b&gt;입니다. 무작위 K 선택을 통해 서로 대기하는 시간 차이를 벌려놓아(타이밍 엇갈림) 먼저 대기를 끝낸 녀석이 채널을 점유하고 나가도록 유도하는 것이 이 알고리즘의 본질입니다.&lt;/li&gt;
&lt;li&gt;충돌 횟수(m)가 거듭될수록 뽑기 집합의 크기를 지수적(2^m)으로 확장하여 동시 다발적 접속 시 번호가 겹칠 충돌 확률을 무력화시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  CSMA/CD 효율성 공식 (Efficiency)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q4GaT/dJMcagMwVPk/HgX6mnAETLPp5WtUhOGHxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q4GaT/dJMcagMwVPk/HgX6mnAETLPp5WtUhOGHxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q4GaT/dJMcagMwVPk/HgX6mnAETLPp5WtUhOGHxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq4GaT%2FdJMcagMwVPk%2FHgX6mnAETLPp5WtUhOGHxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;440&quot; height=&quot;119&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;119&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;효율성이 1(100%)로 수렴하는 극한 환경:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;t_prop = 0&lt;b&gt;&amp;nbsp;일 때:&lt;/b&gt; 노드 간 거리가 매우 가까워 전파 시차가 없으므로 눈치싸움 실패로 인한 충돌 자체가 원천 제거되어 성능 극대화.&lt;/li&gt;
&lt;li&gt;t_trans = infty&lt;b&gt;&amp;nbsp;일 때:&lt;/b&gt; 전송 데이터(프레임 크기)가 매우 길면 초기 극초반의 위험 구간만 충돌 없이 넘기면 채널을 독점하여 계속 밀어 넣을 수 있어 효율성 극대화.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 랜덤 접속 방식: CSMA/CA (Collision Avoidance)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMLaXF/dJMcaftgoRS/vgkeEk4Ao1H1KuGzmILYQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMLaXF/dJMcaftgoRS/vgkeEk4Ao1H1KuGzmILYQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMLaXF/dJMcaftgoRS/vgkeEk4Ao1H1KuGzmILYQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMLaXF%2FdJMcaftgoRS%2FvgkeEk4Ao1H1KuGzmILYQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;398&quot; height=&quot;476&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용 환경:&lt;/b&gt; 무선 와이파이(Wi-Fi, 802.11) 대역. 무선 특성상 자기 송신 출력이 수신 신호를 압도하므로 물리적으로 충돌 감지(CD)를 사용할 수 없어 탄생한 &lt;b&gt;충돌 회피(Avoidance)형&lt;/b&gt; 규약.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 타임라인 변수:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DIFS (Distributed Inter-Frame Space):&lt;/b&gt; 데이터를 전송하기 전, 채널이 완전히 비어 있는지 눈치를 보며 강제 대기해야 하는 긴 안심 기준 시간.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SIFS (Short Inter-Frame Space):&lt;/b&gt; 데이터 수신 직후 수신 확인 신호인 ACK를 쏘기 전 잠깐 숨을 고르는 가장 짧은 간격 시간. (SIFS가 DIFS보다 압도적으로 짧기 때문에 다른 컴퓨터가 끼어들 틈을 주지 않고 수신 측이 즉시 안전하게 ACK 신호를 날릴 수 있도록 권한을 보장함.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ACK (Acknowledgement):&lt;/b&gt; 무선은 충돌 감지가 불가능하므로, 수신 측이 정상 수신 시 무조건 ACK를 송신 측에 답장해 줌으로써 배달 신뢰성을 보장함. ACK를 못 받으면 무조건 에러(충돌)로 치부하고 처음부터 백오프 대기 후 다시 전송.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5) 차례 돌리기 방식 (Taking Turns)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;등장 배경:&lt;/b&gt; * 채널 분할은 저부하(Low load) 시 노는 자원이 많아 성능 낭비가 심함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;랜덤 접속은 고부하(High load) 시 충돌로 인한 성능 붕괴가 심함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목표:&lt;/b&gt; &quot;사람이 적을 땐 혼자 다 쓰고, 사람 많을 땐 순서를 강제하자!&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;① 폴링 (Polling)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pe6rO/dJMcafNDfIg/qeuhTM9hA9rnQ7x5idKYXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pe6rO/dJMcafNDfIg/qeuhTM9hA9rnQ7x5idKYXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pe6rO/dJMcafNDfIg/qeuhTM9hA9rnQ7x5idKYXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpe6rO%2FdJMcafNDfIg%2FqeuhTM9hA9rnQ7x5idKYXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;452&quot; height=&quot;360&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원리:&lt;/b&gt; 하나의 마스터(Master) 노드가 슬레이브(Slaves)들에게 순서대로 &quot;너 보낼 거 있어?&quot; 라고 물어보는 poll 신호를 던지며 제어.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한계:&lt;/b&gt; * 수시로 물어보고 대답하는 과정에서 대역폭을 소모하는 &lt;b&gt;폴링 오버헤드(Polling overhead)&lt;/b&gt; 발생.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;차례가 올 때까지의 무조건적인 &lt;b&gt;대기 딜레이(Latency)&lt;/b&gt; 발생.&lt;/li&gt;
&lt;li&gt;마스터가 고장 나면 전체 통신이 단번에 마비되는 &lt;b&gt;단일 장애점(SPOF, Single Point of Failure)&lt;/b&gt; 리스크 존재.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;② 토큰 패싱 (Token Passing)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccupUZ/dJMcadoCdOX/szk0WG6UikDcXqjdssAGJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccupUZ/dJMcadoCdOX/szk0WG6UikDcXqjdssAGJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccupUZ/dJMcadoCdOX/szk0WG6UikDcXqjdssAGJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccupUZ%2FdJMcadoCdOX%2Fszk0WG6UikDcXqjdssAGJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;471&quot; height=&quot;583&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;583&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원리:&lt;/b&gt; 중앙 제어 노드 없이, 권한 상징 프레임인 토큰(Token)을 링 형태로 구성된 노드들끼리 순서대로 이웃에 순차 전달.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동작:&lt;/b&gt; 토큰을 획득한 노드만 데이터 전송 권한을 얻으며, 다 보내면 토큰을 이웃에게 릴레이 토스. 보낼 게 없는 노드는 토큰을 받는 즉시 지체 없이 다음 노드로 릴레이.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한계:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아무도 보낼 데이터가 없어도 토큰이 계속 돌아야 하는 토큰 오버헤드 존재.&lt;/li&gt;
&lt;li&gt;중간 통신 잡음으로 토큰 자체가 깨지거나(토큰 분실), 특정 장비가 토큰을 먹은 채 먹통이 되면 복잡한 복구 알고리즘이 개입해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>NETWORK</category>
      <category>CS</category>
      <category>Network</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/281</guid>
      <comments>https://ch010104.tistory.com/281#entry281comment</comments>
      <pubDate>Mon, 1 Jun 2026 22:01:35 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술] 4. 검증1 - Validation</title>
      <link>https://ch010104.tistory.com/280</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 검증 요구사항 및 기본 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 상품 관리 시스템 검증 요구사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 상품을 등록하거나 수정할 때, 올바르지 않은 값이 들어오면 검증 오류를 발생시켜야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;타입 검증&lt;/b&gt;: 가격(price), 수량(quantity) 필드에 문자가 입력될 경우 검증 오류 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필드 검증&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;상품명(itemName)&lt;/b&gt;: 필수 값, 공백 금지(X)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가격(price)&lt;/b&gt;: 1,000원 이상 1,000,000원 이하&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수량(quantity)&lt;/b&gt;: 최대 9,999개 이하&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특정 필드의 범위를 넘어서는 검증 (복합 룰)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가격 * 수량&lt;/b&gt;의 합이 최소 10,000원 이상이어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 클라이언트 검증 vs 서버 검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션의 검증은 크게 두 가지 영역으로 나뉘며, 상호 보완적으로 사용되어야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt;구분&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt;장점&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt;단점&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt;특징&lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;클라이언트 검증&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;즉각적인 피드백 제공, 사용자 경험(UX) 극대화&lt;/td&gt;
&lt;td&gt;조작이 쉽고 보안에 취약함 (Postman 등으로 우회 가능)&lt;/td&gt;
&lt;td&gt;최종 방어선이 될 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;서버 검증&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;보안성이 뛰어나며 최종 비즈니스 규칙 준수 보장&lt;/td&gt;
&lt;td&gt;즉각적인 반응 속도가 떨어져 고객 사용성이 불편해짐&lt;/td&gt;
&lt;td&gt;&lt;b&gt;필수적으로 구현되어야 함&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론&lt;/b&gt;: 클라이언트 검증과 서버 검증을 적절히 섞어서 사용하되, &lt;b&gt;최종적으로 서버 검증은 필수&lt;/b&gt;입니다. 만약 API 방식을 사용한다면 API 스펙을 잘 정의하여 검증 오류를 API 응답 결과에 명확하게 남겨주어야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 검증 직접 처리 (V1)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 제공하는 검증 기능을 사용하기 전에, 순수 자바 Map을 사용하여 직접 검증 로직을 구현하는 흐름을 살펴봅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 아키텍처 흐름도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;성공 흐름&lt;/b&gt;: GET /add (상품 등록 폼) &amp;rarr; 사용자 입력 &amp;rarr; POST /add (컨트롤러에서 검증 성공) &amp;rarr; 상품 저장 &amp;rarr; Redirect /items/{id} &amp;rarr; GET /items/{id} (상품 상세 뷰)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wbycB/dJMcacDldIM/pVcmBvtRNoDl6QOAjvXuX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wbycB/dJMcacDldIM/pVcmBvtRNoDl6QOAjvXuX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wbycB/dJMcacDldIM/pVcmBvtRNoDl6QOAjvXuX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwbycB%2FdJMcacDldIM%2FpVcmBvtRNoDl6QOAjvXuX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1047&quot; height=&quot;898&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실패 흐름&lt;/b&gt;: GET /add (상품 등록 폼) &amp;rarr; 잘못된 사용자 입력 &amp;rarr; POST /add (컨트롤러에서 검증 실패) &amp;rarr; &lt;b&gt;오류 결과 수집&lt;/b&gt; &amp;rarr; Model에 errors 맵을 담고 addForm.html로 &lt;b&gt;Forward&lt;/b&gt; (입력 데이터가 그대로 유지됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb8zVk/dJMb990UW6U/XE6jDXgP4C6e46FGZHcff1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb8zVk/dJMb990UW6U/XE6jDXgP4C6e46FGZHcff1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb8zVk/dJMb990UW6U/XE6jDXgP4C6e46FGZHcff1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb8zVk%2FdJMb990UW6U%2FXE6jDXgP4C6e46FGZHcff1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1015&quot; height=&quot;673&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;673&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 컨트롤러 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증 결과를 저장하기 위해 Map&amp;lt;String, String&amp;gt; errors를 사용하며, 필드명이나 특수 에러(globalError)를 Key로 메시지를 저장합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/java/hello/itemservice/web/validation/ValidationItemControllerV1.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.itemservice.web.validation;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Controller
@RequestMapping(&quot;/validation/v1/items&quot;)
@RequiredArgsConstructor
public class ValidationItemControllerV1 {

    private final ItemRepository itemRepository;

    @GetMapping
    public String items(Model model) {
        List&amp;lt;Item&amp;gt; items = itemRepository.findAll();
        model.addAttribute(&quot;items&quot;, items);
        return &quot;validation/v1/items&quot;;
    }

    @GetMapping(&quot;/{itemId}&quot;)
    public String item(@PathVariable long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute(&quot;item&quot;, item);
        return &quot;validation/v1/item&quot;;
    }

    @GetMapping(&quot;/add&quot;)
    public String addForm(Model model) {
    // 검증이 실패했을 때도, 빈 Item을 넘겨서 이전의 model에 작성된 다른 속성 입력값들을 가져올 수 있음
        model.addAttribute(&quot;item&quot;, new Item());
        return &quot;validation/v1/addForm&quot;;
    }

    @PostMapping(&quot;/add&quot;)
    public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {

        // 1. 검증 오류 결과를 보관할 Map 생성
        Map&amp;lt;String, String&amp;gt; errors = new HashMap&amp;lt;&amp;gt;();

        // 2. 개별 필드 검증 로직
        if (!StringUtils.hasText(item.getItemName())) {
            errors.put(&quot;itemName&quot;, &quot;상품 이름은 필수입니다.&quot;);
        }

        if (item.getPrice() == null || item.getPrice() &amp;lt; 1000 || item.getPrice() &amp;gt; 1000000) {
            errors.put(&quot;price&quot;, &quot;가격은 1,000~1,000,000 까지 허용합니다.&quot;);
        }

        if (item.getQuantity() == null || item.getQuantity() &amp;gt; 9999) {
            errors.put(&quot;quantity&quot;, &quot;수량은 최대 9,999 까지 허용합니다.&quot;);
        }

        // 3. 복합 필드 검증 (특정 필드가 아닌 글로벌/복합 룰 검증)
        if (item.getPrice() != null &amp;amp;&amp;amp; item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice &amp;lt; 10000) {
                errors.put(&quot;globalError&quot;, &quot;가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재값 = &quot; + resultPrice);
            }
        }

        // 4. 검증 실패 시 다시 입력 폼으로 이동 (Model에 errors를 담아 렌더링)
        if (!errors.isEmpty()) {
            model.addAttribute(&quot;errors&quot;, errors);
            return &quot;validation/v1/addForm&quot;;
        }

        // 5. 성공 로직
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
        redirectAttributes.addAttribute(&quot;status&quot;, true);
        return &quot;redirect:/validation/v1/items/{itemId}&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) Thymeleaf 뷰 템플릿 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 무엇을 잘못 입력했는지 빨간색 글씨와 테두리로 강조하여 친절하게 안내합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/resources/templates/validation/v1/addForm.html&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot; href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;style&amp;gt;
        .container {
            max-width: 560px;
        }
        /* 오류 강조용 CSS 클래스 추가 */
        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
        &amp;lt;h2&amp;gt;상품 등록&amp;lt;/h2&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;form action=&quot;item.html&quot; th:action th:object=&quot;${item}&quot; method=&quot;post&quot;&amp;gt;

        &amp;lt;!-- 글로벌 오류 메시지 출력 --&amp;gt;
        &amp;lt;div th:if=&quot;${errors?.containsKey('globalError')}&quot;&amp;gt;
            &amp;lt;p class=&quot;field-error&quot; th:text=&quot;${errors['globalError']}&quot;&amp;gt;전체 오류 메시지&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 상품명 필드 오류 처리 --&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;itemName&quot;&amp;gt;상품명&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;itemName&quot; th:field=&quot;*{itemName}&quot;
                   th:class=&quot;${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'&quot;
                   placeholder=&quot;이름을 입력하세요&quot;&amp;gt;
            &amp;lt;div class=&quot;field-error&quot; th:if=&quot;${errors?.containsKey('itemName')}&quot;
                 th:text=&quot;${errors['itemName']}&quot;&amp;gt;
                상품명 오류
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 가격 필드 오류 처리 --&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;price&quot;&amp;gt;가격&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;price&quot; th:field=&quot;*{price}&quot;
                   th:class=&quot;${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control'&quot;
                   placeholder=&quot;가격을 입력하세요&quot;&amp;gt;
            &amp;lt;div class=&quot;field-error&quot; th:if=&quot;${errors?.containsKey('price')}&quot;
                 th:text=&quot;${errors['price']}&quot;&amp;gt;
                가격 오류
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 수량 필드 오류 처리 --&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;quantity&quot;&amp;gt;수량&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;quantity&quot; th:field=&quot;*{quantity}&quot;
                   th:class=&quot;${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control'&quot;
                   placeholder=&quot;수량을 입력하세요&quot;&amp;gt;
            &amp;lt;div class=&quot;field-error&quot; th:if=&quot;${errors?.containsKey('quantity')}&quot;
                 th:text=&quot;${errors['quantity']}&quot;&amp;gt;
                수량 오류
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;hr class=&quot;my-4&quot;&amp;gt;

        &amp;lt;div class=&quot;row&quot;&amp;gt;
            &amp;lt;div class=&quot;col&quot;&amp;gt;
                &amp;lt;button class=&quot;w-100 btn btn-primary btn-lg&quot; type=&quot;submit&quot;&amp;gt;저장&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;col&quot;&amp;gt;
                &amp;lt;button class=&quot;w-100 btn btn-secondary btn-lg&quot;
                        onclick=&quot;location.href='items.html'&quot;
                        th:onclick=&quot;|location.href='@{/validation/v1/items}'|&quot;
                        type=&quot;button&quot;&amp;gt;취소&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;container&quot;&gt;
&lt;div class=&quot;py-5 text-center&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4DXGg/dJMcabdhawK/mxK6lFELgGm5CJeF3RJR10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4DXGg/dJMcabdhawK/mxK6lFELgGm5CJeF3RJR10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4DXGg/dJMcabdhawK/mxK6lFELgGm5CJeF3RJR10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4DXGg%2FdJMcabdhawK%2FmxK6lFELgGm5CJeF3RJR10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;964&quot; height=&quot;774&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고: Safe Navigation Operator (errors?.)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최초로 상품 등록 폼(GET /add)에 접속할 때는 검증 에러가 전혀 없기 때문에 Model에 errors 맵이 담기지 않아 null 상태입니다.&lt;/li&gt;
&lt;li&gt;만약 단순 errors.containsKey('globalError')를 호출하면 NullPointerException이 발생하여 애플리케이션이 먹통이 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;errors?.containsKey(...)&lt;/b&gt; 문법은 errors가 null인 경우 NullPointerException을 던지는 대신 &lt;b&gt;null을 반환&lt;/b&gt;하는 SpringEL의 유용한 기능입니다. Thymeleaf의 th:if 문에서 null은 false로 처리되므로 아무런 오류 메시지도 출력되지 않고 화면이 무사히 로드됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) V1 직접 구현 방식의 남은 문제점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;뷰 템플릿 내의 심한 중복&lt;/b&gt;: 각각의 인풋 태그와 오류 메시지 박스마다 삼항 연산자와 조건문(th:if, th:class)이 매우 유사한 패턴으로 반복해서 나타납니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타입 바인딩 오류 대처 불가&lt;/b&gt;: 가격(price)과 수량(quantity)은 자바 도메인에서 Integer 타입입니다. 사용자가 입력 창에 문자 &quot;A&quot;를 입력할 경우, 컨트롤러에 도달하기 전 스프링 바인딩 단계에서 오류가 터져 400 Bad Request 예외 페이지가 노출됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고객 입력 데이터의 유실&lt;/b&gt;: 타입 바인딩 오류가 발생하면 자바 내부 객체 변환에 실패하여 사용자가 타이핑했던 잘못된 값(예: 문자 &quot;A&quot;)을 뷰 템플릿에 되돌려 유지할 수 없어 입력 내용이 다 사라집니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. BindingResult 도입 (V2)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크가 제공하는 핵심 검증 조력자인 BindingResult를 적용하여 V1의 구조적인 단점을 완벽하게 보완합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) BindingResult의 핵심 개념과 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BindingResult는 검증 오류를 보관하는 스프링 제공 인터페이스로, &lt;b&gt;반드시 검증할 대상 객체(예: @ModelAttribute Item item)의 바로 다음에 위치&lt;/b&gt;해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BindingResult가 파라미터에 존재하면, @ModelAttribute 데이터 바인딩 시 오류가 발생해도 컨트롤러가 즉각 끊기지 않고 정상 호출됩니다!&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BindingResult가 없을 때&lt;/b&gt;: 타입 mismatch 발생 $\rightarrow$ 400 Bad Request 에러 페이지 노출.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BindingResult가 있을 때&lt;/b&gt;: 타입 mismatch 발생 $\rightarrow$ 스프링이 오류 정보(FieldError)를 직접 생성하여 BindingResult에 고이 접어 넣은 다음 컨트롤러를 호출함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) BindingResult1 - 기본 사용 방식 (addItemV1)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드 에러는 FieldError 객체를 생성하고, 글로벌(복합) 에러는 ObjectError 객체를 생성하여 BindingResult에 적립합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/java/hello/itemservice/web/validation/ValidationItemControllerV2.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.itemservice.web.validation;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Controller
@RequestMapping(&quot;/validation/v2/items&quot;)
@RequiredArgsConstructor
public class ValidationItemControllerV2 {

    private final ItemRepository itemRepository;

    @GetMapping
    public String items(Model model) {
        List&amp;lt;Item&amp;gt; items = itemRepository.findAll();
        model.addAttribute(&quot;items&quot;, items);
        return &quot;validation/v2/items&quot;;
    }

    @GetMapping(&quot;/{itemId}&quot;)
    public String item(@PathVariable long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute(&quot;item&quot;, item);
        return &quot;validation/v2/item&quot;;
    }

    @GetMapping(&quot;/add&quot;)
    public String addForm(Model model) {
        model.addAttribute(&quot;item&quot;, new Item()); // 검증이 실패했을 때도, 빈 Item을 넘겨서 이전의 model에 작성된 다른 속성 입력값들을 가져올 수 있음
        return &quot;validation/v2/addForm&quot;;
    }

    @PostMapping(&quot;/add&quot;)
    public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
        // BindingResult는 ModelAttribute인 Item 객체의 바인딩 결과를 담고 있기 때문에, 무조건 @ModelAttribute Item item의 뒤에 와야함

        // 검증 오류 결과를 보관
        // Map&amp;lt;String, String&amp;gt; errors = new HashMap&amp;lt;&amp;gt;();
        // 이제 BlindinfResult가 기존 errors의 역할을 해줌

        // 검증 로직
        if (!StringUtils.hasText(item.getItemName())) {
            // errors.put(&quot;itemName&quot;, &quot;상품 이름은 필수입니다.&quot;);
            bindingResult.addError(new FieldError(&quot;item&quot;, &quot;itemName&quot;, &quot;상품 이름은 필수입니다.&quot;));
        }

        if (item.getPrice() == null || item.getPrice() &amp;lt; 1000 || item.getPrice() &amp;gt; 1000000) {
            // errors.put(&quot;price&quot;, &quot;가격은 1,000 ~ 1,000,000 까지 허용합니다.&quot;);
            bindingResult.addError(new FieldError(&quot;item&quot;, &quot;price&quot;, &quot;가격은 1,000 ~ 1,000,000 까지 허용합니다.&quot;));
        }

        if (item.getQuantity() == null || item.getQuantity() &amp;gt; 9999) {
            // errors.put(&quot;quantity&quot;, &quot;수량은 최대 9,999 까지 허용합니다.&quot;);
            bindingResult.addError(new FieldError(&quot;item&quot;, &quot;quantity&quot;, &quot;수량은 최대 9,999 까지 허용합니다.&quot;));
        }

        // 특정 필드가 아닌 복합 룰 검증
        if (item.getPrice() != null &amp;amp;&amp;amp; item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice &amp;lt; 10000) {
                // errors.put(&quot;globalError&quot;, &quot;가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = &quot; + resultPrice);
                // new ObjectError()로 만들면, GlobalErrors로 등록됨
                bindingResult.addError(new ObjectError(&quot;item&quot;, &quot;가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = &quot; + resultPrice));
            }
        }

        // 검증에 실패하면 다시 입력 폼으로
//        if (!errors.isEmpty()) {
//            log.info(&quot;errors: {}&quot;, errors);
//            model.addAttribute(&quot;errors&quot;, errors);
//            return &quot;validation/v2/addForm&quot;;
//        }
        if (bindingResult.hasErrors()) {
            log.info(&quot;errors={}&quot;, bindingResult);
            // bindingResult는 자동으로 view에 같이 넘어가기 때문에, model.addAttribute에 안 담아도 됨
            return &quot;validation/v2/addForm&quot;;
        }

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
        redirectAttributes.addAttribute(&quot;status&quot;, true);
        return &quot;redirect:/validation/v2/items/{itemId}&quot;;
    }

    @GetMapping(&quot;/{itemId}/edit&quot;)
    public String editForm(@PathVariable Long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute(&quot;item&quot;, item);
        return &quot;validation/v2/editForm&quot;;
    }

    @PostMapping(&quot;/{itemId}/edit&quot;)
    public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
        itemRepository.update(itemId, item);
        return &quot;redirect:/validation/v2/items/{itemId}&quot;;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) Thymeleaf의 BindingResult 통합 연동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf는 스프링의 BindingResult를 깊이 결합하여 검증 오류를 우아하게 표현하는 다양한 전용 기능들을 탑재하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;#fields&lt;/b&gt;: #fields를 사용하여 BindingResult 내부의 전체/개별 에러에 직접 접근합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;th:errors&lt;/b&gt;: 지정한 필드 경로에 오류가 담겨 있다면, 태그를 조건부로 출력해주고 내부의 가짜 텍스트를 실제 오류 메시지로 변환해 줍니다 (th:if 가 필요 없도록 단축한 것).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;th:errorclass&lt;/b&gt;: th:field로 연계된 필드 경로에 검증 오류가 실재할 경우, 기존 클래스 옆에 특정한 CSS 클래스(예: field-error)를 알아서 붙여줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/resources/templates/validation/v2/addForm.html&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;style&amp;gt;
        .container {
            max-width: 560px;
        }
        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;div class=&quot;container&quot;&amp;gt;

    &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
        &amp;lt;h2 th:text=&quot;#{page.addItem}&quot;&amp;gt;상품 등록&amp;lt;/h2&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;form action=&quot;item.html&quot; th:action th:object=&quot;${item}&quot; method=&quot;post&quot;&amp;gt;
        &amp;lt;!-- GlobalErrors의 경우, 오류가 여러개일 수 있기 때문에, th:errors 대신, 전체 GlobalErrors를 받아와서 th:each로 반복문으로 출력해야함--&amp;gt;
        &amp;lt;div th:if=&quot;${#fields.hasGlobalErrors()}&quot;&amp;gt;
            &amp;lt;p class=&quot;field-error&quot; th:each=&quot;err : ${#fields.globalErrors()}&quot; th:text=&quot;${err}&quot;&amp;gt;전체 오류 메시지&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;itemName&quot; th:text=&quot;#{label.item.itemName}&quot;&amp;gt;상품명&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;itemName&quot; th:field=&quot;*{itemName}&quot; th:errorclass=&quot;field-error&quot; class=&quot;form-control&quot; placeholder=&quot;이름을 입력하세요&quot;&amp;gt;
            &amp;lt;!-- th:errors=&quot;*{itemName}&quot; 안에 th:if=&quot;${#fields.hasErrors('itemName')}&quot; 의 확인 로직이 포함되어 있음 --&amp;gt;
            &amp;lt;div class=&quot;field-error&quot; th:errors=&quot;*{itemName}&quot;&amp;gt;
                상품명 오류
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;price&quot; th:text=&quot;#{label.item.price}&quot;&amp;gt;가격&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;price&quot; th:field=&quot;*{price}&quot; th:errorclass=&quot;field-error&quot; class=&quot;form-control&quot; placeholder=&quot;가격을 입력하세요&quot;&amp;gt;
            &amp;lt;div class=&quot;field-error&quot; th:errors=&quot;*{price}&quot;&amp;gt;
                가격 오류
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;quantity&quot; th:text=&quot;#{label.item.quantity}&quot;&amp;gt;수량&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;quantity&quot; th:field=&quot;*{quantity}&quot; th:errorclass=&quot;field-error&quot; class=&quot;form-control&quot; placeholder=&quot;수량을 입력하세요&quot;&amp;gt;
            &amp;lt;div class=&quot;field-error&quot; th:errors=&quot;*{quantity}&quot;&amp;gt;
                수량 오류
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;hr class=&quot;my-4&quot;&amp;gt;

        &amp;lt;div class=&quot;row&quot;&amp;gt;
            &amp;lt;div class=&quot;col&quot;&amp;gt;
                &amp;lt;button class=&quot;w-100 btn btn-primary btn-lg&quot; type=&quot;submit&quot; th:text=&quot;#{button.save}&quot;&amp;gt;저장&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;col&quot;&amp;gt;
                &amp;lt;button class=&quot;w-100 btn btn-secondary btn-lg&quot;
                        onclick=&quot;location.href='items.html'&quot;
                        th:onclick=&quot;|location.href='@{/validation/v2/items}'|&quot;
                        type=&quot;button&quot; th:text=&quot;#{button.cancel}&quot;&amp;gt;취소&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

    &amp;lt;/form&amp;gt;

&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고: BindingResult와 Errors의 상속 구조&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;org.springframework.validation.Errors는 최상위 인터페이스이고 단순 오류 기록/조회용입니다.&lt;/li&gt;
&lt;li&gt;org.springframework.validation.BindingResult는 Errors 인터페이스를 확장한 인터페이스로, 실제 구현 시 훨씬 더 다양한 기능과 유틸리티 메서드(addError 등)를 탑재하고 있습니다.&lt;/li&gt;
&lt;li&gt;관례상 더 강력한 BindingResult를 실무 매개변수로 사용합니다. (구현체로는 BeanPropertyBindingResult가 투입됩니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BindingResult&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링이 제공하는 검증 오류를 보관하는 객체이다. 검증 오류가 발생하면 여기에 보관하면 된다.&lt;/li&gt;
&lt;li&gt;BindingResult가 있으면 @ModelAttribute에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예) @ModelAttribute에 바인딩 시 타입 오류가 발생하면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BindingResult가 없으면 400 오류가 발생하면서 컨트롤러가 호출되지 않고, 오류 페이지로 이동한다. BindingResult가 있으면 오류 정보(FieldError)를 BindingResult에 담아서 컨트롤러를 정상 호출한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BindingResult에 검증 오류를 적용하는 3가지 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@ModelAttribute에 담아서 컨트롤러를 정상 호 의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이 BindingResult에 넣어준다.&lt;/li&gt;
&lt;li&gt;개발자가 직접 넣어준다.&lt;/li&gt;
&lt;li&gt;Validator사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 사용자 입력 값 유지 (FieldError, ObjectError)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BindingResult에 에러를 담더라도 addItemV1 버전은 에러가 나면 사용자의 과거 입력값이 날아가 버리는 부작용이 있습니다. 이를 해결하려면 FieldError, ObjectError가 가지고 있는 더 상세한 파라미터 생성자를 호출해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) FieldError &amp;amp; ObjectError의 생성자 심층 분석&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// FieldError 생성자 시그니처 1
public FieldError(String objectName, String field, String defaultMessage);

// FieldError 생성자 시그니처 2 (입력 보존 및 메시지 다국화 가능)
public FieldError(String objectName, String field, @Nullable Object rejectedValue,
                  boolean bindingFailure, @Nullable String[] codes,
                  @Nullable Object[] arguments, @Nullable String defaultMessage);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파라미터별 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;objectName&lt;/b&gt;: 오류가 발생한 Model 대상 객체의 이름 (예: &quot;item&quot;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;field&lt;/b&gt;: 오류가 터진 구체적 필드명 (예: &quot;price&quot;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;rejectedValue&lt;/b&gt;: &lt;b&gt;사용자가 입력하려다가 거부당한 원래의 입력 데이터&lt;/b&gt; (보존 데이터의 핵심)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;bindingFailure&lt;/b&gt;: 단순 검증(비즈니스 룰) 실패인지(false), 타입 변환 바인딩 단계 자체가 실패했는지(true) 구분하는 플래그&lt;/li&gt;
&lt;li&gt;&lt;b&gt;codes&lt;/b&gt;: 에러 메시지 번들에서 순차적으로 매칭해 찾아낼 오류 메시지 키 배열 (추후 메시지 자동화에 사용)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;arguments&lt;/b&gt;: 에러 메시지에 인자로 주입할 외부 값 객체 배열 (예: {1000, 1000000})&lt;/li&gt;
&lt;li&gt;&lt;b&gt;defaultMessage&lt;/b&gt;: 혹시라도 메시지 코드 매칭에 대실패했을 때 백업으로 출력해줄 기본 고정 메시지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 입력 값 보존 구현 방식 (addItemV2)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에 진입하기도 전에 문자가 숫자로 변환되지 않아 깨진 바인딩 오류 값들도, 스프링이 알아서 원래 글자 그대로 보관해 줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/java/hello/itemservice/web/validation/ValidationItemControllerV2.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;    // addItemV1 대체 버전
    @PostMapping(&quot;/add&quot;)
    public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
        // BindingResult는 ModelAttribute인 Item 객체의 바인딩 결과를 담고 있기 때문에, 무조건 @ModelAttribute Item item의 뒤에 와야함

        // 검증 오류 결과를 보관
        // Map&amp;lt;String, String&amp;gt; errors = new HashMap&amp;lt;&amp;gt;();
        // 이제 BlindinfResult가 기존 errors의 역할을 해줌

        // 검증 로직
        if (!StringUtils.hasText(item.getItemName())) {
            // errors.put(&quot;itemName&quot;, &quot;상품 이름은 필수입니다.&quot;);
            // bindingResult.addError(new FieldError(&quot;item&quot;, &quot;itemName&quot;, &quot;상품 이름은 필수입니다.&quot;));
            // 사용자가 입력한 값을 3번째 파라미터인 item.getItemName()처럼 보존해서, 잘못 입력했을 시에도 사라지지 않고 보존되게 함(필드가 맞지 않은 입력값도 보존함)
            // 5번쨰 파라미터인 code에서 error.properties 파일의 코드를 불러옴. 이게 없으면 default 사용
            // new String 배열로 사용하는 이유는, 첫번째 인덱스의 값이 없으면(선언이 안되어 있으면), 다음 인덱스의 값을 사용
            bindingResult.addError(new FieldError(&quot;item&quot;, &quot;itemName&quot;, item.getItemName(), false, new String[]{&quot;required.item.itemName&quot;}, null, &quot;default 오류 메시지&quot;));
        }

        if (item.getPrice() == null || item.getPrice() &amp;lt; 1000 || item.getPrice() &amp;gt; 1000000) {
            // errors.put(&quot;price&quot;, &quot;가격은 1,000 ~ 1,000,000 까지 허용합니다.&quot;);
            // bindingResult.addError(new FieldError(&quot;item&quot;, &quot;price&quot;, &quot;가격은 1,000 ~ 1,000,000 까지 허용합니다.&quot;));
            bindingResult.addError(new FieldError(&quot;item&quot;, &quot;price&quot;, item.getPrice(), false, new String[]{&quot;range.item.price&quot;}, new Object[]{1000, 1000000}, &quot;default 오류 메시지&quot;));
        }

        if (item.getQuantity() == null || item.getQuantity() &amp;gt; 9999) {
            // errors.put(&quot;quantity&quot;, &quot;수량은 최대 9,999 까지 허용합니다.&quot;);
            // bindingResult.addError(new FieldError(&quot;item&quot;, &quot;quantity&quot;, &quot;수량은 최대 9,999 까지 허용합니다.&quot;));
            bindingResult.addError(new FieldError(&quot;item&quot;, &quot;quantity&quot;, item.getQuantity(), false, new String[]{&quot;max.item.quantity&quot;}, new Object[]{9999}, &quot;default 오류 메시지&quot;));
        }

        // 특정 필드가 아닌 복합 룰 검증
        if (item.getPrice() != null &amp;amp;&amp;amp; item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice &amp;lt; 10000) {
                // errors.put(&quot;globalError&quot;, &quot;가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = &quot; + resultPrice);
                // new ObjectError()로 만들면, GlobalErrors로 등록됨
                bindingResult.addError(new ObjectError(&quot;item&quot;, new String[]{&quot;totalPriceMin&quot;}, new Object[]{10000, resultPrice}, &quot;default 오류 메시지&quot;));
            }
        }

        // 검증에 실패하면 다시 입력 폼으로
//        if (!errors.isEmpty()) {
//            log.info(&quot;errors: {}&quot;, errors);
//            model.addAttribute(&quot;errors&quot;, errors);
//            return &quot;validation/v2/addForm&quot;;
//        }
        if (bindingResult.hasErrors()) {
            log.info(&quot;errors={}&quot;, bindingResult);
            // bindingResult는 자동으로 view에 같이 넘어가기 때문에, model.addAttribute에 안 담아도 됨
            return &quot;validation/v2/addForm&quot;;
        }

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
        redirectAttributes.addAttribute(&quot;status&quot;, true);
        return &quot;redirect:/validation/v2/items/{itemId}&quot;;
    }
    
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임리프의 보존 원리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;정상 상황일 때는 th:field=&quot;*{price}&quot;가 Model 객체 내부의 실제 price 필드 숫자 값을 출력합니다.&lt;/li&gt;
&lt;li&gt;에러 발생 상황(오류 필드 존재)일 때는 컨트롤러에서 model에 담은 본래의 바인딩 데이터 대신, BindingResult가 고이 품고 있는 &lt;b&gt;FieldError의 rejectedValue를 꺼내어 화면에 영리하게 찍어줍니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 오류 코드와 메시지 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 코드 안에서 하드코딩 형태로 문자열 에러 메시지(&quot;가격을 잘못 입력했습니다.&quot;)를 들고 있으면 국제화 및 문장 보수가 매우 힘들어집니다. 오류 코드를 체계적으로 외부 자원으로 격리하여 중앙 제어식으로 관리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 설정 추가 및 메시지 파일 생성&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 부트 설정 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트가 우리가 새로 분리하여 작성할 에러 전용 메시지 파일(errors.properties)을 인지할 수 있게 경로를 매핑해 줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/resources/application.properties&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;spring.messages.basename=messages, errors
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 메시지 프로퍼티 정의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/resources/errors.properties&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 메시지 코드를 적용한 자바 구현 (addItemV3)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FieldError의 codes 매개변수에 우리가 프로퍼티 파일에 적어둔 키 값을 배열로 던져 연동합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/java/hello/itemservice/web/validation/ValidationItemControllerV2.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;    // addItemV2 대체 버전 (메시지 연동 버전)
    @PostMapping(&quot;/add&quot;)
    public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        if (!StringUtils.hasText(item.getItemName())) {
            bindingResult.addError(new FieldError(&quot;item&quot;, &quot;itemName&quot;,
                    item.getItemName(), false, new String[]{&quot;required.item.itemName&quot;}, null, null));
        }

        if (item.getPrice() == null || item.getPrice() &amp;lt; 1000 || item.getPrice() &amp;gt; 1000000) {
            bindingResult.addError(new FieldError(&quot;item&quot;, &quot;price&quot;,
                    item.getPrice(), false, new String[]{&quot;range.item.price&quot;}, new Object[]{1000, 1000000}, null));
        }

        if (item.getQuantity() == null || item.getQuantity() &amp;gt; 10000) {
            bindingResult.addError(new FieldError(&quot;item&quot;, &quot;quantity&quot;,
                    item.getQuantity(), false, new String[]{&quot;max.item.quantity&quot;}, new Object[]{9999}, null));
        }

        if (item.getPrice() != null &amp;amp;&amp;amp; item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice &amp;lt; 10000) {
                bindingResult.addError(new ObjectError(&quot;item&quot;,
                        new String[]{&quot;totalPriceMin&quot;}, new Object[]{10000, resultPrice}, null));
            }
        }

        if (bindingResult.hasErrors()) {
            return &quot;validation/v2/addForm&quot;;
        }

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
        redirectAttributes.addAttribute(&quot;status&quot;, true);
        return &quot;redirect:/validation/v2/items/{itemId}&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 혁신: rejectValue() 및 reject()의 등장 (addItemV4)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 기나긴 FieldError, ObjectError 생성자를 명시하는 작업은 코드가 지저분해지고 번거롭습니다. BindingResult가 본인의 검증 타겟 객체(target인 item)를 이미 알고 있다는 특징을 백분 활용하여 비약적으로 코드를 줄여줍니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// rejectValue() 메서드 원형
void rejectValue(@Nullable String field, String errorCode,
                 @Nullable Object[] errorArgs, @Nullable String defaultMessage);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/java/hello/itemservice/web/validation/ValidationItemControllerV2.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;    // addItemV3 대체 버전 (rejectValue 단축형 버전)
    @PostMapping(&quot;/add&quot;)
    public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        // 로그를 찍어보면 타겟 객체의 정보를 BindingResult가 이미 꽉 쥐고 있음을 확인 가능
        log.info(&quot;objectName={}&quot;, bindingResult.getObjectName()); // 결과: item
        log.info(&quot;target={}&quot;, bindingResult.getTarget()); // 결과: Item(...)

        if (!StringUtils.hasText(item.getItemName())) {
            bindingResult.rejectValue(&quot;itemName&quot;, &quot;required&quot;);
        }

        if (item.getPrice() == null || item.getPrice() &amp;lt; 1000 || item.getPrice() &amp;gt; 1000000) {
            bindingResult.rejectValue(&quot;price&quot;, &quot;range&quot;, new Object[]{1000, 1000000}, null);
        }

        if (item.getQuantity() == null || item.getQuantity() &amp;gt; 10000) {
            bindingResult.rejectValue(&quot;quantity&quot;, &quot;max&quot;, new Object[]{9999}, null);
        }

        if (item.getPrice() != null &amp;amp;&amp;amp; item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice &amp;lt; 10000) {
                // 특정 필드가 없는 글로벌 에러는 reject() 사용
                bindingResult.reject(&quot;totalPriceMin&quot;, new Object[]{10000, resultPrice}, null);
            }
        }

        if (bindingResult.hasErrors()) {
            return &quot;validation/v2/addForm&quot;;
        }

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
        redirectAttributes.addAttribute(&quot;status&quot;, true);
        return &quot;redirect:/validation/v2/items/{itemId}&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의문점&lt;/b&gt;: 분명 errors.properties에는 range.item.price 또는 required.item.itemName 등의 풀-네임 메시지 키를 정의했습니다. 하지만 컨트롤러 내에서 축약형인 &quot;required&quot;, &quot;range&quot;만 적었을 뿐인데도 스프링은 어떻게 풀-네임 메시지를 정확하게 찾아서 화면에 보여주는 걸까요? 그 핵심 비밀은 **MessageCodesResolver**에 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 핵심 원리: MessageCodesResolver&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MessageCodesResolver는 우리가 전달한 단순 축약 에러 코드(예: &quot;required&quot;)와 에러가 발생한 구체적인 도메인 객체명, 필드명, 그리고 필드 데이터의 타입을 유기적으로 결합하여 순서가 명확하게 정렬된 다수의 상세 메시지 후보군 코드를 즉각 자동 매칭해 주는 비밀 해결사 인터페이스입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 코드를 통한 검증 코드 분석&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/test/java/hello/itemservice/validation/MessageCodesResolverTest.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.itemservice.validation;

import org.junit.jupiter.api.Test;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;

import static org.assertj.core.api.Assertions.assertThat;

public class MessageCodesResolverTest {

    MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();

    @Test
    void messageCodesResolverObject() {
        // 객체 전체 에러일 때 메시지 후보코드 생성
        String[] messageCodes = codesResolver.resolveMessageCodes(&quot;required&quot;, &quot;item&quot;);
        assertThat(messageCodes).containsExactly(&quot;required.item&quot;, &quot;required&quot;);
    }

    @Test
    void messageCodesResolverField() {
        // 특정 필드 에러일 때 세밀한 후보코드들 순차 자동 생성
        String[] messageCodes = codesResolver.resolveMessageCodes(&quot;required&quot;, &quot;item&quot;, &quot;itemName&quot;, String.class);

        assertThat(messageCodes).containsExactly(
                &quot;required.item.itemName&quot;,  // 1순위: 가장 구체적
                &quot;required.itemName&quot;,       // 2순위: 덜 구체적
                &quot;required.java.lang.String&quot;,// 3순위: 타입 매칭
                &quot;required&quot;                 // 4순위: 범용적인 최상위 코드
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DefaultMessageCodesResolver의 생성 규칙 상세&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;객체 에러 (ObjectError)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1순위&lt;/b&gt;: 에러코드 + &quot;.&quot; + 객체명&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2순위&lt;/b&gt;: 에러코드&lt;/li&gt;
&lt;li&gt;예시: resolveMessageCodes(&quot;required&quot;, &quot;item&quot;) &amp;rarr; [&quot;required.item&quot;, &quot;required&quot;]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필드 에러 (FieldError)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1순위&lt;/b&gt;: 에러코드 + &quot;.&quot; + 객체명 + &quot;.&quot; + 필드명&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2순위&lt;/b&gt;: 에러코드 + &quot;.&quot; + 필드명&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3순위&lt;/b&gt;: 에러코드 + &quot;.&quot; + 필드 타입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4순위&lt;/b&gt;: 에러코드&lt;/li&gt;
&lt;li&gt;예시: resolveMessageCodes(&quot;typeMismatch&quot;, &quot;user&quot;, &quot;age&quot;, int.class) &amp;rarr; [&quot;typeMismatch.user.age&quot;, &quot;typeMismatch.age&quot;, &quot;typeMismatch.int&quot;, &quot;typeMismatch&quot;]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 컨트롤러 호출 후 로그 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bindingResult.rejectValue(&quot;itemName&quot;, &quot;required&quot;) 호출 시, BindingResult가 가지고 있는 FieldError 내부의 codes 배열 필드에는 다음과 같은 4가지 상세 키가 차곡차곡 적립됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;codes [required.item.itemName, required.itemName, required.java.lang.String, required]&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf 뷰 단에서 th:errors가 실행되면, 이 codes 배열의 1순위 원소부터 4순위 원소까지 순차적으로 errors.properties에서 에치해 보다가 &lt;b&gt;가장 먼저 발견되는 메시지를 골라서 출력&lt;/b&gt;합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5) 효과적인 오류 코드 계층 관리 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 자잘한 검증 에러 코드마다 개별 메시지를 일일이 기입하는 행위는 재앙에 가깝습니다. 평범하고 덜 중요한 메시지는 Level4처럼 아주 넓은 범용의 디폴트 성격 메시지로 커버하고, 정말 정교함이 요구되는 핵심 필드는 Level1 수준의 세부적인 개별 매핑 코드를 투입하는 계층형 수립 전략을 취합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/resources/errors.properties&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# ==========================================================
# ObjectError (복합/글로벌 룰 에러)
# ==========================================================
# Level 1 (구체적)
totalPriceMin.item=상품의 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}

# Level 2 (범용)
totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1}

# ==========================================================
# FieldError (필드 에러)
# ==========================================================
# Level 1 (가장 구체적)
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.

# Level 2 (생략 가능)

# Level 3 (데이터 바인딩 오류 대비용 타입 매칭)
required.java.lang.String=필수 문자입니다.
required.java.lang.Integer=필수 숫자입니다.
min.java.lang.String={0} 이상의 문자를 입력해주세요.
min.java.lang.Integer={0} 이상의 숫자를 입력해주세요.
range.java.lang.String={0} ~ {1} 까지의 문자를 입력해주세요.
range.java.lang.Integer={0} ~ {1} 까지의 숫자를 입력해주세요.
max.java.lang.String={0} 까지의 문자를 허용합니다.
max.java.lang.Integer={0} 까지의 숫자를 허용합니다.

# Level 4 (최하단 범용 - 최종 백업 디폴트 메시지)
required=필수 값 입니다.
min={0} 이상이어야 합니다.
range={0} ~ {1} 범위를 허용합니다.
max={0} 까지 허용합니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구성해두면, Level 1을 모두 주석처리해도 Level 4에서 정의된 &quot;필수 값 입니다.&quot; 등의 범용 메시지가 최후의 보루로서 알아서 매칭되어 나갑니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6) ValidationUtils 편의 제공 클래스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용되는 null 체크 및 공백(&quot;&quot; 혹은 &quot; &quot;) 여부 체크 단계를 간결하게 한 줄로 줄여줍니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// ValidationUtils 적용 전
if (!StringUtils.hasText(item.getItemName())) {
    bindingResult.rejectValue(&quot;itemName&quot;, &quot;required&quot;, &quot;기본: 상품 이름은 필수입니다.&quot;);
}

// ValidationUtils 적용 후 (위 세 줄의 문장과 완벽히 동치)
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, &quot;itemName&quot;, &quot;required&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7) 스프링이 내부적으로 자동 발생시키는 타입 에러 대응&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 직접 비즈니스 로직에서 검증해 낸 에러가 아닌, 스프링 프레임워크가 바인딩 단계에서 숫자가 필요한 Integer 타입 필드에 사용자가 문자 &quot;A&quot;를 집어넣는 등의 비정상 입력을 받았을 경우를 상상해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 스프링은 개발자의 손을 타기 전에 내부적으로 typeMismatch라는 에러 코드를 가지고 스스로 BindingResult에 FieldError를 적재시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MessageCodesResolver를 거쳐 codes 배열 필드에 적재되는 키는 다음과 같이 4가지입니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;typeMismatch.item.price&lt;/li&gt;
&lt;li&gt;typeMismatch.price&lt;/li&gt;
&lt;li&gt;typeMismatch.java.lang.Integer&lt;/li&gt;
&lt;li&gt;typeMismatch&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 &lt;b&gt;자바 소스코드를 한 줄도 만지지 않고&lt;/b&gt;, 오직 프로퍼티 파일 수정만을 통하여 지저분하게 표출되던 스프링 디폴트 영문 에러 메시지(Failed to convert property value...)를 한국어 전용의 예쁜 에러 문구로 완벽하게 덮어쓸 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/resources/errors.properties에 아래 구문을 한 줄 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 스프링 자체 타입 오류 커스텀 매핑 추가
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Validator의 분리 및 검증의 자동화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서 검증 로직이 차지하는 부피는 매우 막대하므로, 별도의 전담 검증 클래스(Validator)로 책임을 깔끔하게 떼어내야 유지보수가 수월하고 차후 동일한 규칙을 재활용할 수도 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 스프링 제공 Validator 인터페이스 구조&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;package org.springframework.validation;

public interface Validator {
    // 1. 해당 검증기 클래스가 인자로 넘어온 클래스 타입을 검증할 능력이 있는지 판단
    boolean supports(Class&amp;lt;?&amp;gt; clazz);

    // 2. 본격적인 검증 수행 및 오류 결과를 Errors(BindingResult의 상위 클래스)에 적립
    void validate(Object target, Errors errors);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) ItemValidator 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validator 인터페이스를 구현하고 비즈니스 검증 로직을 전부 통으로 옮겨옵니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/java/hello/itemservice/web/validation/ItemValidator.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;package hello.itemservice.web.validation;

import hello.itemservice.domain.item.Item;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

@Component // 스프링 빈 등록 필수
public class ItemValidator implements Validator {

    @Override
    public boolean supports(Class&amp;lt;?&amp;gt; clazz) {
        // Item 클래스가 지원 대상인지, 하위 자식 타입까지 안전하게 검사하는 assignable 사용 권장
        return Item.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target; // 대상을 구체 캐스트하여 작업 시작

        ValidationUtils.rejectIfEmptyOrWhitespace(errors, &quot;itemName&quot;, &quot;required&quot;);

        if (item.getPrice() == null || item.getPrice() &amp;lt; 1000 || item.getPrice() &amp;gt; 1000000) {
            errors.rejectValue(&quot;price&quot;, &quot;range&quot;, new Object[]{1000, 1000000}, null);
        }

        if (item.getQuantity() == null || item.getQuantity() &amp;gt; 10000) {
            errors.rejectValue(&quot;quantity&quot;, &quot;max&quot;, new Object[]{9999}, null);
        }

        if (item.getPrice() != null &amp;amp;&amp;amp; item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice &amp;lt; 10000) {
                errors.reject(&quot;totalPriceMin&quot;, new Object[]{10000, resultPrice}, null);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) ItemValidator의 수동 직접 호출 처리 (addItemV5)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 빈으로 직접 주입받은 검증기 객체의 validate()를 파라미터로 명시 호출합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/java/hello/itemservice/web/validation/ValidationItemControllerV2.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;    private final ItemValidator itemValidator; // 생성자 빈 자동 주입

    // addItemV4 대체 버전 (Validator 수동 호출 버전)
    @PostMapping(&quot;/add&quot;)
    public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        // 직접 검증 로직을 타겟(item)과 바인딩결과(bindingResult)를 넘겨 처리
        itemValidator.validate(item, bindingResult);

        if (bindingResult.hasErrors()) {
            log.info(&quot;errors={}&quot;, bindingResult);
            return &quot;validation/v2/addForm&quot;;
        }

        // 성공 로직
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
        redirectAttributes.addAttribute(&quot;status&quot;, true);
        return &quot;redirect:/validation/v2/items/{itemId}&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) WebDataBinder 및 @Validated 애노테이션을 활용한 자동화 (addItemV6)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더욱 진보한 기술을 통해 자바 단에서 우리가 직접 검증기 클래스 인스턴스를 불러와 validate를 쳐대던 한 줄 마저 지워버릴 수 있습니다. 검증 대상을 가리켜 @Validated를 명시하기만 하면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@InitBinder 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 내부의 WebDataBinder에 해당 전담 검증기(itemValidator)를 추가해 줍니다. @InitBinder는 선언된 해당 컨트롤러 클래스 인스턴스 전용 바인더로 한정해서만 발동하여 스코프를 제한합니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;    @InitBinder
    public void init(WebDataBinder dataBinder) {
        log.info(&quot;init binder {}&quot;, dataBinder);
        dataBinder.addValidators(itemValidator);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Validated 적용한 최종 자동화 메서드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/java/hello/itemservice/web/validation/ValidationItemControllerV2.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;    // addItemV5 대체 최종 버전 (@Validated 적용 검증 무인화 자동 처리)
    @PostMapping(&quot;/add&quot;)
    public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        // [특징]: 자바 개발자가 직접 호출하던 검증기 실행 코드가 한 줄도 없습니다!
        if (bindingResult.hasErrors()) {
            log.info(&quot;errors={}&quot;, bindingResult);
            return &quot;validation/v2/addForm&quot;;
        }

        // 성공 로직
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
        redirectAttributes.addAttribute(&quot;status&quot;, true);
        return &quot;redirect:/validation/v2/items/{itemId}&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무인 작동 원리 및 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;매개변수 선두에 달린 &lt;b&gt;@Validated&lt;/b&gt; 애노테이션을 감지한 스프링 프레임워크가 해당 메서드 호출 직전, 우리가 등록한 WebDataBinder 내부를 뒤져서 사용 가능한 모든 Validator 목록을 스캔합니다.&lt;/li&gt;
&lt;li&gt;각각의 Validator들이 구현해 둔 supports(Item.class)를 번갈아 돌려보며, 어떤 검증기가 타겟 클래스를 담당할 수 있는지 체크합니다.&lt;/li&gt;
&lt;li&gt;여기서는 우리가 등록해 둔 ItemValidator 클래스의 supports()가 true를 뱉으므로, 최종 매칭되어 내부의 validate(item, bindingResult)가 우리 몰래 자동으로 기폭되어 모든 에러들을 BindingResult에 파종합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5) 글로벌 설정법 (모든 컨트롤러 적용)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 컨트롤러에서 매번 바인더 설정을 선언하는 과정조차 생략하고, 스프링부트 메인 클래스 등에 연계 설정하여 전체 영역에 글로벌 검증 시스템을 전격 적용할 수도 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일명&lt;/b&gt;: src/main/java/hello/itemservice/ItemServiceApplication.java (메인 애플리케이션 파일 예시)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.itemservice;

import hello.itemservice.web.validation.ItemValidator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.validation.Validator;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(ItemServiceApplication.class, args);
    }

    // 글로벌 검증기를 반환하도록 WebMvcConfigurer 인터페이스 메서드 오버라이드 구현
    @Override
    public Validator getValidator() {
        return new ItemValidator();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;매우 중요한 경고&lt;/b&gt;: 이렇게 글로벌 설정을 수동으로 등록해두면, 차후 스프링 부트에서 절대적으로 사용하는 어마어마한 전용 검증 어노테이션 기반 기능인 BeanValidator (Hibernate Validator)가 수동 오버라이드에 덮어씌워져 자동 등록되지 않는 심각한 부작용을 야기합니다. 실무에서 글로벌 검증 설정을 직접 설계하여 사용하는 사례는 매우 희귀합니다. 반드시 테스트 목적이 종료되면 글로벌 설정은 다시 주석화하거나 조심스럽게 사용해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고: @Valid vs @Validated&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;javax.validation.@Valid&lt;/b&gt;: 자바 표준 스펙(JSR-303 / Jakarta)에 정의된 표준 검증 애노테이션입니다. 이를 활성화해 사용하려면 build.gradle에 implementation 'org.springframework.boot:spring-boot-starter-validation' 의존성 라이브러리를 직접 장착해야 정상 동작합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;org.springframework.validation.annotation.@Validated&lt;/b&gt;: 스프링 프레임워크 전용 검증 유틸 애노테이션으로, 자바 표준 검증 기능에 스프링 특화 편리성(그룹 검증 기능 등)을 한층 업그레이드하여 기본 탑재한 버전입니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>INFLEARN</category>
      <category>inflearn</category>
      <category>spring boot</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/280</guid>
      <comments>https://ch010104.tistory.com/280#entry280comment</comments>
      <pubDate>Sun, 31 May 2026 18:15:58 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술] 3. 메시지와 국제화</title>
      <link>https://ch010104.tistory.com/279</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 메시지, 국제화 소개&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;악덕? 기획자가 화면에 보이는 문구가 마음에 들지 않는다고, 상품명이라는 단어를 모두 상품이름으로 고쳐달라고 하면 어떻게 해야할까?&lt;/li&gt;
&lt;li&gt;여러 화면에 보이는 상품명, 가격, 수량 등, label에 있는 단어를 변경하려면 다음 화면들을 다 찾아가면서 모두 변경해야 한다. 지금처럼 화면 수가 적으면 문제가 되지 않지만 화면이 수십 개 이상이라면 수십 개의 파일을 모두 고쳐야 한다.&lt;/li&gt;
&lt;li&gt;addForm.html, editForm.html, item.html, items.html&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 해당 HTML 파일에 메시지가 하드코딩 되어 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 다양한 메시지를 한 곳에서 관리하도록 하는 기능을 &lt;b&gt;메시지 기능&lt;/b&gt;이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 messages.properties 라는 메시지 관리용 파일을 만들고 아래와 같이 정의한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 HTML들은 다음과 같이 해당 데이터를 key 값으로 불러서 사용하는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;addForm.html&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779969609745&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;label for=&quot;itemName&quot; th:text=&quot;#{item.itemName}&quot;&amp;gt;&amp;lt;/label&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;editForm.html&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779969620549&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;label for=&quot;itemName&quot; th:text=&quot;#{item.itemName}&quot;&amp;gt;&amp;lt;/label&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;국제화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지에서 한 발 더 나가보자.&lt;/li&gt;
&lt;li&gt;메시지에서 설명한 메시지 파일(messages.properties)을 각 나라별로 별도로 관리하면 서비스를 국제화 할 수 있다.&lt;/li&gt;
&lt;li&gt;예를 들어서 다음과 같이 2개의 파일을 만들어서 분류한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  파일명: messages_en.properties&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;item=Item
item.id=Item ID
item.itemName=Item Name
item.price=price
item.quantity=quantity
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  파일명: messages_ko.properties&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;item=상품
item.id=상품 ID
item.itemName=상품명
item.price=가격
item.quantity=수량
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영어를 사용하는 사람이면 messages_en.properties를 사용하고, 한국어를 사용하는 사람이면 messages_ko.properties를 사용하게 개발하면 된다. 이렇게 하면 사이트를 국제화 할 수 있다.&lt;/li&gt;
&lt;li&gt;한국에서 접근한 것인지 영어에서 접근한 것인지 인식하는 방법은 HTTP Accept-Language 헤더 값을 사용하거나 사용자가 직접 언어를 선택하도록 하고, 쿠키 등을 사용해서 처리하면 된다.&lt;/li&gt;
&lt;li&gt;메시지와 국제화 기능을 직접 구현할 수도 있겠지만, 스프링은 기본적인 메시지와 국제화 기능을 모두 제공한다. 그리고 타임리프도 스프링이 제공하는 메시지와 국제화 기능을 편리하게 통합해서 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 스프링 메시지 소스 설정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링은 기본적인 메시지 관리 기능을 제공한다. 메시지 관리 기능을 사용하려면 스프링이 제공하는 MessageSource를 스프링 빈으로 등록하면 되는데, MessageSource는 인터페이스이다.&lt;/li&gt;
&lt;li&gt;따라서 구현체인 ResourceBundleMessageSource를 스프링 빈으로 등록하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직접 등록 예시&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasenames(&quot;messages&quot;, &quot;errors&quot;);
    messageSource.setDefaultEncoding(&quot;utf-8&quot;);
    return messageSource;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;basenames&lt;/b&gt;: 설정 파일의 이름을 지정한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;messages로 지정하면 messages.properties 파일을 읽어서 사용한다.&lt;/li&gt;
&lt;li&gt;추가로 국제화 기능을 적용하려면 messages_en.properties, messages_ko.properties와 같이 파일명 마지막에 언어 정보를 주면 된다. 만약 찾을 수 있는 국제화 파일이 없으면 messages.properties (언어정보가 없는 파일명)를 기본으로 사용한다.&lt;/li&gt;
&lt;li&gt;파일의 위치는 /resources/messages.properties에 두면 된다.&lt;/li&gt;
&lt;li&gt;여러 파일을 한번에 지정할 수 있다. 여기서는 messages, errors 둘을 지정했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;defaultEncoding&lt;/b&gt;: 인코딩 정보를 지정한다. utf-8을 사용하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 부트에서의 자동 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트를 사용하면 스프링 부트가 MessageSource를 자동으로 스프링 빈으로 등록한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 부트 메시지 소스 설정 (application.properties)&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;spring.messages.basename=messages,config.i18n.messages
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 부트 메시지 소스 기본 값&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;spring.messages.basename=messages
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MessageSource를 스프링 빈으로 직접 등록하지 않고, 스프링 부트와 관련된 별도의 설정을 하지 않으면 messages라는 이름으로 기본 등록된다.&lt;/li&gt;
&lt;li&gt;따라서 messages_en.properties, messages_ko.properties, messages.properties 파일만 등록하면 자동으로 인식된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 메시지 파일 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 파일을 만들어보자. 국제화 테스트를 위해서 messages_en 파일도 추가하자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;messages.properties: 기본 값으로 사용(한글)&lt;/li&gt;
&lt;li&gt;messages_en.properties: 영어 국제화 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의!&lt;/b&gt; 파일명은 massage가 아니라 messages다! 마지막 &lt;b&gt;s&lt;/b&gt;에 주의하자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  경로: /resources/messages.properties&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;hello=안녕
hello.name=안녕 {0}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  경로: /resources/messages_en.properties&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;hello=hello
hello.name=hello {0}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  한글 깨짐이 발생하는 경우 해결법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텔리제이에서 한글 깨짐이 발생하면 다음 설정을 확인하자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Setting - File Encodings&lt;/b&gt;로 이동한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Default encoding for properties files&lt;/b&gt;를 ISO-8859-1에서 UTF-8로 변경한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Transparent native-to-ascii conversion&lt;/b&gt; 항목에 체크한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 스프링 메시지 소스 사용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MessageSource 인터페이스 구조&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public interface MessageSource {
    String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
    String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MessageSource 인터페이스를 보면 코드를 포함한 일부 파라미터로 메시지를 읽어오는 기능을 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  경로: src/test/java/hello/itemservice/message/MessageSourceTest.java&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;package hello.itemservice.message;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;

import java.util.Locale;

import static org.assertj.core.api.Assertions.*;

@SpringBootTest
public class MessageSourceTest {

    @Autowired
    MessageSource ms;

    @Test
    void helloMessage() {
        String result = ms.getMessage(&quot;hello&quot;, null, null);
        assertThat(result).isEqualTo(&quot;안녕&quot;);
    }

    @Test
    void notFoundMessageCode() {
        assertThatThrownBy(() -&amp;gt; ms.getMessage(&quot;no_code&quot;, null, null))
                .isInstanceOf(NoSuchMessageException.class);
    }

    @Test
    void notFoundMessageCodeDefaultMessage() {
        String result = ms.getMessage(&quot;no_code&quot;, null, &quot;기본 메시지&quot;, null);
        assertThat(result).isEqualTo(&quot;기본 메시지&quot;);
    }

    @Test
    void argumentMessage() {
        String result = ms.getMessage(&quot;hello.name&quot;, new Object[]{&quot;Spring&quot;}, null);
        assertThat(result).isEqualTo(&quot;안녕 Spring&quot;);
    }

    @Test
    void defaultLang() {
        assertThat(ms.getMessage(&quot;hello&quot;, null, null)).isEqualTo(&quot;안녕&quot;);
        assertThat(ms.getMessage(&quot;hello&quot;, null, Locale.KOREA)).isEqualTo(&quot;안녕&quot;);
    }

    @Test
    void enLang() {
        assertThat(ms.getMessage(&quot;hello&quot;, null, Locale.ENGLISH)).isEqualTo(&quot;hello&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상세 분석 및 테스트 설명&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) ms.getMessage(&quot;hello&quot;, null, null)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;code: hello&lt;/li&gt;
&lt;li&gt;args: null&lt;/li&gt;
&lt;li&gt;locale: null&lt;/li&gt;
&lt;li&gt;가장 단순한 테스트는 메시지 코드로 hello를 입력하고 나머지 값은 null을 입력했다.&lt;/li&gt;
&lt;li&gt;locale 정보가 없으면 basename에서 설정한 기본 이름 메시지 파일을 조회한다. basename으로 messages를 지정했으므로 messages.properties 파일에서 데이터를 조회한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 메시지가 없는 경우와 기본 메시지 처리 (notFoundMessageCode, notFoundMessageCodeDefaultMessage)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가 없는 경우에는 NoSuchMessageException이 발생한다.&lt;/li&gt;
&lt;li&gt;메시지가 없어도 기본 메시지(defaultMessage)를 매개변수로 함께 사용하면 에러 없이 기본 메시지가 반환된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 매개변수 사용 (argumentMessage)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지의 {0} 부분은 매개변수를 전달해서 치환할 수 있다.&lt;/li&gt;
&lt;li&gt;hello.name=안녕 {0} ➡️ Spring 단어를 매개변수로 전달 ➡️ 안녕 Spring으로 매핑된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 국제화 파일 선택 원리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;locale 정보를 기반으로 국제화 파일을 선택한다.&lt;/li&gt;
&lt;li&gt;Locale이 en_US인 경우, messages_en_US ➡️ messages_en ➡️ messages 순서로 찾는다.&lt;/li&gt;
&lt;li&gt;즉, Locale에 맞추어 &lt;b&gt;가장 구체적인 것&lt;/b&gt;이 있으면 먼저 찾고, 없으면 단계별로 상위 범위를 찾다가 디폴트를 찾는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;defaultLang()&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ms.getMessage(&quot;hello&quot;, null, null): locale 정보가 없으므로 messages.properties를 사용한다.&lt;/li&gt;
&lt;li&gt;ms.getMessage(&quot;hello&quot;, null, Locale.KOREA): locale 정보가 있지만 messages_ko 파일이 없으므로 기본인 messages.properties를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;enLang()&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ms.getMessage(&quot;hello&quot;, null, Locale.ENGLISH): locale 정보가 Locale.ENGLISH이므로 messages_en을 찾아서 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  강의 내용 정정 - 영상과 다른 내용 보충&lt;/b&gt;Locale 정보가 null인 경우 내부적으로 Locale.getDefault()를 호출해서 시스템의 기본 로케일을 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예) locale = null 인 경우 시스템 기본 locale이 ko_KR이므로 messages_ko.properties 조회 시도 ➡️ 조회 실패 ➡️ messages.properties 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 웹 애플리케이션에 메시지 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 메시지를 추가 등록하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  경로: /resources/messages.properties (수정 등록)&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;hello=안녕
hello.name=안녕 {0}

label.item=상품
label.item.id=상품 ID
label.item.itemName=상품명
label.item.price=가격
label.item.quantity=수량

page.items=상품 목록
page.item=상품 상세
page.addItem=상품 등록
page.updateItem=상품 수정

button.save=저장
button.cancel=취소
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임리프 메시지 적용 문법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임리프의 메시지 표현식 #{...}를 사용하면 스프링의 메시지를 편리하게 조회할 수 있다. 예를 들어서 방금 등록한 상품이라는 이름을 조회하려면 #{label.item}이라고 하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;렌더링 전&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779969760286&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div th:text=&quot;#{label.item}&quot;&amp;gt;&amp;lt;/h2&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;렌더링 후&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779969777084&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;상품&amp;lt;/h2&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임리프 템플릿 파일 변경 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용할 대상 파일:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;addForm.html&lt;/li&gt;
&lt;li&gt;editForm.html&lt;/li&gt;
&lt;li&gt;item.html&lt;/li&gt;
&lt;li&gt;items.html&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  경로: /resources/templates/message/addForm.html&lt;/h3&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;[http://www.thymeleaf.org](http://www.thymeleaf.org)&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;style&amp;gt;
        .container {
            max-width: 560px;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;div class=&quot;container&quot;&amp;gt;

    &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
        &amp;lt;h2 th:text=&quot;#{page.addItem}&quot;&amp;gt;상품 등록&amp;lt;/h2&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;h4 class=&quot;mb-3&quot;&amp;gt;상품 입력&amp;lt;/h4&amp;gt;

    &amp;lt;form action=&quot;item.html&quot; th:action th:object=&quot;${item}&quot; method=&quot;post&quot;&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;itemName&quot; th:text=&quot;#{label.item.itemName}&quot;&amp;gt;상품명&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;itemName&quot; th:field=&quot;*{itemName}&quot; class=&quot;form-control&quot; placeholder=&quot;이름을 입력하세요&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;price&quot; th:text=&quot;#{label.item.price}&quot;&amp;gt;가격&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;price&quot; th:field=&quot;*{price}&quot; class=&quot;form-control&quot; placeholder=&quot;가격을 입력하세요&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;quantity&quot; th:text=&quot;#{label.item.quantity}&quot;&amp;gt;수량&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;quantity&quot; th:field=&quot;*{quantity}&quot; class=&quot;form-control&quot; placeholder=&quot;수량을 입력하세요&quot;&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;hr class=&quot;my-4&quot;&amp;gt;

        &amp;lt;div class=&quot;row&quot;&amp;gt;
            &amp;lt;div class=&quot;col&quot;&amp;gt;
                &amp;lt;button class=&quot;w-100 btn btn-primary btn-lg&quot; type=&quot;submit&quot; th:text=&quot;#{button.save}&quot;&amp;gt;저장&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;col&quot;&amp;gt;
                &amp;lt;button class=&quot;w-100 btn btn-secondary btn-lg&quot;
                        onclick=&quot;location.href='items.html'&quot;
                        th:onclick=&quot;|location.href='@{/message/items}'|&quot;
                        type=&quot;button&quot; th:text=&quot;#{button.cancel}&quot;&amp;gt;취소&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

    &amp;lt;/form&amp;gt;

&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;container&quot;&gt;
&lt;div class=&quot;py-5 text-center&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;form action=&quot;item.html&quot; method=&quot;post&quot;&gt;&lt;/form&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✏️ addForm.html 주요 변경 사항 상세&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;페이지 이름 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존: &amp;lt;h2&amp;gt;상품 등록 폼&amp;lt;/h2&amp;gt;&lt;/li&gt;
&lt;li&gt;변경: &amp;lt;h2 th:text=&quot;#{page.addItem}&quot;&amp;gt;상품 등록&amp;lt;/h2&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레이블 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;lt;label for=&quot;itemName&quot;&amp;gt;상품명&amp;lt;/label&amp;gt; ➡️ &amp;lt;label for=&quot;itemName&quot; th:text=&quot;#{label.item.itemName}&quot;&amp;gt;상품명&amp;lt;/label&amp;gt;&lt;/li&gt;
&lt;li&gt;가격: th:text=&quot;#{label.item.price}&quot;&lt;/li&gt;
&lt;li&gt;수량: th:text=&quot;#{label.item.quantity}&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버튼 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존: &amp;lt;button type=&quot;submit&quot;&amp;gt;상품 등록&amp;lt;/button&amp;gt;&lt;/li&gt;
&lt;li&gt;변경: &amp;lt;button type=&quot;submit&quot; th:text=&quot;#{button.save}&quot;&amp;gt;저장&amp;lt;/button&amp;gt;&lt;/li&gt;
&lt;li&gt;취소 버튼: th:text=&quot;#{button.cancel}&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  경로: /resources/templates/message/editForm.html&lt;/h3&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;[http://www.thymeleaf.org](http://www.thymeleaf.org)&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;style&amp;gt;
        .container {
            max-width: 560px;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;div class=&quot;container&quot;&amp;gt;

    &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
        &amp;lt;h2 th:text=&quot;#{page.updateItem}&quot;&amp;gt;상품 수정&amp;lt;/h2&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;form action=&quot;item.html&quot; th:action th:object=&quot;${item}&quot; method=&quot;post&quot;&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;id&quot; th:text=&quot;#{label.item.id}&quot;&amp;gt;상품 ID&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;id&quot; th:field=&quot;*{id}&quot; class=&quot;form-control&quot; readonly&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;itemName&quot; th:text=&quot;#{label.item.itemName}&quot;&amp;gt;상품명&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;itemName&quot; th:field=&quot;*{itemName}&quot; class=&quot;form-control&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;price&quot; th:text=&quot;#{label.item.price}&quot;&amp;gt;가격&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;price&quot; th:field=&quot;*{price}&quot; class=&quot;form-control&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;quantity&quot; th:text=&quot;#{label.item.quantity}&quot;&amp;gt;수량&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;quantity&quot; th:field=&quot;*{quantity}&quot; class=&quot;form-control&quot;&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;hr class=&quot;my-4&quot;&amp;gt;

        &amp;lt;div class=&quot;row&quot;&amp;gt;
            &amp;lt;div class=&quot;col&quot;&amp;gt;
                &amp;lt;button class=&quot;w-100 btn btn-primary btn-lg&quot; type=&quot;submit&quot; th:text=&quot;#{button.save}&quot;&amp;gt;저장&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;col&quot;&amp;gt;
                &amp;lt;button class=&quot;w-100 btn btn-secondary btn-lg&quot;
                        onclick=&quot;location.href='item.html'&quot;
                        th:onclick=&quot;|location.href='@{/message/items/{itemId}(itemId=${item.id})}'|&quot;
                        type=&quot;button&quot; th:text=&quot;#{button.cancel}&quot;&amp;gt;취소&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

    &amp;lt;/form&amp;gt;

&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;container&quot;&gt;
&lt;div class=&quot;py-5 text-center&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;form action=&quot;item.html&quot; method=&quot;post&quot;&gt;
&lt;div class=&quot;row&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/form&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  경로: /resources/templates/message/item.html&lt;/h3&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;[http://www.thymeleaf.org](http://www.thymeleaf.org)&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;style&amp;gt;
        .container {
            max-width: 560px;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;div class=&quot;container&quot;&amp;gt;

    &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
        &amp;lt;h2 th:text=&quot;#{page.item}&quot;&amp;gt;상품 상세&amp;lt;/h2&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 추가 --&amp;gt;
    &amp;lt;h2 th:if=&quot;${param.status}&quot; th:text=&quot;'저장 완료'&quot;&amp;gt;&amp;lt;/h2&amp;gt;

    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;itemId&quot; th:text=&quot;#{label.item.id}&quot;&amp;gt;상품 ID&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;itemId&quot; name=&quot;itemId&quot; class=&quot;form-control&quot; value=&quot;1&quot; th:value=&quot;${item.id}&quot; readonly&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;itemName&quot; th:text=&quot;#{label.item.itemName}&quot;&amp;gt;상품명&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;itemName&quot; name=&quot;itemName&quot; class=&quot;form-control&quot; value=&quot;상품A&quot; th:value=&quot;${item.itemName}&quot; readonly&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;price&quot; th:text=&quot;#{label.item.price}&quot;&amp;gt;가격&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;price&quot; name=&quot;price&quot; class=&quot;form-control&quot; value=&quot;10000&quot; th:value=&quot;${item.price}&quot; readonly&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;quantity&quot; th:text=&quot;#{label.item.quantity}&quot;&amp;gt;수량&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;quantity&quot; name=&quot;quantity&quot; class=&quot;form-control&quot; value=&quot;10&quot; th:value=&quot;${item.quantity}&quot; readonly&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;hr class=&quot;my-4&quot;&amp;gt;

    &amp;lt;div class=&quot;row&quot;&amp;gt;
        &amp;lt;div class=&quot;col&quot;&amp;gt;
            &amp;lt;button class=&quot;w-100 btn btn-primary btn-lg&quot;
                    onclick=&quot;location.href='editForm.html'&quot;
                    th:onclick=&quot;|location.href='@{/message/items/{itemId}/edit(itemId=${item.id})}'|&quot;
                    type=&quot;button&quot; th:text=&quot;#{page.updateItem}&quot;&amp;gt;상품 수정&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;col&quot;&amp;gt;
            &amp;lt;button class=&quot;w-100 btn btn-secondary btn-lg&quot;
                    onclick=&quot;location.href='items.html'&quot;
                    th:onclick=&quot;|location.href='@{/message/items}'|&quot;
                    type=&quot;button&quot; th:text=&quot;#{page.items}&quot;&amp;gt;목록으로&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;container&quot;&gt;
&lt;div class=&quot;py-5 text-center&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;row&quot;&gt;
&lt;div class=&quot;col&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  경로: /resources/templates/message/items.html&lt;/h3&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;[http://www.thymeleaf.org](http://www.thymeleaf.org)&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;div class=&quot;container&quot; style=&quot;max-width: 600px&quot;&amp;gt;
    &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
        &amp;lt;h2 th:text=&quot;#{page.items}&quot;&amp;gt;상품 목록&amp;lt;/h2&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;row&quot;&amp;gt;
        &amp;lt;div class=&quot;col&quot;&amp;gt;
            &amp;lt;button class=&quot;btn btn-primary float-end&quot;
                    onclick=&quot;location.href='addForm.html'&quot;
                    th:onclick=&quot;|location.href='@{/message/items/add}'|&quot;
                    type=&quot;button&quot; th:text=&quot;#{page.addItem}&quot;&amp;gt;상품 등록&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;hr class=&quot;my-4&quot;&amp;gt;

    &amp;lt;div&amp;gt;
        &amp;lt;table class=&quot;table&quot;&amp;gt;
            &amp;lt;thead&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;th th:text=&quot;#{label.item.id}&quot;&amp;gt;ID&amp;lt;/th&amp;gt;
                &amp;lt;th th:text=&quot;#{label.item.itemName}&quot;&amp;gt;상품명&amp;lt;/th&amp;gt;
                &amp;lt;th th:text=&quot;#{label.item.price}&quot;&amp;gt;가격&amp;lt;/th&amp;gt;
                &amp;lt;th th:text=&quot;#{label.item.quantity}&quot;&amp;gt;수량&amp;lt;/th&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;/thead&amp;gt;
            &amp;lt;tbody&amp;gt;
            &amp;lt;tr th:each=&quot;item : ${items}&quot;&amp;gt;
                &amp;lt;td&amp;gt;&amp;lt;a href=&quot;item.html&quot; th:href=&quot;@{/message/items/{itemId}(itemId=${item.id})}&quot; th:text=&quot;${item.id}&quot;&amp;gt;회원id&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;&amp;lt;a href=&quot;item.html&quot; th:href=&quot;@{|/message/items/${item.id}|}&quot; th:text=&quot;${item.itemName}&quot;&amp;gt;상품명&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;
                &amp;lt;td th:text=&quot;${item.price}&quot;&amp;gt;10000&amp;lt;/td&amp;gt;
                &amp;lt;td th:text=&quot;${item.quantity}&quot;&amp;gt;10&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;/tbody&amp;gt;
        &amp;lt;/table&amp;gt;
    &amp;lt;/div&amp;gt;

&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;container&quot; style=&quot;max-width: 600px;&quot;&gt;
&lt;div class=&quot;py-5 text-center&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✏️ items.html 테이블 헤더 변경 상세&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 변경 전 --&amp;gt;
&amp;lt;th&amp;gt;ID&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;상품명&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;가격&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;수량&amp;lt;/th&amp;gt;

&amp;lt;!-- 변경 후 --&amp;gt;
&amp;lt;th th:text=&quot;#{label.item.id}&quot;&amp;gt;ID&amp;lt;/th&amp;gt;
&amp;lt;th th:text=&quot;#{label.item.itemName}&quot;&amp;gt;상품명&amp;lt;/th&amp;gt;
&amp;lt;th th:text=&quot;#{label.item.price}&quot;&amp;gt;가격&amp;lt;/th&amp;gt;
&amp;lt;th th:text=&quot;#{label.item.quantity}&quot;&amp;gt;수량&amp;lt;/th&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 및 테스트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잘 동작하는지 확인하기 위해 messages.properties 파일의 내용을 가격 &lt;br /&gt;➡️ 금액과 같이 임의로 변경해서 확인해 보자. 정상 동작이 확인되면 원래대로 되돌려 두자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  참고: 타임리프에서 파라미터 전달 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지에 파라미터가 필요한 경우 다음과 같이 타임리프 메시지 표현식에 파라미터를 넘겨줄 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;properties 정의: hello.name=안녕 {0}&lt;/li&gt;
&lt;li&gt;타임리프 사용 예시:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779969939173&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;p th:text=&quot;#{hello.name(${item.itemName})}&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZbMGL/dJMcadWswJt/ktS6mlmzmppYMsjOkEftt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZbMGL/dJMcadWswJt/ktS6mlmzmppYMsjOkEftt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZbMGL/dJMcadWswJt/ktS6mlmzmppYMsjOkEftt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZbMGL%2FdJMcadWswJt%2FktS6mlmzmppYMsjOkEftt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;983&quot; height=&quot;527&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 웹 애플리케이션에 국제화 적용하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타임리프 파일들에 이미 #{...}를 통해 메시지 표현식을 사용하도록 설정했기 때문에, 이제 영어 메시지 파일만 추가해 주면 모든 국제화 작업이 끝난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  경로: /resources/messages_en.properties&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;hello=hello
hello.name=hello {0}

label.item=Item
label.item.id=Item ID
label.item.itemName=Item Name
label.item.price=price
label.item.quantity=quantity

page.items=Item List
page.item=Item Detail
page.addItem=Item Add
page.updateItem=Item Update

button.save=Save
button.cancel=Cancel
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹에서 동작 확인하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 브라우저의 언어 설정 값을 변경하면서 국제화 적용을 직접 확인해 본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1788&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRBqGh/dJMcaiwIpmp/sVolWVQarVLA2UMOLqfqk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRBqGh/dJMcaiwIpmp/sVolWVQarVLA2UMOLqfqk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRBqGh/dJMcaiwIpmp/sVolWVQarVLA2UMOLqfqk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRBqGh%2FdJMcaiwIpmp%2FsVolWVQarVLA2UMOLqfqk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1788&quot; height=&quot;572&quot; data-origin-width=&quot;1788&quot; data-origin-height=&quot;572&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;크롬 브라우저 ➡️ 설정 ➡️ 언어로 이동한다.&lt;/li&gt;
&lt;li&gt;우선순위 언어를 영어(English)로 변경하고 최상위로 올린다.&lt;/li&gt;
&lt;li&gt;브라우저 새로고침 후 페이지를 테스트해 본다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 브라우저의 언어 설정 값을 변경하면 브라우저가 요청 시 전송하는 HTTP Accept-Language 헤더의 값이 함께 변경된다.&lt;/li&gt;
&lt;li&gt;Accept-Language는 클라이언트가 서버에 기대하는 언어 정보를 담아서 요청하는 HTTP 요청 헤더이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링의 국제화 메시지 선택&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgMuuY/dJMcadB5aJG/QHHfQVyAwt8Nk4auFCc2Ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgMuuY/dJMcadB5aJG/QHHfQVyAwt8Nk4auFCc2Ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgMuuY/dJMcadB5aJG/QHHfQVyAwt8Nk4auFCc2Ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgMuuY%2FdJMcadB5aJG%2FQHHfQVyAwt8Nk4auFCc2Ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1022&quot; height=&quot;583&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;583&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 기능은 Locale 정보를 알아야 그에 맞는 언어를 선택하여 화면에 노출해 줄 수 있다.&lt;/li&gt;
&lt;li&gt;스프링은 기본적으로 HTTP 요청 헤더의 Accept-Language 값을 활용하여 Locale을 선택하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LocaleResolver&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링은 개발자가 Locale 선택 방식을 자유롭게 변경할 수 있도록 LocaleResolver라는 인터페이스를 제공한다.&lt;/li&gt;
&lt;li&gt;스프링 부트는 기본적으로 Accept-Language 헤더를 활용하는 AcceptHeaderLocaleResolver를 사용하도록 설정되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LocaleResolver 인터페이스 구조&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public interface LocaleResolver {
    Locale resolveLocale(HttpServletRequest request);
    void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LocaleResolver 변경하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 Accept-Language 헤더가 아니라 세션이나 쿠키 기반으로 언어를 변경 및 유지하고 싶다면, LocaleResolver의 구현체를 교체하여 사용할 수 있다. (예: 사용자가 직접 화면 상단에서 한국어/영어 국기를 클릭하여 직접 Locale을 선택하도록 구현하는 경우)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>INFLEARN</category>
      <category>inflearn</category>
      <category>spring boot</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/279</guid>
      <comments>https://ch010104.tistory.com/279#entry279comment</comments>
      <pubDate>Thu, 28 May 2026 21:08:14 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] BGP와 SDN</title>
      <link>https://ch010104.tistory.com/278</link>
      <description>&lt;h2 data-pm-slice=&quot;1 5 []&quot; data-ke-size=&quot;size26&quot;&gt;1. BGP (Border Gateway Protocol)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) BGP의 정의 및 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인터넷의 GPS&lt;/b&gt;: 독립적인 네트워크 자치 시스템인 AS(Autonomous System)와 AS 사이에서 데이터를 목적지까지 보내기 위한 최적의 경로를 설정해 주는 대규모 외관문 라우팅 프로토콜(EGP)입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신뢰성 우선&lt;/b&gt;: 라우팅 정보를 정확하고 안전하게 주고받기 위해 &lt;b&gt;TCP 포트 &lt;/b&gt;$179$&lt;b&gt;번&lt;/b&gt;을 기반으로 동작합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경로 벡터(Path Vector) 프로토콜&lt;/b&gt;: 목적지 AS까지 도달하기 위해 거쳐야 하는 AS 경로 목록(AS-PATH)을 직접 확인하여 패킷이 무한 루프에 도달하는 현상을 원천 차단합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) BGP 최적 경로 선택 메커니즘 (Route Selection)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BGP 라우터가 동일 목적지로 가는 다중 경로를 학습했을 때, 다음 우선순위에 따라 단 하나의 '최적 경로'를 도출해 냅니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Local Preference (지역&lt;b&gt; 선호도) [정책 결정]&lt;/b&gt;: 우리 AS 내부에서 외부로 나갈 때 어떤 경로를 더 선호할지 결정하는 속성으로, &lt;b&gt;값이 높을수록 우선&lt;/b&gt;합니다. (통신사 간 비용 계약이나 정책에 따라 수동 조작하는 제1의 제어 도구입니다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Shortest AS-PATH (최단 AS 경로)&lt;/b&gt;: Local Preference가 같다면, 목적지까지 거쳐가는 &lt;b&gt;AS의 개수가 가장 적은 경로&lt;/b&gt;를 선택합니다.&lt;/li&gt;
&lt;li&gt;Closest NEXT-HOP router (뜨거운 감자&lt;b&gt; 라우팅)&lt;/b&gt;: AS-PATH 길이마저 같다면, 우리 AS 내부의 자원(IGP 코스트) 소모를 최소화하기 위해 &lt;b&gt;가장 가까운 게이트웨이(Next-Hop 라우터) 밖으로 패킷을 가장 빠르게 던져버릴 수 있는 경로&lt;/b&gt;를 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Additional Criteria (추가 필터링 기준)&lt;/b&gt;: 모든 조건이 동률일 때 eBGP를 iBGP보다 선호하거나, 최종적으로 가장 낮은 고유의 &lt;b&gt;Router ID(IP 주소)&lt;/b&gt; 값을 가진 경로를 낙점합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 전통적인 라우팅 (Per-Router) vs. SDN (Software Defined Networking)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 기존 Per-Router Control (분산형 제어 방식)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Monolithic &amp;amp; Distributed&lt;/b&gt;: 각각의 개별 라우터 장비 내부에 Data Plane(하드웨어)과 Control Plane(경로 계산용 소프트웨어)이 일체형으로 통합되어 돌아가는 폐쇄적 수직 구조입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동작 원리&lt;/b&gt;: 라우터들이 주변 장비와 복잡한 분산 라우팅 알고리즘(OSPF, BGP 등)을 돌려 서로 눈치를 보며 각자 독립적으로 &lt;b&gt;로컬 포워딩 테이블&lt;/b&gt;을 연산하고 구축합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장단점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 중앙 사령탑이 없기 때문에 특정 라우터가 죽어도 알아서 우회하는 등 생존력(Fault-Tolerance)이 강합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: 장비 설정을 바꾸려면 수천 대의 장비에 일일이 CLI로 원격 접속해야 하여 오설정(Misconfiguration) 발생 위험이 크며, 하드웨어 벤더(Cisco, Juniper 등)의 독점 OS에 종속되어 새로운 네트워크 기술을 프로그래밍하는 식의 유연한 혁신이 불가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 왜 SDN(소프트웨어 정의 네트워킹)인가? (중앙 집중화의 이점)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfQ3UZ/dJMcabdeEIJ/q9C8g2zSKEEHz970xVdS2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfQ3UZ/dJMcabdeEIJ/q9C8g2zSKEEHz970xVdS2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfQ3UZ/dJMcabdeEIJ/q9C8g2zSKEEHz970xVdS2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfQ3UZ%2FdJMcabdeEIJ%2Fq9C8g2zSKEEHz970xVdS2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;825&quot; height=&quot;474&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메인프레임에서 PC 혁명으로의 비유&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;전통 방식(수직 계통)&lt;/b&gt;: 전용 하드웨어, 전용 OS, 전용 앱이 묶여 팔리던 옛날의 IBM 메인프레임과 같습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SDN 방식(수평 분리)&lt;/b&gt;: 인텔 칩 하드웨어 위에 Windows/Linux OS를 깔고 다양한 서드파티 소프트웨어를 실행하듯, 껍데기 하드웨어 스위치와 중앙 소프트웨어 제어 장치를 분리하여 개방형 생태계를 구축합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트래픽 엔지니어링(Traffic Engineering)의 한계 극복&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;전통 방식의 한계&lt;/b&gt;: 오직 목적지 주소 기반의 최단 경로(Dijkstra)만 계산하기 때문에, 링크 가중치(Link Weight)를 인위적으로 조절해도 트래픽을 정교하게 다중 경로로 찢는 로드 밸런싱(Load Balancing)이나, 들어오는 입력 포트에 따라 갈래를 다르게 찢어 보내는 &lt;b&gt;소스 기반 경로 제어&lt;/b&gt;가 원천적으로 불가능했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SDN의 해결책&lt;/b&gt;: 중앙에서 전체 네트워크 맵을 실시간으로 내려다보며 라우팅 규칙을 코드 수준에서 직접 프로그래밍할 수 있기 때문에 제어의 정교함이 대폭 향상됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. SDN 아키텍처 및 동작 시나리오&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) SDN의 3대 계층 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SDN은 네트워크 제어 및 관리를 수평 구조로 삼등분합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BjfCA/dJMcaf01pC6/4Xnl2kciLePIVbtyF24fr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BjfCA/dJMcaf01pC6/4Xnl2kciLePIVbtyF24fr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BjfCA/dJMcaf01pC6/4Xnl2kciLePIVbtyF24fr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBjfCA%2FdJMcaf01pC6%2F4Xnl2kciLePIVbtyF24fr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1047&quot; height=&quot;511&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;511&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Network-Control Applications (최상위 앱 계층 - &quot;Brains&quot;)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실질적인 정책 및 제어 로직을 수행합니다. (예: 최단 경로 알고리즘, 침입 방지, 부하 분산 로직 등)&lt;/li&gt;
&lt;li&gt;기존 하드웨어 장비 벤더나 컨트롤러 자체와 완전히 분리(Unbundled)되어 있으며, 개발자가 원하는 언어(Java, Python 등)로 독립적으로 코딩할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SDN Controller (중앙 제어 계층 - &quot;Network OS&quot;)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;노스바운드 API (Northbound, 북향)&lt;/b&gt;: 상단 제어 앱들이 하드웨어의 상세 스펙을 몰라도 되도록, RESTful API나 추상화된 &lt;b&gt;Network Graph&lt;/b&gt; 형태의 진입 장벽이 낮은 인터페이스를 열어줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 전역 상태 관리 (Network-Wide State Management)&lt;/b&gt;: 장치 상태 통계, 호스트 목록, 링크 유효성 등의 DB를 쥐고 전체 지도 데이터를 실시간으로 동기화합니다. 성능과 안정성을 위해 &lt;b&gt;분산 데이터베이스&lt;/b&gt; 형태로 복제됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사우스바운드 API (Southbound, 남향)&lt;/b&gt;: 하단의 실제 물리 장비들과 통신하기 위한 채널로, 주로 &lt;b&gt;OpenFlow&lt;/b&gt; 표준 프로토콜을 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Data-Plane Switches (최하단 데이터 계층 - &quot;Simple Switches&quot;)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 지능이나 알고리즘 연산 능력이 없는 단순하고 빠르며 저렴한 범용 스위치(Commodity/White-box Switches)들입니다.&lt;/li&gt;
&lt;li&gt;오직 중앙 컨트롤러가 OpenFlow를 통해 주입(Install)해 준 &lt;b&gt;플로우 테이블(Flow/Forwarding Table)&lt;/b&gt; 규칙을 하드웨어 칩 레벨에서 매칭하여 패킷을 고속으로 전달(Forwarding)하는 역할만 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 컨트롤/데이터 평면 상호작용 시나리오 (링크 단선 발생 예시)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 상에서 스위치 s1과 s2 사이의 연결선이 끊겼을 때의 실시간 처리 절차는 다음과 같이 정밀하게 맞물려 작동합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;429&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbNUhw/dJMcahxOIMr/G5lNm1a1cVRpftKthFIcaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbNUhw/dJMcahxOIMr/G5lNm1a1cVRpftKthFIcaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbNUhw/dJMcahxOIMr/G5lNm1a1cVRpftKthFIcaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbNUhw%2FdJMcahxOIMr%2FG5lNm1a1cVRpftKthFIcaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;429&quot; height=&quot;526&quot; data-origin-width=&quot;429&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[ s1 장치 단선 감지 ]
        │
        ▼ (1 단계)
[ s1이 OpenFlow Port Status Message로 컨트롤러에 통보 ]
        │
        ▼ (2 단계)
[ SDN 컨트롤러가 수신 후 중앙 DB의 Link-state info 업데이트 ]
        │
        ▼ (3 단계)
[ 업데이트 이벤트가 등록된 다익스트라(Dijkstra) 라우팅 제어 앱 호출 ]
        │
        ▼ (4 단계)
[ 라우팅 앱이 컨트롤러의 Network Graph를 참조하여 새로운 우회 경로 계산 ]
        │
        ▼ (5 단계)
[ 계산된 결과 경로가 컨트롤러 내부의 Flow Table 구조 데이터로 정제 및 변환 ]
        │
        ▼ (6 단계)
[ 컨트롤러가 OpenFlow로 업데이트가 필요한 하위 스위치들에게 신규 Flow Table 주입 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. ICMP 및 ICMPv6&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) ICMP (Internet Control Message Protocol)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP의&lt;b&gt; 신호수&lt;/b&gt;: 패킷이 유실되어도 보고하지 않는 IP 프로토콜을 보완하기 위해, 호스트와 라우터가 네트워크 장애 상황을 공유하고 진단하기 위해 사용하는 프로토콜입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캡슐화&lt;/b&gt;: IP 데이터그램의 데이터(Payload) 영역에 직접 탑재되어 함께 운송됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 구조&lt;/b&gt;: 장애 성격을 담은 &lt;b&gt;Type&lt;/b&gt;과 세부 코드를 담은 &lt;b&gt;Code&lt;/b&gt;를 기본으로 삼고, 에러를 유발한 오리지널 IP 패킷의 헤더(첫 $8$바이트)를 반송 시 함께 실어 보냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 Type &amp;amp; Code&lt;/b&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;397&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvn7MY/dJMcafmtRmh/cjBX3uObV8Gl3AYSGIVoo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvn7MY/dJMcafmtRmh/cjBX3uObV8Gl3AYSGIVoo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvn7MY/dJMcafmtRmh/cjBX3uObV8Gl3AYSGIVoo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdvn7MY%2FdJMcafmtRmh%2FcjBX3uObV8Gl3AYSGIVoo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;421&quot; height=&quot;397&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;397&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) Traceroute의 동작 메커니즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Traceroute 명령어는 수명(TTL) 조작 및 목적지 유효 검증이라는 두 가지 지능형 설계를 통해 목적지까지의 구간별 경로를 추적합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;TTL 조작을 통한 순차적 탐색&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;출발지에서 목적지 포트 번호를 절대 열려있지 않을 법한 임의의 값(예: 33434)으로 세팅하고, TTL = 1 값을 부여한 UDP 패킷 3개를 쏩니다.&lt;/li&gt;
&lt;li&gt;첫 번째 라우터에서 TTL이 0이 되면서 패킷을 드롭시키고, 출발지로 ICMP Type 11 (TTL Expired)을 돌려보냅니다. 이 패킷의 헤더에서 첫 번째 라우터의 IP 주소를 획득하고 시간을 측정합니다.&lt;/li&gt;
&lt;li&gt;다음으로 TTL = 2, TTL = 3 순서대로 하나씩 숫자를 올려가며 동일 프로세스를 밟으며 구간 내 모든 라우터들의 IP를 차근차근 긁어옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;종료 조건 (Stopping Criteria)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마침내 패킷이 경로상의 모든 라우터를 건너뛰고 진짜 목적지 서버에 도달합니다.&lt;/li&gt;
&lt;li&gt;목적지 서버는 TTL 값과 무관하게 포트 유효성을 살피는데, 일부러 세팅했던 33434 포트가 닫혀있으므로 출발지에 ICMP Type 3, Code 3 (Port Unreachable)을 응답합니다.&lt;/li&gt;
&lt;li&gt;출발지는 이 특정 코드를 받으면 마침내 목적지에 안착했다고 판정하고 경로 추적 루프를 &lt;b&gt;최종 종료&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) ICMPv6의 혁신적 변화 및 기능 통합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IPv6 생태계에 맞춰 ICMPv6는 다수의 주변 프로토콜들을 자기 통제권 안으로 대거 흡수 통합하였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;Packet Too Big&quot; (Type 2)&lt;/b&gt;: IPv6에서는 라우터가 임의로 패킷 조각화(Fragmentation)를 하지 않습니다. 만약 라우터 자신의 MTU보다 큰 패킷이 들어오면 버리고 이 메시지를 뱉어냄으로써, 송신 호스트가 스스로 최적의 크기를 파악하게 만드는 PMTUD(Path MTU Discovery)의 핵심 수단이 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MLD (Multicast Listener Discovery)&lt;/b&gt;: 기존 IPv4에서 쓰던 멀티캐스트 전용 관리 프로토콜(IGMP)의 기능을 ICMPv6가 프로토콜 스택 내부로 병합하였습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NDP (Neighbor Discovery Protocol)&lt;/b&gt;: IP 주소로 맥 주소를 찾아오던 ARP 프로토콜의 영역까지 흡수하여 네트워크 전반의 통일성을 구현했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 네트워크 관리 (Network Management) 아키텍처&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 네트워크 관리의 4대 요소&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1081&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzLkxL/dJMcafGMv2B/TM5LghxmBgbR3KkOM6DDv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzLkxL/dJMcafGMv2B/TM5LghxmBgbR3KkOM6DDv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzLkxL/dJMcafGMv2B/TM5LghxmBgbR3KkOM6DDv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzLkxL%2FdJMcafGMv2B%2FTM5LghxmBgbR3KkOM6DDv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1081&quot; height=&quot;495&quot; data-origin-width=&quot;1081&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Managing Server (관리 서버)&lt;/b&gt;: 관리자(사람)가 네트워크 상태를 수집, 모니터링 및 전체 통제하는 중앙 콘솔 시스템입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Managed Device (피관리 장비)&lt;/b&gt;: 관리를 받는 라우터, 스위치, 호스트 장치들입니다. 내부에서 대리 역할을 수행하는 작은 백그라운드 프로세스인 Agent(에이전트)가 실시간 구동됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Data (상태 데이터)&lt;/b&gt;: 에이전트가 기록하고 보관하는 장치들의 세부 프로필입니다. 장치의 셋팅 정보(Configuration), 구동 라이브 맵(Operational), 패킷 카운트 등의 Statistics 정보가 담깁니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Network Management Protocol (네트워크 관리 프로토콜)&lt;/b&gt;: 관리 서버와 에이전트가 통계 데이터를 수집하고 이상 징후 알림(Trap/Event)을 교환하기 위한 공용 통신 언어입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 역사적 제어 접근 방식의 3단계 패러다임 변화&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;CLI (Command Line Interface)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;방식&lt;/b&gt;: ssh나 텔넷으로 직접 장비에 붙어 한 줄씩 장비 전용 수동 텍스트 명령어(types)나 작성해 둔 외부 스크립트 파일(scripts)을 실행시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;평가&lt;/b&gt;: 하드웨어 제조사 의존도가 심하고 개별 장치 단위 수동 노가다라 대규모 자동화에 비적합합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SNMP / MIB (Simple Network Management&lt;b&gt; Protocol)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;방식&lt;/b&gt;: 표준화된 언어를 통해 장치 내부의 정보 저장 체계인 &lt;b&gt;MIB(Management Information Base)&lt;/b&gt; 데이터를 주기적으로 읽어오거나(Query), 원격 설정 변경(Set)을 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;평가&lt;/b&gt;: &lt;b&gt;모니터링(Read-only)&lt;/b&gt; 수단으로는 크게 성공했으나, 여러 라우터들의 복합적인 환경 설정을 일시에 일관되게 동기화하는 도구로는 트랜잭션 안전성 부재로 한계가 드러났습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NETCONF / YANG&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;YANG (데이터 모델링 언어)&lt;/b&gt;: 네트워크 설정 구조와 변수들이 가져야 할 사양(Schema)을 구조적으로 꼼꼼하게 기술하는 정형화된 약속 언어입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NETCONF (네트워크 설정 프로토콜)&lt;/b&gt;: YANG 모델에 의거해 만들어진 대규모 설정 변경 데이터를 가져다 장치들에 무결하게 동기화하고 적용시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>NETWORK</category>
      <category>CS</category>
      <category>Network</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/278</guid>
      <comments>https://ch010104.tistory.com/278#entry278comment</comments>
      <pubDate>Wed, 27 May 2026 21:01:03 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술] 2. 타임리프 - 스프링 통합과 폼</title>
      <link>https://ch010104.tistory.com/277</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 타임리프와 스프링 MVC 통합 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임리프는 스프링 프레임워크와 유연하게 통합되어 단순한 뷰 템플릿 역할을 넘어선 강력한 엔터프라이즈 기능을 지원합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 통합으로 추가되는 주요 기능&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;스프링의 SpringEL 문법 통합&lt;/b&gt;: ${@myBean.doSomething()}과 같이 스프링 빈을 직접 호출할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;편리한 폼(Form) 관리 속성&lt;/b&gt;: th:object, th:field, th:errors, th:errorclass 등을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폼 컴포넌트의 편의 기능&lt;/b&gt;: 체크박스(Checkbox), 라디오 버튼(Radio button), 셀렉트 박스(Select/List)를 쉽게 렌더링하도록 돕습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 및 국제화 기능 통합&lt;/b&gt;: 스프링의 다국어 메시지 설정을 타임리프 템플릿 내에서 손쉽게 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증(Validation) 및 오류 처리 통합&lt;/b&gt;: 스프링의 BindingResult와 연동하여 폼 필드 에러를 일괄 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변환 서비스(ConversionService) 통합&lt;/b&gt;: 스프링이 제공하는 포맷터 및 컨버터를 자동으로 적용합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 추가 (Spring Boot)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트 환경에서는 아래의 단 한 줄의 의존성 선언만으로 타임리프 엔진 및 뷰 리졸버(View Resolver) 등의 설정이 자동화됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 입력 폼 처리 (Input Form Processing)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임리프는 데이터 바인딩과 폼 생성을 자동화하기 위해 세 가지 핵심 속성을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;th:object&lt;/b&gt;: 폼에서 바인딩하여 사용할 커맨드 객체(Command Object)를 지정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;{...}&lt;/b&gt;: 선택 변수 식(Selection Variable Expression). th:object로 지정된 객체의 프로퍼티에 간결하게 접근합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;th:field&lt;/b&gt;: HTML 태그의 id, name, value 속성을 자동으로 생성 및 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;th:field 렌더링 전후 비교&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개발자가 작성한 타임리프 코드 (렌더링 전)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;input type=&quot;text&quot; th:field=&quot;*{itemName}&quot; /&amp;gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버가 해석하여 출력한 HTML 결과 (렌더링 후)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;input type=&quot;text&quot; id=&quot;itemName&quot; name=&quot;itemName&quot; th:value=&quot;*{itemName}&quot; /&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[코드] 등록 및 수정 폼 구현&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller: src/main/java/hello/itemservice/web/form/FormItemController.java (폼 진입 및 바인딩용 빈 객체 주입)&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.itemservice.web.form;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Slf4j
@Controller
@RequestMapping(&quot;/form/items&quot;)
@RequiredArgsConstructor
public class FormItemController {

    private final ItemRepository itemRepository;

    /**
     * 상품 등록 폼 진입
     * th:object 적용을 위해 값이 비어있는 빈 Item 객체를 생성하여 Model에 넘겨줍니다.
     */
    @GetMapping(&quot;/add&quot;)
    public String addForm(Model model) {
        model.addAttribute(&quot;item&quot;, new Item());
        return &quot;form/addForm&quot;;
    }

    /**
     * 상품 수정 폼 진입
     */
    @GetMapping(&quot;/{itemId}/edit&quot;)
    public String editForm(@PathVariable Long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute(&quot;item&quot;, item);
        return &quot;form/editForm&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (등록): src/main/resources/templates/form/addForm.html&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;form action=&quot;item.html&quot; th:action th:object=&quot;${item}&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;itemName&quot;&amp;gt;상품명&amp;lt;/label&amp;gt;
        &amp;lt;!-- th:field를 사용하면 id, name 속성이 자동으로 itemName으로 지정됩니다. --&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;itemName&quot; th:field=&quot;*{itemName}&quot; class=&quot;form-control&quot; placeholder=&quot;이름을 입력하세요&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;price&quot;&amp;gt;가격&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;price&quot; th:field=&quot;*{price}&quot; class=&quot;form-control&quot; placeholder=&quot;가격을 입력하세요&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;quantity&quot;&amp;gt;수량&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;quantity&quot; th:field=&quot;*{quantity}&quot; class=&quot;form-control&quot; placeholder=&quot;수량을 입력하세요&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (수정): src/main/resources/templates/form/editForm.html&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;form action=&quot;item.html&quot; th:action th:object=&quot;${item}&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;id&quot;&amp;gt;상품 ID&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;id&quot; th:field=&quot;*{id}&quot; class=&quot;form-control&quot; readonly&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;itemName&quot;&amp;gt;상품명&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;itemName&quot; th:field=&quot;*{itemName}&quot; class=&quot;form-control&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;price&quot;&amp;gt;가격&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;price&quot; th:field=&quot;*{price}&quot; class=&quot;form-control&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label for=&quot;quantity&quot;&amp;gt;수량&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;quantity&quot; th:field=&quot;*{quantity}&quot; class=&quot;form-control&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 요구사항 추가 및 도메인 모델 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크박스, 라디오 버튼, 셀렉트 박스의 다양한 상황(ENUM, Class, List 등)을 검증하기 위한 추가 요구사항 및 도메인 모델 설계입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요구사항 목록&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uTeUg/dJMcaaMaW5C/nIi1p9ZxInbEZ3SwW8GuH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uTeUg/dJMcaaMaW5C/nIi1p9ZxInbEZ3SwW8GuH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uTeUg/dJMcaaMaW5C/nIi1p9ZxInbEZ3SwW8GuH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuTeUg%2FdJMcaaMaW5C%2FnIi1p9ZxInbEZ3SwW8GuH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;984&quot; height=&quot;557&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;판매 여부&lt;/b&gt; (단일 체크박스): Boolean 타입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;등록 지역&lt;/b&gt; (다중 체크박스): 서울, 부산, 제주 멀티 선택 (List 타입)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상품 종류&lt;/b&gt; (라디오 버튼): 도서, 식품, 기타 중 단일 선택 (ENUM 타입)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배송 방식&lt;/b&gt; (셀렉트 박스): 빠른/일반/느린 배송 중 단일 선택 (클래스 모델 객체 타입)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[코드] 도메인 모델 구현&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ENUM: src/main/java/hello/itemservice/domain/item/ItemType.java (상품 종류)&lt;/h3&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;package hello.itemservice.domain.item;

public enum ItemType {
    BOOK(&quot;도서&quot;), FOOD(&quot;식품&quot;), ETC(&quot;기타&quot;);

    private final String description;

    ItemType(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Class: src/main/java/hello/itemservice/domain/item/DeliveryCode.java (배송 방식)&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;package hello.itemservice.domain.item;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * FAST: 빠른 배송
 * NORMAL: 일반 배송
 * SLOW: 느린 배송
 */
@Data
@AllArgsConstructor
public class DeliveryCode {
    private String code;         // 시스템 전달 값 (예: FAST)
    private String displayName;  // 사용자 노출 값 (예: 빠른 배송)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Class: src/main/java/hello/itemservice/domain/item/Item.java (상품 정보 핵심 클래스)&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.itemservice.domain.item;

import lombok.Data;
import java.util.List;

@Data
public class Item {
    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    private Boolean open;              // 판매 여부 (단일 체크박스 바인딩용)
    private List&amp;lt;String&amp;gt; regions;      // 등록 지역 (멀티 체크박스 바인딩용)
    private ItemType itemType;         // 상품 종류 (라디오 버튼 바인딩용)
    private String deliveryCode;       // 배송 방식 (셀렉트 박스 바인딩용)

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Class: src/main/java/hello/itemservice/domain/item/ItemRepository.java (데이터 수정 처리 추가)&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package hello.itemservice.domain.item;

import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
public class ItemRepository {
    private static final Map&amp;lt;Long, Item&amp;gt; store = new HashMap&amp;lt;&amp;gt;();
    private static long sequence = 0L;

    public Item save(Item item) {
        item.setId(++sequence);
        store.put(item.getId(), item);
        return item;
    }

    public Item findById(Long id) {
        return store.get(id);
    }

    public List&amp;lt;Item&amp;gt; findAll() {
        return new ArrayList&amp;lt;&amp;gt;(store.values());
    }

    /**
     * 상품 수정 정보 반영 메서드 (신규 요구사항 반영)
     */
    public void update(Long itemId, Item updateParam) {
        Item findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());

        // 새로 추가된 필드들 업데이트
        findItem.setOpen(updateParam.getOpen());
        findItem.setRegions(updateParam.getRegions());
        findItem.setItemType(updateParam.getItemType());
        findItem.setDeliveryCode(updateParam.getDeliveryCode());
    }

    public void clearStore() {
        store.clear();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 단일 체크박스 처리 (Single Checkbox)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML 스펙의 체크박스 제약과 스프링의 해결법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HTML 동작 원리&lt;/b&gt;: HTML checkbox는 체크되지 않은 상태로 폼을 제출하면, 클라이언트(브라우저)에서 서버로 아예 &lt;b&gt;해당 필드 키 값 자체를 전송하지 않습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버에서의 부작용&lt;/b&gt;: 값이 공백으로 들어오는 수정 상황에서, 서버는 바인딩을 아예 건너뛰므로 기존 데이터가 수정되지 않고 유지되는 버그가 발생할 수 있습니다. (서버 측 데이터가 null로 유지됨)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스프링 MVC의 해결 트릭 (_ 언더스코어)&lt;/b&gt;: 원래 이름 앞에 _를 붙인 히든 필드를 제공하여 체크 여부를 명시적으로 알립니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;체크 상태: open=on&amp;amp;_open=on -&amp;gt; 스프링 MVC가 open 우선 파싱 후 true 바인딩.&lt;/li&gt;
&lt;li&gt;미체크 상태: _open=on -&amp;gt; 스프링 MVC가 _open만 넘어온 것을 보고 체크 해제로 판별 후 false 바인딩.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;input type=&quot;hidden&quot; name=&quot;_open&quot; value=&quot;on&quot; /&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[코드] 타임리프의 단일 체크박스 자동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임리프는 th:field를 적용하면 브라우저 사양에 호환되는 &lt;b&gt;히든 필드를 백그라운드에서 자동으로 생성&lt;/b&gt;해 줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (등록): src/main/resources/templates/form/addForm.html (체크박스 추가)&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- single checkbox --&amp;gt;
&amp;lt;div&amp;gt;판매 여부&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div class=&quot;form-check&quot;&amp;gt;
        &amp;lt;!-- 타임리프 th:field 사용으로 히든필드가 수동 작성 없이 자동 생성됩니다. --&amp;gt;
        &amp;lt;input type=&quot;checkbox&quot; id=&quot;open&quot; th:field=&quot;*{open}&quot; class=&quot;form-check-input&quot;&amp;gt;
        &amp;lt;label for=&quot;open&quot; class=&quot;form-check-label&quot;&amp;gt;판매 오픈&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서버에서 자동으로 생성된 실제 HTML 결과&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779781120886&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;hr class=&quot;my-4&quot;&amp;gt;
        &amp;lt;!-- single checkbox(체크박스 open이 체크가 안되면 false라는 값조차 서버로 넘기지 않음 -&amp;gt; open이 null로 저장됨) -&amp;gt; 이를 위해 히든 필드를 추가함 --&amp;gt;
        &amp;lt;div&amp;gt;판매 여부&amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;div class=&quot;form-check&quot;&amp;gt;
                &amp;lt;input type=&quot;checkbox&quot; id=&quot;open&quot; name=&quot;open&quot; class=&quot;form-check-input&quot;&amp;gt;
                &amp;lt;input type=&quot;hidden&quot; name=&quot;_open&quot; value=&quot;on&quot;/&amp;gt; 
                &amp;lt;!-- 히든 필드 추가(체크 박스가 체크되어 있을 시에는 _open은 무시, 그렇지 않을 경우에만 Spring MVC가 _open만 있는 것을 확인하고, open이 체크되지 않았다고 인식함 -&amp;gt; open은 null이 아닌 false로 인식)--&amp;gt;
                &amp;lt;label for=&quot;open&quot; class=&quot;form-check-label&quot;&amp;gt;판매 오픈&amp;lt;/label&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (상세 조회): src/main/resources/templates/form/item.html (체크박스 추가)&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;hr class=&quot;my-4&quot;&amp;gt;
&amp;lt;!-- single checkbox --&amp;gt;
&amp;lt;div&amp;gt;판매 여부&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div class=&quot;form-check&quot;&amp;gt;
        &amp;lt;!-- th:object를 미사용하므로 ${item.open} 전체 경로를 작성합니다. --&amp;gt;
        &amp;lt;!-- 상세 뷰는 조작 방지를 위해 disabled 속성을 추가합니다. --&amp;gt;
        &amp;lt;input type=&quot;checkbox&quot; id=&quot;open&quot; th:field=&quot;${item.open}&quot; class=&quot;form-check-input&quot; disabled&amp;gt;
        &amp;lt;label for=&quot;open&quot; class=&quot;form-check-label&quot;&amp;gt;판매 오픈&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동작 원리&lt;/b&gt;: item.open 값이 true인 경우, 타임리프가 자동 감지하여 HTML 내부 태그에 checked=&quot;checked&quot; 속성을 추가해 줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (수정): src/main/resources/templates/form/editForm.html (체크박스 추가)&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;hr class=&quot;my-4&quot;&amp;gt;
&amp;lt;!-- single checkbox --&amp;gt;
&amp;lt;div&amp;gt;판매 여부&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div class=&quot;form-check&quot;&amp;gt;
        &amp;lt;input type=&quot;checkbox&quot; id=&quot;open&quot; th:field=&quot;*{open}&quot; class=&quot;form-check-input&quot;&amp;gt;
        &amp;lt;label for=&quot;open&quot; class=&quot;form-check-label&quot;&amp;gt;판매 오픈&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 다중 체크박스 처리 (Multi Checkbox)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 지역(서울, 부산, 제주)을 멀티 선택하기 위한 로직입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ModelAttribute의 효율적인 공통 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 컨트롤러 클래스에 @ModelAttribute 애노테이션이 붙은 별도의 메서드가 존재하면, &lt;b&gt;해당 컨트롤러 내부의 어떤 URL 호출이 들어와도 반환된 값이 지정한 Key(예: &quot;regions&quot;) 이름으로 모델에 항상 자동으로 담기게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동적 ID 관리와 #ids.prev(...), #ids.next(...)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;th:each 반복문으로 체크박스를 연속 생성할 때, 태그의 name은 동일해야 하지만 HTML 표준 상 id는 유일무이해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임리프는 반복 도중 자동으로 뒤에 인덱스 숫자(1, 2, 3...)를 붙여서 id를 유니크하게 다듬어 줍니다 (예: regions1, regions2).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 대응하는 &amp;lt;label for=&quot;...&quot;&amp;gt; 역시 동적으로 변경된 id를 추적해야 하므로 #ids.prev('regions') 함수를 사용해 바로 이전에 생성된 checkbox의 동적 id 값을 가져와 연결합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[코드] 멀티 체크박스 연동 구현&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller: src/main/java/hello/itemservice/web/form/FormItemController.java (공통 데이터 추가)&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import java.util.LinkedHashMap;
import java.util.Map;

// FormItemController 클래스 내부에 추가
@ModelAttribute(&quot;regions&quot;)
public Map&amp;lt;String, String&amp;gt; regions() {
    // 순서 유지를 위해 LinkedHashMap 사용
    Map&amp;lt;String, String&amp;gt; regions = new LinkedHashMap&amp;lt;&amp;gt;();
    regions.put(&quot;SEOUL&quot;, &quot;서울&quot;);
    regions.put(&quot;BUSAN&quot;, &quot;부산&quot;);
    regions.put(&quot;JEJU&quot;, &quot;제주&quot;);
    return regions;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (등록): src/main/resources/templates/form/addForm.html (멀티 체크박스 영역)&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- multi checkbox --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;등록 지역&amp;lt;/div&amp;gt;
    &amp;lt;div th:each=&quot;region : ${regions}&quot; class=&quot;form-check form-check-inline&quot;&amp;gt;
        &amp;lt;!-- th:field=&quot;*{regions}&quot;에 바인딩되어 다중 선택된 결과가 List&amp;lt;String&amp;gt; regions 필드로 바인딩됨 --&amp;gt;
        &amp;lt;input type=&quot;checkbox&quot; th:field=&quot;*{regions}&quot; th:value=&quot;${region.key}&quot; class=&quot;form-check-input&quot;&amp;gt;
        &amp;lt;label th:for=&quot;${#ids.prev('regions')}&quot; th:text=&quot;${region.value}&quot; class=&quot;form-check-label&quot;&amp;gt;서울&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동적 생성된 HTML 변환 결과&lt;/b&gt; (서울, 부산, 제주 차례대로 id가 매핑됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779781178546&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;form-check form-check-inline&quot;&amp;gt;
    &amp;lt;input type=&quot;checkbox&quot; value=&quot;SEOUL&quot; class=&quot;form-check-input&quot; id=&quot;regions1&quot; name=&quot;regions&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;_regions&quot; value=&quot;on&quot; /&amp;gt;
    &amp;lt;label for=&quot;regions1&quot; class=&quot;form-check-label&quot;&amp;gt;서울&amp;lt;/label&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;form-check form-check-inline&quot;&amp;gt;
    &amp;lt;input type=&quot;checkbox&quot; value=&quot;BUSAN&quot; class=&quot;form-check-input&quot; id=&quot;regions2&quot; name=&quot;regions&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;_regions&quot; value=&quot;on&quot; /&amp;gt;
    &amp;lt;label for=&quot;regions2&quot; class=&quot;form-check-label&quot;&amp;gt;부산&amp;lt;/label&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (상세 조회): src/main/resources/templates/form/item.html&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- multi checkbox --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;등록 지역&amp;lt;/div&amp;gt;
    &amp;lt;div th:each=&quot;region : ${regions}&quot; class=&quot;form-check form-check-inline&quot;&amp;gt;
        &amp;lt;input type=&quot;checkbox&quot; th:field=&quot;${item.regions}&quot; th:value=&quot;${region.key}&quot; class=&quot;form-check-input&quot; disabled&amp;gt;
        &amp;lt;label th:for=&quot;${#ids.prev('regions')}&quot; th:text=&quot;${region.value}&quot; class=&quot;form-check-label&quot;&amp;gt;서울&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (수정): src/main/resources/templates/form/editForm.html&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- multi checkbox --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;등록 지역&amp;lt;/div&amp;gt;
    &amp;lt;div th:each=&quot;region : ${regions}&quot; class=&quot;form-check form-check-inline&quot;&amp;gt;
        &amp;lt;input type=&quot;checkbox&quot; th:field=&quot;*{regions}&quot; th:value=&quot;${region.key}&quot; class=&quot;form-check-input&quot;&amp;gt;
        &amp;lt;label th:for=&quot;${#ids.prev('regions')}&quot; th:text=&quot;${region.value}&quot; class=&quot;form-check-label&quot;&amp;gt;서울&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 라디오 버튼 처리 (Radio Button)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라디오 버튼은 여러 개 중 오직 하나만 선택할 때 요긴하며 자바의 ENUM 타입과 편리하게 통합됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라디오 버튼에 히든 필드가 필요 없는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크박스는 수정 상황에서 전체 선택을 해제(0개 체크)할 시 아무것도 브라우저가 송신하지 않는 예외 상황이 발생해 히든 필드가 반드시 요구됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, &lt;b&gt;라디오 버튼은 사용자가 한 번 무언가를 선택한 이후에는 무조건 한 개의 항목을 고정적으로 제출해야 하는 제약&lt;/b&gt;을 지니므로, 체크박스처럼 수정을 위한 언더스코어(_) 기반의 무선택 방지용 히든 필드를 별도로 구성할 필요가 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[코드] 라디오 버튼 연동 구현&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller: src/main/java/hello/itemservice/web/form/FormItemController.java (공통 데이터 추가)&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// FormItemController 클래스 내부에 추가
@ModelAttribute(&quot;itemTypes&quot;)
public ItemType[] itemTypes() {
    // ENUM의 모든 상수를 배열로 반환 ([BOOK, FOOD, ETC])
    return ItemType.values();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (등록): src/main/resources/templates/form/addForm.html (라디오 버튼 영역)&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- radio button --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;상품 종류&amp;lt;/div&amp;gt;
    &amp;lt;div th:each=&quot;type : ${itemTypes}&quot; class=&quot;form-check form-check-inline&quot;&amp;gt;
        &amp;lt;input type=&quot;radio&quot; th:field=&quot;*{itemType}&quot; th:value=&quot;${type.name()}&quot; class=&quot;form-check-input&quot;&amp;gt;
        &amp;lt;label th:for=&quot;${#ids.prev('itemType')}&quot; th:text=&quot;${type.description}&quot; class=&quot;form-check-label&quot;&amp;gt;도서&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (상세 조회): src/main/resources/templates/form/item.html&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- radio button --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;상품 종류&amp;lt;/div&amp;gt;
    &amp;lt;div th:each=&quot;type : ${itemTypes}&quot; class=&quot;form-check form-check-inline&quot;&amp;gt;
        &amp;lt;input type=&quot;radio&quot; th:field=&quot;${item.itemType}&quot; th:value=&quot;${type.name()}&quot; class=&quot;form-check-input&quot; disabled&amp;gt;
        &amp;lt;label th:for=&quot;${#ids.prev('itemType')}&quot; th:text=&quot;${type.description}&quot; class=&quot;form-check-label&quot;&amp;gt;도서&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (수정): src/main/resources/templates/form/editForm.html&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- radio button --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;상품 종류&amp;lt;/div&amp;gt;
    &amp;lt;div th:each=&quot;type : ${itemTypes}&quot; class=&quot;form-check form-check-inline&quot;&amp;gt;
        &amp;lt;input type=&quot;radio&quot; th:field=&quot;*{itemType}&quot; th:value=&quot;${type.name()}&quot; class=&quot;form-check-input&quot;&amp;gt;
        &amp;lt;label th:for=&quot;${#ids.prev('itemType')}&quot; th:text=&quot;${type.description}&quot; class=&quot;form-check-label&quot;&amp;gt;도서&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동적 생성된 HTML 변환 결과 (수정 시, 식품(FOOD)을 선택한 케이스)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779781222432&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;form-check form-check-inline&quot;&amp;gt;
    &amp;lt;input type=&quot;radio&quot; value=&quot;BOOK&quot; class=&quot;form-check-input&quot; id=&quot;itemType1&quot; name=&quot;itemType&quot;&amp;gt;
    &amp;lt;label for=&quot;itemType1&quot; class=&quot;form-check-label&quot;&amp;gt;도서&amp;lt;/label&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;form-check form-check-inline&quot;&amp;gt;
    &amp;lt;!-- 타임리프가 기존 바인딩 정보를 분석하여 자동으로 checked=&quot;checked&quot;를 입력함 --&amp;gt;
    &amp;lt;input type=&quot;radio&quot; value=&quot;FOOD&quot; class=&quot;form-check-input&quot; id=&quot;itemType2&quot; name=&quot;itemType&quot; checked=&quot;checked&quot;&amp;gt;
    &amp;lt;label for=&quot;itemType2&quot; class=&quot;form-check-label&quot;&amp;gt;식품&amp;lt;/label&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[참고] 타임리프에서 ENUM 직접 사용하기 (Model에 추가하지 않는 방식)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러단 모델에 ENUM 정보를 담아주지 않고 스프링 EL 문법으로 뷰 내에서 직통 참조할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;&amp;lt;div th:each=&quot;type : ${T(hello.itemservice.domain.item.ItemType).values()}&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주의점&lt;/b&gt;: 패키지 경로를 HTML 소스상에 수동으로 하드코딩하기 때문에, 패키지 리팩토링이나 클래스 이름 변경 작업 시 타임리프 파일 내부의 패키지 문자열까지 자동 컴파일 오류 감지가 되지 않아 유지보수 측면에서 추천하는 방식은 아닙니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 셀렉트 박스 처리 (Select Box)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 선택지 리스트 중 드롭다운 메뉴 형식으로 단 하나의 옵션을 택할 때 주로 활용하며, 자바 내 커스텀 객체 컬렉션(DeliveryCode) 데이터와 유연하게 연결됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[코드] 셀렉트 박스 연동 구현&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller: src/main/java/hello/itemservice/web/form/FormItemController.java (공통 데이터 추가)&lt;/h3&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;

// FormItemController 클래스 내부에 추가
@ModelAttribute(&quot;deliveryCodes&quot;)
public List&amp;lt;DeliveryCode&amp;gt; deliveryCodes() {
    List&amp;lt;DeliveryCode&amp;gt; deliveryCodes = new ArrayList&amp;lt;&amp;gt;();
    deliveryCodes.add(new DeliveryCode(&quot;FAST&quot;, &quot;빠른 배송&quot;));
    deliveryCodes.add(new DeliveryCode(&quot;NORMAL&quot;, &quot;일반 배송&quot;));
    deliveryCodes.add(new DeliveryCode(&quot;SLOW&quot;, &quot;느린 배송&quot;));
    return deliveryCodes;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;효율성 팁&lt;/b&gt;: @ModelAttribute로 선언된 공통 데이터 공급용 메서드는 컨트롤러로 오는 모든 호출마다 매번 객체 컬렉션을 신규 생성하므로 실제 고성능 상용 시스템에서는 한 곳에서 한 번 미리 구성(Static 등)해두고 재사용하는 편이 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (등록): src/main/resources/templates/form/addForm.html (셀렉트 박스 영역)&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- SELECT --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;배송 방식&amp;lt;/div&amp;gt;
    &amp;lt;select th:field=&quot;*{deliveryCode}&quot; class=&quot;form-select&quot;&amp;gt;
        &amp;lt;option value=&quot;&quot;&amp;gt;==배송 방식 선택==&amp;lt;/option&amp;gt;
        &amp;lt;option th:each=&quot;deliveryCode : ${deliveryCodes}&quot;
                th:value=&quot;${deliveryCode.code}&quot;
                th:text=&quot;${deliveryCode.displayName}&quot;&amp;gt;FAST&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (상세 조회): src/main/resources/templates/form/item.html&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- SELECT --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;배송 방식&amp;lt;/div&amp;gt;
    &amp;lt;select th:field=&quot;${item.deliveryCode}&quot; class=&quot;form-select&quot; disabled&amp;gt;
        &amp;lt;option value=&quot;&quot;&amp;gt;==배송 방식 선택==&amp;lt;/option&amp;gt;
        &amp;lt;option th:each=&quot;deliveryCode : ${deliveryCodes}&quot;
                th:value=&quot;${deliveryCode.code}&quot;
                th:text=&quot;${deliveryCode.displayName}&quot;&amp;gt;FAST&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML (수정): src/main/resources/templates/form/editForm.html&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- SELECT --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;배송 방식&amp;lt;/div&amp;gt;
    &amp;lt;select th:field=&quot;*{deliveryCode}&quot; class=&quot;form-select&quot;&amp;gt;
        &amp;lt;option value=&quot;&quot;&amp;gt;==배송 방식 선택==&amp;lt;/option&amp;gt;
        &amp;lt;option th:each=&quot;deliveryCode : ${deliveryCodes}&quot;
                th:value=&quot;${deliveryCode.code}&quot;
                th:text=&quot;${deliveryCode.displayName}&quot;&amp;gt;FAST&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동적 생성된 HTML 변환 결과 (수정 시, 빠른 배송(FAST)을 선택한 케이스)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779781301354&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;form-select-box-wrapper&quot;&amp;gt;
    &amp;lt;select class=&quot;form-select&quot; id=&quot;deliveryCode&quot; name=&quot;deliveryCode&quot;&amp;gt;
        &amp;lt;option value=&quot;&quot;&amp;gt;==배송 방식 선택==&amp;lt;/option&amp;gt;
        &amp;lt;!-- 선택 상태 값에 매치하여 자동으로 selected=&quot;selected&quot; 속성이 주입됨 --&amp;gt;
        &amp;lt;option value=&quot;FAST&quot; selected=&quot;selected&quot;&amp;gt;빠른 배송&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;NORMAL&quot;&amp;gt;일반 배송&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;SLOW&quot;&amp;gt;느린 배송&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 최종 정리 요약&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt; 타임리프 제공 속성/개념 &lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt; 설명 및 렌더링 특징 &lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;th:object&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;폼에 주입할 커맨드 객체를 바인딩. 자식 태그에서 *{...} 선택 변수 식을 통해 하위 필드에 빠르게 접근함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;th:field&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;HTML 폼 엘리먼트에서 반드시 일치해야 하는 id, name, value 속성을 한 번에 동시 생성.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;단일 체크박스 히든 필드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;HTML 기본 제약(미선택 시 서버 전송 누락) 해결을 위해 name 앞에 언더스코어(_)를 붙인 히든 필드를 자동 렌더링.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;멀티 체크박스 동적 ID&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;th:each 반복 생성 시 중복 ID를 피하고자 숫자 인덱스를 가변 주입하며, &amp;lt;label&amp;gt;은 #ids.prev(...)를 통해 타겟팅.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;라디오 버튼&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;선택 사항 중 단 하나만 활성화하므로, 수정 상황 시 미전송 예외가 생기지 않아 히든 필드가 불필요함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;@ModelAttribute 추가 용법&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;특정 컨트롤러 범위 내에서 공통적으로 사용할 정적 셀렉트 데이터, 옵션 목록 등을 일관되게 공급할 수 있도록 헬퍼 메서드화함.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>INFLEARN</category>
      <category>inflearn</category>
      <category>mvc</category>
      <category>spring boot</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/277</guid>
      <comments>https://ch010104.tistory.com/277#entry277comment</comments>
      <pubDate>Tue, 26 May 2026 16:42:34 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 11. Cluster DB</title>
      <link>https://ch010104.tistory.com/276</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 데이터베이스 클러스터링(Clustering)의 본질&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 Q. &quot;클러스터로 쓴다&quot;는 것의 정의와 핵심 목적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스를 &quot;클러스터로 구성하여 사용한다&quot;는 것은 물리적 혹은 가상으로 분리된 여러 대의 데이터베이스 서버를 네트워크로 묶어, 백엔드 애플리케이션 입장에서는 &lt;b&gt;마치 하나의 단일 시스템처럼 작동하도록 설계&lt;/b&gt;하는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 분산 아키텍처를 도입하는 핵심 목적은 크게 세 가지입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;고가용성 (High Availability, HA):&lt;/b&gt; 단일 DB 장비가 고장 났을 때 발생하는 서비스 전체 마비(SPOF, Single Point of Failure)를 방지합니다. 주 장비가 다운되어도 예비 장비가 즉각 가동되어 무중단 운영을 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;읽기 트래픽 부하 분산 (Read Scaling):&lt;/b&gt; 대용량 웹 서비스의 트래픽은 일반적으로 쓰기(10~20%)보다 읽기(80~90%)가 압도적입니다. 읽기 요청을 서브 데이터베이스로 적절히 분산하여 메인 데이터베이스의 연산 부하를 크게 덜어냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가성비 중심의 수평적 확장 (Scale-out):&lt;/b&gt; 무조건 서버 자체 스펙을 늘리는 수직적 확장(Scale-up)은 비용이 기하급수적으로 발생합니다. 저렴한 노드를 물리적으로 추가하는 수평적 확장(Scale-out)이 경제적이고 영리한 인프라 전략입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 Q. &quot;개인 캡스톤 프로젝트나 소규모 환경에서도 가능할까요? Supabase에선 어떨까요?&quot;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Supabase 환경 분석:&lt;/b&gt; Supabase는 백엔드 인프라가 완전히 추상화된 &lt;b&gt;Managed(완전 관리형) PostgreSQL 서비스&lt;/b&gt;입니다. 내부적으로 가용성 관리나 데이터 백업이 내장되어 있습니다. 유료 플랜(Pro 플랜 이상)에서는 몇 번의 설정만으로 'Read Replicas(읽기 전용 복제본 노드)'를 클러스터에 손쉽게 추가할 수 있습니다. 하지만 무료 플랜에서는 다중 인스턴스를 활용한 직접적인 클러스터링 설정을 허용하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캡스톤 디자인 포트폴리오 제언:&lt;/b&gt; 단순히 API만 호출하는 것보다, 데이터베이스 설계 및 분산 인프라 역량을 확실하게 증명하고 싶다면 &lt;b&gt;Docker 환경에서 PostgreSQL 인스턴스 2대를 직접 띄우고 데이터 동기화와 클러스터를 수동 구축하는 경험&lt;/b&gt;을 갖는 것을 권장합니다. 기술 면접 및 평가관들에게 인프라 설계 능력을 매우 훌륭하게 어필할 수 있는 차별화 포인트가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. PostgreSQL 복제(Replication) 구조와 내부 동작 원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 WAL 스트리밍 복제 (Streaming Replication)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 실시간 복제 기술은 &lt;b&gt;WAL (Write-Ahead Log, 미리 쓰기 로그)&lt;/b&gt; 백업 매커니즘을 기반으로 수행됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;로그 선기록:&lt;/b&gt; Primary(Master) 노드에 데이터 생성/수정/삭제(CUD) 요청이 수신되면, 실제 디스크 데이터 페이지에 쓰기 전 해당 이력을 WAL 세그먼트에 먼저 순차 기록합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 전송:&lt;/b&gt; Primary 노드의 백그라운드 프로세스인 WAL Sender가 네트워크 소켓을 통해 변경 로그를 Secondary(Slave) 노드로 전송합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 Replay:&lt;/b&gt; Secondary 노드의 WAL Receiver 프로세스가 전송받은 변경 이력을 가져와서 로컬 데이터에 한 줄씩 반영(Replay)하며 완벽한 정합성을 맞춥니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Q. &quot;docker-compose의 depends_on 설정 때문에 자동으로 동기화와 고장 대체가 되는 건가요?&quot;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;depends_on의 명확한 역할:&lt;/b&gt; 절대 아닙니다. Docker Compose 파일에서 depends_on은 오직 컨테이너의 부팅 및 가동 순서(마스터를 먼저 완전히 띄우고 슬레이브를 가동함)만을 강제합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 동기화의 주체:&lt;/b&gt; 동기화 처리는 컨테이너에 전달한 환경 변수 설정을 보고 구동되는 컨테이너 이미지 내부의 초기화 스크립트가 PostgreSQL의 스트리밍 복제 모듈을 직접 활성화했기 때문에 일어납니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Failover(고장 대체)의 부재:&lt;/b&gt; 마스터 노드가 죽었을 때 슬레이브가 이를 인지하고 자동으로 읽기 전용 상태에서 마스터(쓰기 가능) 상태로 신분을 상승시키는 &lt;b&gt;Auto-Failover&lt;/b&gt; 메커니즘은 기본 Docker Compose 설정만으로는 작동하지 않으며, 감시 역할을 수행할 추가 도구(Patroni 등)가 필수적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Docker Compose 기반의 실습 클러스터 구축&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 docker-compose.yml 설정 파일&lt;/h3&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;version: '3.8'

services:
  # =========================================================================
  # 1. Primary (Master) Database Node
  # =========================================================================
  postgres-master:
    image: bitnami/postgresql:15
    container_name: postgres-master
    environment:
      - POSTGRESQL_REPLICATION_MODE=master
      - POSTGRESQL_REPLICATION_USER=repl_user
      - POSTGRESQL_REPLICATION_PASSWORD=repl_password
      - POSTGRESQL_USERNAME=myuser
      - POSTGRESQL_PASSWORD=mypassword
      - POSTGRESQL_DATABASE=mydb
      - ALLOW_EMPTY_PASSWORD=no
    ports:
      - &quot;5432:5432&quot;
    volumes:
      - postgres_master_data:/bitnami/postgresql

  # =========================================================================
  # 2. Secondary (Slave) Database Node
  # =========================================================================
  postgres-slave:
    image: bitnami/postgresql:15
    container_name: postgres-slave
    depends_on:
      - postgres-master # Master가 부팅된 이후 구동될 수 있도록 안전한 실행 순서 제어
    environment:
      - POSTGRESQL_REPLICATION_MODE=slave
      - POSTGRESQL_MASTER_HOST=postgres-master # 연결할 Master 컨테이너의 네트워크 호스트명
      - POSTGRESQL_MASTER_PORT_NUMBER=5432
      - POSTGRESQL_REPLICATION_USER=repl_user
      - POSTGRESQL_REPLICATION_PASSWORD=repl_password
      - POSTGRESQL_PASSWORD=mypassword # Master 비밀번호와 일치 필수
    ports:
      - &quot;5433:5432&quot; # 포트를 5433으로 충돌 방지 세팅
    volumes:
      - postgres_slave_data:/bitnami/postgresql

volumes:
  postgres_master_data:
  postgres_slave_data:
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 복제 및 동작 테스트 절차&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;가동:&lt;/b&gt; docker compose up -d&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쓰기 테스트:&lt;/b&gt; Master DB(5432 포트)에 접속하여 가상 데이터를 생성합니다.&lt;/li&gt;
&lt;li&gt;CREATE TABLE products (id SERIAL PRIMARY KEY, title VARCHAR(50)); INSERT INTO products (title) VALUES ('Database Guide Book');&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동기화 확인:&lt;/b&gt; Slave DB(5433 포트)에 접속해 정상적으로 실시간 복제가 일어났는지 확인합니다.&lt;/li&gt;
&lt;li&gt;SELECT * FROM products; -- 'Database Guide Book'이 출력되면 복제 성공!&lt;/li&gt;
&lt;li&gt;&lt;b&gt;읽기 전용 상태 확인:&lt;/b&gt; Slave DB(5433 포트)에서 무단으로 쓰기 동작을 시도합니다.&lt;/li&gt;
&lt;li&gt;INSERT INTO products (title) VALUES ('Unpermitted Book'); -- 에러 발생 확인: &quot;ERROR: cannot execute INSERT in a read-only transaction&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 백엔드(Java/Spring Boot) 다중 데이터소스 및 동적 라우팅 연동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 Q. &quot;이렇게 클러스터로 바꿨을 때 백엔드 코드의 DB 접근/작성 로직도 다 바뀌나요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;데이터 조작을 처리하는 백엔드의 자바 SQL 쿼리 코드나 레포지토리(Repository) 로직은 전혀 바꿀 필요가 없습니다.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 구동될 때 트랜잭션의 속성에 따라 타겟 DB 장비를 알아서 교체해 주는 &lt;b&gt;Dynamic Routing DataSource&lt;/b&gt; 구조를 추가하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자는 쓰기 작업이 발생하는 서비스 레이어의 비즈니스 메서드에는 기존처럼 @Transactional을 명시하고, 읽기 연산만 필요한 단순 피드 조회, 정보 확인 등의 메서드에는 @Transactional(readOnly = true)를 붙여주기만 하면 스프링 프레임워크가 알아서 올바른 포트와 IP로 분기해 줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 application.properties 설정 파일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트 애플리케이션 접속 정보를 정의하는 &lt;b&gt;application.properties&lt;/b&gt; 파일입니다. YAML 방식을 완전히 배제한 표준 포맷입니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# =========================================================================
# 1. Master DB DataSource (CUD - Create, Update, Delete)
# =========================================================================
spring.datasource.master.jdbc-url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.master.username=myuser
spring.datasource.master.password=mypassword
spring.datasource.master.driver-class-name=org.postgresql.Driver
# HikariCP 커넥션 풀 성능 최적화 세팅
spring.datasource.master.hikari.pool-name=Master-HikariPool
spring.datasource.master.hikari.maximum-pool-size=10
spring.datasource.master.hikari.minimum-idle=5
spring.datasource.master.hikari.idle-timeout=30000
spring.datasource.master.hikari.connection-timeout=20000

# =========================================================================
# 2. Slave DB DataSource (R - Read-Only)
# =========================================================================
spring.datasource.slave.jdbc-url=jdbc:postgresql://localhost:5433/mydb
spring.datasource.slave.username=myuser
spring.datasource.slave.password=mypassword
spring.datasource.slave.driver-class-name=org.postgresql.Driver
# HikariCP 커넥션 풀 성능 최적화 세팅
spring.datasource.slave.hikari.pool-name=Slave-HikariPool
spring.datasource.slave.hikari.maximum-pool-size=10
spring.datasource.slave.hikari.minimum-idle=5
spring.datasource.slave.hikari.idle-timeout=30000
spring.datasource.slave.hikari.connection-timeout=20000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 동적 데이터소스 라우팅 (Dynamic Routing) 자바 구현 소스 코드&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) Routing Enum 정의&lt;/h3&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;public enum DataSourceType {
    MASTER, SLAVE
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) RoutingDataSource 상속 구현체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 실행되고 있는 스레드의 트랜잭션이 읽기 전용(readOnly = true) 상태인지 스프링 동기화 매니저를 통해 수시 검증하여 알맞은 DataSourceType Key를 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        return isReadOnly ? DataSourceType.SLAVE : DataSourceType.MASTER;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) Configuration 빈(Bean) 등록 설정&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DataSourceConfig {

    @Bean(name = &quot;masterDataSource&quot;)
    @ConfigurationProperties(prefix = &quot;spring.datasource.master&quot;)
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = &quot;slaveDataSource&quot;)
    @ConfigurationProperties(prefix = &quot;spring.datasource.slave&quot;)
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = &quot;routingDataSource&quot;)
    public DataSource routingDataSource(
            @Qualifier(&quot;masterDataSource&quot;) DataSource masterDataSource,
            @Qualifier(&quot;slaveDataSource&quot;) DataSource slaveDataSource) {

        RoutingDataSource routingDataSource = new RoutingDataSource();

        Map&amp;lt;Object, Object&amp;gt; dataSourceMap = new HashMap&amp;lt;&amp;gt;();
        dataSourceMap.put(DataSourceType.MASTER, masterDataSource);
        dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource);

        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(masterDataSource); // 트랜잭션이 정의되지 않았을 때의 기본 세팅

        return routingDataSource;
    }

    //   핵심 가이드: 스프링은 기본적으로 트랜잭션 수립 시점에 바로 DataSource 커넥션을 영속하려 시도합니다.
    // LazyConnectionDataSourceProxy 대리자를 거쳐야 실제 쿼리가 호출되는 런타임 타이밍에 라우팅 분기가 온전히 성사됩니다.
    @Bean
    @Primary
    public DataSource dataSource(@Qualifier(&quot;routingDataSource&quot;) DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 대규모 엔터프라이즈 환경에서의 Multi-Master 아키텍처 논의&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 Q. &quot;읽기도 2개, 쓰기도 2개의 다중 마스터 서버로 확장하려면 어떻게 해야 하나요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 기능이 작동하는 마스터 서버를 2대 이상(Multi-Master 혹은 Multi-Write) 두고 서로 실시간 교차 동기화를 시도하는 아키텍처는 기술적 난도가 극도로 올라갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 양쪽 마스터 DB에서 동일한 식별자를 가진 유저의 닉네임을 변경하는 사건이 터졌을 경우, 분산 제어 합의가 없다면 &lt;b&gt;데이터 충돌(Write Conflict)&lt;/b&gt; 현상으로 인해 데이터가 심각하게 깨지고 루프 현상이 걸리며 정합성이 완전히 붕괴됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 PostgreSQL 진영의 해결 방안&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Citus (시터스):&lt;/b&gt; 마이크로소프트 산하에서 분산 샤딩 데이터베이스 형태로 개발을 고도화하고 있는 PostgreSQL용 확장 플러그인입니다. Coordinator 노드들이 분산되어 들어오는 쓰기 쿼리를 분배하며 물리적인 Worker DB 노드들에 해시 기반 분산 적재(Sharding)를 처리함으로써 다중 쓰기 성능 병목을 해결합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Bucardo (부카르도) / Spock:&lt;/b&gt; 트리거 기술을 활용해 마스터 노드끼리 변경 사항을 양방향 복제(Bi-directional Replication)하는 복제 전용 도구입니다. 동시성 데이터 마찰이 빚어질 시 이를 조율할 명확한 복구 알고리즘 및 우선순위 세팅을 세워야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 인프라 레이어의 독립: HAProxy와 PgBouncer의 역할&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dr7oo7/dJMcajhYQYe/XEP3MZrLTFp0UUAcL6Eja1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dr7oo7/dJMcajhYQYe/XEP3MZrLTFp0UUAcL6Eja1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dr7oo7/dJMcajhYQYe/XEP3MZrLTFp0UUAcL6Eja1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdr7oo7%2FdJMcajhYQYe%2FXEP3MZrLTFp0UUAcL6Eja1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;892&quot; height=&quot;580&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 Q. &quot;HAProxy를 도입하면 결국 백엔드 코드가 프록시를 보도록 다 변경해야 하는 것 아닌가요?&quot;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설정 파일의 주소 단일화:&lt;/b&gt; 네, 맞습니다. DB 접속을 가리키는 application.properties 설정 주소는 변경해 주어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백엔드 코드의 복잡성 보호:&lt;/b&gt; 하지만 이는 단순히 백엔드 내부 properties 설정에서 수많은 개별 DB의 복잡한 주소를 다 없애버리고, 오직 &lt;b&gt;HAProxy 장비의 단 한 줄의 IP 주소&lt;/b&gt;만 바라보게 통합하는 혁신적인 설계입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# HAProxy 도입 이전: 개별 마스터, 슬레이브 노드들의 주소를 백엔드가 직접 하드코딩 형태로 전부 알았어야 함
# spring.datasource.master.jdbc-url=jdbc:postgresql://localhost:5432/mydb
# spring.datasource.slave.jdbc-url=jdbc:postgresql://localhost:5433/mydb

# HAProxy 도입 이후: 백엔드는 뒷단의 DB 대수나 물리 IP 변화와 무관하게 오직 HAProxy 주소 1개만 매핑함
spring.datasource.url=jdbc:postgresql://haproxy-loadbalancer-ip:5000/mydb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 로드 밸런싱(Load Balancing) 분배 처리와 특정 노드 장비 장애 시 서버 격리(Health Check)는 HAProxy 프록시 계층이 인프라 레벨에서 완수하기 때문에, 서비스가 확장되어 DB가 수십 대 규모로 불어나도 백엔드 코드는 수정과 재배포를 완전히 면제받습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 Q. &quot;PgBouncer는 무엇이고 어떤 성능 문제를 해결해 주나요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 접속이 한 개 수립될 때마다 운영체제의 &lt;b&gt;독립된 전용 프로세스를 일일이 생성(Process-per-connection)하는 독특한 아키텍처&lt;/b&gt;를 채택하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로세스 생성 부하의 한계:&lt;/b&gt; 백엔드 도커 컨테이너가 쏟아져 들어오는 세션을 받기 위해 동시 커넥션을 무제한 늘리기 시작하면, DB 서버 메모리가 프로세스 컨텍스트 스위칭 유지비 감당을 못하고 금세 다운되거나 마비됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PgBouncer의 압축 중개:&lt;/b&gt; PgBouncer는 백엔드와 데이터베이스의 사이에 입점하여 수많은 앱의 장기 연결 대기 상태를 가볍게 수집해 안고 있습니다. 그리고 실제 PostgreSQL 엔진을 상대로는 하드웨어 사양에 맞춰 사전에 조율된 소수(예: 30개)의 실질 물리 커넥션만을 공유합니다. 그리고 쿼리가 들어올 때만 짧게 진짜 통로를 대여해주고 환수하는 &quot;돌려막기 방식&quot;으로 DB 자원을 드라마틱하게 보호합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 HAProxy와 PgBouncer 핵심 개념 대조표&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;u&gt;&lt;b&gt; 비교 지표 &lt;/b&gt;&lt;/u&gt;&lt;/td&gt;
&lt;td&gt;&lt;u&gt;&lt;b&gt; HAProxy (로드 밸런서) &lt;/b&gt;&lt;/u&gt;&lt;/td&gt;
&lt;td&gt;&lt;u&gt;&lt;b&gt; PgBouncer (커넥션 풀러) &lt;/b&gt;&lt;/u&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;태생적 카테고리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;레이어 4 TCP/HTTP 하드웨어형 프록시 소프트웨어&lt;/td&gt;
&lt;td&gt;PostgreSQL 전문 경량 세션 관리기 (Connection Pooler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;메인 미션&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;쿼리의 성격(R/W) 및 주소에 따라 적합한 노드로 &lt;b&gt;분산 토스&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;다중 커넥션을 극소수의 물리 소켓으로 압축해 &lt;b&gt;서버 자원 수호&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;동작 원리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;헬스체크 및 트래픽 게이트웨이 분기 흐름 정의&lt;/td&gt;
&lt;td&gt;쿼리 수립 주기에 맞춰 커넥션 연결 풀 일시 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;쉬운 비유&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;오피스 빌딩에서 내방객 목적지에 맞는 엘리베이터를 안내하는 &lt;b&gt;로비 안내원&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;제한된 좌석에 대기번호표를 끊어 순서대로 순환 착석시키는 &lt;b&gt;웨이팅 매니저&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>SPRING BOOT</category>
      <category>cluster</category>
      <category>DB</category>
      <category>spring boot</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/276</guid>
      <comments>https://ch010104.tistory.com/276#entry276comment</comments>
      <pubDate>Wed, 20 May 2026 22:56:19 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] BGP와 인터넷 AS 라우팅</title>
      <link>https://ch010104.tistory.com/275</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. BGP의 개요 및 핵심 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷은 수만 개의 독립적인 네트워크 영역인 AS(Autonomous System, 자율 시스템)들의 거대한 결합체입니다. BGP(Border Gateway Protocol)는 이 수많은 AS들을 서로 연결해 주는 &quot;사실상(de facto)의 인터넷 인터-도메인(Inter-domain) 라우팅 프로토콜&quot;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인터넷의 접착제 (Glue of the Internet):&lt;/b&gt; 전 세계의 독립된 네트워크들을 하나로 묶어 거대한 글로벌 인터넷을 작동시키는 핵심 유기체 역할을 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;존재 및 도달 가능성 광고:&lt;/b&gt; 특정 서브넷(Subnet)이 자신의 존재를 인터넷 전체에 알리고, 자신이 도달할 수 있는 목적지 목록을 광고할 수 있도록 지원합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쉽게 말해, &quot;내가 여기 존재하며, 나와 내 이웃을 통해 어디로 연결해 줄 수 있는지&quot;*를 광고하는 수단입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  BGP가 각 AS에 제공하는 이중 핵심 기능&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btHJIK/dJMcah5AORM/3oGGBhqxUo375ZwQxajKIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btHJIK/dJMcah5AORM/3oGGBhqxUo375ZwQxajKIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btHJIK/dJMcah5AORM/3oGGBhqxUo375ZwQxajKIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtHJIK%2FdJMcah5AORM%2F3oGGBhqxUo375ZwQxajKIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;802&quot; height=&quot;481&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;eBGP (external BGP):&lt;/b&gt; 인접한 다른 외부 AS로부터 서브넷 도달 가능성(Reachability) 정보를 획득합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;iBGP (internal BGP):&lt;/b&gt; eBGP를 통해 얻은 외부 도달 가능성 정보를 AS 내부의 모든 라우터에 전파합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경로 결정:&lt;/b&gt; 도달 가능성 정보와 사전에 정의된 정책(Policy)을 기반으로 타 네트워크로 가는 최적의 경로를 산출합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. BGP의 기본 동작 및 세션 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BGP는 신뢰성 있는 정보 교환을 위해 네트워크 계층 프로토콜임에도 불구하고 &lt;b&gt;전송 계층의 TCP&lt;/b&gt; 위에서 동작합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1017&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/80XjB/dJMb997wGEk/65PsudRGcL8jKSkbFN2vq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/80XjB/dJMb997wGEk/65PsudRGcL8jKSkbFN2vq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/80XjB/dJMb997wGEk/65PsudRGcL8jKSkbFN2vq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F80XjB%2FdJMb997wGEk%2F65PsudRGcL8jKSkbFN2vq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1017&quot; height=&quot;292&quot; data-origin-width=&quot;1017&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BGP 세션 (BGP Session):&lt;/b&gt; 두 BGP 라우터(서로를 &lt;b&gt;&quot;피어(Peers)&quot;&lt;/b&gt; 또는 &quot;이웃(Neighbors)&quot;이라 부름)는 반영구적인 TCP 연결(Port 179)을 수립하여 BGP 메시지를 교환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경로 벡터 프로토콜 (Path Vector Protocol):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BGP는 목적지 네트워크 프리픽스(Prefix)로 가기 위해 거쳐 가야 할 &lt;b&gt;AS들의 순서가 적힌 경로 목록(Vector)&lt;/b&gt; 자체를 광고합니다. 이 덕분에 무한 루프 경로가 발생하는 현상을 원천 차단할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BGP 광고의 약속 (Promise):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, AS3의 게이트웨이 라우터가 AS2 게이트웨이에게 &lt;b&gt;&quot;path AS3, X&quot;&lt;/b&gt; 경로를 광고했다는 것은, &quot;네가 나(AS3)에게 목적지 X로 가는 패킷을 던져주면, 내가 내 내부망을 책임지고 거쳐서 X까지 확실하게 전달해 주겠다&quot;고 약속하는 것과 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. BGP 경로 속성과 정책 기반 라우팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BGP에서 단순히 하나의 경로를 수신하는 것은 '목적지 주소'만을 얻는 것을 의미하지 않습니다. BGP의 경로는 프리픽스(Prefix)와 여러 경로 속성(Path Attributes)의 조합으로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BGP Route = text + Attributes&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 가장 중요한 2대 핵심 속성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BGP 라우터가 최적의 경로를 판별하고 결정하는 데 사용하는 가장 중추적인 속성입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AS-PATH (AS 경로):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프리픽스 광고가 발신지에서 출발하여 목적지까지 도달하는 과정에서 &lt;b&gt;거쳐 온 AS들의 일련의 번호 목록&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적 1 (루프 방지):&lt;/b&gt; 만약 특정 라우터가 수신한 광고의 AS-PATH에 &lt;b&gt;자기 자신의 AS 번호&lt;/b&gt;가 포함되어 있다면, 경로에 루프가 발생했다고 판단하고 해당 광고를 즉시 폐기(Decline)합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목적 2 (경로 평가):&lt;/b&gt; 거쳐 가는 AS 개수가 적은 경로(즉, AS-PATH의 길이가 짧은 경로)를 더 좋은 경로로 우선 고려합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NEXT-HOP (다음 홉):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 AS를 벗어나 NEXT-HOP에서 가리키는 외부 AS로 진입하기 위한 &lt;b&gt;가장 첫 번째 외부 라우터 인터페이스의 IP 주소&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;주의: 일반 내부 라우팅에서의 Next-Hop은 물리적으로 인접한 바로 다음 라우터를 의미하지만, BGP의 NEXT-HOP 속성은 대개 다음 외부 AS의 경계 라우터 인터페이스를 가리킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 정책 기반 라우팅 (Policy-Based Routing)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BGP는 기술적 속도보다 회사 간 비즈니스 계약이나 경제성, 보안성을 더 최우선시합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;수입 정책 (Import Policy):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이웃 AS로부터 경로 정보를 담은 광고를 수신했을 때, 이를 라우팅 테이블에 &lt;b&gt;추가할 것인지 거부할 것인지&lt;/b&gt; 결정하는 기준입니다.&lt;/li&gt;
&lt;li&gt;예: &quot;경쟁사나 보안상 취약한 AS Y를 거쳐오는 경로는 수입 정책에서 차단하여 절대 사용하지 않는다.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;광고 정책 (Export / Advertise Policy):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가 알고 있는 목적지 경로 정보를 &lt;b&gt;이웃 AS들에게 알려줄 것인지 말 것인지&lt;/b&gt; 결정하는 기준입니다.&lt;/li&gt;
&lt;li&gt;예: &quot;나에게 돈을 내는 고객의 경로 정보는 광고하지만, 나에게 돈을 내지 않는 다른 경쟁사 ISP들끼리의 트래픽 경로 정보는 내가 알고 있더라도 광고하지 않는다.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. BGP 경로 광고 및 전파 과정 (eBGP &amp;amp; iBGP)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BGP 라우팅 광고가 실제 네트워크 망 전체로 흘러가며 AS-PATH를 생성하는 과정은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by2ion/dJMcaaL6ZtJ/EOlgts1Cwd1ni35JasirCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by2ion/dJMcaaL6ZtJ/EOlgts1Cwd1ni35JasirCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by2ion/dJMcaaL6ZtJ/EOlgts1Cwd1ni35JasirCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby2ion%2FdJMcaaL6ZtJ%2FEOlgts1Cwd1ni35JasirCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;943&quot; height=&quot;268&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단계별 흐름 예시&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;AS3&lt;/b&gt; -&amp;gt; &lt;b&gt;AS2 (eBGP 광고):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목적지 $X$를 보유한 AS3의 경계 라우터 3a가 인접한 AS2의 경계 라우터 2c에게 [AS3, X]라는 경로를 광고합니다. (서로 다른 AS 간이므로 &lt;b&gt;eBGP&lt;/b&gt; 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AS2 내부 전파 (iBGP 전파):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경로 정보를 받은 2c는 수입 정책 통과 확인 후, &lt;b&gt;iBGP&lt;/b&gt;를 통해 AS2 내부의 모든 라우터(2a, 2b, 2d)에게 이 경로를 그대로 전파합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AS2&lt;/b&gt; -&amp;gt; &lt;b&gt;AS1 (eBGP 광고 및 AS-PATH 업데이트):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AS2의 반대쪽 경계 라우터인 2a가 AS1의 경계 라우터 1c에게 이 경로를 다시 광고합니다.&lt;/li&gt;
&lt;li&gt;이때 AS2는 경로 앞에 자신의 AS 번호를 추가(Prepend)하여 광고 포맷을 [AS2, AS3, X]로 업데이트하여 전송합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  중복 경로 발생 시의 선택 (Multiple Paths)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qoe2O/dJMcaf7IhG2/ZuheXXcKk0kHpQn8GYFi4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qoe2O/dJMcaf7IhG2/ZuheXXcKk0kHpQn8GYFi4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qoe2O/dJMcaf7IhG2/ZuheXXcKk0kHpQn8GYFi4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQoe2O%2FdJMcaf7IhG2%2FZuheXXcKk0kHpQn8GYFi4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;948&quot; height=&quot;273&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 경계 라우터 1c가 다음 그림과 같이 두 가지 경로를 동시에 알게 된다면 어떻게 될까요?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;경로 1:&lt;/b&gt; AS2를 경유하는 경로 -&amp;gt; &lt;b&gt;[AS2, AS3, X]&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경로 2:&lt;/b&gt; AS3와 직접 연결된 링크를 통한 경로 -&amp;gt; &lt;b&gt;[AS3, X]&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1c는 자사 수입 정책(Import Policy)을 검토하여 &lt;b&gt;더 비용이 적고 홉 수가 짧은 [AS3, X] 경로를 선택&lt;/b&gt;합니다. 그리고 이 최종 선택된 최적 경로만을 &lt;b&gt;iBGP&lt;/b&gt;를 사용하여 AS1 내부의 동료 라우터들(1a, 1b, 1d)에게 광고합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. BGP 4대 핵심 메시지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BGP 피어 관계가 맺어지고 유지되는 동안 TCP 세션 내부에서는 아래의 &lt;b&gt;4가지 규격화된 메시지&lt;/b&gt;가 오고 갑니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt; 메시지 타입 (Type) &lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt; 주요 역할 및 기능 &lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;u&gt; 상세 설명 &lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;OPEN&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;TCP 연결 개시 및 피어 관계 설정&lt;/td&gt;
&lt;td&gt;상대 피어와 TCP 연결을 성공적으로 맺은 직후 전송되며, AS 번호, BGP 버전, 인증 키 등을 확인해 정상 세션을 맺는 악수(Handshake) 역할을 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;UPDATE&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;새로운 경로 광고 및 만료된 경로 철회&lt;/td&gt;
&lt;td&gt;네트워크에 변경 사항이 생겼을 때 전송되는 핵심 메시지입니다. 신규 목적지($Prefix$+$Attributes$) 정보를 알리거나, 유효하지 않게 된 기존의 '철회 경로(Withdrawn Routes)' 목록을 배포합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;KEEPALIVE&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;세션의 정상 상태 유지 및 확인&lt;/td&gt;
&lt;td&gt;업데이트할 경로가 오랫동안 없어도, 상대방과의 BGP 세션이 죽지 않고 살아있음을 주기적으로 확인하기 위해 보냅니다. 또한 OPEN 요청에 대한 수락(ACK) 응답 역할도 수행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;NOTIFICATION&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;에러 발생 통보 및 세션 즉시 종료&lt;/td&gt;
&lt;td&gt;메시지에 오류가 감지되었거나(포맷 에러 등), 피어 타이웃 등으로 연결을 더 이상 유지할 수 없을 때 에러 상세를 상대방에게 통보하고 BGP 연결을 끊어버립니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. BGP와 도메인 내부 라우팅(IGP/OSPF)의 결합&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BGP가 외국의 주소(목적지 X)를 수집하여 자사 AS 내부 동료 라우터들에게 iBGP로 알려주더라도, 내부 라우터들이 그 게이트웨이까지 가는 경로를 모르면 아무 소용이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 외부 라우팅(BGP)과 내부 라우팅(IGP/OSPF)은 서로 유기적으로 상호 작용하며 최종 포워딩 테이블(Forwarding Table)을 결정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚙️ 동작 메커니즘 예시 (라우터 1d 시점)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1105&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVBYW4/dJMcad26DAY/rtOKLStNdiNKFqQJpUkYf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVBYW4/dJMcad26DAY/rtOKLStNdiNKFqQJpUkYf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVBYW4/dJMcad26DAY/rtOKLStNdiNKFqQJpUkYf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVBYW4%2FdJMcad26DAY%2FrtOKLStNdiNKFqQJpUkYf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1105&quot; height=&quot;508&quot; data-origin-width=&quot;1105&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;iBGP 정보 확인:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라우터 1d는 iBGP 광고를 수신해 *&quot;목적지 X로 패킷을 보내려면, 우선 우리 AS의 출구인 게이트웨이 라우터 1c로 패킷을 배달해야 한다&quot;*는 사실을 배웁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OSPF(IGP) 테이블 조회:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1d는 내부 라우팅 프로토콜(OSPF)을 조회하여, 자사 네트워크 내부망 안에서 1c에게 패킷을 보내기 위한 실제 물리 인터페이스 포트가 어디인지 확인합니다.&lt;/li&gt;
&lt;li&gt;조회 결과: &quot;1c로 가려면 인터페이스 1(interface 1)을 사용하여 내보내라&quot;라는 경로를 찾아냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최종 결합 (Recursive Lookup):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 두 정보를 종합하여 1d는 자신의 최종 포워딩 테이블을 다음과 같이 완성합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목적지:&lt;/b&gt; X&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나가는 포트(Interface):&lt;/b&gt; 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 뜨거운 감자 라우팅 (Hot Potato Routing)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;뜨거운 감자 라우팅&lt;/b&gt;은 자율 시스템(AS)이 내부 트래픽 처리 비용을 최소화하기 위해 사용하는 극단적이고도 합리적인 이기주의적 라우팅 방식입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비유:&lt;/b&gt; 손에 아주 뜨거운 감자가 쥐어지면, 아무에게나 일단 가장 빨리 던져서 손에서 없애버려야 살 수 있습니다. 네트워크도 마찬가지입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동작 원리:&lt;/b&gt; 목적지까지 가기 위한 외부 인터넷 세계의 거리나 비용은 일절 무시하고, &quot;우리 AS 내부 영역에서 가장 적은 비용(Least Intra-domain Cost)으로 도달할 수 있는 가장 가까운 출구&quot;로 패킷을 일단 던져버리는 라우팅 방식입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  실제 분석 사례 (라우터 2d 시점)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQC1ck/dJMcad26DAq/YfHdKbYAUh7k2K4KYsOIu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQC1ck/dJMcad26DAq/YfHdKbYAUh7k2K4KYsOIu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQC1ck/dJMcad26DAq/YfHdKbYAUh7k2K4KYsOIu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQC1ck%2FdJMcad26DAq%2FYfHdKbYAUh7k2K4KYsOIu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;936&quot; height=&quot;300&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목적지&lt;/b&gt; X&lt;b&gt;로 가기 위한 두 출구:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;출구 A (경계 라우터 2a):&lt;/b&gt; 외부 경로는 [AS1, AS3, X] (홉 수: 2) -&amp;gt; OSPF 내부 비용: &lt;b&gt;201&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;출구 B (경계 라우터 2c):&lt;/b&gt; 외부 경로는 [AS3, X] (홉 수: 1) -&amp;gt; OSPF 내부 비용: &lt;b&gt;263&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BGP의 결정:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 경로만 보면 출구 B(2c)를 쓰는 것이 훨씬 효율적입니다.&lt;/li&gt;
&lt;li&gt;그러나 &lt;b&gt;뜨거운 감자 라우팅&lt;/b&gt;에 의해 라우터 2d는 내부 비용이 더 적게 소모되는(201 &amp;lt; 263) &lt;b&gt;출구 A(2a)를 선택&lt;/b&gt;해 패킷을 AS 외부로 방출합니다. 자사 네트워크 망 내부의 장비와 대역폭을 조금이라도 덜 사용하기 위함입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 현실 비즈니스 관계와 BGP 광고 정책&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzSj0L/dJMcai4o8kP/0WZafK4MuoMxH3aaaotT20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzSj0L/dJMcai4o8kP/0WZafK4MuoMxH3aaaotT20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzSj0L/dJMcai4o8kP/0WZafK4MuoMxH3aaaotT20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzSj0L%2FdJMcai4o8kP%2F0WZafK4MuoMxH3aaaotT20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;414&quot; height=&quot;196&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BGP는 상업 인터넷망의 질서를 잡기 위해 &lt;b&gt;철저하게 경제적 이익 중심&lt;/b&gt;으로 라우팅 경로 정보를 통제(광고 여부 결정)합니다. 인터넷 서비스 공급자(ISP)들은 크게 아래와 같은 계약 관계에 처하게 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Provider (공급자):&lt;/b&gt; 서비스를 제공하는 상위 대형 ISP&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Customer (고객):&lt;/b&gt; 돈을 지불하고 트래픽을 위탁하는 하위 망 또는 기업 네트워크&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;통과 트래픽 제한 정책 (No-Transit Policy)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8o56G/dJMcagMn6da/wVWVPXS5p7yZzkikb9PBt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8o56G/dJMcagMn6da/wVWVPXS5p7yZzkikb9PBt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8o56G/dJMcagMn6da/wVWVPXS5p7yZzkikb9PBt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8o56G%2FdJMcagMn6da%2FwVWVPXS5p7yZzkikb9PBt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;712&quot; height=&quot;198&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대원칙:&lt;/b&gt; ISP(공급자)는 오직 &lt;b&gt;자신의 돈을 내는 고객과 통신하는 트래픽&lt;/b&gt;만 실어 날라주기를 원합니다. 자기와 비즈니스 계약이 없는 제3자 공급자 간의 단순 연결 통로(Transit) 노릇을 해주는 것은 대역폭 낭비이므로 철저히 거부합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 시나리오 분석 (B의 영리한 광고 제어)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;네트워크 구성:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A는 w의 공급자이며, 동시에 B와 C의 고객입니다.&lt;/li&gt;
&lt;li&gt;B와 C는 동등한 레벨의 상위 공급자(ISP)입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상황:&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;A가 w로 가기 위해 자신을 거치라는 의미로 [A, w] 경로를 B와 C 모두에게 광고합니다.&lt;/li&gt;
&lt;li&gt;B는 [A, w] 경로를 학습하지만, &lt;b&gt;이를 경쟁사 대형 ISP인&lt;/b&gt; C&lt;b&gt;에게는 절대 광고하지 않기로(chooses not to advertise) 결정&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이유:&lt;/b&gt; 만약 B가 C에게 이 사실을 광고한다면, C는 w로 가는 트래픽을 무상으로 [C, B, A, w] 경로를 통해 보낼 것입니다. 이 과정에서 C, A, w 중 어느 누구도 B에게 이 경유 트래픽에 대한 비용(&lt;b&gt;'Revenue'&lt;/b&gt;)을 지불하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과:&lt;/b&gt; 이 광고 정책의 통제 덕분에 C는 B를 경유하여 w로 갈 수 있다는 것을 알지 못하게 되며, 직접 연결된 경로인 [C, A, w]를 강제로 사용하게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 내부 라우팅(Intra-AS) vs 외부 라우팅(Inter-AS) 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁극적으로 인터넷 아키텍처가 단일 프로토콜을 쓰지 않고, 내부 라우팅과 외부 라우팅을 명확히 이원화하여 구축된 이유는 다음과 같은 구조적 차이점 때문입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 180px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;&lt;u&gt; 비교 요소 &lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;&lt;u&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;AS 내부 라우팅 (Intra-AS / IGP)&lt;/span&gt; &lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;&lt;u&gt; AS 간 라우팅 (Inter-AS / EGP) &lt;/u&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;주요 적용 프로토콜&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;OSPF (Open Shortest Path First), RIP&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;BGP (Border Gateway Protocol)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;정책 통제 수준 (Policy)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;낮음 (조직이 단 하나이므로 복잡한 비즈니스 정책 개입이 없음)&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;매우 높음&lt;/b&gt; (관리자가 자신의 트래픽과 지나가는 트래픽 흐름을 완벽히 통제해야 함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;확장성 관리 (Scale)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;중소 규모에 적합 (모든 라우팅 정보를 공유하므로 규모 성장에 한계 존재)&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;초거대 규모&lt;/b&gt; (인터넷 전체를 계층화하여 라우팅 테이블 크기와 부하를 극적으로 절감)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;최적화 성능 (Performance)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;극대화 지향&lt;/b&gt; (최단 거리, 지연 최소화 등 오직 물리적 성능에 집중)&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;정책이 성능보다 우위&lt;/b&gt; (아무리 우수하고 빠른 최단 경로가 있어도 비즈니스 이익 정책이 앞섬)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>NETWORK</category>
      <category>CS</category>
      <category>Network</category>
      <author>ch010104</author>
      <guid isPermaLink="true">https://ch010104.tistory.com/275</guid>
      <comments>https://ch010104.tistory.com/275#entry275comment</comments>
      <pubDate>Wed, 20 May 2026 18:13:22 +0900</pubDate>
    </item>
  </channel>
</rss>