<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>공부</title>
    <link>https://codingstudy95.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 14 Aug 2026 16:17:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>김검정</managingEditor>
    <image>
      <title>공부</title>
      <url>https://tistory1.daumcdn.net/tistory/6544896/attach/7c3e555aced4465ca5ddc3613d7acf62</url>
      <link>https://codingstudy95.tistory.com</link>
    </image>
    <item>
      <title>Docker, 왜 써야 할까?</title>
      <link>https://codingstudy95.tistory.com/159</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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;엥? 로컬에서는 잘 됐는데... 왜 서버에서는 안되는걸까?&quot;&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;이 문제의 원인은 내 컴퓨터와 서버의 환경(OS, 자바 버전, 설치된 라이브러리 등)이 다르기 때문이다. Docker는 이런 문제를 해결하기 위해 나왔다.&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;Docker는 '&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;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;꼭 알아야 할 Docker 핵심 용어 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 이미지 (Image) :&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;비유 : 붕어빵 틀에 해당한다. 붕어빵을 만들기 위한 모든 재료(밀가루 반죽, 팥)와 레시피가 담겨있는 '설계도' 또는 '압축 파일'같은 것이다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;애플리케이션을 실행하는 데 필요한 모든 것(Java, Spring 애플리케이션 코드, 라이브러리 등)이 포함된 읽기 전용(Read-Only) 템플릿이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 컨테이너 (Container) :&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비유 : 붕어빵 틀(이미지)로 찍어낸 실제 붕어빵이다. 우리는 붕어빵을 먹는 것이지, 붕어빵 틀을 먹는게 아니다.&lt;/li&gt;
