<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>신입 개발자의 하루</title>
    <link>https://programmer7895.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 29 Jun 2026 19:51:45 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>개발만파볼까</managingEditor>
    <item>
      <title>메시징 시스템 비교: Kafka, Redis, 그리고 전통적 MQ</title>
      <link>https://programmer7895.tistory.com/126</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대의 분산 시스템에서는 다양한 메시징 시스템이 존재하며, 각 시스템은 특정 요구사항에 따라 장점과 단점을 보입니다.&lt;br /&gt;이 글에서는 대표적인 세 가지 메시징 솔루션&amp;mdash;Kafka, Redis Pub/Sub, 그리고 전통적인 메시지 큐(RabbitMQ, ActiveMQ 등)를 비교하면서,&lt;br /&gt;어떤 상황에서 어떤 시스템을 선택하는 것이 유리한지 실제 구현 예제와 테스트 결과를 통해 살펴보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 메시징 시스템 개요 및 특징&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Kafka&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;고처리량, 확장성, 내구성(영속적인 로그 저장)&lt;/li&gt;
&lt;li&gt;토픽, 파티션, 오프셋 관리 방식을 통해 메시지 순서와 병렬 처리를 효과적으로 지원&lt;/li&gt;
&lt;li&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;대규모 데이터 스트림 처리에 적합&lt;/li&gt;
&lt;li&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;초기 설정과 운영이 상대적으로 복잡할 수 있음&lt;/li&gt;
&lt;li&gt;실시간성이 중요하면서 메시지의 영속성이 반드시 필요하지 않은 경우 오버헤드가 있을 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Redis Pub/Sub&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;메모리 기반의 빠른 실시간 메시징 기능 제공&lt;/li&gt;
&lt;li&gt;Pub/Sub 모델을 사용해 단순하고 빠른 메시지 전달&lt;/li&gt;
&lt;li&gt;다양한 데이터 구조(List, Set, Hash, Sorted Set) 지원&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;/li&gt;
&lt;li&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;메시지 영속성이 없어서 서버 재시작 시 데이터 소실&lt;/li&gt;
&lt;li&gt;다수의 생산자가 동시에 메시지를 발행할 경우 순서 보장이 어려울 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 전통적 MQ (RabbitMQ, ActiveMQ 등)&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;메시지 브로커를 통한 정교한 라우팅, 주제 기반 구독, 요청-응답 패턴 등 다양한 메시징 패턴 지원&lt;/li&gt;
&lt;li&gt;메시지 신뢰성(ack, 트랜잭션 지원) 보장&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;/li&gt;
&lt;li&gt;레거시 시스템과의 연동, JMS 표준 준수&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;Kafka에 비해 처리량 및 확장성에서 한계가 있을 수 있음&lt;/li&gt;
&lt;li&gt;시스템 설정과 운영이 상대적으로 복잡한 경우도 존재&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;3. 실제 구현 사례: Kafka vs Redis Pub/Sub&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 Kafka를 이용한 메시징 시스템&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;Spring Boot 기반 Kafka Producer와 Consumer를 설정하고, 단일 파티션 내에서 동일한 키를 사용해 순서를 보장하는 구조&lt;/li&gt;
&lt;li&gt;Producer가 /notify API를 통해 메시지를 발행하면, Consumer가 해당 토픽을 구독하고 메시지를 소비&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;부하 테스트(예: Locust를 통한 다수의 동시 요청)에서 Kafka는 단일 파티션을 활용할 경우 메시지 순서가 잘 유지되는 것을 확인할 수 있음&lt;/li&gt;
&lt;li&gt;메시지의 영속성과 오프셋 관리 덕분에 메시지 순서와 재처리(재전송) 기능이 뛰어남&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Redis Pub/Sub를 이용한 메시징 시스템&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;Redis Pub/Sub 기능을 활용해 간단하게 메시지를 발행하고 구독하는 구조&lt;/li&gt;
&lt;li&gt;메시지는 빠르게 전송되지만, Redis 서버 재시작 시 데이터가 소실되며, 다수의 생산자 환경에서 순서 보장이 어려울 수 있음&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;/li&gt;
&lt;li&gt;단일 생산자 환경에서는 순서가 어느 정도 유지되지만, 동시 다발적인 요청 상황에서는 순서 보장이 어려울 수 있음&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;4. Kafka와 WebSocket 연동: 실시간 알림 전달&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 시스템 구성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka Producer를 통해 메시지를 전송하고, Kafka Consumer가 해당 메시지를 수신&lt;/li&gt;
&lt;li&gt;수신한 메시지를 WebSocket(STOMP)으로 프론트엔드에 전달하여 실시간 알림을 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 구현 상세&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;WebSocket 설정 (WebSocketConfig):&lt;/b&gt;&lt;br /&gt;STOMP 엔드포인트(/ws)와 메시지 브로커(/topic)를 설정하여 클라이언트와 서버 간의 실시간 연결 구축&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka Consumer와 WebSocket 연동:&lt;/b&gt;&lt;br /&gt;Kafka Consumer가 메시지를 수신하면, SimpMessagingTemplate을 통해 &quot;/topic/notifications&quot; destination으로 메시지를 전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프론트엔드:&lt;/b&gt;&lt;br /&gt;SockJS와 STOMP.js를 사용하여 WebSocket에 연결하고, &quot;/topic/notifications&quot;를 구독하여 화면에 메시지를 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 장점 및 결과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka의 내구성과 순서 보장 기능 덕분에, 프론트엔드에 실시간 알림을 순서대로 전달할 수 있음&lt;/li&gt;
&lt;li&gt;Redis와 비교했을 때, 동시 다발적인 요청에서도 메시지 순서가 더 안정적으로 유지됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 전통적 MQ의 사용 상황&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;메시징 패턴:&lt;/b&gt;&lt;br /&gt;복잡한 라우팅, 트랜잭션, 요청-응답 패턴이 중요한 경우에는 RabbitMQ, ActiveMQ 등이 적합&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신뢰성:&lt;/b&gt;&lt;br /&gt;메시지의 확실한 전달과 재전송, 트랜잭션 지원이 필요한 애플리케이션에 유리함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;연동:&lt;/b&gt;&lt;br /&gt;기존 JMS 기반 시스템이나 레거시 시스템과의 통합 시 전통적 MQ가 더 익숙하고 표준화된 솔루션 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 결론 및 학습 포인트&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;b&gt;Kafka:&lt;/b&gt; 대규모 데이터 처리와 순서 보장이 중요한 경우 적합하며, WebSocket과 연동해 실시간 알림 시스템을 구현하는 데 매우 유리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis Pub/Sub:&lt;/b&gt; 빠른 실시간 처리와 간단한 구현이 필요할 때 사용하지만, 메시지 영속성 및 순서 보장에는 한계가 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전통적 MQ:&lt;/b&gt; 신뢰성, 트랜잭션, 복잡한 메시징 패턴이 요구되는 환경에서 적합하며, 레거시 시스템과의 연동 시 유리합니다.&lt;/li&gt;
&lt;/ul&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;메시징 시스템의 기본 개념 및 아키텍처 이해:&lt;/b&gt;&lt;br /&gt;각 시스템의 내부 동작 방식(토픽, 파티션, 오프셋, Pub/Sub 모델 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Boot를 통한 실제 구현:&lt;/b&gt;&lt;br /&gt;Kafka, Redis, 전통적 MQ 각각의 Producer/Consumer 설정 방법&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간 알림 시스템 구성:&lt;/b&gt;&lt;br /&gt;Kafka와 WebSocket(STOMP)을 통합해 프론트엔드에 실시간으로 메시지를 전달하는 방법&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하 테스트 및 운영:&lt;/b&gt;&lt;br /&gt;Locust 등 부하 테스트 도구를 통해 다수의 동시 요청 환경에서 시스템이 어떻게 동작하는지 분석&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 환경에 맞는 시스템 선택:&lt;/b&gt;&lt;br /&gt;요구사항에 따라 높은 처리량, 순서 보장, 신뢰성, 실시간성이 어떻게 달라지는지 비교 분석&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/126</guid>
      <comments>https://programmer7895.tistory.com/126#entry126comment</comments>
      <pubDate>Wed, 5 Mar 2025 01:06:16 +0900</pubDate>
    </item>
    <item>
      <title>Spring 2.x -&amp;gt; 3.x 마이그레이션: CORS 이슈 해결하기</title>
      <link>https://programmer7895.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 프레임워크를 2.x 버전에서 3.x로 마이그레이션하는 과정에서 예상치 못하게 여러 이슈가 발생해 롤백을 했던 경험이 있었습니다. 이번 블로그 포스트에서는 실제로 발생했던 CORS 이슈 및 기타 주요 문제와 이를 해결했던 방법에 대해 공유하고자 합니다.&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;1. CORS 이슈 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 3.x로 마이그레이션한 이후, 프론트엔드와 백엔드 간 통신을 시도할 때 CORS 관련 오류가 발생했습니다. 브라우저 콘솔에는 다음과 같은 오류 메시지가 나타났습니다:&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;CORS redirect is not allowed for a preflight request.&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;Spring 2.x 였을 때는 특별한 설정없이 cors 이슈가 없었는데, 버젼 문제일 거라고 생각을 하고 어떻게 해결할 것인지 생각을 해보았습니다.&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;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. CORS 프리플라이트 요청의 원리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;브라우저에서 다른 도메인으로의 요청이 발생하면 보안상의 이유로 먼저 &lt;/span&gt;&lt;span&gt;&lt;b&gt;프리플라이트 요청&lt;/b&gt;&lt;/span&gt;&lt;span&gt;(preflight request)을 보냅니다. 이는 주로 &lt;/span&gt;&lt;span&gt;OPTIONS&lt;/span&gt;&lt;span&gt; 메서드를 사용하여 서버가 실제 요청을 허용할 수 있는지 확인하는 과정입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;프리플라이트 요청의 과정은 다음과 같습니다:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;브라우저가 프리플라이트 요청을 전송&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 실제 요청(GET, POST 등)을 보내기 전에 &lt;/span&gt;&lt;span&gt;OPTIONS&lt;/span&gt;&lt;span&gt; 메서드를 사용해 서버에 허용 여부를 확인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;서버가 응답&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 허용된 출처(origin), 메서드, 헤더 등이 포함된 CORS 헤더를 반환해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;프리플라이트 요청이 성공하면 실제 요청 전송&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 서버가 허용을 명확히 응답한 경우에만 브라우저는 본 요청을 실행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만약 서버가 정확한 CORS 설정으로 응답하지 않거나 리디렉션이 발생하면 CORS 에러가 발생하게 됩니다&lt;/span&gt;&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;3. Spring 2.x에서의 CORS 설정 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 2.x에서는 주로 WebMvcConfigurer를 사용하여 CORS 설정을 간단히 구성할 수 있었습니다:&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping(&quot;/**&quot;)
                        .allowedOrigins(&quot;http://localhost:3000&quot;)
                        .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;)
                        .allowedHeaders(&quot;*&quot;)
                        .allowCredentials(true);
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 잘 작동했으나, Spring 3.x로 마이그레이션 후 문제가 발생했습니다.&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;4. Spring 3.x에서의 변화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 3.x에서는 CORS 관련 설정 방식에 몇 가지 주요 변경점이 생겼습니다:&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;기본 CORS 설정 적용 범위의 변화&lt;/b&gt;: Spring Security와의 통합으로 인해 CORS 설정이 더 정교해졌고, Spring Security 설정에서 별도로 추가 설정이 필요할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Boot 기본 설정 변경&lt;/b&gt;: Spring Boot 3.x에서는 일부 기본 CORS 정책이 강화되어 추가적인 커스터마이징이 필요합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CORS 이슈 해결 방법&lt;/h2&gt;
&lt;p data-pm-slice=&quot;1 3 []&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring 3.x로의 마이그레이션 과정에서 발생했던 CORS 문제는 &lt;/span&gt;&lt;span&gt;CorsFilter&lt;/span&gt;&lt;span&gt;를 직접 구현하고 &lt;/span&gt;&lt;span&gt;OPTIONS&lt;/span&gt;&lt;span&gt; 메서드에 대한 응답을 명시적으로 처리하여 해결했습니다. 다른 방식은 효과가 없었기 때문에 최종적으로 아래와 같은 필터를 사용했습니다:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Component;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomCorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        httpResponse.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;http://localhost:3000&quot;);
        httpResponse.setHeader(&quot;Access-Control-Allow-Methods&quot;, &quot;GET, POST, PUT, DELETE, OPTIONS&quot;);
        httpResponse.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;Content-Type, Authorization&quot;);
        httpResponse.setHeader(&quot;Access-Control-Allow-Credentials&quot;, &quot;true&quot;);

        if (&quot;OPTIONS&quot;.equalsIgnoreCase(httpRequest.getMethod())) {
            httpResponse.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        chain.doFilter(request, response);
    }
}&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;span&gt;&lt;b&gt;해결 방법 설명:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;모든 요청에 대해 CORS 헤더를 설정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;OPTIONS&lt;/span&gt;&lt;span&gt; 메서드로 들어오는 프리플라이트 요청은 명시적으로 &lt;/span&gt;&lt;span&gt;200 OK&lt;/span&gt;&lt;span&gt; 상태를 반환하도록 처리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;이 필터를 사용함으로써 브라우저의 프리플라이트 요청이 정상적으로 처리되고, 리디렉션 문제나 CORS 오류가 더 이상 발생하지 않았습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 문제 해결 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 문제를 해결한 후에는 단위 테스트와 통합 테스트를 통해 CORS 문제, Redirect 405 오류 등이 재발하지 않는 것을 확인했습니다. 브라우저와 서버 간 통신, 데이터베이스 쿼리, JSON 직렬화 등 모든 핵심 기능이 정상적으로 동작함을 최종적으로 확인했습니다.&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;7. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 3.x로의 마이그레이션은 단순히 버전 업그레이드가 아니라 다양한 설정과 라이브러리 호환성을 종합적으로 점검해야 하는 작업입니다. 이 블로그 포스트가 비슷한 문제를 겪는 개발자들에게 도움이 되길 바라며, 마이그레이션을 준비할 때 발생할 수 있는 잠재적 문제들을 미리 검토하는 것이 중요합니다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>cors</category>
      <category>springboot</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/125</guid>
      <comments>https://programmer7895.tistory.com/125#entry125comment</comments>
      <pubDate>Tue, 4 Feb 2025 23:39:06 +0900</pubDate>
    </item>
    <item>
      <title>[성능 비교] Springboot vs FastAPI</title>
      <link>https://programmer7895.tistory.com/124</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;현재 회사에서 FastAPI를 쓰고 있다. 자바를 전문적으로 쓸 수 있는 개발자도, 팀장님도 잘 모르시는 상황이기에 (혹은 여러가지 이유로 인해) 자바가 아닌 상대적으로 다루기 쉬운 파이썬을 사용하고 있으며, 그 중에 FastAPI 프레임워크를 사용하고 있고 앞으로도 그렇게 사용할거 같다.&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;둘 다 사용해본 입장에서 성능 측면에서는 컴파일러이니까 springboot가 fastapi보다 빠르다고 생각은 하지만 얼마나 성능차이가 날까 궁금해서 한 번 직접 더미 데이터로 실험을 해보려고 한다.&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;/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;프로세스 : 12th Gen Intel(R) Core(TM) i7-12700F (20 CPUs), ~2.1GHz&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 : 16384MB&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Springboot와 FastAPI에 대한 사양은 다음과 같다. (Java, Python 버젼 포함)&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;Java 17&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Springboot version - 3.2.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;Python 3.12&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI version -&amp;nbsp; 0.110.2&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터는 다음과 같이 구성했고, 100만개로 세팅하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 버젼은 다음과 같다.&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;MariaDB&amp;nbsp;[(none)]&amp;gt;&amp;nbsp;select&amp;nbsp;version(); &lt;br /&gt;+-----------------+ &lt;br /&gt;|&amp;nbsp;version()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;| &lt;br /&gt;+-----------------+ &lt;br /&gt;|&amp;nbsp;10.11.6-MariaDB&amp;nbsp;| &lt;br /&gt;+-----------------+ &lt;br /&gt;1&amp;nbsp;row&amp;nbsp;in&amp;nbsp;set&amp;nbsp;(0.001&amp;nbsp;sec)&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;CREATE&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;TABLE&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;employees&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;(&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;id&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;i&gt;SERIAL&lt;/i&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;PRIMARY&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;KEY,&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;first_name&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;i&gt;VARCHAR&lt;/i&gt;(100)&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NOT&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NULL,&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;last_name&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;i&gt;VARCHAR&lt;/i&gt;(100)&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NOT&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NULL,&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;email&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;i&gt;VARCHAR&lt;/i&gt;(100)&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;UNIQUE&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NOT&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NULL,&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;phone_number&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;i&gt;VARCHAR&lt;/i&gt;(15),&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;hire_date&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;i&gt;DATE&lt;/i&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NOT&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NULL,&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;job_title&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;i&gt;VARCHAR&lt;/i&gt;(100)&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NOT&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NULL,&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;salary&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;i&gt;DECIMAL&lt;/i&gt;(10,&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;2)&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NOT&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;NULL,&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;department&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;i&gt;VARCHAR&lt;/i&gt;(100),&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;has_parking_space&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;BOOLEAN&lt;/i&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;DEFAULT&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;false,&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;contract_duration&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;INTERVAL&lt;/i&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;);&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&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;/h3&gt;
