<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Purple like a sunset</title>
    <link>https://bora-ovo.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 1 Aug 2026 06:14:49 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>bora-ovo</managingEditor>
    <image>
      <title>Purple like a sunset</title>
      <url>https://tistory1.daumcdn.net/tistory/7623224/attach/fdcd8a15004e4fe29d4604254e3997fe</url>
      <link>https://bora-ovo.tistory.com</link>
    </image>
    <item>
      <title>thingo 프로젝트 성능 개선 2</title>
      <link>https://bora-ovo.tistory.com/7</link>
      <description>&lt;h1&gt;&lt;b&gt;2. 문제 후보 카테고리&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 정의가 끝났다면 그 다음은 &amp;ldquo;그 문제가 어디서 생기는지&amp;ldquo; 보아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 문제는 겉으로는 전부 &amp;ldquo;느림&amp;rdquo;으로 보일 수 있지만 실제 원인은 매우 다르기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 기능은 인덱스 부족 때문일 수 있고, 어떤 기능은 JPA fetch 문제일 수 있으며, 어떤 기능은 애초에 구조가 부적절하여 느릴 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgresSQL 문서에서도 적절한 쿼리 계획과 인덱스 선택이 성능에 매우 중요하다고 설명하고, Hibernate 문서에서는 fetching 전략에 따라 N+1 같은 비효율이 발생할 수 있다고 설명했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 thingo 프로젝트의 성능 문제 후보에 대해 아래와 같이 카테고리화를 진행하였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2-1. DB 인덱싱 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 인덱스 설계 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 인덱스가 WHERE, JOIN 조건에 쓰이는 컬럼을 빠르게 찾는 게 중요하다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 자주 조회되는 조건 컬럼에 인덱스가 없으면 전체 스캔 비용이 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 비교적 구조적으로 분석하기에 쉽기도 하고 실행 계획으로 근거를 제시하기에 용이하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 EXPLAIN 같은 경우에도 쿼리 플랜을 분석하여 어떤 접근 방식이 선택됐는지 보여주기 때문에 인덱싱 문제는 성능개선 파트에서 가장 먼저 점검하기 좋은 항목이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2-2. 쿼리 작성 및 ORM 사용 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 쿼리 자체가 비효율 적인 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스가 있더라도 쿼리 구조가 좋지 않으면 성능은 여전히 나쁠 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate의 문서에서는 연관 객체 로딩 방식에 따라 N+1 문제를 유의해야 한다고 말한다.&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;우리 프로젝트에서는 N+1 조회 문제, 불필요한 join을 다회 사용하는 문제, 필요한 컬럼보다 더 많은 데이터를 조회하는 문제, 목록 조회에서 pagination의 비효율 문제, 같은 데이터를 반복 조회하는 문제, fetch 전략이 실제 사용 패턴과 맞지 않는 문제 등이 있을 수 있다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 Spring/JPA 기반 프로젝트에서는 성능 저하가 체감되는 부분이 DB 자체보다는 ORM 사용 방식에서 발생하기 쉽기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 DB가 느리다기 보다는 DB를 느리게 쓰고 있다의 문제일 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate는 N+1과 같은 문제를 join fetch, batch fetching, subselect fetching 등으로 줄일 수 있다고 설명했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2-3. 애플리케이션 로직 및 코드 구조 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 계층에서의 로직, 반복 계산, 불필요한 데이터 변환 등 애플리케이션 코드 자체의 비효율을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 카테고리는 DB와 연관이 있기 보다는 Java &amp;amp; Spring 코드에서의 문제를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 동일한 요청에서 같은 계산을 반복하거나, 한 번만 해도 되는 데이터를 여러 번 가공하거나, 외부 호출을 순차적으로 묶어놓아 전체 시간이 길어지는 경우 등이 있을 것이다.&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;우리 프로젝트에서는 한 요청 안에서 같은 서비스or레포지토리 호출이 반복되는 경우, 불필요한 DTO 변환이나 리스트 재가공이 많은 경우, 순차 처리 때문에 전체 응답시간이 늘어나는 경우 등이 있을 수 있다.&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;이 문제에서는 성능문제가 항상 SQL, DB와 관련되어서만 발생하는 것이 아니라는 것을 상기시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 DB 시간 보다 서비스 로직 시간이 더 길 수 있기 때문에 유의해야한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2-4. 요청 - 응답 흐름 및 병목 구간 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 들어와서 응답이 나가기까지의 전체 파이프라인 중 특정 구간이 병목인 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 endpoint 하나를 전체로 보지말고, Controller &amp;rarr; Service &amp;rarr; Repository &amp;rarr; DB &amp;rarr; 외부 API &amp;rarr; 응답 직렬화 로 이어져 있다는 기본적인 것을 상기시켜야한다.&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;우리 프로젝트에서는 외부 API 호출이 느려 전체 응답이 지연되는 경우, 응답 데이터가 너무 커서 직렬화 또는 전송의 비용이 증가하는 경우, 파일 또는 대용량 JSON 처리에서 병목이 발생하는 경우 등이 있을 수 있다.&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;단순히 DB 문제일 것 같다는 추측보다는 이 과정에서 실제 병목되는 구간을 더 정확히 찾을 수 있도록 해야한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2-5. 시스템 아키텍쳐 및 작업 분리 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 역할을 한 곳에 과도하게 몰아둔 경우이다.&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;우리 프로젝트에서는 사용자 요청 처리와 배치&amp;amp;크롤링이 같은 인스턴스에서 발생하는 경우, 읽기 비중이 높은데 읽기 최적화 구조가 부족한 경우, 동기로 처리할 피룡가 없는 작업임에도 요청 흐름 안에 포함되어있는 경우, 확장성을 고려하지 않고 단일 구조로 개발한 탓에 부하에 취약한 경우 등이 있을 수 있따.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 무리한 아키텍쳐 리팩토링이 필요하다면 장기적으로 진행해야할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2-6. 배치&amp;amp;크롤링&amp;amp;주기 작업 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롤링 스케줄러, 배치, 데이터 수집 작업 관련 성능 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 사용자 API와는 별도로 진행되는데, 이유는 내부 작업이 눈에 보이지 않더라도 CPU, 메모리, 네트워크, DB를 차지 하기 때문에 사용자 요청 성능에서 영향을 끼칠 우려가 있기 때문이다.&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;우리 프로젝트에서는 크롤링 주기가 필요 이상으로 짧은 경우, 중복 수집or저장으로 자원을 낭비하는 경우, 스케줄링 시간이 사용자 활동 시간과 겹치는데 해당 시간에 성능저하가 발생하는 경우, 크롤링 실패or재시도가 과도하게 발생하는 경우, 사용자 요청과 내부 배치가 같은 DB/서버를 동시에 사용하는 경우 등이 있을 수 있다.&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;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2-7. 관측/측정 체계 부족 문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 저하 그 자체는 아니지만 성능 문제를 확인할 수 없는 상태를 뜻한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;측정이 없기에 개선도 성능 저하도 증명할 수 없는 상황이다.&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;예를 들어 우리의 공지사항 API가 느린데, 로그가 없으면 DB가 느린지, DTO 변환이 느린지, 정렬이 느린지 등등 알 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 문제가 있을 때 문제를 모른다 라고 말하기 보다는 문제를 진단할 도구가 부족한 상황이라는 상황을 제대로 전달하기 위해 넣어둔 카테고리 이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2-8. 문제 카테고리 우선순위&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리와 같은 일반적인 웹 백엔드 프로젝트에서는 아래와 같이 우선적으로 점검하는 것으로 결정하였다.&lt;/p&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;/li&gt;