&lt;li&gt;이미지(Image)를 실제로 메모리에 올리고 실행한 인스턴스(프로세스)이다. 하나의 이미지로 여러 개의 컨테이너를 만들 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Dockerfile :&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비유 : 붕어빵 레시피이다. &quot;밀가루 반죽을 넣고, 팥을 넣고, 3분간 굽는다&quot;와 같이 이미지를 어떻게 만들지 순서대로 적어놓은 텍스트 파일이다.&lt;/li&gt;
&lt;li&gt;개발자가 이미지를 만들기 위해 작성하는 스크립트이다. 이 Dockerfile을 실행하면 Docker가 알아서 이미지를 만들어준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해보면 개발자는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Dockerfile&lt;/span&gt;을 작성해서, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Image&lt;/span&gt;를 만들고, 그 이미지를 실행해서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Container&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;그렇다면 직접 내 애플리케이션을 Docker에 띄워보자&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;STEP 0 : Docker 설치하기&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;가장 먼저 내 컴퓨터에 Docker를 설치해야 한다. 아래 링크에서 'Docker Desktop'을 다운로드하여 설치한다. (Windows나 Mac에 맞는 버전을 설치하시면 된다.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;44:1-45:0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;44:1-45:0&quot;&gt;&lt;a href=&quot;https://www.docker.com/products/docker-desktop/&quot;&gt;Docker Desktop 다운로드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-sourcepos=&quot;46:1-46:35&quot; data-ke-size=&quot;size16&quot;&gt;설치 후 Docker Desktop을 실행하면 준비 완료이다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;46:1-46:35&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 1: Spring Boot 애플리케이션 준비&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;1. 간단한 &quot;Hello World&quot;를 출력하는 Spring Boot 애플리케이션을 준비한다.&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;2. 가장 중요한 단계! &lt;b&gt;애플리케이션을 .jar 파일로 빌드해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;52:5-53:68&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;52:5-52:71&quot;&gt;&lt;b&gt;Gradle 사용자:&lt;/b&gt; 터미널에서 .\gradlew build (또는 ./gradlew build) 실행&lt;/li&gt;
&lt;li data-sourcepos=&quot;53:5-53:68&quot;&gt;&lt;b&gt;Maven 사용자:&lt;/b&gt; 터미널에서 .\mvnw package (또는 ./mvnw package) 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 빌드가 성공하면 build/libs 또는 target 폴더 안에 프로젝트명-0.0.1-SNAPSHOT.jar 같은 파일이 생성된 것을 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 2: Dockerfile 작성하기 (레시피 만들기)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750119142739&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. 베이스 이미지 선택 (Java 17 버전을 사용)
# 'slim' 버전은 불필요한 것들을 빼서 용량이 가볍다.

FROM openjdk:17-jdk-slim

# 2. 컨테이너 내에서 작업할 디렉토리 생성

WORKDIR /app

# 3. 빌드된 .jar 파일을 컨테이너의 /app 디렉토리로 복사
# Gradle: build/libs/*.jar, Maven: target/*.jar 경로를 확인한다.

COPY build/libs/*.jar app.jar

# 4. 컨테이너가 시작될 때 실행할 명령어
# &quot;java -jar app.jar&quot; 명령어를 실행하여 애플리케이션을 구동한다.

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 3: Docker 이미지 빌드하기 (붕어빵 틀 만들기)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 터미널을 열고 프로젝트 최상위 폴더(Dockerfile이 있는 위치)에서 아래 명령어를 실행한다.&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: #666666; color: #dddddd;&quot;&gt;docker build -t my-spring-app .&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;87:1-87:34&quot;&gt;docker build: 이미지를 만드는 명령어이다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;88:1-88:82&quot;&gt;-t my-spring-app: 이미지에 my-spring-app이라는 이름(태그)을 붙여줍니다. 이름은 원하는 대로 바꿀 수 있다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;89:1-90:0&quot;&gt;. : 현재 디렉토리에 있는 Dockerfile을 사용하라는 의미이고,&amp;nbsp;&lt;b&gt;마지막에 점(.)을 꼭 찍어야한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 4: Docker 컨테이너 실행하기 (붕어빵 굽기)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #666666; color: #dddddd;&quot;&gt;docker run -p 8080:8080 my-spring-app&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;101:1-101:39&quot;&gt;docker run: 이미지를 컨테이너로 실행하는 명령어이다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;102:1-102:151&quot;&gt;-p 8080:8080: 포트 포워딩(Port Forwarding) 설정이다. &lt;b&gt;&quot;내 컴퓨터(Host)의 8080 포트를 컨테이너의 8080 포트와 연결해줘&quot;&lt;/b&gt; 라는 의미이다. Spring Boot는 기본적으로 8080 포트를 사용하므로 이렇게 설정한다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;103:1-104:0&quot;&gt;my-spring-app: 아까 STEP 3에서 만든 이미지의 이름이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어를 실행하면 Spring Boot 애플리케이션이 실행될 때의 로그가 터미널에 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-sourcepos=&quot;107:1-107:22&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STEP 5: 실행 확인&lt;/b&gt;&lt;/h4&gt;
&lt;p data-sourcepos=&quot;109:1-109:78&quot; data-ke-size=&quot;size16&quot;&gt;웹 브라우저를 열고 주소창에 http://localhost:8080 을 입력해보면&amp;nbsp; 애플리케이션이 정상적으로 보인다면 성공이다!&lt;/p&gt;
&lt;p data-sourcepos=&quot;111:1-111:53&quot; 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>Docker</category>
      <category>Container</category>
      <category>docker</category>
      <category>docker 기초</category>
      <category>IMAGE</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/159</guid>
      <comments>https://codingstudy95.tistory.com/159#entry159comment</comments>
      <pubDate>Sat, 21 Jun 2025 10:18:43 +0900</pubDate>
    </item>
    <item>
      <title>기능은 됐는데 성능이 안 나오는 코드의 특징 5가지</title>
      <link>https://codingstudy95.tistory.com/158</link>
      <description>&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;그중에서도 가장 자주 반복되는 특징 5가지를 한번 정리해보자.&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. 반복문 안에서 DB or 네트워크 호출&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나쁜 예 :&lt;/p&gt;
&lt;pre id=&quot;code_1750060329669&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Long&amp;gt; userIds = getAllUserIds();
for (Long userId : userIds) {
    User user = userRepository.findById(userId); // DB 요청 n번 발생
    process(user);
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;n건마다 DB/network 왕복 요청 발생&lt;/li&gt;
&lt;li&gt;1만 건이면 1만 번 DB에 연결한다.&lt;/li&gt;
&lt;li&gt;N+1 문제로 대표되는 성능 병목&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이 로직을 어떻게 개선할 수 있을까?&lt;/p&gt;
&lt;pre id=&quot;code_1750060393454&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;User&amp;gt; users = userRepository.findAllById(userIds); // 한번에 요청
for (User user : users) {
    process(user);
}&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Stream API 혹은 람다에서 성능 생각 없이 사용&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750060425084&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; emails = users.stream()
    .filter(u -&amp;gt; u.getEmail().endsWith(&quot;@gmail.com&quot;))
    .map(u -&amp;gt; u.getEmail())
    .distinct()
    .collect(Collectors.toList());&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;filter&amp;nbsp;&amp;rarr; map &amp;rarr; distinct &amp;rarr; collect 모두 내부적으로 새로운 리스트를 생성한다&lt;/li&gt;
&lt;li&gt;데이터가 많을수록 GC 부하와 메모리 사용량이 급증한다&lt;/li&gt;
&lt;li&gt;특히 nested Stream 구조가 있는 경우 O(n^2) 급 성능이 하락한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 포인트는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대량 처리 시엔 stream 대신 명시적 for문을 사용한다&lt;/li&gt;
&lt;li&gt;성능 측정 필요 시 JMH나 System.nanoTime()으로 비교한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 컬렉션 구조 선택 미스 (LinkedList vs ArrayList)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750060579916&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new LinkedList&amp;lt;&amp;gt;();
for (int i = 0; i &amp;lt; 1_000_000; i++) {
    list.add(i, &quot;data&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;위 로직은 그냥 봤을땐 문제가 없어 보인다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LinkedList.add(index, element)는 O(n)&lt;/li&gt;
&lt;li&gt;실제로는 전체를 다 뒤져야 한다&lt;/li&gt;
&lt;li&gt;ArrayList 보다 수십 배 느리다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삽입/삭제가 많으면 LinkedList, 조회가 많으면 ArrayList&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 대부분의 경우 ArrayList가 빠르다.&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;b&gt;4. 쓸데없는 객체 생성 or 스트링 조합&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750060715084&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String result = &quot;&quot;;
for (int i = 0; i &amp;lt; 10000; i++) {
    result += i; // 매번 새로운 문자열 생성됨
}&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;rarr; O(n^2) 그로인해 GC 빈도가 증가하고 CPU가 과도하게 사용된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750060784780&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;StringBuilder sb = new StringBuilder();
for (int i = 0; i &amp;lt; 10000; i++) {
    sb.append(i);
}&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;StringBuilder 를 사용해 쓸데없는 문자열 생성을 방지할 수 있다.&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;b&gt;5. 캐시 or DB index 미사용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복해서 부르는 데이터인데 매번 DB를 요청하거나, 검색 쿼리에 인덱스를 미사용하는 경우 &amp;rarr; Full Table Scan이 발생할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1750060878147&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM orders WHERE status = 'PENDING';&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;rarr; status에 인덱스가 없다면 전체 테이블을 검색한다.&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>Back-End/Java</category>
      <category>성능이 안나오는 경우</category>
      <category>확인해봐야하는 항목</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/158</guid>
      <comments>https://codingstudy95.tistory.com/158#entry158comment</comments>
      <pubDate>Fri, 20 Jun 2025 18:01:59 +0900</pubDate>
    </item>
    <item>
      <title>OAuth2 로그인 시 커스터마이징 포인트</title>
      <link>https://codingstudy95.tistory.com/156</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;OAuth2가 무엇인가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2는 로그인을 대신해주는 표준 방식이다. 사용자가 비밀번호를 입력하지 않고, 구글/카카오 계정으로 로그인하는 것을 가능하게 한다.&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;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;Spring Security&amp;nbsp;&amp;rarr; 구글 서버로 리다이렉트&lt;/li&gt;
&lt;li&gt;구글 로그인 &amp;rarr; 우리 서버로 code 반환&lt;/li&gt;
&lt;li&gt;Spring Security가 code 로 access token 요청&lt;/li&gt;
&lt;li&gt;access token으로 사용자 정보 요청&lt;/li&gt;
&lt;li&gt;사용자 정보 기반으로 인증 처리 완료&lt;/li&gt;
&lt;/ol&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;Spring Security OAuth2 구조를 이해해보자&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 OAuth2 로그인을 처리하기 위해 다음 컴포넌트를 사용한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 76.8605%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.6512%;&quot;&gt;컴포넌트&lt;/td&gt;
&lt;td style=&quot;width: 37.2093%;&quot;&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.6512%;&quot;&gt;OAuth2LoginAuthenticationFilter&lt;/td&gt;
&lt;td style=&quot;width: 37.2093%;&quot;&gt;로그인 요청 가로채기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.6512%;&quot;&gt;DefaultOauth2UserService&lt;/td&gt;
&lt;td style=&quot;width: 37.2093%;&quot;&gt;사용자 정보 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.6512%;&quot;&gt;OAuth2User&lt;/td&gt;
&lt;td style=&quot;width: 37.2093%;&quot;&gt;조회된 사용자 정보 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.6512%;&quot;&gt;AuthenticationSuccessHandler&lt;/td&gt;
&lt;td style=&quot;width: 37.2093%;&quot;&gt;로그인 성공 후 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 39.6512%;&quot;&gt;ClientReistration&lt;/td&gt;
&lt;td style=&quot;width: 37.2093%;&quot;&gt;클라이언트 정보(google, kakao 등)&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;카카오 / 구글 연동을 위한 기본 설정 예시&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1749702747266&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.yml 예시

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: &amp;lt;GOOGLE_CLIENT_ID&amp;gt;
            client-secret: &amp;lt;GOOGLE_CLIENT_SECRET&amp;gt;
            scope: profile, email
            redirect-uri: &quot;{baseUrl}/login/oauth2/code/{registrationId}&quot;
            client-name: Google
          kakao:
            client-id: &amp;lt;KAKAO_REST_API_KEY&amp;gt;
            client-secret: &amp;lt;KAKAO_CLIENT_SECRET&amp;gt;
            scope: profile_nickname, account_email
            authorization-grant-type: authorization_code
            redirect-uri: &quot;{baseUrl}/login/oauth2/code/{registrationId}&quot;
            client-name: Kakao
        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id&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;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 사용자 정보 가공 - &lt;span style=&quot;background-color: #dddddd;&quot;&gt;CustomOAuth2UserService&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;구글 / 카카오에서 가져오는 사용자 정보는 정형화돼 있지 않다. 서비스마다 반환 형식도 다르고, 내가 원하는 도메인 정보와 맞지 않을 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749702833581&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId(); // google, kakao
        String userNameAttr = userRequest.getClientRegistration()
                .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

        Map&amp;lt;String, Object&amp;gt; attributes = oAuth2User.getAttributes();

        OAuthAttributes mappedAttributes = OAuthAttributes.of(registrationId, userNameAttr, attributes);

        // DB에 사용자 저장
        Member member = saveOrUpdate(mappedAttributes);

        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;)),
                mappedAttributes.getAttributes(),
                mappedAttributes.getNameAttributeKey());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&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;2. 최초 로그인 시 회원가입 처리&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749705675682&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Member saveOrUpdate(OAuthAttributes attributes) {
    Optional&amp;lt;Member&amp;gt; user = memberRepository.findByEmail(attributes.getEmail());

    if (user.isEmpty()) {
        return memberRepository.save(attributes.toEntity());
    }

    return user.get(); // 기존 사용자
}&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;size18&quot;&gt;&lt;b&gt;3. 사용자 도메인 모델 매핑&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749705697832&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public class OAuthAttributes {
    private Map&amp;lt;String, Object&amp;gt; attributes;
    private String nameAttributeKey;
    private String name;
    private String email;

    public static OAuthAttributes of(String provider, String userNameAttributeName, Map&amp;lt;String, Object&amp;gt; attributes) {
        if (&quot;kakao&quot;.equals(provider)) {
            return ofKakao(userNameAttributeName, attributes);
        }
        return ofGoogle(userNameAttributeName, attributes);
    }

    private static OAuthAttributes ofGoogle(String userNameAttributeName, Map&amp;lt;String, Object&amp;gt; attributes) {
        return new OAuthAttributes(attributes, userNameAttributeName,
                (String) attributes.get(&quot;name&quot;),
                (String) attributes.get(&quot;email&quot;));
    }

    private static OAuthAttributes ofKakao(String userNameAttributeName, Map&amp;lt;String, Object&amp;gt; attributes) {
        Map&amp;lt;String, Object&amp;gt; kakaoAccount = (Map&amp;lt;String, Object&amp;gt;) attributes.get(&quot;kakao_account&quot;);
        Map&amp;lt;String, Object&amp;gt; profile = (Map&amp;lt;String, Object&amp;gt;) kakaoAccount.get(&quot;profile&quot;);

        return new OAuthAttributes(attributes, userNameAttributeName,
                (String) profile.get(&quot;nickname&quot;),
                (String) kakaoAccount.get(&quot;email&quot;));
    }

    public Member toEntity() {
        return new Member(name, email, Role.USER);
    }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. JWT 발급 연동&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2 로그인 성공 후 JWT를 발급하고 응답에 실어주는 구조도 자주 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1749705747731&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {

    private final JwtUtil jwtUtil;

    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) {
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        String email = (String) oAuth2User.getAttributes().get(&quot;email&quot;);

        String token = jwtUtil.createToken(email);

        response.setHeader(&quot;Authorization&quot;, &quot;Bearer &quot; + token);
    }
}&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;&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 /oauth2/authorication/kakao 클릭&amp;nbsp;&lt;/li&gt;
&lt;li&gt;카카오 로그인&lt;/li&gt;
&lt;li&gt;code 반환&lt;/li&gt;
&lt;li&gt;Spring이 token 요청&lt;/li&gt;
&lt;li&gt;사용자 정보 조회&lt;/li&gt;
&lt;li&gt;CustomOAuth2UserService 실행&lt;/li&gt;
&lt;li&gt;DB 회원가입 처리&lt;/li&gt;
&lt;li&gt;SecurityContext 인증 저장&lt;/li&gt;
&lt;li&gt;SuccessHandler에서 JWT 발급&lt;/li&gt;
&lt;li&gt;클라이언트로 응답&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2 로그인은 단순해 보이지만 실제 서비를 만들 때는 사용자 정보 가공, JWT 발급, 회원가입 처리 등 커스터마이징이 꼭 필요한 포인트들이 많다.&lt;/p&gt;</description>
      <category>Back-End/Spring Boot + Kotlin</category>
      <category>OAuth2</category>
      <category>구글 로그인</category>
      <category>카카오 로그인</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/156</guid>
      <comments>https://codingstudy95.tistory.com/156#entry156comment</comments>
      <pubDate>Wed, 18 Jun 2025 15:25:17 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security는 뭘까?</title>
      <link>https://codingstudy95.tistory.com/155</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 쉽게 말해 보안 담당자이다.&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;기본 기능만 사용해도 로그인/로그아웃, 세션 관리 등이 된다. 하지만 실무에서 사용하려면 100% 커스터마이징이 필수이다.&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;왜 커스터마이징이 필요할까?&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;요구사항&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.8682%;&quot;&gt;&lt;b&gt;기본 Spring Security로 가능?&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.7984%;&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;JWT 인증으로 로그인하고 싶다&lt;/td&gt;
&lt;td style=&quot;width: 27.8682%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 38.7984%;&quot;&gt;커스텀 필터 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;로그인 실패 시 로그 찍고 싶다&lt;/td&gt;
&lt;td style=&quot;width: 27.8682%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 38.7984%;&quot;&gt;핸들러 오버라이드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;/admin/**&lt;/span&gt; 경로는 관리자만 보게 하고 싶다&lt;/td&gt;
&lt;td style=&quot;width: 27.8682%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 38.7984%;&quot;&gt;hasRole() 설정 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;인증 실패 응답을 JSON으로 주고 싶다&lt;/td&gt;
&lt;td style=&quot;width: 27.8682%;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;width: 38.7984%;&quot;&gt;AuthenticationEntryPoint 등록&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;/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;실무에서 자주 쓰는 Spring Security 커스터마이징 TOP 5 에 대해서 알아보자&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. JWT 인증 필터 추가하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 기본적으로 세션 기반인데, 요즘은 대부분 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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;UsernamePasswordAuthhenticationFilter 이전에 커스텀 필터 등록&lt;/li&gt;
&lt;li&gt;요청 헤더에서 JWT 호출&amp;nbsp;&amp;rarr; 유효성 검사 &amp;rarr; 인증객체 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1749691398172&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) {
        String token = request.getHeader(&quot;Authorization&quot;);
        if (token != null &amp;amp;&amp;amp; jwtUtil.validateToken(token)) {
            Authentication auth = jwtUtil.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.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;/p&gt;
&lt;pre id=&quot;code_1749691419921&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);&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;&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;로그인 성공/실패 시, 특정 페이지로 리디렉션하거나 로그를 남기고 싶을 때 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1749691514970&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) {
        System.out.println(&quot;로그인 성공: &quot; + authentication.getName());
        response.sendRedirect(&quot;/dashboard&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;등록은 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.successHandler()&lt;/span&gt; / &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.failuerHandler()&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. URL 별로 접근 권한 다르게 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설정만으로는 URL마다 세세한 권한 제어가 어렵다.&lt;/p&gt;
&lt;pre id=&quot;code_1749691593936&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http.authorizeHttpRequests()
    .requestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
    .requestMatchers(&quot;/user/**&quot;).authenticated()
    .anyRequest().permitAll();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hasRole(&quot;ADMIN&quot;) : 관리자만 접근 가능&lt;/li&gt;
&lt;li&gt;authenticated() : 로그인한 사람만 가능&lt;/li&gt;
&lt;li&gt;permitAll() : 아무나 접근 가능&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;pre id=&quot;code_1749691675769&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) {
        Member member = memberRepository.findByEmail(username)
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;사용자 없음&quot;));
        return new User(member.getEmail(), member.getPassword(),
                List.of(new SimpleGrantedAuthority(&quot;ROLE_&quot; + member.getRole())));
    }
}&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;등록은 Security 설정에서 이렇게 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1749691694602&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.auth.userDetailsService(customUserDetailsService)&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;&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. 예외 응답을 JSON으로 주기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 인증 실패하면 403 페이지가 뜬다. 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;* 인증 실패(401 Unauthorized)&lt;/p&gt;
&lt;pre id=&quot;code_1749691767751&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(&quot;application/json&quot;);
        response.getWriter().write(&quot;{\&quot;error\&quot;: \&quot;인증이 필요합니다\&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;* 권한 없음(403 Forbidden)&lt;/p&gt;
&lt;pre id=&quot;code_1749691796084&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType(&quot;application/json&quot;);
        response.getWriter().write(&quot;{\&quot;error\&quot;: \&quot;접근 권한이 없습니다\&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;등록은 마찬가지로 Security 설정에서 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1749691825519&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http.exceptionHandling()
    .authenticationEntryPoint(jwtAuthenticationEntryPoint)
    .accessDeniedHandler(jwtAccessDeniedHandler);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Back-End/Spring Boot + Kotlin</category>
      <category>JWT 인증</category>
      <category>security</category>
      <category>spring</category>
      <category>spring boot</category>
      <category>Spring Security</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/155</guid>
      <comments>https://codingstudy95.tistory.com/155#entry155comment</comments>
      <pubDate>Mon, 16 Jun 2025 11:31:10 +0900</pubDate>
    </item>
    <item>
      <title>데이터베이스에 대량 데이터를 Insert 할 때, 속도 차이가 나는 이유</title>
      <link>https://codingstudy95.tistory.com/157</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에 대량의 데이터를 삽입(insert) 할 때, 같은 데이터를 넣더라도 방식에 따라 속도 차이가 최대 5배 이상 나는 경우가 있다.&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;100만 건의 데이터를 DB에 넣는다고 가정해보자. (Oracle, PostgreSQL, MySQL 모두 해당된다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 51.1628%; height: 104px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 19px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 17px;&quot;&gt;1. 단건 insert (1건씩 insert 수행, 오토커밋)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 17px;&quot;&gt;2. 단건 insert + 수동 커밋 (트랜잭션 한 번에 묶음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 17px;&quot;&gt;3. PreparedStatement 반복 (JDBC 배치 처리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 17px;&quot;&gt;4. ORM (BatchSize 설정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 17px;&quot;&gt;5. Bulk insert SQL (insert into values (...), (....))&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;/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. 단건 insert (1건씩 insert + 오토커밋)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750035919932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; 1000000; i++) {
    String sql = &quot;INSERT INTO users (name, email) VALUES (?, ?)&quot;;
    try (Connection conn = dataSource.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setString(1, &quot;User&quot; + i);
        pstmt.setString(2, &quot;user&quot; + i + &quot;@example.com&quot;);
        pstmt.executeUpdate(); // 매번 DB round-trip 발생
    }
}&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;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;매번 오토커밋 : DB 트랜잭션 비용 증가&lt;/li&gt;
&lt;li&gt;I/O 부하 증가 : 디스크 flush가 반복됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 단점들로 인해 가장 느리다(100만 건 기준 수 분 이상 소요될 수 있음)&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;b&gt;2. 단건 insert + 수동 커밋 (트랜잭션 묶기)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750036195082&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
PreparedStatement pstmt = conn.prepareStatement(&quot;INSERT INTO users (name, email) VALUES (?, ?)&quot;);

for (int i = 0; i &amp;lt; 1000000; i++) {
    pstmt.setString(1, &quot;User&quot; + i);
    pstmt.setString(2, &quot;user&quot; + i + &quot;@example.com&quot;);
    pstmt.executeUpdate();
}
conn.commit();&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;위의 1번 방식보다는 개선되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션이 한 번만 발생&amp;nbsp;&amp;rarr; 비용 절감&lt;/li&gt;
&lt;li&gt;커넥션 재사용&lt;/li&gt;
&lt;li&gt;단건 처리지만 I/O flush 횟수 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여전히 100만 건 DB 요청을 100만 번 한다.&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;b&gt;3. JDBC Batch Insert&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750036994313&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
PreparedStatement pstmt = conn.prepareStatement(&quot;INSERT INTO users (name, email) VALUES (?, ?)&quot;);

for (int i = 0; i &amp;lt; 1000000; i++) {
    pstmt.setString(1, &quot;User&quot; + i);
    pstmt.setString(2, &quot;user&quot; + i + &quot;@example.com&quot;);
    pstmt.addBatch();

    if (i % 1000 == 0) {
        pstmt.executeBatch();
        pstmt.clearBatch();
    }
}
pstmt.executeBatch(); // 남은 것 처리
conn.commit();&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;addBatch() 로 모아서 executeBatch()로 일괄처리&lt;/li&gt;
&lt;li&gt;DB round-trip이 100만 번 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&amp;rarr;&lt;span&gt; 1,000번으로 줄어든다&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;트랜잭션도 한 번만 발생&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;그렇다면 성능은 개선되었을까?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적인 기본 insert의 3~5배 이상 빠름&lt;/li&gt;
&lt;li&gt;PostgreSQL, Oracle, MySQL 모두 지원한다&lt;/li&gt;
&lt;li&gt;메모리 사용량만 주의가 필요하다 (Batch 크기가 너무 크면 OOM 발생 가능성이 있다)&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. ORM (JAP/Hibernate) + BatchSize 설정&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750038792595&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String email;
}&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;기본 insert (JAP) :&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750038801261&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; 1000000; i++) {
    User u = new User(&quot;User&quot; + i, &quot;user&quot; + i + &quot;@email.com&quot;);
    entityManager.persist(u);
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;insert 쿼리 100만 번 발생&lt;/li&gt;
&lt;li&gt;flush/commit 지연 발생&lt;/li&gt;
&lt;li&gt;트랜잭션이 너무 길어지면 성능 저하/락 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선하는 방법은 Batch 설정이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;properties :&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750038875645&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.jpa.properties.hibernate.jdbc.batch_size=1000
spring.jpa.properties.hibernate.order_inserts=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;rarr; Hibernate가 내부적으로 JDBC addBatch()를 호출하도록 해준다&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거의 JDBC batch insert 수준까지 도달 가능하다&lt;/li&gt;
&lt;li&gt;ORM 코드 유지 + 성능 모두 확보가 가능하다&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;5. Raw SQL bulk insert (특정 DB 지원)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750038972411&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INSERT INTO users (name, email)
VALUES 
('User1', 'user1@email.com'),
('User2', 'user2@email.com'),
('User3', 'user3@email.com');
-- 수천 개 VALUES 묶음&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 빠른 방식 (DB 엔진 최적화)&lt;/li&gt;
&lt;li&gt;단점 : 가변 데이터 처리에 어려움이 있다&lt;/li&gt;
&lt;li&gt;일반적으로 ORM에서는 사용하기 힘들다&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;table style=&quot;border-collapse: collapse; width: 85.1163%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7213%;&quot;&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.2476%;&quot;&gt;&lt;b&gt;실행 시간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.1473%;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7213%;&quot;&gt;단건 insert (오토커밋)&lt;/td&gt;
&lt;td style=&quot;width: 24.2476%;&quot;&gt;약 800초&lt;/td&gt;
&lt;td style=&quot;width: 39.1473%;&quot;&gt;최악&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7213%;&quot;&gt;단건 insert + 수동 커밋&lt;/td&gt;
&lt;td style=&quot;width: 24.2476%;&quot;&gt;약 400초&lt;/td&gt;
&lt;td style=&quot;width: 39.1473%;&quot;&gt;약간 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7213%;&quot;&gt;JDBC batch insert&lt;/td&gt;
&lt;td style=&quot;width: 24.2476%;&quot;&gt;약 90초&lt;/td&gt;
&lt;td style=&quot;width: 39.1473%;&quot;&gt;훨씬 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7213%;&quot;&gt;JPA + batch 설정&lt;/td&gt;
&lt;td style=&quot;width: 24.2476%;&quot;&gt;약 110초&lt;/td&gt;
&lt;td style=&quot;width: 39.1473%;&quot;&gt;안정적 + 실무 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.7213%;&quot;&gt;raw bulk insert&lt;/td&gt;
&lt;td style=&quot;width: 24.2476%;&quot;&gt;약 40초&lt;/td&gt;
&lt;td style=&quot;width: 39.1473%;&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;</description>
      <category>Back-End/DB</category>
      <category>insert</category>
      <category>MySQL</category>
      <category>Oracle</category>
      <category>PostgreSQL</category>
      <category>대용량 데이터</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/157</guid>
      <comments>https://codingstudy95.tistory.com/157#entry157comment</comments>
      <pubDate>Mon, 16 Jun 2025 10:59:53 +0900</pubDate>
    </item>
    <item>
      <title>Spring Bean?</title>
      <link>https://codingstudy95.tistory.com/154</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Bean이라는 말, 대체 뭘까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bean은 Spring이 대신 만들어주고 관리해주는 Java 객체(물건) 이다.&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;/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;Spring Bean도 똑같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서는 우리가 객체를 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;new&lt;/span&gt; 키워드를 사용해 직접 만들어야 하는데, Spring은 객체를 미리 만들어서 냉장고처럼 보관해놓는다.&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;그럼 Bean은 왜 필요할까?&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예로 커피 앱을 만들었다고 가정해보면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커피를 내리는 객체&lt;/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;등이 있을것이고 이 객체들을 하나하나 new로 만들고 연결하고 관리가 필요한데 이건 너무 복잡하고 비효율적일 것이다.&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;그래서 Spring이 &quot;내가 다 해줄게~&quot; 라고 해서 Bean을 대신 만들어주고, 서로 연결까지 해준다.&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;Bean은 어떻게 만들까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;방법1. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Component&lt;/span&gt; 스티커 붙이기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749686953967&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class CoffeeMachine {
    public String make() {
        return &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;이렇게 @Component 라는 표시를 붙이면, Spring이 알아서 이 클래스를 Bean으로 만들어서 보관해준다.&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;방법2. @Bean 사용하기(조금 더 수동적)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749687011916&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {
    @Bean
    public CoffeeMachine coffeeMachine() {
        return new CoffeeMachine();
    }
}&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;&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;이렇게 생성한 Bean은 어떻게 사용할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bean은 Spring이 만들어서 저장해 놓은 객체라고 했다. 우리는 그 Bean을 그냥 요청만 하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1749687078016&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class Cafe {
    private final CoffeeMachine machine;

    public Cafe(CoffeeMachine machine) {
        this.machine = machine;
    }

    public void serve() {
        System.out.println(machine.make());
    }
}&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이 자동으로 CoffeeMachine을 Cafe에 딱 끼워 넣어준다. 이걸 의존성 주입(Dependency Injection)이라고 부른다.&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;Bean의 특징 3가지&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 71.2791%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 32.7907%;&quot;&gt;특징&lt;/td&gt;
&lt;td style=&quot;width: 40.0821%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 32.7907%;&quot;&gt;자동으로 만들어진다&lt;/td&gt;
&lt;td style=&quot;width: 40.0821%;&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Component&lt;/span&gt; 나 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Bean&lt;/span&gt; 으로 표시만 하면 끝!&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 32.7907%;&quot;&gt;하나만 만들어진다&lt;/td&gt;
&lt;td style=&quot;width: 40.0821%;&quot;&gt;기본적으로 Bean은 하나만 만들어진다 (Singleton)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 32.7907%;&quot;&gt;서로 연결이 가능하다&lt;/td&gt;
&lt;td style=&quot;width: 40.0821%;&quot;&gt;하나의 Bean이 다른 Bean을 사용할 수 있다&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;&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;Bean이 없는 상황을 한번 살펴보면 편리함이 더 잘 느껴질것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1749687315598&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class App {
    public static void main(String[] args) {
        CoffeeMachine machine = new CoffeeMachine();
        Cafe cafe = new Cafe(machine);
        cafe.serve();
    }
}&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;이렇게 직접 new로 다 만들어야 한다. 코드가 길어지고, 테스트하기도 어렵고, 관리도 힘들어진다.&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>Back-End/Spring Boot + Kotlin</category>
      <category>Bean</category>
      <category>Component</category>
      <category>Java</category>
      <category>spring</category>
      <category>spring boot</category>
      <category>빈</category>
      <category>자바</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/154</guid>
      <comments>https://codingstudy95.tistory.com/154#entry154comment</comments>
      <pubDate>Thu, 12 Jun 2025 09:17:57 +0900</pubDate>
    </item>
    <item>
      <title>null 체크, try-catch가 난무하는 코드 수정해보기</title>
      <link>https://codingstudy95.tistory.com/153</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보다 보면 아래와 같은 코드를 계속해서 마주친다.&lt;/p&gt;
&lt;pre id=&quot;code_1749430133148&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    if (user != null) {
        if (user.profile != null) {
            if (user.profile.email != null) {
                sendEmail(user.profile.email)
            }
        }
    }
} catch (Exception e) {
    e.printStackTrace()
}&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;이런 방어적 코딩의 지옥에서 벗어나기 위해선 어떤 개선 전략들이 필요할까?&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.&amp;nbsp; 왜 null 체크와 try-catch가 많아지는가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&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;객체 간 관계를 강하게 표현하지 못함&amp;nbsp;&amp;rarr; null 가능성이 많아짐&lt;/li&gt;
&lt;li&gt;ex) user.profile?.email &amp;rarr; 왜 profile이 null일 수 있지?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&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;catch(Exception e)로 모든 예외를 흡수 &amp;rarr; 예외의 원인이 불명확해진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인 3 : 디폴트 값 처리 전략 부재&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;null을 피하려고 임시 디폴트 값을 억지로 넣는 코드가 증가할 수 있다.&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;table style=&quot;border-collapse: collapse; width: 68.4884%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.9535%;&quot;&gt;&lt;b&gt;실제 결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;try-catch가 5겹 중첩&lt;/td&gt;
&lt;td style=&quot;width: 33.9535%;&quot;&gt;로그만 쌓이고, 실패 이유 알기 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;모든 예외를 캐치 후 무시&lt;/td&gt;
&lt;td style=&quot;width: 33.9535%;&quot;&gt;장애는 발생했는데 아무도 모름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;null일 때 기본값을 return&lt;/td&gt;
&lt;td style=&quot;width: 33.9535%;&quot;&gt;버그가 감춰짐 &amp;rarr; 나중에 더 큰 문제 발생&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2-1&amp;nbsp; try-catch는 최소한의 범위로 설정한다&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;pre id=&quot;code_1749430821355&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    // DB 호출
    // 로직 처리
    // 다른 서비스 호출
    // 응답 생성
} catch (Exception e) {
    // 어디서 실패했는지 모름
}&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;pre id=&quot;code_1749430837471&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;User user;
try {
    user = userRepository.findById(id);
} catch (DataAccessException e) {
    logger.error(&quot;DB 오류&quot;, e);
    throw new ServiceException(&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;이처럼 실패 구간을 분리하면 디버깅, 로그 추적, 예외 처리가 쉬워진다. 필요하다면 사용자 정의 예외를 만드는 것도 도움이 될 수 있다.&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;2-2 예외를 로직 처리 수단으로 사용하지 않는다.&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;pre id=&quot;code_1749438756031&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    Product p = findProduct(code);
} catch (ProductNotFoundException e) {
    return defaultProduct;
}&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;존재 여부 판단은 로직으로, 예외는 &quot;진짜 예외'로만 써야 유지보수성이 높아진다.&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;pre id=&quot;code_1749438822816&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Product product = productRepository.findByCode(code);
if (product == null) {
    return defaultProduct;
}&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;size18&quot;&gt;&lt;b&gt;2-3 서비스 레이어에서 흐름을 단순화한다.&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;pre id=&quot;code_1749438856048&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (user != null) {
    if (user.profile != null) {
        try {
            sendEmail(user.profile.email);
        } catch (MessagingException e) {
            ...
        }
    }
}&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;pre id=&quot;code_1749438872365&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun trySendEmail(user: User?) {
    val email = user?.profile?.email ?: return
    runCatching { sendEmail(email) }
        .onFailure { log.error(&quot;이메일 전송 실패&quot;, it) }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 null 을 안전하게 처리하는 방법에 대해서도 알아보자&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;Java에서 null은 피할 수 없는 동료이다. Java는 오랫동안 null을 처리하기 위한 명시적 키워드나 구문을 제공하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음과 같은 코드가 자주 등장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1749438973747&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (user != null &amp;amp;&amp;amp; user.getProfile() != null &amp;amp;&amp;amp; user.getProfile().getEmail() != null) {
    sendEmail(user.getProfile().getEmail());
}&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;그럼 이러한 null 체크가 만들어내는 문제점들은 없을까? 있다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;예외 누락 : NPE 외의 다른 에러를 삼킨다. 이렇게 되면 장애 추적에 어려움이 있다.&lt;/li&gt;
&lt;li&gt;책임 분산 실패 : 데이터가 왜 null인지 명확하게 파악할 수 없다.&lt;/li&gt;
&lt;/ol&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;size18&quot;&gt;&lt;b&gt;1. 명확한 설계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시: 사용자의 프로필은 필수다 vs 선택이다.&lt;/p&gt;
&lt;pre id=&quot;code_1749439122729&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {
    private Profile profile; // null 가능
    private String name; // null 불가
}&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;rarr; 가능 여부를 명확히 주석 혹은 도메인 명세에 기록한다. 그에 따라 Getter 생성자, API Response를 구분한다.&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. Optional을 반환 값에 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 8부터 등장한 Optional&amp;lt;T&amp;gt;은 null을 직접 반환하지 않기 위한 Wrapper이다.&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;pre id=&quot;code_1749439252229&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public User findUser(String id) {
    User user = userRepository.findById(id);
    if (user == null) {
        return new User(); // 무의미한 디폴트
    }
    return user;
}&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;pre id=&quot;code_1749439261695&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Optional&amp;lt;User&amp;gt; findUser(String id) {
    return Optional.ofNullable(userRepository.findById(id));
}&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;Optional을 사용시 주의사항은 return value에만 사용해야 한다는 것이다. 필드에서 사용하게 되면 JPA가 처리하지 못하고 성능 저하를 유발할 수 있다.&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. Null Object 패턴 - 빈 객체를 대신 리턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null 대신 기본 동작을 하는 객체를 리턴한다.&lt;/p&gt;
&lt;pre id=&quot;code_1749439337345&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class NullUser extends User {
    public String getName() { return &quot;알 수 없음&quot;; }
    public Profile getProfile() { return new NullProfile(); }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1749439347393&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;User user = userService.findOrNullUser();
log.info(user.getProfile().getEmail()); // NPE 발생 X&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;장점 : 호출자에서 null 체크 생략이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점 : NullObject가 진짜 의미를 오해할 수 있다.&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;4. 유틸리티 함수 사용 (Objects 클래스)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749439440360&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (Objects.nonNull(user) &amp;amp;&amp;amp; Objects.nonNull(user.getProfile())) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2364&quot; data-start=&quot;2341&quot;&gt;Objects.nonNull()&lt;/li&gt;
&lt;li data-end=&quot;2409&quot; data-start=&quot;2365&quot;&gt;Objects.requireNonNull(obj, &quot;null 불가&quot;)&lt;/li&gt;
&lt;li data-end=&quot;2451&quot; data-start=&quot;2410&quot;&gt;Objects.equals(a, b) &amp;rarr; a가 null일 때도 안전하다.&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;</description>
      <category>Back-End/Java</category>
      <category>null</category>
      <category>null 체크</category>
      <category>자바</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/153</guid>
      <comments>https://codingstudy95.tistory.com/153#entry153comment</comments>
      <pubDate>Mon, 9 Jun 2025 12:25:07 +0900</pubDate>
    </item>
    <item>
      <title>서버-클라이언트 간 데이터 전송 성능 극대화를 위한 프로토콜 분석 (TCP vs UDP vs QUIC)</title>
      <link>https://codingstudy95.tistory.com/143</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;&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;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;TCP, UDP, QUIC&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP : 1970년대 초 창시되어 인터넷의 기본 프로토콜로 자리 잡았다. 안정적인 데이터 전송이 요구되는 서비스에 주로 사용된다.&lt;/li&gt;
&lt;li&gt;UDP : TCP보다 단순한 구조로 빠른 전송이 필요하지만, 신뢰성은 덜 요구되는 서비스에 적합하다. 실시간 스트리밍, VoIP등이 대표적이다.&lt;/li&gt;
&lt;li&gt;QUIC : 최근 Google에 의해 제안되어 빠른 연결 복원, 낮은 지연 시간, 내장 암호화를 특징으로 한다. HTTP/3의 기반 프로토콜로 각광받고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 프로토콜은 당대의 기술적 한계와 요구사항에 맞춰 개발되었는데 TCP는 초기 인터넷의 안정성과 신뢰성을, UDP는 실시간 통신의 필요성을, QUIC은 모바일 환경과 클라우드 서비스 시대의 빠른 응답 속도 요구를 반영한다.&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. TCP(Transmission Control Protocol)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1 TCP의 기본 원리와 동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 연결 지향형 프로토콜로, 데이터 전송 전에 반드시 연결을 수립한다. 이를 통해 데이터의 순서와 무결성을 보장한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3-way 핸드쉐이크 : 클라이언트와 서버가 서로 연결을 확인하는 과정&amp;nbsp;&lt;/li&gt;
&lt;li&gt;데이터 패킷 : 데이터를 작은 단위로 분활하여 전송 후 재조립&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.2 연결 지향형 통신의 특징&lt;/b&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.3 신뢰성 보장 메커니즘&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 다음과 같은 메커니즘으로 데이터 전송의 안정성을 보장한다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.4 TCP의 장단점 및 성능 분석&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점 : 높은 신뢰성, 데이터 순서 보장, 오류 복가 기능&lt;/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;2. UDP (User Datagram Protocol)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.1 UDP의 기본 원리와 특징&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP는 비연결형 프로토콜로, 연결 수립 과정 없이 데이터를 빠르게 전송한다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.2 비연결형 통신의 개념과 장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP는 연결 설정 없이 데이터를 보내기 때문에, 지연이 적고 실시간 응답이 중요한 환경에서 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 : 온라인 게임, VoIP, 실시간 스트리밍 등&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.3 신뢰성 보장 부재가 가져오는 효과와 사례&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP는 신뢰성을 보장하지 않기 때문에, 패킷 손실이나 순서 뒤바뀜이 발생할 수 있다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.4 UDP 사용 시 발생할 수 있는 문제와 해결 방안&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷 손실이나 중복 전송 문제를 해결하기 위해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 레벨에서의 재전송 로직&lt;/li&gt;
&lt;li&gt;시퀀스 번호 추가 및 검증&lt;/li&gt;
&lt;li&gt;FEC(Forward Error Correction) 기법 등을 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.5 UDP 성능 최적화 및 활용 전략&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP는 실시간 서비스에서 높은 성능을 발휘하지만,&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;/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;3. QUIC (Quick UDP Internet Connections)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.1 QUIC의 개요 및 설계 목적&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC는 Google에서 제안한 최신 전송 프로토콜로, 기본적으로 UDP 위에서 동작하지만 TCP의 신뢰성과 UDP의 속도를 결합한 형태이다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.2 QUIC의 주요 기능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC는 다음과 같은 혁신적인 기능들을 제공한다.&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;암호화 내장 : TLS/SSL 기반의 보안 기능이 기본 제공되어 데이터 보안이 강화된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.3 TCP와 UDP의 장점을 결합한 QUIC의 차별점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC는 기존 프로토콜의 단점을 보완한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP 대비 : 초기 연결 지연이 크게 줄어들며, 혼잡 제어 기법이 개선되었다.&lt;/li&gt;
&lt;li&gt;UDP 대비 : 데이터 전송의 신뢰성을 높이고, 연결 복원 기능을 통해 안정성을 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>네트워크</category>
      <category>QUIC</category>
      <category>tcp</category>
      <category>udp</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/143</guid>
      <comments>https://codingstudy95.tistory.com/143#entry143comment</comments>
      <pubDate>Mon, 2 Jun 2025 17:35:47 +0900</pubDate>
    </item>
    <item>
      <title>HTTP 요청 하나당 스레드는 몇 개나 동작할까?</title>
      <link>https://codingstudy95.tistory.com/152</link>
      <description>&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;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;@Async 붙였는데 오히려 느려졌다.&lt;/li&gt;
&lt;li&gt;WebClient 썼는데 로그 순서가 뒤죽박죽이다.&lt;/li&gt;
&lt;li&gt;ThreadLocal 데이터가 갑자기 사라진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제들의 공통점은 &quot;스레드&quot; 이다.&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;그래서 HTTP 요청 하나당 실제로 몇 개의 스레드가 작동하는지 환경별로 한번 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 67.6735%; height: 87px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 31.8605%; height: 19px;&quot;&gt;환경&lt;/td&gt;
&lt;td style=&quot;width: 35.8139%; height: 19px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 31.8605%; height: 17px;&quot;&gt;Spring MVC&lt;/td&gt;
&lt;td style=&quot;width: 35.8139%; height: 17px;&quot;&gt;기본 동기 처리(Tomcat)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 31.8605%; height: 17px;&quot;&gt;Spring MVC + @Async&lt;/td&gt;
&lt;td style=&quot;width: 35.8139%; height: 17px;&quot;&gt;일부 작업 비동기 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 31.8605%; height: 17px;&quot;&gt;Spring MVC + WebClient&lt;/td&gt;
&lt;td style=&quot;width: 35.8139%; height: 17px;&quot;&gt;외부 API 호출 시 비동기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 31.8605%; height: 17px;&quot;&gt;WebFlux + Netty&lt;/td&gt;
&lt;td style=&quot;width: 35.8139%; 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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;요청 하나 = 스레드 하나?는 옛말?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 아래와 같이 생각한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-07 15.12.57.png&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tmiJT/btsNMeXA9cJ/X5KLx0sWET2ZI0w4KVNSa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tmiJT/btsNMeXA9cJ/X5KLx0sWET2ZI0w4KVNSa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tmiJT/btsNMeXA9cJ/X5KLx0sWET2ZI0w4KVNSa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtmiJT%2FbtsNMeXA9cJ%2FX5KLx0sWET2ZI0w4KVNSa0%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;504&quot; height=&quot;99&quot; data-filename=&quot;스크린샷 2025-05-07 15.12.57.png&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;138&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;size16&quot;&gt;이 구조는 맞다 그런데 여기에 다음이 붙는 순간 스레드가 &lt;b&gt;&lt;u&gt;복제&lt;/u&gt;&lt;/b&gt;된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Async 사용&lt;/li&gt;
&lt;li&gt;WebClient 비동기 콜백&lt;/li&gt;
&lt;li&gt;로깅 비동기 처리&lt;/li&gt;
&lt;li&gt;Netty 기반 이벤트 루프&lt;/li&gt;
&lt;/ul&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;b&gt;1. [Spring MVC] 동기 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746598528584&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
public class TestController {

    @GetMapping(&quot;/sync&quot;)
    public String sync() {
        log.info(&quot;스레드: {}&quot;, Thread.currentThread().getName());
        return &quot;ok&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-07 15.15.43.png&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czXGaN/btsNL06jcNi/7o4OMe5MIbHxXYWhm4ZaYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czXGaN/btsNL06jcNi/7o4OMe5MIbHxXYWhm4ZaYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czXGaN/btsNL06jcNi/7o4OMe5MIbHxXYWhm4ZaYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczXGaN%2FbtsNL06jcNi%2F7o4OMe5MIbHxXYWhm4ZaYK%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;244&quot; height=&quot;39&quot; data-filename=&quot;스크린샷 2025-05-07 15.15.43.png&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;86&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller &amp;rarr; Service &amp;rarr; Repository &amp;rarr; 응답까지 모두 하나의 워커 스레드(exec-4) 에서 작동한다.&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;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. [Spring MVC + @Async] - 작업을 나눴더니 스레드가 늘어났다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746598688918&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MyService {
    @Async
    public void doAsync() {
        log.info(&quot;비동기 스레드: {}&quot;, Thread.currentThread().getName());
    }
}

@GetMapping(&quot;/async&quot;)
public String async() {
    log.info(&quot;요청 스레드: {}&quot;, Thread.currentThread().getName());
    myService.doAsync();
    return &quot;ok&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;실행 결과&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-07 15.18.22.png&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coByfQ/btsNMB55Qmm/N53TbAj6L3upsqCtE2zP11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coByfQ/btsNMB55Qmm/N53TbAj6L3upsqCtE2zP11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coByfQ/btsNMB55Qmm/N53TbAj6L3upsqCtE2zP11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoByfQ%2FbtsNMB55Qmm%2FN53TbAj6L3upsqCtE2zP11%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;286&quot; height=&quot;56&quot; data-filename=&quot;스크린샷 2025-05-07 15.18.22.png&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;114&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청은 exec-1 이 처리했다.&lt;/li&gt;
&lt;li&gt;내부 로직은 별도 스레드(task-1)로 넘어갔다.&lt;/li&gt;
&lt;li&gt;요청은 하나지만 2개의 스레드가 동작했다.&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;size18&quot;&gt;&lt;b&gt;3. [Spring MVC + WebClient] - 응답을 기다리는 동안 다른 스레드가 개입&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746598818764&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/webclient&quot;)
public Mono&amp;lt;String&amp;gt; callApi() {
    log.info(&quot;요청 시작: {}&quot;, Thread.currentThread().getName());

    return webClient.get()
        .uri(&quot;http://localhost:8081/slow&quot;)
        .retrieve()
        .bodyToMono(String.class)
        .doOnNext(response -&amp;gt; log.info(&quot;응답 받은 스레드: {}&quot;, Thread.currentThread().getName()));
}&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 WebClinet + 비동기)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-07 15.20.26.png&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v7liE/btsNMSs0c5w/f8zLSxfN1eBCNdKza5LDk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v7liE/btsNMSs0c5w/f8zLSxfN1eBCNdKza5LDk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v7liE/btsNMSs0c5w/f8zLSxfN1eBCNdKza5LDk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv7liE%2FbtsNMSs0c5w%2Ff8zLSxfN1eBCNdKza5LDk0%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;244&quot; height=&quot;56&quot; data-filename=&quot;스크린샷 2025-05-07 15.20.26.png&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시작은 exec-2 에서 진행했다.&lt;/li&gt;
&lt;li&gt;WebClient는 내부적으로 다른 스레드 풀에서 응답을 처리했다.&lt;/li&gt;
&lt;li&gt;요청 하나에 최소 2개 이상의 스레드가 동작했다.&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;size18&quot;&gt;&lt;b&gt;4. [Spring WebFlux + Netty] - 더 이상 스레드 흐름은 예측 불가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746598938880&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/webflux&quot;)
public Mono&amp;lt;String&amp;gt; reactive() {
    log.info(&quot;요청 시작: {}&quot;, Thread.currentThread().getName());

    return Mono.just(&quot;응답&quot;)
        .delayElement(Duration.ofMillis(500))
        .doOnNext(data -&amp;gt; log.info(&quot;응답 처리 스레드: {}&quot;, Thread.currentThread().getName()));
}&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-07 15.22.29.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3vNw3/btsNNHYHzvT/EOadLARiuh59bANVgkHHY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3vNw3/btsNNHYHzvT/EOadLARiuh59bANVgkHHY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3vNw3/btsNNHYHzvT/EOadLARiuh59bANVgkHHY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3vNw3%2FbtsNNHYHzvT%2FEOadLARiuh59bANVgkHHY1%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;212&quot; height=&quot;46&quot; data-filename=&quot;스크린샷 2025-05-07 15.22.29.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청은 reactor-http-nio-* 에서 수신했다.&lt;/li&gt;
&lt;li&gt;응답은 완전히 다른 워커 스레드에서 처리했다 (parallel-*)&lt;/li&gt;
&lt;li&gt;1 요청에 최소 2개 이상 스레드, 심지어 바뀔 수도 있다.&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;table style=&quot;border-collapse: collapse; width: 57.7907%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%;&quot;&gt;환경&lt;/td&gt;
&lt;td style=&quot;width: 14.9612%;&quot;&gt;요청당 스레드 수&lt;/td&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;특징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%;&quot;&gt;Spring MVC&lt;/td&gt;
&lt;td style=&quot;width: 14.9612%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;예측 가능, 단순&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%;&quot;&gt;MVC + @Async&lt;/td&gt;
&lt;td style=&quot;width: 14.9612%;&quot;&gt;2+&lt;/td&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;스레드 분기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%;&quot;&gt;MVC + WebClient&lt;/td&gt;
&lt;td style=&quot;width: 14.9612%;&quot;&gt;2+&lt;/td&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;응답 처리 별도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%;&quot;&gt;WebFlux&lt;/td&gt;
&lt;td style=&quot;width: 14.9612%;&quot;&gt;2 ~ n&lt;/td&gt;
&lt;td style=&quot;width: 24.1473%;&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;스레드가 바뀌면 ThreadLocl에 저장한 값은 사라지고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅, 트랜잭션, MDC, 세션 등 &quot;스레드 고정이 전제된 기능&quot;이 깨질 수 있다.&lt;/p&gt;</description>
      <category>Back-End/Java</category>
      <category>http 요청 하나당 스레드는 몇 개나 동작할까?</category>
      <category>Thread</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/152</guid>
      <comments>https://codingstudy95.tistory.com/152#entry152comment</comments>
      <pubDate>Mon, 12 May 2025 16:26:49 +0900</pubDate>
    </item>
    <item>
      <title>기능은 됐는데 성능이 안 나오는 코드의 특징 5가지</title>
      <link>https://codingstudy95.tistory.com/151</link>
      <description>&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;그럼 성능이 안나오는 코드는 어떤 특징을 가지고 있는지 알아보자&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;일을 하다보면 무의식적으로 무조건 List에 담고 for문을 돌리는 경우가 있다. 그러다 해시맵을 써야 할 곳에 배열을 쓰게 되는 경우가 발생한다.&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;pre id=&quot;code_1745901673738&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 10,000건 데이터 중에서 특정 ID 찾기
for (User user : userList) {
    if (user.getId() == targetId) {
        return user;
    }
}&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;처음부터 Map&amp;lt;Integer, User&amp;gt; 로 관리했다면 O(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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 쓸데없이 많은 IO (DB/네트워크 호출)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for문 안에서 매번 DB나 API 호출하는 경우나 같은 데이터를 반복해서 조회하는 경우 네트워크 지연, DB부하 폭발 &amp;rarr; 시스템 전체가 느려짐으로 연결될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745901955733&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (Order order : orders) {
    Customer customer = customerRepository.findById(order.getCustomerId());
    // ...
}&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;위 코드에서 orders가 1만개라면 DB를 1만 번 호출하게 된다.&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;이럴경우 한 번에 Customer 데이터를 IN 조건으로 미리 가져오거나 조인(Join)으로 조회해 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;&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;3. 캐시를 쓰지 않는다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 조회하는 데이터를 매번 새로 계산하는 경우나 매번 외부 API를 호출하는 경우 CPU, 메모리 낭비 + 응답 지연이 발생할 수 있다.&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;pre id=&quot;code_1745902383143&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Product getBestSeller() {
    // 매번 DB를 때려서 상위 10개를 조회
    return productRepository.findTop10BySalesOrderByDesc();
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기상품 같은 건 5분 간격으로 캐시에 저장하거나 Redis, Caffeine 같은 캐시를 활용한다.&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;b&gt;4. 데이터 양에 대한 고려가 없을 때&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 데이터가 몇 건 없을거야 가정하고 코딩을했을 때 만약 한 번에 100만건 select 후 메모리에 올리게 되면 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 늘어나면 OOM(OutOfMemoryError) 혹은 서버가 터질수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745906118797&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;User&amp;gt; allUsers = userRepository.findAll(); // 수십만 건&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;페이징 처리(LIMIT, OFFSET) 혹은 스트리밍 처리(cursor 기반 조회) 으로 개선이 필요하다.&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;b&gt;5. 비효율적인 반복 로직&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이중 for문, 삼중 for문을 남발하는 경우 or 같은 계산을 여러 번 반복하는 경우 시간복잡도가 기하급수적으로 증가할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1745906247281&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (Item itemA : itemList) {
    for (Item itemB : itemList) {
        if (itemA.equals(itemB)) continue;
        // 비교 처리
    }
}&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;O(n^2) 알고리즘이다. 사전에 정렬하거나 해시셋(HashSet)을 활용, 필요없는 반복을 최소화해서 개선을 할 수 있다.&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;table style=&quot;border-collapse: collapse; width: 66.7442%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.7209%;&quot;&gt;&lt;b&gt;핵심 요약&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;데이터 구조&lt;/td&gt;
&lt;td style=&quot;width: 38.7209%;&quot;&gt;상황에 맞는 자료구조를 써야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;IO 최소화&lt;/td&gt;
&lt;td style=&quot;width: 38.7209%;&quot;&gt;DB, API 호출을 줄여야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;캐시 적극 사용&lt;/td&gt;
&lt;td style=&quot;width: 38.7209%;&quot;&gt;비싼 연산은 저장하고 재사용한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;데이터 양 고려&lt;/td&gt;
&lt;td style=&quot;width: 38.7209%;&quot;&gt;언제든 데이터가 늘어날 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 28.0233%;&quot;&gt;반복 최소화&lt;/td&gt;
&lt;td style=&quot;width: 38.7209%;&quot;&gt;이중/삼중 for문은 반드시 경계한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>Back-End</category>
      <category>성능개선</category>
      <category>자바</category>
      <category>코틀린</category>
      <author>김검정</author>
      <guid isPermaLink="true">https://codingstudy95.tistory.com/151</guid>
      <comments>https://codingstudy95.tistory.com/151#entry151comment</comments>
      <pubDate>Fri, 9 May 2025 16:00:20 +0900</pubDate>
    </item>
  </channel>
</rss>