&lt;pre id=&quot;code_1714520315642&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class SimpleExecutorController {

....

@GetMapping(&quot;/employees&quot;)
    @Cacheable(&quot;employees&quot;)
    public Page&amp;lt;Employee&amp;gt; getAllEmployees(Pageable pageable) {
        return simpleExecutorService.findAll(pageable);
    }
    
    
}


@Service
@Log4j2
@RequiredArgsConstructor
public class SimpleExecutorService {

    private final EmployeeRepository employeeRepository;
    ...
    
 	   
    @Cacheable(&quot;employees&quot;) // 결과를 캐시
    public Page&amp;lt;Employee&amp;gt; findAll(Pageable pageable) {
        return employeeRepository.findAll(pageable);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1714520468412&quot; class=&quot;python&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# main.py

from faker import Faker
from fastapi import FastAPI

from app.database import engineconn
from app.models import models
from app.models.models import Employees

app = FastAPI()
engine = engineconn()
session = engine.sessionmaker()


@app.get(&quot;/employees&quot;)
async def read_employees():
    example = session.query(models.Employees).all()
    return example
    
    
    
    
    


# models.py

from sqlalchemy import Column, Integer, String, Date, Float, Boolean
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Employees(Base):
    __tablename__ = &quot;employees&quot;

    id = Column(Integer, primary_key=True, autoincrement=True)
    first_name = Column(String(100), nullable=False)
    last_name = Column(String(100), nullable=False)
    email = Column(String(100), nullable=False)
    phone_number = Column(String(15), nullable=True)
    hire_date = Column(Date, nullable=False)
    job_title = Column(String(100), nullable=False)
    salary = Column(Float, nullable=False)
    department = Column(String(100), nullable=True)
    has_parking_space = Column(Boolean, default=False, nullable=True)
    contract_duration = Column(Integer, nullable=True)&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;h3 data-ke-size=&quot;size23&quot;&gt;테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저수는 2000명 기준으로 했고, 1분동안 테스트&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: auto;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background-color: #f2f2f2; height: 42px;&quot;&gt;
&lt;th style=&quot;width: 10%;&quot;&gt;Framework&lt;/th&gt;
&lt;th style=&quot;width: 10%;&quot;&gt;Sample&lt;/th&gt;
&lt;th style=&quot;width: 10%;&quot;&gt;Average&lt;/th&gt;
&lt;th style=&quot;width: 10%;&quot;&gt;Min&lt;/th&gt;
&lt;th style=&quot;width: 10%;&quot;&gt;Max&lt;/th&gt;
&lt;th style=&quot;width: 10%;&quot;&gt;Std. Dev&lt;/th&gt;
&lt;th style=&quot;width: 10%;&quot;&gt;Error %&lt;/th&gt;
&lt;th style=&quot;width: 10%;&quot;&gt;Throughput&lt;/th&gt;
&lt;th style=&quot;width: 10%;&quot;&gt;Received KB/sec&lt;/th&gt;
&lt;th style=&quot;width: 5%;&quot;&gt;Sent KB/sec&lt;/th&gt;
&lt;th style=&quot;width: 5%;&quot;&gt;Avg. Bytes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Spring Boot&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;3315&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;62383&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;855&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;98059&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;29500.09&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;0.00%&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;22.2&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;124.87&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;3.08&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;5753.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f9f9f9;&quot;&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;FastAPI&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;3800&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;346&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;2227&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;644.06&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;87.18%&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;1470.6&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;4311.76&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;26.14&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;3002.4&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 style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Samples&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 응답 갯수 (표본 수)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Average&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;- 응답 평균 (ms)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Min&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;- 응답 최소 (ms)&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Max&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 응답 최대 (ms)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Std. Dev.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;- 응답 표준편차&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Error %&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 에러율&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Throughput&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;- 시간당 처리량 (TPS)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Received KB/sec&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 시간당(sec)&amp;nbsp;받은 데이터(KB)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Sent KB/sec&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 시간당(sec) 보낸 데이터(KB)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6b66ff;&quot;&gt;Avg. Bytes&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 평균 바이트&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;지금 보면 springboot가 fastapi에 비해 수치가 낮은 것을 볼 수 있는데 error를 보면 사실 fastapi는 얼마못가서 애플리케이션이 버티질 못하는 상황이기에 thoughtput 보는것이 의미가 없긴하다. 그리고 회사에서는 workers 수치를 높여 tps를 개선하긴 했지만 윈도우에서는 그 수치를 높여도 프로세스 숫자가 높아지지 않는다는 것을 오늘에서야 알았다. 그래서 윈도우에서는 workers 수치를 높여도 2000개의 요청을 버티지 못하고 애플리케이션이 죽어버리는 현상이 일어났다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각대로 springboot가 많은 요청을 fastapi 비해서 많이 처리를 할 수 있다는 것을 알게 되었다. 다만 내가 잘못 테스트를 하는지를 모르겠지만 단순 select query 테스트를 하는건데도 springboot에서도 22 TPS 수치밖에 안 나왔다. 적어도 200~300정도는 나와야 하지 않을까 생각했는데 생각보다 너무 적게 나온거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음 글에는 springboot 기반으로 개선 관련된 것을 한 번 포스팅을 하려 한다.&amp;nbsp;&lt;/p&gt;</description>
      <category>기타</category>
      <category>python</category>
      <category>springboot</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/124</guid>
      <comments>https://programmer7895.tistory.com/124#entry124comment</comments>
      <pubDate>Sun, 5 May 2024 18:09:27 +0900</pubDate>
    </item>
    <item>
      <title>[Springboot] 힙 메모리 누수에 관한 이야기</title>
      <link>https://programmer7895.tistory.com/123</link>
      <description>&lt;h3 style=&quot;color: #4c4c4c; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;배경&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;내가 운영하고 있는 서비스가 실서버에 배포 및 운영을 하는 상태인데, 회사 내부 상품 정보를 많이 가져오면서도 처리속도를 빠르게 하기 위해 API 내부에는 ExecutorService 라는 객체를 쓰면서 멀티쓰레드를 활용해 여러번 API 호출하는 부분이 있었는데,&amp;nbsp; 그걸로 인해 힙 메모리가 일정비율로 채워졌음에도 불구하고 GC가 제대로 일어나지 않은 것처럼 보이고 힙 메모리 사용량 70% 이상임에도 계속 늘어나는 메모리 누수가 일어나는 현상이 일어나고 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;현재 다른 서비스 개발에 집중을 하고 있어서 왜 그런지에 대해서 명&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;확히 찾지를 못했채, 힙 메모리 사용량 일정 비율에 의해 모니터링 알람 울리면 강제로 GC 명령어를 입력해서 임시방편으로 대응하고 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #4c4c4c; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #4c4c4c; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;원인 파악&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;내가 생각한 메모리 누수 원인은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;1. 사용하는 객체 크기가 엄청 큼&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품에 대한 정보가 정말 많기 때문에 이 부분에 대한 필드 갯수가 많음 (대략적으로 100개 정도)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;2. 어떤 특정 필드 중 데이터 크기가 굉장히 큰 부분이 있음&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모니터링으로 응답 크기를 체크한 결과 최대 3-4MB (3,000,000 byte 이상) 인 크기가 많았었고, 이로 인해 메모리 누수가 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #4c4c4c; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;분석 파악&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;회사에서 만든 코드를 최대한 재현해서 예제형식으로 바꿔본 코드이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1709395881528&quot; class=&quot;arduino&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public List&amp;lt;Product&amp;gt; executeTask2() {
        List&amp;lt;String&amp;gt; productIdList = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 1; i &amp;lt;= 100; i++) {
            productIdList.add(&quot;Product&quot; + i);
        }