&lt;li&gt;쿼리/ORM 문제&lt;/li&gt;
&lt;li&gt;인덱싱 문제&lt;/li&gt;
&lt;li&gt;배치&amp;middot;크롤링 문제&lt;/li&gt;
&lt;li&gt;애플리케이션 로직 문제&lt;/li&gt;
&lt;li&gt;아키텍처 문제&lt;/li&gt;
&lt;li&gt;관측 체계 부족 문제&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>bora-ovo</author>
      <guid isPermaLink="true">https://bora-ovo.tistory.com/7</guid>
      <comments>https://bora-ovo.tistory.com/7#entry7comment</comments>
      <pubDate>Sun, 22 Mar 2026 21:46:20 +0900</pubDate>
    </item>
    <item>
      <title>thingo 프로젝트 성능 개선 1</title>
      <link>https://bora-ovo.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;내가 백엔드로 참여하고 있는 MJS 프로젝트가 thingo로 명칭을 바꾸게 되면서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 새로운 기능 개발 보다는 성능 개선 파트를 집중적으로 담당하며 진행하기로 하였다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 문제로 볼 기준, 2. 문제 후보 카테고리 를 정리한 후&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;1. 한 기능에서 어떤 문제가 나타나고 있는지, 2. 개선 방향성 잡기, 3. 개선 진행 후 결과의 형태로 나타낼 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;1. 문제로 볼 기준&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 문제는 단순히 &amp;ldquo;느린 것 같다.&amp;rdquo;, &amp;ldquo;서버가 힘들어 보인다&amp;rdquo;, &amp;ldquo;코드가 비효율적인 것 같다&amp;rdquo; 같이 감각적인 판단으로 해서는 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google SRE는 사용자에게 제공되는 시스템을 바라볼 때 최소 latency, traffic, errors, saturation 네 가지 신호를 체크해야한다고 말했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 AWS Well-Architected의 Performance Efficiency관점에서도 성능은 &amp;ldquo;더 빠르게 보이느냐&amp;rdquo; 뿐 아니라 요구사항을 만족하는 수준으로 자원을 효율적으로 사용하느냐의 문제라고 말했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 성능 문제라는 것은 단순 체감이 아닌 응답시간, 처리량, 에러율, 자원사용량, 병목 위치를 기준으로 잡는 것이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1-1. 성능 문제의 1차 정의&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 사용자 요청 또는 내부 작업이 기대한 시간 안에 처리되지 않거나, 처리 과정에서 과도한 자원 사용&amp;middot;에러&amp;middot;병목을 유발하여 서비스 품질을 떨어뜨리는 상태&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자 시점에서의 &amp;ldquo;느림&amp;rdquo; 뿐 만 아니라 서버 내부의 &amp;ldquo;비효율&amp;rdquo; 또한 포함한다.&lt;/li&gt;
&lt;li&gt;API 요청 뿐 만 아니라 크롤링, 배치, 동기화 같은 내부 작업도 성능 범위에 포함시킬 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1-2. 띵고 프로젝트에서 사용할 수 있는 구체적 판단 기준&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;(1) 응답시간이 과도하게 길다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 성능에서 가장 직접적으로 체감되는 기준은 응답시간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 사용자 경험 기준에서 반응은 대략 50~200ms 수준이 바람직하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1초를 넘어가면 지연이 체감되기 시작한다고 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 thingo에서 주요 API나 핵심 화면 로딩이 해당 범위를 반복적으로 크게 넘긴다면 성능 문제로 체크해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 번 느리게 나왔다 (X), 지속적으로 체감 지연이 발생한다. (O)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;(2) 요청량이 늘어날 때 처리 효율이 급격히 떨어진다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SRE에서 latency와 함께 traffic을 같이 보는 이유는, 평소에는 괜찮던 시스템이 요청량이 많아질 때 갑자기 느려지는 경우가 많기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 평상시 속도만이 아닌 부하가 걸렸을 떄 얼마나 안정적으로 처리되는로 판단해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 번 호출했을 때 빠르냐 (X), 지속적인 요청이 많을 때에도 유지되느냐(O)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;(3) 에러율 상승이 성능 저하와 함께 나타난다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 문제를 단순 속도의 문제로만 보면 안되는 이유는, 성능 저하가 종종 에러의 증가와 같이 오기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google SRE는 error를 핵심 지표 중 하나로 보고, 사용자 입장에서는 응답 속도 보다 실패한 응답에 대해 더 직접적인 품질저하라고 느낀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 HTTP 상태코드 기준으로도 4xx/5xx는 요청 실패 또는 서버 문제를 의미하기 때문에 느려짐과 함께 이러한 응답이 많아진다면 성능 관점의 장애가 있다고 판단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;(4) CPU, 메모리, DB커넥션, I/O 등 자원 포화가 발생한다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SRE의 네 번째 신호인 saturation은 시스템 자원이 어느 정도 차있는지, 즉 병목이 어디서 생기는지 파악하는 데에 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 느리다고 느끼지만 실제 원인은 CPU 포화일 수 있고, 혹은 DB 커넥션 고갈, 디스크 I/O 병목일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 응답이 늦다는 것에만 머무르기 보다는 왜 늦어지는가를 자원 포화 기준으로 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기주은 특히 EC2와 같은 제한된 환경이나 단일 서버 구조에서 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 크롤링, 배치, API 요청 처리가 같은 자원을 공유하면 특정 시간대에 서로 영향을 줄 가능성이 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;(5) 시스템 내부에서 병목 지점을 설명할 수 있다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller - Service - Repository - DB - 외부 API - 직렬화/역직렬화 중 어디서 시간이 가장 많이 소요되는지를 설명할 수 있을 때 제대로 정의 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenTelemetry는 observability를 &amp;ldquo;내부를 직접 보지 않고도 왜 이런 일이 일어나는지 질문할 수 있게 하는 능력&amp;rdquo;이라고 말한다,.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 성능 문제 정의는 측정 가능성과 원인 추적 가능성이 포함되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 우리의 thingo 프로젝트의 성능 문제는 아래처럼 정의하는 것이 더 좋다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색 API가 느리다 (X)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색 API 전체 1.8초 중 DB 조회가 1.2초를 차지해 병목으로 추정된다. (O)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;목록 조회가 느리다 (X)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목록 조회 시 연관 엔티티 접근 때문에 추가 쿼리가 반복 실행된다 (O)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1-3. 문제 기준을 위와 같이 잡은 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 기준을 종합한다면 thingo 프로젝트에서 성능 문제는 아래의 네 질문으로 요약가능하다.&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;요청이 늘면 더 나빠지는가&lt;/li&gt;
&lt;li&gt;에러나 타임아웃이 함께 증가하는가&lt;/li&gt;
&lt;li&gt;어떤 자원이나 처리 단계가 병목인지 설명 가능한가&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 네 가지는 latency, traffic, errors, saturation이라는 검증된 운영 지표와 연결되므로, 성능개선의 출발 기준으로 쓰기에 적절하다.&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;&amp;nbsp;&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;&amp;nbsp;&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;span style=&quot;color: #666666;&quot;&gt;출처&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sre.google/sre-book/monitoring-distributed-systems/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sre.google/sre-book/monitoring-distributed-systems/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/indexes-intro.html?&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.postgresql.org/docs/current/indexes-intro.html?&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/using-explain.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.postgresql.org/docs/current/using-explain.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.hibernate.org/orm/5.0/userguide/en-US/html/ch09.html?&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.hibernate.org/orm/5.0/userguide/en-US/html/ch09.html?&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/pdfs/wellarchitected/latest/performance-efficiency-pillar/wellarchitected-performance-efficiency-pillar.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/pdfs/wellarchitected/latest/performance-efficiency-pillar/wellarchitected-performance-efficiency-pillar.pdf&lt;/a&gt;&lt;/p&gt;</description>
      <category>BackEnd/성능개선</category>
      <author>bora-ovo</author>
      <guid isPermaLink="true">https://bora-ovo.tistory.com/6</guid>
      <comments>https://bora-ovo.tistory.com/6#entry6comment</comments>
      <pubDate>Mon, 16 Mar 2026 17:18:40 +0900</pubDate>
    </item>
    <item>
      <title>JWT + WebSocket + Stateless 환경에서 실시간 채팅 구현 시 알아야 할 핵심 정리</title>
      <link>https://bora-ovo.tistory.com/5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 채팅 기능은 일반적으로 HTTP 통신만으로는 사용성이 떨어지기 때문에 WebSocket을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 WebSocket은 기본적으로 statefule 프로토콜이기 때문에 JWT 기반의 SessionStateLess 서버 구조에서는 주의해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 WebSocket의 기본, HTTP와의 차이, 그리고 JWT 기반 Stateless를 어떻게 유지할지까지 실제 개발에 들어가기 전 확인할 이론을 정리한다.&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;&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;736&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8GLlg/dJMcadNVnnN/bOxyoUWriwqGgFv3rkrKwk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8GLlg/dJMcadNVnnN/bOxyoUWriwqGgFv3rkrKwk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8GLlg/dJMcadNVnnN/bOxyoUWriwqGgFv3rkrKwk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8GLlg%2FdJMcadNVnnN%2FbOxyoUWriwqGgFv3rkrKwk%2Fimg.jpg&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;414&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. WebSocket이란?&amp;nbsp;&lt;/b&gt;&lt;/h2&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;u&gt;양방향 데이터를 실시간으로 주고받을 수 있는 프로토콜&lt;/u&gt;&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 채팅이나 알림, 증권 시세나 게임 서버와 같이 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;u&gt;&lt;b&gt;실시간성이 중요한 작업&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;에서 주로 사용한다.&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;여기서 사용자가 웹소켓을 사용하려고 할 때, 처음부터 웹소켓으로 요청이 시작되는 것은 아니고 일반 HTTP 요청을 보낸 후 WebSocket 연결로 변경하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 서버는 사용자에게 HTTP 요청을 받고 웹소켓이 가능하다면 연결을 웹소켓으로 변경하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후부터는 같은 TCP 연결 위에서 웹소켓 전용 프레임의 형식으로 데이터를 주고 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 물리적으로 연결된 TCP 소켓은 그대로 유지하되, 그 위에서 돌아가는 프로토콜만 HTTP에서 웹소켓으로 변경한다.&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;u&gt;&lt;b&gt;처음엔 HTTP로 요청을 보낸 후 WebSocket으로 바꾸는 과정을 Handshake&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;라고 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. WebSocket을 사용하는 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 HTTP 기반 요청 및 응답은 실시간으로 이루어지는 작업에 있어 비교적 약하기 때문이다.&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;HTTP는 요청이 있어야만 응답이 가능하다는 사실은 다들 알고 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 HTTP로 실시간 채팅을 구현한다면 매번 사용자가 서버로 요청을 날려야하는데, 이 때 서버 및 네트워크는 지연이 발생하기 쉽고, 또한 과부하의 위험도 큰 편이다.&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;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;WebSocket의 경우 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;HTTP 처럼&amp;nbsp;연결이&amp;nbsp;&lt;/span&gt;요청 이후에 종료되는 것이 아니라 한 번의 HTTP 요청으로 연결이 지속&lt;/span&gt;&lt;/b&gt;된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 필요 시에 즉시 발신 및 수신이 가능하여 실시간 채팅에 적합하다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. HTTP와 WebSocket의 차이 한 눈에 보기&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 86px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;width: 39.7286%; height: 17px;&quot;&gt;HTTP&lt;/td&gt;
&lt;td style=&quot;width: 42.0543%; height: 17px;&quot;&gt;WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;연결방식&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 39.7286%; height: 17px;&quot;&gt;요청 후에 종료되기 떄문에 매번 요청을 해야한다.&lt;/td&gt;
&lt;td style=&quot;width: 42.0543%; height: 17px;&quot;&gt;1번의 HTTP 요청으로 연결이 지속된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;방향성&lt;/td&gt;
&lt;td style=&quot;width: 39.7286%; height: 17px;&quot;&gt;단방향적 (요청을 보내야만 응답이 올 수 있기 때문)&lt;/td&gt;
&lt;td style=&quot;width: 42.0543%; height: 17px;&quot;&gt;양방향적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 18px;&quot;&gt;상태&lt;/td&gt;
&lt;td style=&quot;width: 39.7286%; height: 18px;&quot;&gt;Stateless&lt;/td&gt;
&lt;td style=&quot;width: 42.0543%; height: 18px;&quot;&gt;Stateful&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;사용목적&lt;/td&gt;
&lt;td style=&quot;width: 39.7286%; height: 17px;&quot;&gt;문서 및 데이터 요청&lt;/td&gt;
&lt;td style=&quot;width: 42.0543%; height: 17px;&quot;&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;* Stateful (상태 기반)&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 서버가 사용자의 상태를 기억하고 유지하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 다음 요청이 오면 이전 상태를 기억한 상태로 참고하여 처리. (ex. 전통적인 로그인 세션)&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;** Stateless (상태 없음)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 서버가 사용자의 상태를 저장하지 않는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 요청마다 필요한 정보를 모두 보내야 하며 서버에서는 모두 이를 새로운 요청으로 취급한다. (ex. JWT 기반 인증)&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. WebSocket의 특징&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 초기 연결은 HTTP로 시작한 다음 WebSocket으로 업그레이드 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 업그레이드 후에는 별도의 연결을 계속해서 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) stateful한 특성 때문에 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;연결이 끊겼을 경우 대응이 필요하다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) HTTP와 동일한 포트(80/443를 사용할 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. WebSocket 사용할 때 고려사항&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1) 연결이 유지되기 떄문에 서버 부담이 증가할 수 있다.&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 HTTP와 달리 연결이 끊이지 않고 지속적으로 유지하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 늘어날 수록 서버는 더 많은 소켓을 연결해야하고, 더 많은 메모리를 사용해야하며, 더 많은 스레드 관리를 해야한다.&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;만약 동시에 10,000명의 사용자가 접속해있다면 서버는 10,000개의 WebSocket을 모두 열린 상태로 유지해야하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 연결하면서 필요한 버퍼, 컨텍스트, 스레드가 쌓이면서 메모리 사용량이 증가하게 된다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2) 연결이 끊길 수 있음을 미리 감지하고 대응할 수 있어야 한다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 다양한 이유로 연결이 끊어질 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 인터넷이 잠시 꺼지거나, 인터넷 종류(휴대폰 LTE, WIFI)가 전화되거나, 사용자가 앱을 백그라운드로 보내버렸거나, 서버를 재배포 하면서 연결이 재설정 되었다거나...&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상황을 대비하기 위해 아래 두 가지 전략은 필수적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;✅&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt; ping/pong 프레임을 통해 연결상태 확인하기&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서버 또는 사용자가 일정 주기로 ping을 보내고, 상대가 pong으로 응답하는 방식.&lt;/li&gt;
&lt;li&gt;응답이 없으면 연결이 끊어진 것으로 판단한 후 정리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #f89009; text-align: start;&quot;&gt;✅&lt;span style=&quot;color: #009a87;&quot;&gt; 사용자의 재연결을 위한 로직을 만들어두기&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;연결이 끊어졌을 때 자동으로 재연결을 시도하고, 백오프(재시도하는 시간을 점점 증가시킴)를 하며, 재연결 시 마지막으로 받은 메시지 이후부터 다시 불러오는 이러한 기능이 없다면 실제로 사용하는 환경에서는 채팅이 자주 끊기면서 심지어는 메시지가 누락되는 문제까지 생길 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 WebSocket 연결은 보통 하나의 서버에서 지속되지만,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버 과부화 등의 이유로 인해 서버를 둘로 나누었다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;메시지 브로커&lt;/b&gt;&lt;/span&gt;&lt;/u&gt;가 필수이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메시지 브로커 없이 여러 서버를 띄우면 A서버의 이용자와 B서버의 이용자는 서로 연결될 수 없기 때문이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. Spring에서의 WebSocket 주요 컴포넌트&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1889&quot; data-start=&quot;1845&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;@EnableWebSocket&lt;/b&gt;&lt;/span&gt; : WebSocket 설정 클래스임을 명시&lt;/li&gt;
&lt;li data-end=&quot;1889&quot; data-start=&quot;1845&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;WebSocketConfigurer.configureWebSocketHandlers()&lt;/span&gt; :&lt;/b&gt; 핸들러 등록&lt;/li&gt;
&lt;li data-end=&quot;1889&quot; data-start=&quot;1845&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;TextWebSocketHandler :&lt;/span&gt;&lt;/b&gt; 텍스트 메시지를 처리할 핸들러 구현&lt;/li&gt;
&lt;li data-end=&quot;1889&quot; data-start=&quot;1845&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;afterConnectionEstablished :&lt;/span&gt; &lt;/b&gt;연결 성공 시 실행&lt;/li&gt;
&lt;li data-end=&quot;1889&quot; data-start=&quot;1845&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;handleTextMessage&lt;/b&gt;&lt;/span&gt; : 메시지 수신 시 실행&lt;/li&gt;
&lt;li data-end=&quot;1889&quot; data-start=&quot;1845&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;handleTransportError&lt;/b&gt;&lt;/span&gt; : 오류 발생 시 실행&lt;/li&gt;
&lt;/ul&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;/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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. WebSocket에서 JWT 인증이 필요한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 연결시점에서 사용자가 인증된 사용자인지 확인해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 HTTP 처럼 매 요청마다 자신의 인증정보를 보내지 않고, 이후엔 인증절차가 없이 연결되어있기 때문이다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;like. 에버랜드 입장권&lt;/span&gt;&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;span style=&quot;color: #000000;&quot;&gt;따라서 연결한 사용자 정보를 미리미리 관리하지 않으면 누가 메시지를 보내는 지 알 수 없기 때문에 보안 문제가 생길 수 있다.&lt;/span&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. WebSocket + JWT 인증 방식&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. WebSocket에서 JWT는 필수이다.&lt;/span&gt;&lt;/span&gt;&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 이유는 HTTP는 요청마다 Authorization Header를 넣기 떄문에 매 요청마다 인증 상태를 확인할 수 있지만,&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;WebSocket은 한 번 연결되면 요청 및 응답에서의 구조가 없고 연결이 계속 유지되기 때문에 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Handshake 단계에서 인증 과정을 확실하게 거쳐야 하기 떄문이다.&lt;/span&gt;&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;&lt;u&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. WebSocket을 쓰면서도 JWT를 쓰려면 결국 JWT의 기반인 Stateless 환경이 되어야 한다.&lt;/span&gt;&lt;/span&gt;&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다면 WebSocket에서 인증된 사용자 정보를 WebSocketSession에 저장하게 한다면 어떻게 될까?&lt;/span&gt;&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;u&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. WebSocketSession에 로그인 상태를 저장해서는 안된다.&lt;/span&gt;&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그인 상태를 WebSocketSesssion에 저장하게 되면,&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 사용자 상태를 서버에 저장하지 않는 Stateless에서 벗어나게 되어&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; JWT 인증의 본질이 무너지기 때문에 stateful 서버가 되어서 확장성이 떨어진다.&lt;/span&gt;&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;4. 그래서 인증과정은 첫 요청인 HandShake 과정 속에서 한 번만 진행을 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기서 유효성이 검증이 된다면 JWT 안에 있는 &lt;b&gt;&lt;u&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;User의 ID나 name같은 식별값&lt;/span&gt;&lt;/u&gt;&lt;/b&gt;을 꺼낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;u&gt;&lt;b&gt;그 식별값을 WebSocket의 Principal로 설정&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;하게 된다.&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;5. 이후 WebSocket 연결이 성공하고 나면 이후 메시지 통신단계에서는 Authorization 헤더가 없기 떄문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 라우팅, 누가 어떤 방에 들어왔는지, 누가 메시지를 보냈는지 이러한 판단을 할 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt; 세션ID가 아닌 Principal.getName() 혹은 JWT에서 꺼낸 UserId로 사용자를 식별&lt;/span&gt;&lt;/u&gt;&lt;/b&gt;한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. WebSocket 연결 중에 JWT가 만료된다면?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;WebSocket은 계속 연결되어있다보니 비교적 장시간 연결되어있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 토큰이 만료되는 상황이 올 수 있다.&lt;/span&gt;&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;span style=&quot;color: #000000;&quot;&gt;이 때는 아래 두 가지 방법으로 해결한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1) 서버가 재연결을 강제한다.&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: &lt;b&gt;&lt;u&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;access token이 만료되면 서버가 연결을 끊어버리고, 사용자가 refresh token으로 재발급을 받은 후 다시 연결한다.&lt;/span&gt;&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 입장에서는 구조가 단순하기 때문에 쉽게 구현할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유효하지 않은 토큰으로 계속 통신하는 상황을 막을 수 있다,&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단점&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;토큰이 만료 될 떄 마다 연결이 끊기기 떄문에 사용자가 불편함을 겪을 수 있음(채팅방 복원, 재연결 로직 등)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2) 메시지 안에 JWT를 포함하여 매번 검증한다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: &lt;b&gt;&lt;u&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;WebSocket 메시지 속 하나하나에 access token을 같이 넣어서 보내는 방식&lt;/span&gt;&lt;/u&gt;&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 메시지를 받을 떄마다 토큰을 검증하고, 만료 및 위조 여부를 체크하며 유효하면 메시지를 전송하고, 아니면 거부한다.&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;WebSocket을 쓰지만 HTTP처럼 요청마다 인증을 하는 느낌이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이 방식이 오히려 완전한 Stateless에 더 가깝다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;토큰이 만료되거나 블랙리스트에 올라가면 그 즉시 차단이 가능하다.&lt;/li&gt;
&lt;li&gt;서버입장에서 보면 HTTP 인증방식과 거의 동일한 방식이니 통일성이 있다.&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: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모든 메시지에 토큰이 붙기 때문에 payload가 커진다.&lt;/li&gt;
&lt;li&gt;채팅 메시지가 짧든 길든 JWT 문자열이 길기 때문에 상대적으로 효율이 떨어진다&lt;/li&gt;
&lt;li&gt;메시지마다 인증과 파싱, 그리고 검증과정이 있어 짧은 시간에 메시지나 요청이 극한으로 발생할 떈 성능에 영향을 줄 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>BackEnd/Spring</category>
      <category>back</category>
      <category>backend</category>
      <category>HTTP</category>
      <category>JWT</category>
      <category>Spring</category>
      <category>stateful</category>
      <category>stateless</category>
      <category>WebSocket</category>
      <category>인증</category>
      <author>bora-ovo</author>
      <guid isPermaLink="true">https://bora-ovo.tistory.com/5</guid>
      <comments>https://bora-ovo.tistory.com/5#entry5comment</comments>
      <pubDate>Tue, 25 Nov 2025 00:33:17 +0900</pubDate>
    </item>
    <item>
      <title>RDB vs Elasticsearch</title>
      <link>https://bora-ovo.tistory.com/4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Elasticsearch란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Lucene 기반의 Java 오픈소스 분산형 RESTful 검색 및 분석 엔진이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방대한 양의 데이터에 대해 실시간으로 저장과 검색 및 분석 등의 작업을 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elasticsearch는 텍스트, 숫자, 위치 기반 정보, 정형 및 비정형 데이터 등 모든 유형의 데이터를 위한 무료 검색 및 분석 엔진으로 분산형 및 개방형을 특징으로 한다.&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;RDB vs Elasticsearch&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 우리에게 익숙한 RDB와 Elasticsearch의 차이점에 대해 알아보자&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;RDB는 구조적 데이터 저장&amp;middot;조회, Elasticsearch는 대용량 비정형 데이터의 빠른 검색&amp;middot;분석에 특화되어 있다.&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;RDB와 Elasticsearch는 경쟁관계라기 보다 보완관계라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 둘 중 하나가 언제나 정답이 되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 저장, 검색, 확장성 등 접근 방식이 다르기 때문에 내가 하는 프로젝트의 요구사항이 뭔지 먼저 분석하고, 그에 맞게 더 나은 선택을 해야한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 데이터 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB&lt;/b&gt;는 데이터를 테이블에 저장하고, 테이블 내붑는 행(Row)과 열(Column)로 정형화된 구조를 갖고 있다.&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;Elasticsearch&lt;/b&gt;의 경우 데이터는 &lt;b&gt;인덱스(Index)에 문서(Document) 형태로 저장&lt;/b&gt;하고 문서는 &lt;b&gt;JSON 타입&lt;/b&gt;이다. 문서의 각 속성들은 필드(Field) 역할을 하고, 이 덕분에 테이블과 행 구조보다 더 &lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;자유롭고 다양한 형태의 데이터를 담을 수 있다&lt;/span&gt;는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 스키마&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB&lt;/b&gt;는 정해진 타입이나 다양한 제약조건이 있기 때문에 엄격한 스키마가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 안정성과 일관성 측면에서는 유리하지만 유연성 측면에서는 불리하다.&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;Elasticsearch&lt;/b&gt;는 &lt;b&gt;스키마리스(Schema-less)가 가능&lt;/b&gt;하고, 데이터를 넣으면 자동으로 필드와 타입을 추론하는&lt;b&gt; 동적 매핑이 가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 필요하다면 명시적인 매핑 정의가 가능하다. 이를 통해 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;다양한 데이터 구조(텍스트, 숫자, 배열, JSON 중첩 구조) 저장&lt;/span&gt;에 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 쿼리 언어&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB&lt;/b&gt;는 기본적으로 SQL(SELECT, JOIN, GROUP BY, WHERE 등)을 사용한다.&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;Elasticsearch&lt;/b&gt;는 JSON 기반 질의인 &lt;b&gt;DSL(Query DSL)&lt;/b&gt;을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 SQL처럼 구조화된 연산보다 &lt;b&gt;검색과 분석 중심&lt;/b&gt; 질의에 특화되어있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;query&quot;: {
    &quot;match&quot;: { &quot;message&quot;: &quot;에러 로그&quot; }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 검색 기능&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB&lt;/b&gt;는 쿼리에 대한 정확한 일치 검색이 기본이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;  SELECT * FROM HUMAN
  WHERE name = &amp;lsquo;보라&amp;rsquo;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rArr; HUMAN 테이블에서 보라라는 이름을 가진 행을 검색&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;Elasticsearch&lt;/b&gt;는 &lt;b&gt;Full-text search&lt;/b&gt;(형태소 분석, 유사도 점수 기반 검색) 를 지원한다. 이는 &lt;b&gt;오타가 허용&lt;/b&gt;되고, &lt;b&gt;연관성 점수&lt;/b&gt;(relevance scoring)에 따라 검색이 가능하며, &lt;b&gt;자연어 처리&lt;/b&gt;가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 트랜잭션&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB&lt;/b&gt;는 &lt;b&gt;ACID&lt;/b&gt;를 보장한다. (Atomicity, Consistency, Isolation, Durability) 이는 원자성, 일관성, 고립성, 지속성을 뜻한다.&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;Elasticsearch&lt;/b&gt;는 &lt;b&gt;실시간 검색&lt;/b&gt;에 초점을 맞춰두고 있고, 데이터를 입력하면 즉시 검색이 가능하지만 ACID 수준의 트랙잭션은 약하다. 또한 분산환경에서&lt;b&gt; 최종 일관성&lt;/b&gt;(Eventual Consistency) 을 지향한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 확장성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB&lt;/b&gt;는 서버 성능을 높이는 방식으로 주로 수직적 확장인 &lt;b&gt;Scale-up&lt;/b&gt;을 한다. 이 때문에 일정 이상의 규모를 넘어가게 된다면 확장하는데에 한계가 있다.&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;Elasticsearch&lt;/b&gt;는 수평적 확장인 &lt;b&gt;Scale-out&lt;/b&gt;을 한다. (Scale-out은 여러 서버&amp;lt;노드&amp;gt;를 추가해 클러스터를 구성한다.) 또한 데이터를 자동으로 분산하기 때문에 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;빅데이터 처리에 적합&lt;/span&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7. 속도 최적화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB&lt;/b&gt;는 쓰기 &amp;middot; 갱신이 자주 발생하는 &lt;b&gt;OLTP 시스템에 적합&lt;/b&gt;하다. 주로 은행 계좌 관리를 예시로 들 수 있다. 인덱스(Index)를 걸면 특정 쿼리는 빨라지지만 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;대규모 텍스트 검색은 느리다.&lt;/span&gt;&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;Elasticsearch&lt;/b&gt;은 &lt;b&gt;검색과 분석(집계와 필터링)에 최적화&lt;/b&gt;되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Inverted Index(역색인)&lt;/b&gt; 구조 덕에 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;대량의 텍스트 검색 또한 빠르게 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8. 사용 사례&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB :&lt;/b&gt; 은행 시스템, ERP, 회원 관리 시스템, 회계 프로그램 등 정형화된 데이터를 관리할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Elasticsearch :&lt;/b&gt; 로그 분석(ELK Stack), 검색 엔진(ex. 쇼핑몰 상품 검색 등), 추천 시스템, 모니터링(Kibana 시각화)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;9. 저장방식&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB&lt;/b&gt;는 디스크를 기반으로 행 단위 저장을 하게 된다.&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;Elasticsearch&lt;/b&gt;는 Inverted Index(역색인)을 기반으로 하여 &amp;ldquo;보라가 책을 읽는다&amp;rdquo; 라면 &amp;ldquo;보라&amp;rdquo;, &amp;ldquo;책&amp;rdquo;, &amp;ldquo;읽는다&amp;rdquo; 처럼 &lt;b&gt;단어 단위로 색인&lt;/b&gt;해두어 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;특정 키워드가 들어간 문서를 빠르게 찾을 수 있다.&lt;/span&gt; (검색 속도 최적화된 구조)&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;&amp;nbsp;&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;따라서 RDB는 정확하고 안전하게 데이터를 관리하는데에 강점을 두고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elasticsearch는 빅데이터에서 빠르고 유연하게 검색&amp;middot;분석 하는데에 강점을 가지고 있다.&lt;/p&gt;</description>
      <category>BackEnd/Elasticsearch</category>
      <author>bora-ovo</author>
      <guid isPermaLink="true">https://bora-ovo.tistory.com/4</guid>
      <comments>https://bora-ovo.tistory.com/4#entry4comment</comments>
      <pubDate>Wed, 10 Sep 2025 14:41:43 +0900</pubDate>
    </item>
    <item>
      <title>서블릿 컨테이너 심화 (멀티스레딩, 부가기능)</title>
      <link>https://bora-ovo.tistory.com/3</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;0. 서블릿 컨테이너의 멀티스레딩&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 대의 서버 프로세스 안에서 수많은 요청을 동시에 처리하기 위해 컨테이너가 스레드를 이용해 내부적으로 작업을 분산시키는 것.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 멀티스레딩&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드(Thread) : 한 프로세스 내에서 독립적으로 실행되는 흐름의 단위&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티스레딩 : 여러 스레드를 동시에 가동시켜 병렬로 작업을 수행하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;멀티스레딩을 사용하는 이유&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 동시처리&lt;/b&gt;&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;/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;2. 서비스안정성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 요청이 오류로 인해 멈추게 되더라도 다른 스레드는 계속해서 동작함.&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;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2.&amp;nbsp; &lt;b&gt;스레드 풀&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;매 요청이 들어올 때마다 새롭게 스레드가 생기고 또 이게 종료된다고 하면 스레드 생성 및 소멸 시 비용이 큼&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;심지어 동시접속이 폭주하게 된다면 스레드 오버헤드 문제로 인해 서버가 느려지거나 서버가 다운될 가능성도 있음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 이러한 멀티스레딩이라는 메커니즘을 더 효율적으로 운영하기 위해 스레드풀이라는 기법을 사용.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 동시 작업 개수를 제한 시키면 요청이 폭주하더라도 스레드를 무한정 만들어서 서버가 나가는 일이 없어짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. 서블릿 컨테이너의 스레드 모델&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;싱글톤 서블릿 인스턴스&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 컨테이너가 서블릿 클래스를 한 번만 메모리에 올려 init() 을 거쳐 하나의 객체를 생성&lt;/p&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;다만 하나의 객체를 여러 스레드가 동시에 사용하므로,&amp;nbsp; 필드에 값을 저장하면 스레드 간 충돌이 발생할 수 있음&lt;/li&gt;