        ExecutorService executor = Executors.newFixedThreadPool(20);
        CompletionService&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt; completionService = new ExecutorCompletionService&amp;lt;&amp;gt;(executor);

        List&amp;lt;Product&amp;gt; allResults = new ArrayList&amp;lt;&amp;gt;();
        List&amp;lt;List&amp;lt;String&amp;gt;&amp;gt; partitions = partition(productIdList);
        log.info(partitions.size());

        for (List&amp;lt;String&amp;gt; partition : partitions) {
            completionService.submit(() -&amp;gt; {
                List&amp;lt;Product&amp;gt; products = new ArrayList&amp;lt;&amp;gt;();
                for (String productId : partition) {
                    try {
                        List&amp;lt;Product&amp;gt; apiResult = apiService.callApi();

                        // 데이터 크기 계산 및 로그 출력
                        if (apiResult.size() &amp;gt; 0) {
                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
                            ObjectOutputStream oos = new ObjectOutputStream(baos);
                            oos.writeObject(apiResult);
                            oos.close();
                            double sizeInKB = baos.size() / 1024.0; // 바이트 단위의 크기를 KB 단위로 변환
                            log.info(&quot;API result size for productId: &quot; + productId + &quot; is &quot; + sizeInKB + &quot; KB&quot;);

                            if (!apiResult.isEmpty()) {
                                products.addAll(apiResult); // 모든 결과를 추가
                            }
                        }

                        for (Product product : products) {
                            try {
                                long size = estimateObjectSize(product); // Product 객체의 크기 추정
                                System.out.println(&quot;Product size: &quot; + size + &quot; bytes (&quot; + (size / 1024.0) + &quot; KB)&quot;);
                            } catch (IOException e) {
                                System.err.println(&quot;Failed to estimate object size for product: &quot; + e.getMessage());
                            }
                        }

                    } catch (Exception e) {
                        log.error(&quot;API call failed for productId: &quot; + productId, e);
                    }
                }
                return products; // List&amp;lt;Product&amp;gt; 반환
            });
        }


        for (int i = 0; i &amp;lt; partitions.size(); i++) {
            try {
                Future&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt; future = completionService.take(); // 완료된 작업의 결과를 가져옴
                synchronized (allResults) {
                    allResults.addAll(future.get()); // Blocking call
                }
                // products 리스트를 처리
            } catch (InterruptedException | ExecutionException e) {
                log.error(&quot;Error while fetching products&quot;, e);
            }
        }

        executor.shutdown();
        log.info(String.format(&quot;Total processed products: %d&quot;, allResults.size()));
        return allResults.stream().limit(300).toList();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1709395984501&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public List&amp;lt;Product&amp;gt; callApi() {

        return IntStream.rangeClosed(1, 100).mapToObj(i -&amp;gt; {
            Product product = new Product();
            product.setShopNo(1L);
            product.setProductNo((long) i);
            product.setProductCode(&quot;P000000&quot; + i);
            product.setCustomProductCode(&quot;CUST&quot; + i);
            product.setProductName(&quot;Product &quot; + i);
            product.setEngProductName(&quot;Product English Name &quot; + i);
            product.setSupplyProductName(&quot;Supply Product &quot; + i);
            product.setInternalProductName(&quot;Internal Name &quot; + i);
            product.setModelName(&quot;Model &quot; + i);
            product.setPriceExcludingTax(&quot;100.00&quot;);
            product.setPrice(new Product.Price(1000L + i, &quot;1000&quot;));
            product.setRetailPrice(new Product.Price(1200L + i, &quot;1200&quot;));
            product.setSupplyPrice(new Product.Price(800L + i, &quot;800&quot;));
            product.setDisplay(&quot;Y&quot;);
            product.setSelling(&quot;Y&quot;);
            product.setProductCondition(&quot;New&quot;);
            product.setProductUsedMonth(0L);
            product.setSummaryDescription(&quot;This is a summary for product &quot; + i);
            product.setMarginRate(&quot;10%&quot;);
            product.setTaxCalculation(&quot;Included&quot;);
            product.setTaxType(&quot;VAT&quot;);
            product.setTaxRate(10L);
            product.setBuyLimitByProduct(&quot;N&quot;);
            product.setBuyLimitType(&quot;None&quot;);
            product.setBuyGroupList(Arrays.asList(1L, 2L));
            product.setBuyMemberIDList(Arrays.asList(&quot;user1&quot;, &quot;user2&quot;));
            product.setRepurchaseRestriction(&quot;N&quot;);
            product.setSinglePurchaseRestriction(&quot;N&quot;);
            product.setBuyUnitType(&quot;Unit&quot;);
            product.setBuyUnit(1L);
            product.setOrderQuantityLimitType(&quot;Limit&quot;);
            product.setMinimumQuantity(1L);
            product.setMaximumQuantity(10L);
            product.setPointsByProduct(&quot;Y&quot;);
            product.setPointsSettingByPayment(&quot;Fixed&quot;);
            product.setPointsAmount(Arrays.asList(new Product.PointsAmount(&quot;Cash&quot;, &quot;5%&quot;), new Product.PointsAmount(&quot;Card&quot;, &quot;3%&quot;)));
            product.setExceptMemberPoints(&quot;N&quot;);
            product.setProductVolume(new Product.ProductVolume(&quot;Y&quot;, &quot;10cm&quot;, &quot;20cm&quot;, &quot;30cm&quot;));
            product.setAdultCertification(&quot;N&quot;);
            product.setDetailImage(&quot;http://example.com/detail.jpg&quot;);
            product.setListImage(&quot;http://example.com/list.jpg&quot;);
            product.setTinyImage(&quot;http://example.com/tiny.jpg&quot;);
            product.setSmallImage(&quot;http://example.com/small.jpg&quot;);
            product.setUseNaverpay(&quot;Y&quot;);
            product.setNaverpayType(&quot;Instant&quot;);
            product.setUseKakaopay(&quot;Y&quot;);
            product.setManufacturerCode(&quot;MANU&quot; + i);
            product.setTrendCode(&quot;TREND&quot; + i);
            product.setBrandCode(&quot;BRAND&quot; + i);
            product.setSupplierCode(&quot;SUPP&quot; + i);
            product.setMadeDate(&quot;2023-01-01&quot;);
            product.setReleaseDate(&quot;2023-01-10&quot;);
            Product.ExpirationDate expirationDate = new Product.ExpirationDate();
            expirationDate.setStartDate(new Product.Date(LocalDate.of(2023, 1, 1), OffsetDateTime.now()));
            expirationDate.setEndDate(new Product.Date(LocalDate.of(2023, 12, 31), OffsetDateTime.now()));
            product.setExpirationDate(expirationDate);
            product.setOriginClassification(&quot;Domestic&quot;);
            product.setOriginPlaceNo(100L);
            product.setOriginPlaceValue(&quot;Origin Place&quot;);
            product.setMadeInCode(&quot;KR&quot;);
            Product.ExpirationDate iconShowPeriod = new Product.ExpirationDate();
            iconShowPeriod.setStartDate(new Product.Date(LocalDate.of(2023, 1, 1), OffsetDateTime.now()));
            iconShowPeriod.setEndDate(new Product.Date(LocalDate.of(2023, 12, 31), OffsetDateTime.now()));
            product.setIconShowPeriod(iconShowPeriod);
            product.setIcon(Arrays.asList(&quot;icon1&quot;, &quot;icon2&quot;));
            product.setHscode(&quot;123456&quot;);
            product.setProductWeight(&quot;1kg&quot;);
            product.setProductMaterial(&quot;Material&quot;);
            product.setCreatedDate(OffsetDateTime.now());
            product.setUpdatedDate(OffsetDateTime.now());
            Product.ListIcon listIcon = new Product.ListIcon();
            listIcon.setSoldoutIcon(true);
            listIcon.setRecommendIcon(false);
            listIcon.setNewIcon(true);
            product.setListIcon(listIcon);
            product.setApproveStatus(&quot;Approved&quot;);
            product.setClassificationCode(&quot;CLASS&quot; + i);
            product.setSoldOut(&quot;N&quot;);
            product.setAdditionalPrice(&quot;0.00&quot;);
            product.setClearanceCategoryEng(&quot;Category ENG&quot;);
            product.setClearanceCategoryKor(&quot;Category KOR&quot;);
            product.setClearanceCategoryCode(&quot;CAT&quot; + i);
            product.setExposureLimitType(&quot;Limit&quot;);
            product.setExposureGroupList(Arrays.asList(1L, 2L));
            product.setShippingFeeByProduct(&quot;N&quot;);
            product.setShippingFeeType(&quot;Free&quot;);
            product.setMain(Arrays.asList(1L, 2L));
            product.setMarketSync(&quot;N&quot;);
            product.setSummaryDescription(&quot;Description for product &quot; + generateLargeString());
            return product;
        }).collect(Collectors.toList());
    }

    public String generateLargeString() {
        double size = 400 * 1024;
        char[] chars = new char[(int) size];
        Arrays.fill(chars, 'A'); // 단순화를 위해 모든 문자를 'A'로 채움
        return new String(chars);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;해당 로직은 스레드 풀로 쓰레드 20개 세팅하고 해당 쓰레드 기반으로 해서 데이터 임의대로 만든 후, 해당 데이터들을 응답값을 리턴하는 로직을 만들었다. 내부에서는 API가 총 8천번 정도 호출하는 거랑 동일하게 데이터를 생성했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;자바 버젼 및 힙메모리 사양은 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1709397874798&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Java: version 17.0.9 2023-10-17 LTS, vendor Amazon.com Inc.
Heap Size : 4GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그 뒤에 한 번 호출을 했는데, visual GC에서 본 결과는 다음과 같다.&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;1558&quot; data-origin-height=&quot;967&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6ZDiJ/btsFmhWvLLL/kEkCTZGSyPhlmlU2zcfDhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6ZDiJ/btsFmhWvLLL/kEkCTZGSyPhlmlU2zcfDhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6ZDiJ/btsFmhWvLLL/kEkCTZGSyPhlmlU2zcfDhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6ZDiJ%2FbtsFmhWvLLL%2FkEkCTZGSyPhlmlU2zcfDhk%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;1558&quot; height=&quot;967&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;967&quot;/&gt;&lt;/span&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;size14&quot;&gt;한 번 호출한 결과, 다음과 같은 스샷 결과가 나왔다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;스샷을 보시면 알다시피, API 호출이 끝났음에도 불구하고 여전히 Old 영역은 약 90% 가 되고 있는 상태고 호출하고 나서 30분이 지나도 여전히 힙 메모리 사용량이 줄지 않고 있다.&amp;nbsp; 회사에서도 거의 비슷한 상황에 놓여져 있다.&lt;br /&gt;현재 시간이 지나면 지날수록 메모리 사용량은 점점 늘고 있으며, 추후 힙 메모리 사용량이 가득차게 되어 프로세스가 죽을 거 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #4c4c4c; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;해결 방안&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span&gt;원인이 어떤지는 어느정도 파악이 된 상태이고, 내가 해결 방안을 생각했는데 다음과 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span&gt;1. 필요한 필드만 제외&lt;/span&gt;&lt;span&gt;이 부분은 한 번 체크를 해봐야겠지만 어떤 필드가 가장 크기를 많이 차지하는지를 체크가 필요하고, 그게 파악이 되었으면 API 호출했을 때 그 필드를 제외해도 되는지 여부를 체크해야한다. 하지만 해당 필드가 클라이언트 단에서 정말 필요한 필드일 수도 있기 떄문에 이 부분은 참고해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;2. 프론트에서 설명 부분이 앞부분만 필요하다면 10줄 이상은 '...' 으로 처리하고 나머지는 지우는 걸로 처리하면 되지 않을까 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;3. 처음부터 &quot;설명&quot;이라는 필드는 1000 자 이상을 못 넘게 정책을 정하면 되지 않을까 생각을 하고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;4. 3가지 이외의 방법은 없을까?&amp;nbsp;&lt;/p&gt;</description>
      <category>트러블 슈팅</category>
      <category>GC</category>
      <category>springboot</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/123</guid>
      <comments>https://programmer7895.tistory.com/123#entry123comment</comments>
      <pubDate>Thu, 18 Apr 2024 00:28:56 +0900</pubDate>
    </item>
    <item>
      <title>MSA에 대한 장단점</title>
      <link>https://programmer7895.tistory.com/122</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSA의 장점&lt;/h3&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;li&gt;**기술 다양성**: 서비스마다 가장 적합한 기술 스택을 자유롭게 선택할 수 있어 기술적 유연성을 제공합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSA의 단점&lt;/h3&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;**개발 및 운영 오버헤드**: 마이크로서비스는 개발 초기 단계에서는 관리가 더 복잡할 수 있으며, DevOps 능력이 강화되어야 합니다. 배포, 모니터링, 로깅 등의 작업이 더 복잡해지고, 이를 관리하기 위한 추가적인 도구와 기술이 필요합니다.&lt;/li&gt;
&lt;li&gt;**보안 문제**: 서비스 간에 데이터를 주고받는 과정에서 보안 취약점이 발생할 수 있습니다. 각&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스마다 보안 정책을 적용하고 유지하기 위한 추가적인 노력이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA는 많은 장점을 제공하지만, 그 구현과 관리는 단점들을 인지하고 적절히 대응하는 것을 요구합니다. 따라서, 조직의 기술적 준비 상태, 개발 및 운영 팀의 역량, 그리고 비즈니스 요구사항을 면밀히 검토한 후 MSA 도입을 결정하는 것이 중요합니다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>MSA</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/122</guid>
      <comments>https://programmer7895.tistory.com/122#entry122comment</comments>
      <pubDate>Tue, 9 Apr 2024 00:26:49 +0900</pubDate>
    </item>
    <item>
      <title>Java 멀티스레딩: Runnable과 Callable 인터페이스의 차이점</title>
      <link>https://programmer7895.tistory.com/121</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Runnable 인터페이스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Runnable 인터페이스는 매우 단순합니다. 단 하나의 메서드, void run()을 정의하며, 이 메서드는 반환 값도, 예외도 없습니다. 이는 Runnable이 작업 실행의 단순함을 반영하며, 실행 결과나 오류를 직접 반환하지 않습니다. 결과적으로, Runnable 구현체는 별도의 로직(예: 공유 객체를 통한 결과 저장)을 통해 결과를 관리해야 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Runnable은 직접적으로 스레드를 생성하고 실행할 때, 혹은 Executor Framework를 사용하여 스레드 풀에 작업을 제출할 때 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class RunnableExample implements Runnable {

    @Override
    public void run() {
        // 실행하고자 하는 작업
        System.out.println(&quot;Hello from Runnable thread!&quot;);
    }

    public static void main(String[] args) {
        Runnable runnableTask = new RunnableExample();
        Thread thread = new Thread(runnableTask);
        thread.start();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Callable 인터페이스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Callable 인터페이스는 Runnable보다 복잡한 작업 실행을 위해 설계되었습니다. V call() throws Exception 메서드를 통해, 작업 실행 후 결과를 반환하고, 필요한 경우 예외를 던질 수 있습니다. 이러한 특징 덕분에, Callable은 작업의 실행 결과를 쉽게 처리하고, 실행 중 발생한 예외를 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Callable 작업은 Executor Framework를 통해 실행되며, 작업 제출 시 반환되는 Future 객체를 사용하여 비동기적으로 결과를 조회하고, 실행 중 발생한 예외를 처리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample implements Callable&amp;lt;Integer&amp;gt; {

    @Override
    public Integer call() throws Exception {
        // 실행하고자 하는 작업, 여기서는 간단한 계산
        return 123 + 456;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Callable&amp;lt;Integer&amp;gt; callableTask = new CallableExample();
        Future&amp;lt;Integer&amp;gt; future = executorService.submit(callableTask);

        // 작업의 실행이 완료되면 결과를 출력
        System.out.println(&quot;Callable result: &quot; + future.get());

        executorService.shutdown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 차이점&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;반환 값:&lt;/b&gt; Runnable은 작업 실행 후 결과를 반환할 수 없습니다. 반면, Callable은 작업의 결과를 반환할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예외 처리:&lt;/b&gt; Runnable의 run() 메서드는 예외를 던지지 않습니다. Callable의 call() 메서드는 예외를 던질 수 있으며, 이는 실행 결과 조회 시 처리될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실행 방법:&lt;/b&gt; Runnable은 직접 스레드를 생성하거나 Executor Framework를 사용하여 실행할 수 있습니다. Callable은 Executor Framework를 통해서만 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특성&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Runnable&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Callable&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;메서드 정의&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void run()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;V call() throws Exception&lt;/code&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;/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;code&gt;Thread&lt;/code&gt; 생성자에 전달하거나 Executor Framework를 사용&lt;/td&gt;
&lt;td&gt;주로 Executor Framework를 사용하여 실행&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;/tbody&gt;
&lt;/table&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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Runnable과 Callable 인터페이스는 Java의 멀티스레딩과 병렬 처리를 위한 중요한 구성 요소입니다. 각각의 특징을 이해하고 적절한 상황에서 사용함으로써, 효율적인 멀티스레드 애플리케이션을 구현할 수 있습니다. 작업의 실행 결과를 반환하거나 예외 처리가 필요한 경우 Callable을, 그 외의 경우에는 Runnable을 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 당신은 두 인터페이스의 차이점을 명확히 이해하고, 자바 멀티스레딩을 더 효과적으로 활용할 준비가 되었습니다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>callable</category>
      <category>Runnable</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/121</guid>
      <comments>https://programmer7895.tistory.com/121#entry121comment</comments>
      <pubDate>Sun, 7 Apr 2024 13:38:58 +0900</pubDate>
    </item>
    <item>
      <title>SHA-1과 SHA-2 해시 알고리즘의 차이점: 보안성을 중심으로</title>
      <link>https://programmer7895.tistory.com/120</link>
      <description>&lt;h3 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;SHA-1: 이제는 지나간 보안 기준&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SHA-1은 160비트의 해시 값을 생성하는 알고리즘으로, 한 때는 디지털 서명, SSL 인증서, 소프트웨어 패키지 검증 등에 널리 사용되었습니다. 하지만 2017년, Google과 CWI Amsterdam의 연구팀이 SHA-1 충돌을 성공적으로 시연하며, 이 알고리즘의 취약성이 명확하게 드러났습니다. 이후로 SHA-1은 더 이상 안전하지 않은 것으로 간주되며, 새로운 프로젝트나 시스템에서의 사용이 권장되지 않습니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;SHA-2: 강화된 보안성을 제공하는 현대의 선택&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SHA-2는 SHA-1의 후속으로 개발되었으며, 224, 256, 384, 512비트 등 여러 버전의 해시 값을 생성할 수 있는 능력을 가집니다. 이 알고리즘은 현대의 보안 요구사항을 충족시키며, 현재 금융 거래, 디지털 인증서, 국가안보 수준의 애플리케이션 등에서 널리 사용되고 있습니다. SHA-2는 그 선배인 SHA-1보다 훨씬 강력한 보안성을 제공하며, 충돌 공격에 대해서도 훨씬 더 강한 저항력을 보입니다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;결론: 보안이 우선시되는 세계에서의 선택&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보안성은 오늘날 디지털 세계에서 가장 중요한 고려사항 중 하나입니다. SHA-1과 SHA-2의 차이점을 이해하는 것은 데이터의 무결성과 보안을 보장하기 위해 필수적입니다. SHA-1의 취약성이 드러난 이후로, SHA-2의 강력한 보안 기능이 더욱 중요해졌습니다. 따라서, 민감한 정보를 다루는 모든 시스템과 애플리케이션에서는 SHA-2의 사용을 적극적으로 고려해야 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보안 기술의 발전은 끊임없이 진행되고 있으며, SHA-3 등 더욱 발전된 해시 알고리즘도 등장하고 있습니다. 그러나 현재로서는 SHA-2가 널리 인정받고 사용되는 강력한 해시 알고리즘으로서, 디지털 보안을 유지하는 데 있어 핵심적인 역할을 하고 있습니다. 보안을 중시하는 모든 환경에서 SHA-2의 사용은 필수적이며, 앞으로도 그 중요성은 계속해서 증가할 것으로 예상됩니다.&lt;/p&gt;</description>
      <category>기타</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/120</guid>
      <comments>https://programmer7895.tistory.com/120#entry120comment</comments>
      <pubDate>Wed, 3 Apr 2024 23:43:49 +0900</pubDate>
    </item>
    <item>
      <title>&amp;quot;HMAC: 보안의 핵심을 이해하기&amp;quot;</title>
      <link>https://programmer7895.tistory.com/119</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보안은 디지털 세계에서 가장 중요한 요소 중 하나입니다. 데이터의 무결성과 인증은 모든 소프트웨어 개발 프로젝트에서 우선시되어야 합니다. 이러한 보안 요구 사항을 충족하기 위해 HMAC(Hash-based Message Authentication Code)이 널리 사용됩니다. 이 블로그 글에서는 HMAC의 기본 개념, 작동 방식 및 개발자가 이를 알아야 하는 이유를 살펴보겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;HMAC이란 무엇인가?&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HMAC은 특정 데이터의 무결성과 인증을 보장하기 위해 해시 함수를 사용하는 메시지 인증 코드입니다. 이는 데이터가 전송 중에 변경되지 않았으며, 신뢰할 수 있는 소스로부터 왔음을 확인하는 데 사용됩니다. HMAC 구현은 두 가지 주요 구성 요소에 의존합니다: 강력한 암호화 해시 함수(예: SHA-256)와 비밀 키입니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;HMAC의 작동 방식&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HMAC의 생성과 검증 과정은 다음과 같습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;생성 단계&lt;/b&gt;: 송신자는 비밀 키와 메시지를 결합하여 HMAC 값을 생성합니다. 이 값과 원본 메시지는 수신자에게 전송됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 단계&lt;/b&gt;: 수신자는 받은 메시지와 동일한 비밀 키를 사용하여 HMAC 값을 계산합니다. 이후, 계산된 HMAC 값과 송신자로부터 받은 HMAC 값을 비교합니다. 두 값이 일치하면 메시지가 무결하고 인증된 것으로 간주됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;왜 개발자는 HMAC을 알아야 하는가?&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보안은 현대 소프트웨어 개발에서 빼놓을 수 없는 요소입니다. 특히, 네트워크 보안, 애플리케이션 보안, 또는 암호화 기술과 관련된 직무에서는 HMAC과 같은 보안 메커니즘의 이해가 필수적입니다. 개발 경력이 6년차에 이르렀다 하더라도, 기본적인 보안 원리와 개념을 이해하는 것은 매우 중요합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러나, 모든 개발자가 HMAC을 심도 있게 이해하고 있을 필요는 없습니다. 그것은 당신의 전문 분야와 프로젝트의 특성에 따라 달라집니다. 그럼에도 불구하고, 데이터 보안과 관련된 기본적인 이해는 모든 개발자에게 큰 도움이 됩니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;결론&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HMAC은 데이터 보안을 강화하는 데 있어 핵심적인 역할을 합니다. 이는 무결성 검증 및 메시지 인증을 통해 데이터의 안전성을 보장합니다. 디지털 보안 환경이 지속적으로 발전함에 따라, HMAC과 같은 기술의 이해는 개발자에게 필수적인 자산이 됩니다. 보안에 대한 기본적인 지식은 단지 요구 사항을 넘어서, 우리가 개발하는 시스템과 애플리케이션을 더 안전하게 만드는 데 기여합니다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>hmac</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/119</guid>
      <comments>https://programmer7895.tistory.com/119#entry119comment</comments>
      <pubDate>Tue, 2 Apr 2024 01:11:46 +0900</pubDate>
    </item>
    <item>
      <title>JWT (JSON Web Token) 기초: 구조와 작동 원리 이해하기</title>
      <link>https://programmer7895.tistory.com/118</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서론:&lt;/b&gt; 인터넷 상에서 안전한 정보 교환은 어떠한 웹 서비스에서도 필수적인 부분입니다. 이를 위해 개발된 다양한 기술 중 하나가 바로 JWT (JSON Web Token)입니다. JWT는 간결하고 자가 수용적인 방법으로 정보를 안전하게 전달할 수 있게 해줍니다. 본 글에서는 JWT의 기본 구조와 그것이 어떻게 작동하는지에 대해 알아보겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JWT의 구조:&lt;/b&gt; JWT는 크게 세 부분으로 구성되어 있습니다: 헤더(Header), 페이로드(Payload), 그리고 서명(Signature). 각 부분은 점(.)으로 구분되며, Base64Url 인코딩 방식으로 인코딩됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; 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-filename=&quot;jwt_ng1_en.png&quot; data-origin-width=&quot;2381&quot; data-origin-height=&quot;1096&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5UlP5/btsGiQaH1AB/D5TB2Jd2KKQvYjXWUkMLxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5UlP5/btsGiQaH1AB/D5TB2Jd2KKQvYjXWUkMLxK/img.png&quot; data-alt=&quot;JWT (JSON Web Token) (in)security - research.securitum.com&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5UlP5/btsGiQaH1AB/D5TB2Jd2KKQvYjXWUkMLxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5UlP5%2FbtsGiQaH1AB%2FD5TB2Jd2KKQvYjXWUkMLxK%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;2381&quot; height=&quot;1096&quot; data-filename=&quot;jwt_ng1_en.png&quot; data-origin-width=&quot;2381&quot; data-origin-height=&quot;1096&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JWT (JSON Web Token) (in)security - research.securitum.com&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;헤더(Header):&lt;/b&gt; 헤더는 토큰의 타입과 사용된 알고리즘에 대한 정보를 담고 있습니다. 예를 들어, 알고리즘은 HS256이나 RS256과 같은 암호화 방식을 나타내며, 토큰 타입으로는 'JWT'가 일반적으로 사용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;페이로드(Payload):&lt;/b&gt; 페이로드는 토큰에 담길 실제 정보(클레임)를 포함합니다. 여기에는 사용자의 정보, 토큰의 유효 기간, 발행자 등 다양한 클레임이 포함될 수 있습니다. 클레임의 종류에는 등록된 클레임, 공개 클레임, 비공개 클레임이 있으며, 각각의 목적과 사용 방법이 다릅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서명(Signature):&lt;/b&gt; 서명은 헤더와 페이로드를 합친 후, 비밀키나 공개키/개인키 쌍을 사용하여 암호화하는 과정을 거칩니다. 이 서명을 통해 JWT의 무결성과 인증이 보장됩니다. 서명 과정은 JWT가 변조되지 않았음을 증명하며, 서버는 이를 검증하여 안전한 정보 교환을 보장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작동 원리:&lt;/b&gt; 웹 애플리케이션에서 사용자가 로그인을 하면, 서버는 사용자의 정보를 기반으로 JWT를 생성하고, 이를 사용자에게 반환합니다. 사용자는 이후의 모든 요청에 이 JWT를 포함시켜 서버에 보냅니다. 서버는 요청을 받을 때마다 JWT의 서명을 검증하고, 유효한 경우 요청을 처리합니다. 이 과정을 통해 사용자의 인증 상태를 유지할 수 있으며, 매번 로그인하지 않아도 되는 편리함을 제공합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론:&lt;/b&gt; JWT는 웹 개발에서 널리 사용되는 인증 방식으로, 그 구조와 작동 원리를 이해하는 것은 안전한 웹 서비스를 제공하기 위해 매우 중요합니다. 본 글을 통해 JWT의 기본적인 개념과 구조에 대한 이해를 돕고자 하였습니다. 안전한 웹 서비스 개발을 위해 JWT를 적극 활용해보세요.&lt;/p&gt;</description>
      <category>기타</category>
      <category>JWT</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/118</guid>
      <comments>https://programmer7895.tistory.com/118#entry118comment</comments>
      <pubDate>Mon, 1 Apr 2024 23:31:31 +0900</pubDate>
    </item>
    <item>
      <title>mcrouter의 라우팅 기법 이해하기: 기본 설정과 사용 방법</title>
      <link>https://programmer7895.tistory.com/117</link>
      <description>&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;mcrouter의 기본 라우팅 기법&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;mcrouter를 사용하면서 특별한 설정을 하지 않았다면, mcrouter는 다음과 같은 기본 또는 가장 일반적인 라우팅 설정을 사용하게 됩니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Consistent Hashing&lt;/b&gt;: 분산 캐시 시스템에서 널리 사용되는 라우팅 방식으로, 키를 기반으로 요청을 캐시 서버에 일관적으로 분산시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pool Route&lt;/b&gt;: 캐시 서버들을 그룹화하여 구성된 풀을 통해 요청을 라우팅합니다. 이는 가장 기본적인 형태의 라우팅 설정입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;설정 파일을 통한 라우팅 기법 확인 및 조정&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;mcrouter의 동작은 설정 파일을 통해 세부적으로 조정할 수 있습니다. JSON 형식의 이 파일에서는 서버 풀, 라우팅 정책, 복제 및 샤딩 설정 등을 포함하여 다양한 옵션을 지정할 수 있습니다. 설정 파일을 통해 다음과 같은 라우팅 관련 설정을 조정할 수 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #0d0d0d; text-align: start;&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;고가용성을 위한 Failover 정책 설정&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타</category>
      <category>mcrouter</category>
      <author>개발만파볼까</author>
      <guid isPermaLink="true">https://programmer7895.tistory.com/117</guid>
      <comments>https://programmer7895.tistory.com/117#entry117comment</comments>
      <pubDate>Mon, 1 Apr 2024 01:43:39 +0900</pubDate>
    </item>
  </channel>
</rss>