&lt;li&gt;따라서 저장은 지역변수나 동기화로 처리해야 안전함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;요청별 스레드&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 요청이 들어올 때마다 컨테이너가 스레드 풀을 통해 service(request, response) 호출을 담당할 스레드를 배정함&lt;/p&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;li&gt;다만 스레드 풀에서 꺼낸 스레드를 쓰고 나면 반드시 해당 스레드를 반납해야 함.&lt;/li&gt;
&lt;li&gt;스레드가 계속 살아있다면 리소스에 좋지 않기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 세션관리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이란. 해당 요청이 어떤 곳에서 왔는지 기억하기 위해 저장하는 곳&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 요청이 올 때 web.xml 의 session-config에 의해 자동으로 고유한 세션 ID가 생성됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 재요청을 할 시 쿠키를 응답헤더에 추가해서 다시 보냄&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;/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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 필터&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;: 서블릿이 요청을 처리하기 전과 응답을 내보낸 뒤 개발자의 코드를 넣을 수 있는 장치.&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;예를 들어 모든 요청에 대하여 문자 인코딩을 UTF-8로 강제시키거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 여부를 체크하여 인가권한이 없는 사용자의 접근을 차단할 수 있음&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드를 일관되게 한 곳에서 관리할 수 있기 떄문에 유지보수가 쉬움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6. 리스너&lt;/b&gt;&lt;/h4&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;/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;보통 어노테이션으로 @WebListener를 클래스 위에 붙이면 자동으로 등록됨&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 공통적으로 할 초기화, 정리 로직, 통계 수집, 보안 검사와 같은 기능을 핵심 기능에서 분리하여 관리할 수 있음&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>BackEnd/Spring</category>
      <author>bora-ovo</author>
      <guid isPermaLink="true">https://bora-ovo.tistory.com/3</guid>
      <comments>https://bora-ovo.tistory.com/3#entry3comment</comments>
      <pubDate>Mon, 7 Jul 2025 20:25:00 +0900</pubDate>
    </item>
    <item>
      <title>서블릿 / 서블릿 컨테이너</title>
      <link>https://bora-ovo.tistory.com/2</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;서블릿이란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 &lt;span style=&quot;color: #006dd7;&quot;&gt;자바로 작성된 웹 요청 처리기 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이다.&lt;/span&gt;&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;span style=&quot;color: #333333;&quot;&gt;클라이언트가 자바 클래스(HttpServlet)으로 작성된 HTTP 요청을 보낸다면, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;서블릿은 해당 요청을 받아 내부적으로 처리한 후 동적인 웹페이지를 생성&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;한다.&lt;/span&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;b&gt;서블릿은 정적인 웹페이지와 다르게 동적인 웹페이지라는 특징을 가진다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자의 입력, 세션 정보, 데이터베이스 조회 결과 등에 따라 매번 다른 결과를 만들어낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 정적인 웹페이지와 다르게 사용자 및 상황 별로 상호작용이 가능한 맞춤 페이지를 만들어낼 수 있다는 장점이 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;b&gt;또한 서블릿은 HTML, JSON, XML 등 다양한 형식의 응답을 지원한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로는 HTML의 응답을 지원하는데, 이는 웹 브라우저가 바로 렌더링 할 수 있기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 HTML은 전통적인 서버 렌더링 웹페이지를 만들 때 사용되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON은 AJAX 요청이나 SPA 프론트와 데이터를 교환시 사용되며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML은 레거시 시스템 연동이나 SOAP 기반 웹 서비스를 만들 때 사용된다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;b&gt;서블릿은 JAVA 스레드를 이용하여 동시처리를 진행할 수 있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 하나의 서블릿 객체를 만들고, 요청이 올 때마다 별도 스레드를 할당하여 service()를 호출하는 방식으로 동작되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 여러 클라이언트에게서 오는 요청에 대해 동시에 처리가 가능하다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 우리가 서블릿을 쓰지 않는다면 &lt;b&gt;HTTP Request Message&lt;/b&gt;를 직접 파싱해야하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대해&amp;nbsp; &lt;b&gt;HTTP Response Message&lt;/b&gt; 또한 파싱해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 복잡한 과정을 거치지 않고 서블릿 API는 HTTP 요청 메시지를 자동으로 파싱하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿은 개발자들의 편의를 위해 필수적인 요소가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;서블릿 컨테이너&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 컨테이너는 자바 기반 웹 서버 측 애플리케이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 혼자서는 동작할 수 없기에 이를 관리해주는 툴이라고 생각하면 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 인터페이스를 구현한 자바 클래스를 관리(CRUD)해주고, HTTP 요청 및 응답을 편리하게 다룰 수 있다.&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;또한 웹서버와의 소켓 통신을 지원하는데, 여기서 소켓이란 네트워크 상에서 두 프로그램이 데이터를 주고받기 위한 EndPoint 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 &lt;b&gt;TCP/IP&lt;/b&gt; 프로토콜을 사용하고 IP주소와 포트번호를 조합하여 연결한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;웹서버와의 통신을 지원&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 요청을 하면, 웹서버가 이 요청을 서블릿 컨테이너로 넘기고, 컨테이너는 적당한 서블릿에 해당 요청을 전달하여 처리하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 우리는 서블릿 컨테이너가 없다면 모든 과정을 복잡하게 진행해야하지만 서블릿 컨테이너 덕분에 알아서 연결, 전달, 응답을 생각하지 않고 진행할 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;b&gt;멀티스레드 지원 및 관리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 컨테이너는 정해진 수의 작업용 스레드를 만들어, 새로운 요청이 들어올 때마다 이 중 하나를 꺼내&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;service()&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 처리가 끝나면 해당 스레드는 다시 풀에 반환된 상태로 다음 작업을 기다리기 때문에 매번 만들거나 버리는 상황이 없어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 응답속도가 빨리지고, 메모리 사용도 효율적으로 관리할 수 있다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 내부에 변수 값을 저장할 시, 스레드간 데이터 충돌이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 상태를 유지해야하는 상황에서는 메소드 내의 지역변수만 사용하거나 &lt;b&gt;synchronized&lt;/b&gt; 블록 또는 &lt;b&gt;ThreadLocal&lt;/b&gt; 같은 방식으로 동기화 처리를 해야한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;b&gt;선언적 보안관리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 컨테이너의 선언적 보안 관리는 개발자가 소스코드를 건드리지 않고도 보안 정책을 설정할 수 있도록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;web.xml&lt;/b&gt; 파일에 &lt;b&gt;security-constraint&lt;/b&gt; 와 &lt;b&gt;login-config&lt;/b&gt;를 정의하거나, 자바 코드 위에 &lt;b&gt;@ServletSecurity&lt;/b&gt; 어노테이션을 달면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 필요 여부나 사용자 역할 별 URL 접근권한 등 보안규칙을 적용할 수 있다.&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;이렇게 설정된 보안 정책은 컨테이너에서 자동으로 검사하고 적용한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 개발자는 로직에 손대지 않고도 보안 요구사항을 빠르게 바꾸고 배포할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서블릿 생명주기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;b&gt;init()&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;객체가 처음 생성할 때만 호출되는 것으로 서블릿이 사용할 설정값을 읽거나 초기화 매개변수를 설정하고 자원(예시: DB)을 준비한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;b&gt;service()&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서블릿이 초기화된 후 요청이 올 때마다 호출된다. 여기서는 &lt;b&gt;HttpServletRequest&lt;/b&gt;와 &lt;b&gt;HttpServletResponse&lt;/b&gt; 객체를 매개변수로 받아 실제 요청 처리할 코드를 처리하고 응답을 생성한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;b&gt;destroy()&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버를 내리거나 서블릿이 더이상 필요없을 때 호출된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서블릿 내부 동작 흐름&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-30 오후 5.32.47.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZcGvk/btsOY4kUwNk/oDU5Iikr0WE1uyQT8vpajk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZcGvk/btsOY4kUwNk/oDU5Iikr0WE1uyQT8vpajk/img.png&quot; data-alt=&quot;서블릿과 서블릿 컨테이너의 동작 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZcGvk/btsOY4kUwNk/oDU5Iikr0WE1uyQT8vpajk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZcGvk%2FbtsOY4kUwNk%2FoDU5Iikr0WE1uyQT8vpajk%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;1598&quot; height=&quot;772&quot; data-filename=&quot;스크린샷 2025-06-30 오후 5.32.47.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서블릿과 서블릿 컨테이너의 동작 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 클라이언트 요청&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 브라우저나 REST 클라이언트가 특정 URL로 요청을 보내면, 이 HTTP 메시지는 웹 애플리케이션 서버로 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 서블릿 처리 대상 판별&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 서버는 해당 요청이 서블릿으로 처리해야 하는지에 대하여 검사한다. 만약 정적 리소스라면 바로 파일 시스템에서 응답하지만, 서블릿 매핑이 걸린 URL이라면 컨테이너로 넘긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. URL 매핑 검색&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 컨테이너는 요청된 경로와&lt;b&gt; web.xml&lt;/b&gt; 또는 &lt;b&gt;@WebServlet&lt;/b&gt; 어노테이션에 정의된 매핑 정보를 대조한다. 가장 구체적으로 일치하는 패턴을 찾아 어떤 서블릿이 담당해야하는지 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. Request/Response 객체 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 원시 HTTP 헤더와 바디는 곧바로 다루기 힘들기 때문에, 컨테이너가 &lt;b&gt;HttpServletRequest&lt;/b&gt;와 &lt;b&gt;HttpServletResponse&lt;/b&gt; 인스턴스를 만들어 요청정보를 캡슐화하고, 응답을 작성할 빈 객체를 준비한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. 서블릿 인스턴스 확인 및 로드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 컨테이너 내부에 이미 해당 서블릿 객체가 있는지 확인한다. 만약 없다면 클래스 로더를 통해 &lt;b&gt;.class&lt;/b&gt; 파일을 로드하고 new 연산자를 호출하여 인스턴스를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6. 초기화(init())&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 위의 과정에서 생성된 서블릿 객체에 대해&lt;b&gt; init()&lt;/b&gt; 메서드를 한 번 실행한다. 이 단계에서 데이터베이스 커넥션, 리소스 로딩 같은 1회성 시작 준비 작업이 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;7. 요청처리 (service -&amp;gt; doGet or doPost)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 준비가 끝났다면 컨테이너는&lt;b&gt; service(request, response)&lt;/b&gt; 를 호출하고, 이 메소드가 내부적으로 HTTP 메서드(GET, POST 등)를 판단해 &lt;b&gt;doGet()&lt;/b&gt;, &lt;b&gt;doPost()&lt;/b&gt; 등으로 비즈니스 로직을 수행하도록 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;8. 응답 작성&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 서블릿은 &lt;b&gt;resonse.getWriter()&lt;/b&gt;나 &lt;b&gt;response.getOutputStream()&lt;/b&gt;을 이용하여 HTML, JSON, 이미지 등 원하는 바디를 채운다. 동시에 &lt;b&gt;setStatus(), setHeader()&lt;/b&gt; 호출로 상태코드와 헤더도 설정한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;9. 응답 직렬화 및 전송&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 다 채워진 &lt;b&gt;HttpServletResponse&lt;/b&gt; 객체를 컨테이너가 받아 &lt;b&gt;HTML 프로토콜&lt;/b&gt; 형태로 &lt;b&gt;바이트 스트림&lt;/b&gt;으로 바꿔, 원래 연결된 &lt;b&gt;소켓&lt;/b&gt;을 통해 클라이언트로 되돌려 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;10. 커넥션 정리 또는 재활용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 응답이 끝나면&lt;b&gt; Connection 헤더&lt;/b&gt;를 보고 &lt;b&gt;소켓을 닫을지&lt;/b&gt;, 아니면 다음 요청을 위해 &lt;b&gt;남겨둘지&lt;/b&gt; 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;11. 서블릿 종료(destroy)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 애플리케이션이 언로드되거나 서버가 종료될 때, 컨테이너는 각 서블릿의 &lt;b&gt;destroy()&lt;/b&gt; 메서드를 호출하여 열린 자원(파일, 스레드, DB 커넥션 등)을 깔끔하게 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&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;&amp;lt;출처&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wildeveloperetrain.tistory.com/372&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wildeveloperetrain.tistory.com/372&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751291543559&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;서블릿과 서블릿 컨테이너 이해하기(Servlet, ServletContainer)&quot; data-og-description=&quot;서블릿과 서블릿 컨테이너 이해하기(Servlet, ServletContainer)서블릿(Servlet)과 서블릿 컨테이너(ServletContainer)는 자바 기반 웹 애플리케이션을 개발하는데 기본이 되고 또 자주 만나게 되는 개념입니&quot; data-og-host=&quot;wildeveloperetrain.tistory.com&quot; data-og-source-url=&quot;https://wildeveloperetrain.tistory.com/372&quot; data-og-url=&quot;https://wildeveloperetrain.tistory.com/372&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/g02Jl/hyZbAhxMYo/g2qFwYK8JyTlKIl9MlJZKK/img.jpg?width=400&amp;amp;height=292&amp;amp;face=0_0_400_292,https://scrap.kakaocdn.net/dn/bspbAs/hyZf75Nwzw/mxJtDpmjkDnVjgfJqTvIc1/img.jpg?width=400&amp;amp;height=292&amp;amp;face=0_0_400_292&quot;&gt;&lt;a href=&quot;https://wildeveloperetrain.tistory.com/372&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wildeveloperetrain.tistory.com/372&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/g02Jl/hyZbAhxMYo/g2qFwYK8JyTlKIl9MlJZKK/img.jpg?width=400&amp;amp;height=292&amp;amp;face=0_0_400_292,https://scrap.kakaocdn.net/dn/bspbAs/hyZf75Nwzw/mxJtDpmjkDnVjgfJqTvIc1/img.jpg?width=400&amp;amp;height=292&amp;amp;face=0_0_400_292');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;서블릿과 서블릿 컨테이너 이해하기(Servlet, ServletContainer)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서블릿과 서블릿 컨테이너 이해하기(Servlet, ServletContainer)서블릿(Servlet)과 서블릿 컨테이너(ServletContainer)는 자바 기반 웹 애플리케이션을 개발하는데 기본이 되고 또 자주 만나게 되는 개념입니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wildeveloperetrain.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;a href=&quot;https://programmer-may.tistory.com/202&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://programmer-may.tistory.com/202&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751291550404&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;서블릿 생명주기와 서블릿의 내부 동작 방식&quot; data-og-description=&quot;개요웹 애플리케이션이 인터넷 환경에서 널리 사용되기 시작하면서, 서버 측에서 동적인 콘텐츠를 생성하고 사용자 요청을 처리하는 효율적인 방법이 필요해졌습니다. 초기의 웹 서버는 정적&quot; data-og-host=&quot;programmer-may.tistory.com&quot; data-og-source-url=&quot;https://programmer-may.tistory.com/202&quot; data-og-url=&quot;https://programmer-may.tistory.com/202&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/X22sk/hyZf5z9pvR/SGniVnOr3wIoPwXjYrDac1/img.png?width=800&amp;amp;height=328&amp;amp;face=0_0_800_328,https://scrap.kakaocdn.net/dn/UFL16/hyZf9WPjw5/oLEGovfd9zgBwjCOweOkQk/img.png?width=800&amp;amp;height=328&amp;amp;face=0_0_800_328,https://scrap.kakaocdn.net/dn/KLmDM/hyZf3PRcmU/mPbkMnVnTl2gB2pakZwN6K/img.jpg?width=1280&amp;amp;height=1812&amp;amp;face=421_475_537_599&quot;&gt;&lt;a href=&quot;https://programmer-may.tistory.com/202&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://programmer-may.tistory.com/202&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/X22sk/hyZf5z9pvR/SGniVnOr3wIoPwXjYrDac1/img.png?width=800&amp;amp;height=328&amp;amp;face=0_0_800_328,https://scrap.kakaocdn.net/dn/UFL16/hyZf9WPjw5/oLEGovfd9zgBwjCOweOkQk/img.png?width=800&amp;amp;height=328&amp;amp;face=0_0_800_328,https://scrap.kakaocdn.net/dn/KLmDM/hyZf3PRcmU/mPbkMnVnTl2gB2pakZwN6K/img.jpg?width=1280&amp;amp;height=1812&amp;amp;face=421_475_537_599');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;서블릿 생명주기와 서블릿의 내부 동작 방식&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요웹 애플리케이션이 인터넷 환경에서 널리 사용되기 시작하면서, 서버 측에서 동적인 콘텐츠를 생성하고 사용자 요청을 처리하는 효율적인 방법이 필요해졌습니다. 초기의 웹 서버는 정적&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmer-may.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;a href=&quot;https://velog.io/@yoho98/%EC%84%9C%EB%B8%94%EB%A6%BFServlet%EA%B3%BC-%EC%84%9C%EB%B8%94%EB%A6%BF-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88Servlet-Container-y88kny7g&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@yoho98/%EC%84%9C%EB%B8%94%EB%A6%BFServlet%EA%B3%BC-%EC%84%9C%EB%B8%94%EB%A6%BF-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88Servlet-Container-y88kny7g&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751291577604&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;서블릿(Servlet)과 서블릿 컨테이너(Servlet Container)&quot; data-og-description=&quot;클라이언트의 요청을 처리하고, 그 결과를 반환하는 Servlet 클래스의 구현 규칙을 지킨 자바 웹 프로그래밍 기술자바 서블릿(Java Servlet)은 자바를 사용하여 웹페이지를 동적으로 생성하는 서버측&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@yoho98/%EC%84%9C%EB%B8%94%EB%A6%BFServlet%EA%B3%BC-%EC%84%9C%EB%B8%94%EB%A6%BF-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88Servlet-Container-y88kny7g&quot; data-og-url=&quot;https://velog.io/@yoho98/서블릿Servlet과-서블릿-컨테이너Servlet-Container-y88kny7g&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/PoMEw/hyZbBU4LnI/dhiBwoKJjDO1LjbnMh9dsK/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/sKekk/hyZcnbnQMq/QI9vTF5380N44WM0QrkIkk/img.png?width=840&amp;amp;height=859&amp;amp;face=174_479_376_699,https://scrap.kakaocdn.net/dn/bbdf2g/hyZf1YObCw/L0pBavIfXd4R298gTh7xi1/img.png?width=1147&amp;amp;height=501&amp;amp;face=0_0_1147_501&quot;&gt;&lt;a href=&quot;https://velog.io/@yoho98/%EC%84%9C%EB%B8%94%EB%A6%BFServlet%EA%B3%BC-%EC%84%9C%EB%B8%94%EB%A6%BF-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88Servlet-Container-y88kny7g&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@yoho98/%EC%84%9C%EB%B8%94%EB%A6%BFServlet%EA%B3%BC-%EC%84%9C%EB%B8%94%EB%A6%BF-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88Servlet-Container-y88kny7g&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/PoMEw/hyZbBU4LnI/dhiBwoKJjDO1LjbnMh9dsK/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/sKekk/hyZcnbnQMq/QI9vTF5380N44WM0QrkIkk/img.png?width=840&amp;amp;height=859&amp;amp;face=174_479_376_699,https://scrap.kakaocdn.net/dn/bbdf2g/hyZf1YObCw/L0pBavIfXd4R298gTh7xi1/img.png?width=1147&amp;amp;height=501&amp;amp;face=0_0_1147_501');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;서블릿(Servlet)과 서블릿 컨테이너(Servlet Container)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청을 처리하고, 그 결과를 반환하는 Servlet 클래스의 구현 규칙을 지킨 자바 웹 프로그래밍 기술자바 서블릿(Java Servlet)은 자바를 사용하여 웹페이지를 동적으로 생성하는 서버측&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>BackEnd/Spring</category>
      <category>java</category>
      <category>servlet</category>
      <category>ServletContainer</category>
      <category>서블릿</category>
      <category>서블릿컨테이너</category>
      <category>자바</category>
      <author>bora-ovo</author>
      <guid isPermaLink="true">https://bora-ovo.tistory.com/2</guid>
      <comments>https://bora-ovo.tistory.com/2#entry2comment</comments>
      <pubDate>Mon, 30 Jun 2025 22:53:47 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Interface</title>
      <link>https://bora-ovo.tistory.com/1</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 인터페이스란 무엇인가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 클래스들이 반드시 구현해야만 하는 메소드들을 정리해둔 틀&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;목적 : &lt;/b&gt;다양한 기능을 하는 클래스들 간에 공통적인 기능을 강제하기 위한 것, like 설계도/계약서&lt;/li&gt;
&lt;li&gt;&lt;b&gt;형태 : &lt;/b&gt;상수 + 추상메소드 + default/정적메소드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징 :&lt;/b&gt; 다중구현 가능, 객체 생성(new) 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 Animal이라는 interface와 강아지, 새의 class를 만들어 보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1742985747012&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 인터페이스
public interface Animal {
	// 추상 메소드 선언
    public void move();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742985910345&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Dog 클래스
public class Dog implements Animal {
	// Animal에 있는 move() 메소드 구현하기
	@Override
    public void move() {
    	System.out.println(&quot;강아지가 걸어갑니다.&quot;)
    }
}

// Bird 클래스
public class Bird implements Animal {
	// Animal에 있는 move() 메소드 구현하기
    @Override
    public void move() {
    	System.out.println(&quot;새가 걸어갑니다.&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Dog와 Bird는 걸어다닐 수 있다는 공통점을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 공통점을 Animal이라는 인터페이스에서 move()라는 추상메소드로 제공할 수 있다는 것이다.&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;/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;만약 Animal에 move() 뿐 아니라 fly()라는 추상메소드가 존재한다면, 강아지는 날지 못하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dog Class 는 Animal 인터페이스내의 모든 기능을 구현해낼 수 없어&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;해당 인터페이스를 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 인터페이스는 왜 써야할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 인터페이스가 없는 경우 클래스로만 작성한다면 중복된 메소드를 계속해서 새로 만들어주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 카드, 계좌이체, 간편결제 등의 결제 시스템을 각각 Class라고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742987173136&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결제하는 수단마다 메소드가 존재해야 함
public  class PaymentProcessor {
    public void payWithCard(Card card) {
        System.out.println(&quot;카드로 결제합니다.&quot;);
    }

    public void payWithBankTransfer(BankTransfer bankTransfer) {
        System.out.println(&quot;계좌이체로 결제합니다.&quot;);
    }

    public void payWithKakaoPay(KakaoPay kakaoPay) {
        System.out.println(&quot;카카오페이로 결제합니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;이러한 경우에는 하나의 중복된 기능을 고치려고 해도 모든 class 에서 수정을 해야하며, 새로운 방식이 도입될 떄에도 매번 새롭게 작성해주어야 하는 단점이 있다. 이를 유지보수가 힘들고, 확장성이 낮다고 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742987404280&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface PayMethod {
    void pay();  // 모든 결제 수단이 반드시 구현해야 할 돈을 지불하는 기능
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742987414017&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Card implements PayMethod {
    @Override
    public void pay() {
        System.out.println(&quot;카드로 결제합니다.&quot;);
    }
}

public class BankTransfer implements PayMethod {
    @Override
    public void pay() {
        System.out.println(&quot;계좌이체로 결제합니다.&quot;);
    }
}

public class KakaoPay implements PayMethod {
    @Override
    public void pay() {
        System.out.println(&quot;카카오페이로 결제합니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742987574121&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// No interface 예시와 같은 PaymentProcessor 이지만
// 여기서는 어떤 결제수단이던지 pay() 메소드 하나면 끝남
public class PaymentProcessor {
    public void processPayment(PayMethod method) {
        method.pay();
    }
}

public class Main {
    public static void main(String[] args) {
        PaymentProcessor processor = new PaymentProcessor();

        PayMethod card = new Card();
        PayMethod bank = new BankTransfer();
        PayMethod kakao = new KakaoPay();

        processor.processPayment(card);
        processor.processPayment(bank);
        processor.processPayment(kakao);
    }
}&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유지보수를 할 때에도 PayMethod에서만 수정하면 모든 곳에 적용할 수 있으며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 클래스만 추가하면 끝이기 때문에 확장성에도 용이하다.&lt;/p&gt;</description>
      <category>BackEnd/Spring</category>
      <author>bora-ovo</author>
      <guid isPermaLink="true">https://bora-ovo.tistory.com/1</guid>
      <comments>https://bora-ovo.tistory.com/1#entry1comment</comments>
      <pubDate>Wed, 26 Mar 2025 20:18:50 +0900</pubDate>
    </item>
  </channel>
</rss>