<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>true-false</title>
    <link>https://true-false.tistory.com/</link>
    <description>이젠 개발 메모장이 되어버린</description>
    <language>ko</language>
    <pubDate>Wed, 8 Apr 2026 06:13:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>isTrue</managingEditor>
    <image>
      <title>true-false</title>
      <url>https://tistory1.daumcdn.net/tistory/7204271/attach/c5b36fa3b3804b6080a6c6043eb0c00a</url>
      <link>https://true-false.tistory.com</link>
    </image>
    <item>
      <title>OS 4편 - 실무 병목 찾기</title>
      <link>https://true-false.tistory.com/149</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 로그도 없는데 특정 시간대에 API가 느려진다. 이걸 어떻게 찾아야 할까. 지금까지 배운 OS 개념을 전부 연결하는 파트다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 전형적인 장애 패턴&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;스케줄러 대용량 쿼리 실행
└── DB 커넥션 오래 점유
    └── HikariCP 커넥션 풀 소진
        └── 일반 API 요청들이 커넥션 대기
            └── 스레드 점유 시간 길어짐
                └── Tomcat Thread Pool 소진
                    └── 전체 API 느려짐 (에러는 없음)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러가 없으니 로그만 봐선 원인을 못 찾는다. 스레드와 커넥션 풀 상태를 직접 봐야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Tomcat 스레드 풀 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Actuator로 현재 스레드 상태를 볼 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;yaml&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;# application.yml
management:
  endpoints:
    web:
      exposure:
        include: metrics&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;GET /actuator/metrics/tomcat.threads.busy
GET /actuator/metrics/tomcat.threads.config.max

busy / max = 0.9 이상 &amp;rarr; 스레드 풀 거의 소진&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 스레드 덤프 - 스레드가 뭘 하다 묶였나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀이 소진됐다면, 스레드가 정확히 어디서 대기 중인지 찍어봐야 한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;jps                  # 실행 중인 Java 프로세스 PID 확인
jstack {PID}         # 스레드 덤프 출력&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과에서 이런 게 보이면 DB 커넥션 대기 중인 거다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;&quot;http-nio-8080-exec-1&quot; - WAITING
    at sun.misc.Unsafe.park
    at HikariPool.getConnection   &amp;larr; DB 커넥션 못 잡고 대기 중&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. HikariCP 커넥션 풀 확인&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;GET /actuator/metrics/hikaricp.connections.active
GET /actuator/metrics/hikaricp.connections.pending

pending &amp;gt; 0 &amp;rarr; 커넥션 기다리는 스레드 있음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Tomcat 스레드 vs HikariCP 커넥션 비율&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 포인트가 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;Tomcat 스레드 200개
HikariCP 커넥션 10개 (기본값)

&amp;rarr; 요청 200개가 와도 DB 작업은 10개씩만 처리
&amp;rarr; 나머지 190개 스레드는 커넥션 기다리며 대기
&amp;rarr; 스레드 200개가 있어도 의미 없음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 풀은 항상 같이 봐야 한다. 둘 중 하나만 크다고 해결이 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP 권장 설정은 이렇다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;커넥션 수 = (CPU 코어 수 * 2) + 유효 디스크 수
ex) 4코어 서버 &amp;rarr; 약 10개&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 수를 무조건 늘린다고 좋은 게 아니다. DB 서버가 감당할 수 있는 커넥션 수에 한계가 있고, 무리하게 늘리면 DB 서버 부하가 증가해서 오히려 전체가 느려진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 모니터링 순서 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 시간대에 API가 느려지면 이 순서로 확인한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;1. 해당 시간에 도는 스케줄러 있는지 확인
2. tomcat.threads.busy &amp;rarr; 스레드 풀 얼마나 찼나
3. hikaricp.connections.pending &amp;rarr; DB 커넥션 대기 있나
4. jstack으로 스레드 덤프 &amp;rarr; 스레드가 정확히 뭘 하다 묶였나&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS/OS</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/149</guid>
      <comments>https://true-false.tistory.com/149#entry149comment</comments>
      <pubDate>Wed, 8 Apr 2026 00:18:53 +0900</pubDate>
    </item>
    <item>
      <title>OS 3편 - Race Condition, synchronized (feat. volatile)</title>
      <link>https://true-false.tistory.com/148</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_6888.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8NsLT/dJMcabDLQFj/nuedRmT7cVUEbMQR6j3021/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8NsLT/dJMcabDLQFj/nuedRmT7cVUEbMQR6j3021/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8NsLT/dJMcabDLQFj/nuedRmT7cVUEbMQR6j3021/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8NsLT%2FdJMcabDLQFj%2FnuedRmT7cVUEbMQR6j3021%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4284&quot; height=&quot;5712&quot; data-filename=&quot;IMG_6888.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 스레드가 같은 메모리를 동시에 건드릴 때 생기는 문제다. &lt;br /&gt;로컬에선 안 터지다가 운영에서 트래픽 몰릴 때 갑자기 데이터가 이상해지는 버그의 주범이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Race Condition이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;count++ 한 줄처럼 보이지만 CPU 입장에선 3단계다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. count 값을 메모리에서 읽어옴  (READ)
2. 1을 더함                      (ADD)
3. 결과를 메모리에 저장           (WRITE)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 2개가 동시에 실행되면 이 사이에 끼어들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;스레드 1                  스레드 2
READ  &amp;rarr; count = 0
                          READ  &amp;rarr; count = 0
ADD   &amp;rarr; 0 + 1 = 1
                          ADD   &amp;rarr; 0 + 1 = 1
WRITE &amp;rarr; count = 1
                          WRITE &amp;rarr; count = 1

최종 결과: count = 1  (원래 2가 되어야 함)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 순서에 따라 결과가 달라지는 이 상황이 Race Condition이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무에서 주로 터지는 상황&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;├── 재고 차감 (동시에 마지막 1개 주문)
├── 포인트 / 잔액 업데이트
├── 좋아요 / 조회수 카운터
└── 선착순 쿠폰 발급
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 테스트에선 요청이 1~2개라 안 터지다가, 운영에서 트래픽 몰릴 때 갑자기 숫자가 안 맞는다. 로그엔 에러도 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 해결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;synchronized&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 스레드 하나만 이 메서드에 들어올 수 있게 막는다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public synchronized void like() {
    count++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 스레드는 밖에서 대기한다. Race Condition은 잡히지만, 읽기까지 전부 직렬화되니 트래픽 많을 때 성능이 떨어진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AtomicInteger&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;READ-ADD-WRITE를 CPU 레벨에서 하나의 원자적 연산으로 처리한다. synchronized보다 훨씬 빠르고, 단순 카운터엔 이걸 쓰는 게 맞다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;AtomicInteger count = new AtomicInteger(0);

public void like() {
    count.incrementAndGet();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. volatile - 가시성 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 메모리에서 매번 읽으면 느리니까 캐시에 값을 복사해서 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 스레드 1이 값을 업데이트해도 스레드 2가 CPU 캐시에서 오래된 값을 읽을 수 있다는 거다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무에서 직접 겪다가 발견한 volatile 키워드!&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;메모리: count = 5

CPU 1 캐시: count = 5    CPU 2 캐시: count = 5

스레드 1이 count = 6으로 업데이트
&amp;rarr; CPU 1 캐시엔 6
&amp;rarr; 메모리엔 아직 5
&amp;rarr; 스레드 2는 캐시에서 5를 읽음 (오래된 값)
&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;volatile은 캐시를 우회해서 항상 메모리에서 직접 읽고 쓰게 강제한다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;volatile int count = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단, volatile만으로는 Race Condition이 안 잡힌다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;volatile이 해결하는 건 가시성(Visibility)&lt;/b&gt; 문제다. READ-ADD-WRITE 사이에 다른 스레드가 끼어드는 건 여전히 막지 못한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;volatile count = 0

스레드 1: READ  &amp;rarr; 0 (메모리에서 직접 읽음)
스레드 2: READ  &amp;rarr; 0 (메모리에서 직접 읽음)
스레드 1: ADD, WRITE &amp;rarr; 1
스레드 2: ADD, WRITE &amp;rarr; 1  &amp;larr; 여전히 Race Condition
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 케이스 - Map 참조 교체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Component에서 여러 빈이 공유하는 Map을 주기적으로 새 버전으로 교체하는 구조라면 volatile이 정확한 선택이다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Component
public class SomeComponent {
    private volatile Map&amp;lt;String, String&amp;gt; cache = new HashMap&amp;lt;&amp;gt;();

    public void update() {
        Map&amp;lt;String, String&amp;gt; newMap = new HashMap&amp;lt;&amp;gt;();
        newMap.put(&quot;key1&quot;, &quot;value1&quot;); // 다 채우고
        newMap.put(&quot;key2&quot;, &quot;value2&quot;); // 다 채우고
        cache = newMap;               // 마지막에 참조 교체
    }

    public String get(String key) {
        return cache.get(key);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 위험한 건 Race Condition이 아니라 스레드가 CPU 캐시에서 이전 참조를 읽는 가시성 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;volatile이 참조 교체를 모든 스레드에 즉시 보이게 해준다.&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;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 맵으로 먼저 교체하고 채우면, 그 사이에 읽는 스레드가 빈 맵을 보게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;만약 맵 내부를 직접 수정하는 구조로 바뀐다면 그때는 ConcurrentHashMap&lt;/b&gt;을 써야 한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;private Map&amp;lt;String, String&amp;gt; cache = new ConcurrentHashMap&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 서버가 2대 이상이면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;synchronized와 volatile은 같은 JVM 안에서만 유효하다. 서버가 2대면 JVM도 2개고 메모리도 따로다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서버 1 (JVM 1)          서버 2 (JVM 2)
재고 = 1                재고 = 1

둘 다 동시에 차감 시도
&amp;rarr; 둘 다 성공
&amp;rarr; 재고 -2 되어야 하는데 -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 환경에서는 DB 레벨 락이나 Redis 분산 락으로 해결한다. 이건 Redis 챕터에서 제대로 다룬다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정리&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;해결하는 문제&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&amp;nbsp;한계&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;synchronized&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Race Condition 방지&lt;/td&gt;
&lt;td&gt;같은 JVM 안에서만, 성능 저하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;volatile&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CPU 캐시 가시성 문제&lt;/td&gt;
&lt;td&gt;Race Condition은 못 막음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;AtomicInteger&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Race Condition 방지&lt;/td&gt;
&lt;td&gt;단순 연산에만 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ConcurrentHashMap&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;맵 내부 동시 수정&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DB / Redis 락&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;분산 환경 Race Condition&lt;/td&gt;
&lt;td&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>CS/OS</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/148</guid>
      <comments>https://true-false.tistory.com/148#entry148comment</comments>
      <pubDate>Tue, 7 Apr 2026 23:57:01 +0900</pubDate>
    </item>
    <item>
      <title>OS 2편 - 동기 vs 비동기, 그리고 언제 써야 하는가</title>
      <link>https://true-false.tistory.com/147</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드가 &quot;기다리는 시간&quot;을 어떻게 쓰느냐의 차이다. 단순히 빠르고 느린 게 아니라, 상황에 따라 골라 써야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 동기(Synchronous)란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기 방식에서 스레드는 작업이 끝날 때까지 그 자리에서 기다린다. DB 쿼리를 날리면 응답이 올 때까지, 외부 API를 호출하면 결과가 올 때까지 스레드는 아무것도 못 하고 점유만 한다. 이걸 &lt;b&gt;블로킹(Blocking)&lt;/b&gt; 이라 한다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;스레드 1
├── 요청 받음
├── DB 쿼리 날림
├── ⏳ DB 응답 기다리는 중... (CPU 안 씀, 스레드 점유 중)
├── ⏳ 기다리는 중...
├── DB 응답 옴
├── 결과 가공
└── 응답 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 쿼리 100ms 중 실제 CPU를 쓰는 시간은 5ms 정도다. 나머지 95ms는 그냥 대기다. 그런데 스레드는 그 시간 내내 자리를 차지한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat 스레드 풀이 200개인데 동시 요청이 몰리면? 200개 스레드가 전부 DB 기다리느라 멈춰있고, 201번째 요청부터 큐에서 대기하게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 비동기(Asynchronous)란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 방식에서는 I/O 작업을 던져놓고 스레드를 즉시 반납한다. 응답이 오면 이벤트가 발생하고, 그때 스레드를 다시 꺼내 나머지 처리를 한다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;스레드 1
├── 요청 받음
├── DB 쿼리 날림
├── &quot;응답 오면 알려줘&quot; 등록하고 &amp;rarr; 스레드 즉시 반납
│
│   (스레드 1은 이미 다른 요청 처리 중)
│
├── DB 응답 도착 (이벤트 발생)
└── 스레드 꺼내서 결과 처리 &amp;rarr; 응답 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기다리는 시간에 스레드가 다른 일을 할 수 있다. 같은 스레드 풀로 더 많은 요청을 처리할 수 있게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring에서 비동기 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async를 붙이면 호출한 스레드와 별도의 스레드 풀에서 메서드가 실행된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
public class SubtitleService {

    @Async
    public CompletableFuture&amp;lt;List&amp;lt;Subtitle&amp;gt;&amp;gt; findSubtitles(Long callId) {
        List&amp;lt;Subtitle&amp;gt; result = subtitleRepository.findByCallId(callId);
        return CompletableFuture.completedFuture(result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, @EnableAsync를 설정 클래스에 추가해야 하고, Thread Pool 설정도 함께 잡아줘야 제대로 동작한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 그럼 항상 비동기가 좋은가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니다. 비동기가 효과 있는 상황과 없는 상황이 명확하게 갈린다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;효과 있는 경우 - 여러 I/O를 병렬로 날릴 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 API 결과가 서로 의존하지 않는다면 동시에 날리면 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 동기 - 순차 실행 (총 500ms)
User user = userApi.getUser(id);       // 200ms 기다림
Payment pay = paymentApi.getPay(id);   // 300ms 기다림

// 비동기 - 병렬 실행 (총 300ms)
CompletableFuture&amp;lt;User&amp;gt; userFuture = userApi.getUserAsync(id);
CompletableFuture&amp;lt;Payment&amp;gt; payFuture = paymentApi.getPayAsync(id);
CompletableFuture.allOf(userFuture, payFuture).join();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;효과 없는 경우 - A 결과로 B를 해야 할 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서가 정해진 흐름은 비동기로 바꿔도 결국 기다리는 건 똑같다. 코드만 복잡해진다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 어차피 순서가 있으면 동기로 충분
User user = userApi.getUser(id);         // 이 결과로
Payment pay = paymentApi.getPay(user);   // 이걸 해야 함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;동기&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;비동기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;코드 복잡도&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;단순&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;복잡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;디버깅&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;쉬움&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;어려움 (스택 트레이스 추적 힘듦)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;효과적인 상황&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;순차 의존 흐름&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;독립적인 I/O 병렬 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Spring 기본&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✓&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;별도 설정 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 기다리는 시간이 생기는 I/O 상황들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기를 고려해야 하는 건 전부 I/O 대기 상황이다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;I/O 대기 상황
├── DB 쿼리 대기
├── 외부 API 호출 대기  (RestClient, RestTemplate, WebClient)
├── 파일 읽기 / 쓰기 대기
└── 메시지 큐 대기      (Kafka, RabbitMQ 등)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통점은 CPU가 아니라 네트워크나 디스크에 던져놓고 기다리는 것들이라는 점이다. CPU 입장에선 전부 낭비되는 시간이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 외부 API 호출 시 반드시 해야 할 것 - 타임아웃&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API가 느려지는 날, 타임아웃이 없으면 내 서버 스레드들이 전부 거기서 묶인다. 외부 서비스 장애가 내 서비스 장애로 전파되는 것이다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;RestClient.builder()
    .baseUrl(&quot;https://payment-api.com&quot;)
    .requestFactory(factory -&amp;gt; {
        factory.setConnectTimeout(Duration.ofSeconds(3));
        factory.setReadTimeout(Duration.ofSeconds(5));
    })
    .build();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 여부와 상관없이, 외부 API 호출엔 타임아웃 설정이 필수다. 이게 없으면 외부 의존성 하나가 전체 서비스를 잡아먹을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 판단 기준 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API나 DB 호출이 있을 때 이 질문을 먼저 해보자.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;이 결과가 나와야 다음 걸 할 수 있나?
├── Yes &amp;rarr; 동기로 충분, 타임아웃만 잘 설정
└── No  &amp;rarr; 병렬 처리 가능, 비동기 고려
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기는 무조건 좋은 게 아니다. 병렬로 처리 가능한 I/O가 있을 때 쓰는 것이고, 괜히 전부 비동기로 바꾸면 디버깅이 훨씬 어려워진다.&lt;/p&gt;</description>
      <category>CS/OS</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/147</guid>
      <comments>https://true-false.tistory.com/147#entry147comment</comments>
      <pubDate>Tue, 7 Apr 2026 23:41:06 +0900</pubDate>
    </item>
    <item>
      <title>OS 1편 - 프로세스, 스레드, Thread Pool</title>
      <link>https://true-false.tistory.com/146</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로세스 vs 스레드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스는 OS가 프로그램 실행 시 만드는 독립된 단위다. 각자 메모리 공간을 가지고 있어서 서로 침범하지 못한다. java -jar로 Spring 앱을 띄우면 프로세스 1개가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드는 프로세스 안에서 실제로 코드를 실행하는 단위다. 같은 프로세스 안의 스레드끼리는 메모리를 공유한다. Tomcat은 HTTP 요청이 들어오면 스레드 1개를 할당해서 처리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨텍스트 스위칭&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 한 번에 스레드 하나만 실행한다. &quot;동시에 처리&quot;처럼 보이는 건 CPU가 스레드들을 극도로 빠르게 오가기 때문이다. 이 전환 과정 자체가 CPU 자원을 소모한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드가 많아질수록 전환 비용이 증가해서, 일정 수를 넘으면 오히려 전체 처리 속도가 떨어진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Thread Pool&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드를 매번 만들고 없애는 건 비용이 크다. 그래서 Tomcat은 스레드를 미리 만들어두고 재사용한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;server:
  tomcat:
    threads:
      max: 200        # 최대 스레드 수
      min-spare: 10   # 최소 대기 스레드
    accept-count: 100 # 대기 큐 크기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 들어오면 풀에서 스레드를 꺼내 쓰고, 처리가 끝나면 반납한다. 풀이 꽉 차면 큐에서 대기하고, 큐도 꽉 차면 에러를 반환한다.&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;실무 연결 포인트&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;배포 후 전체 API가 갑자기 느려졌다&quot; &amp;rarr; 원인을 DB에서 찾아라&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 API의 쿼리가 느려서 스레드를 오래 점유하면, 풀이 소진되면서 다른 멀쩡한 API까지 대기하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인은 DB, 증상은 전체 서비스 지연.&lt;/b&gt;&lt;/p&gt;</description>
      <category>CS/OS</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/146</guid>
      <comments>https://true-false.tistory.com/146#entry146comment</comments>
      <pubDate>Tue, 7 Apr 2026 23:26:19 +0900</pubDate>
    </item>
    <item>
      <title>[실무] window.location.href vs API 호출 차이 (SSO 구현하며)</title>
      <link>https://true-false.tistory.com/145</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSO 연동을 하면서 이런 고민이 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 요청을 API로 처리할지&lt;/li&gt;
&lt;li&gt;아니면 window.location.href로 리다이렉트할지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 그냥 API로 처리하려고 했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;axios.get(&quot;/sso/request&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 방식으로는 해결이 안 되는 문제가 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 결론 먼저&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSO는 대부분 API 호출로 처리하는 게 아니라&lt;br /&gt;브라우저 리다이렉트(window.location.href)로 처리한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;window.location.href = &quot;/sso/request&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 필요한지 아래에서 정리해본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. API 호출 방식이 안 되는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;axios로 요청하면 이런 흐름이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS 코드 &amp;rarr; 서버 요청 &amp;rarr; 응답(JSON 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 SSO는 여기서 끝나는 게 아니라&lt;br /&gt;&lt;b&gt;외부 인증 서버로 이동해야 한다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서버에서 이렇게 응답을 준다고 해도&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;redirectUrl&quot;: &quot;https://sso-server/login&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 다시 이동을 해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;window.location.href = response.redirectUrl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 한 번 더 처리해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. window.location.href 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 브라우저를 이동시키는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;window.location.href = &quot;/sso/request&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 흐름이 단순해진다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저가 /sso/request 호출&lt;/li&gt;
&lt;li&gt;서버에서 SSO 서버로 redirect&lt;/li&gt;
&lt;li&gt;인증 완료 후 다시 돌아옴&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 중간에 JS 개입이 없다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실제로 겪은 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 API로 처리하려다가 막혔던 부분이 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 도메인 문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 환경 (Apache)&lt;/li&gt;
&lt;li&gt;여러 도메인 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 API 호출하면&lt;br /&gt;&lt;b&gt;사용자가 어떤 도메인으로 들어왔는지 서버가 알기 어려웠다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 window.location.href는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  브라우저 기준으로 이동하기 때문에&lt;br /&gt;  원래 도메인 흐름이 자연스럽게 유지된다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 쿠키 / 세션 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSO는 보통 쿠키 기반 인증을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 호출로 처리하면&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;SameSite 정책 영향 받음&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;  브라우저가 직접 이동하기 때문에&lt;br /&gt;  쿠키 흐름이 정상적으로 유지된다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 인증 흐름 깨짐&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSO는 기본적으로&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 호출로 중간에 끼면&lt;br /&gt;흐름이 꼬이는 경우가 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 언제 API 방식 써도 되나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 안 되는 건 아니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰 기반 인증 (JWT)&lt;/li&gt;
&lt;li&gt;SPA 내부 인증 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 API 호출로 처리해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  외부 SSO (SAML, OAuth redirect 기반)는&lt;br /&gt;  거의 무조건 redirect 방식 사용&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식 차이는 이거 하나로 정리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 호출:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS &amp;rarr; 서버 &amp;rarr; 응답 처리 필요&lt;/li&gt;
&lt;li&gt;흐름 제어를 직접 해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;window.location.href:&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSO는 &amp;ldquo;API로 로그인 처리&amp;rdquo;가 아니라&lt;br /&gt;&amp;ldquo;브라우저 이동 기반 인증 흐름&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 대부분 구현이&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;window.location.href = &quot;/sso/request&quot;;
&lt;/code&gt;&lt;/pre&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;</description>
      <category>Front-End</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/145</guid>
      <comments>https://true-false.tistory.com/145#entry145comment</comments>
      <pubDate>Mon, 6 Apr 2026 22:51:12 +0900</pubDate>
    </item>
    <item>
      <title>앞으로의 매력있는 사람</title>
      <link>https://true-false.tistory.com/144</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;2416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk9Xy6/dJMcagZlknz/p218LhdOToMtVNZyiKDgeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk9Xy6/dJMcagZlknz/p218LhdOToMtVNZyiKDgeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk9Xy6/dJMcagZlknz/p218LhdOToMtVNZyiKDgeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk9Xy6%2FdJMcagZlknz%2Fp218LhdOToMtVNZyiKDgeK%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;2250&quot; height=&quot;2416&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;2416&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;a href=&quot;https://blog.naver.com/doctordk/224241404491&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.naver.com/doctordk/224241404491&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775400576618&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;앞으로 어떤 사람이 매력 있는 사람이 될까?&quot; data-og-description=&quot;매일같이 시끄럽게 들리는 전쟁 이야기 말고 오늘은 모처럼 삶에 대해서 이야기를 해보고자 한다. 얼마 전 ...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://blog.naver.com/doctordk/224241404491&quot; data-og-url=&quot;https://blog.naver.com/doctordk/224241404491&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/74jc0/dJMb86O2ntk/IJhUlkiplw7Xr2SDVD9Bm1/img.png?width=743&amp;amp;height=557&amp;amp;face=0_0_743_557&quot;&gt;&lt;a href=&quot;https://blog.naver.com/doctordk/224241404491&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.naver.com/doctordk/224241404491&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/74jc0/dJMb86O2ntk/IJhUlkiplw7Xr2SDVD9Bm1/img.png?width=743&amp;amp;height=557&amp;amp;face=0_0_743_557');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 어떤 사람이 매력 있는 사람이 될까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;매일같이 시끄럽게 들리는 전쟁 이야기 말고 오늘은 모처럼 삶에 대해서 이야기를 해보고자 한다. 얼마 전 ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우연히 이런 글을 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적인 내용은 AI가 발전하면서 지식의 평준화가 이루어졌고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 어떤 지식을 깊게 알고, 특정 분야의 전문가가 매력있는 사람이었다면 이젠 얕고 넓게 아는 사람&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 누군가와 대화 할 때 여러 방면에서 공감해주고 이야기를 이끌 수 있는 사람이 이 매력있는 사람이 아니겠냐는 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 그런 것 같기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 누군가와 대화하는 게 점점 힘들어지고, 대화 주제를 만들어 내는게 어려워져서 이런 걸 잘 하는 사람이 부러울 지경까지 됐는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 내가햇던 고민을 정리된 글로 보니 내가 느낀게 단순한 이상함은 아니었구나 싶었다.&lt;/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;</description>
      <category>생각</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/144</guid>
      <comments>https://true-false.tistory.com/144#entry144comment</comments>
      <pubDate>Sun, 5 Apr 2026 23:57:26 +0900</pubDate>
    </item>
    <item>
      <title>보안 전체 구조</title>
      <link>https://true-false.tistory.com/143</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmQYiu/dJMcaduGMhv/ILIaklFcQG6nQg179HKoWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmQYiu/dJMcaduGMhv/ILIaklFcQG6nQg179HKoWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmQYiu/dJMcaduGMhv/ILIaklFcQG6nQg179HKoWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmQYiu%2FdJMcaduGMhv%2FILIaklFcQG6nQg179HKoWk%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;1474&quot; height=&quot;582&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;582&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;/b&gt; &amp;mdash; 데이터를 변환해서 숨기는 것. AES(대칭키, 빠름)랑 RSA(비대칭키, 인증/키교환에 씀)가 핵심.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해싱&lt;/b&gt; &amp;mdash; 단방향 변환. 복호화 안 됨. 비밀번호 저장에 쓰고, 솔트는 여기에 붙는 보조 개념&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;키 교환/인증&lt;/b&gt; &amp;mdash; SSH나 TLS처럼 &quot;통신 시작 전 신뢰를 수립하는&quot; 과정.&amp;nbsp;&lt;/p&gt;</description>
      <category>Back-End/보안</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/143</guid>
      <comments>https://true-false.tistory.com/143#entry143comment</comments>
      <pubDate>Thu, 2 Apr 2026 23:08:49 +0900</pubDate>
    </item>
    <item>
      <title>5.2 랜더트리</title>
      <link>https://true-false.tistory.com/142</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;DOM과 CSSOM을 합쳐서 &lt;b&gt;실제로 화면에 그려질 요소만&lt;/b&gt; 추려낸 트리다.&lt;b&gt;display: none은 렌더 트리에서 완전히 제외된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM에는 존재하지만, 렌더 트리에는 아예 노드 자체가 없다. 그래서 공간도 차지하지 않고 레이아웃 계산 대상도 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/84Hko/dJMcabwUcVk/oaPYJlWoVQkK3xMb69UnlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/84Hko/dJMcabwUcVk/oaPYJlWoVQkK3xMb69UnlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/84Hko/dJMcabwUcVk/oaPYJlWoVQkK3xMb69UnlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F84Hko%2FdJMcabwUcVk%2FoaPYJlWoVQkK3xMb69UnlK%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;1336&quot; height=&quot;988&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;988&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프론트엔드에서 실제로 연결되는 포인트:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue의 v-if는 display: none처럼 DOM 자체를 제거한다. 렌더 트리에도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v-show는 display: none 스타일만 토글한다. DOM과 렌더 트리 모두 유지하다가 스타일만 바꾸는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 자주 토글되는 요소는 v-show, 거의 안 바뀌는 조건부 요소는 v-if가 성능상 낫다. 렌더 트리 재구성 비용 때문이다.&lt;/p&gt;</description>
      <category>Front-End/네트워크(브라우저)</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/142</guid>
      <comments>https://true-false.tistory.com/142#entry142comment</comments>
      <pubDate>Wed, 1 Apr 2026 23:38:09 +0900</pubDate>
    </item>
    <item>
      <title>5.1 DOM</title>
      <link>https://true-false.tistory.com/141</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;DOM은 단순히 태그를 나열한 게 아니라 &lt;b&gt;태그(+ 텍스트, 주석 등)를 객체로 만들어서 트리로 연결한 것&lt;/b&gt;이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;&quot;객체&quot;&lt;/b&gt; 라는 점이에요. 단순한 문자열 &amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;이 아니라, JS에서 조작할 수 있는 프로퍼티와 메서드를 가진 객체예요.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;lt;h1 class=&quot;title&quot;&amp;gt;Hello&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 파싱되면 이런 객체가 돼요:&lt;/p&gt;
&lt;pre class=&quot;nimrod&quot;&gt;&lt;code&gt;{
  tagName: &quot;H1&quot;,
  className: &quot;title&quot;,
  textContent: &quot;Hello&quot;,
  style: {...},
  parentNode: &amp;lt;body 노드&amp;gt;,
  children: [텍스트 노드],
  addEventListener: function() {...},
  getAttribute: function() {...},
  // ... 수십 가지 메서드
}
&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;그래서 JS에서 이런 게 가능한 거예요:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;const el = document.querySelector('h1')

el.textContent = '바뀐 텍스트'   // 프로퍼티 수정
el.style.color = 'red'           // 스타일 변경
el.addEventListener('click', fn) // 이벤트 등록
el.parentNode                    // 부모 노드 접근
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML은 그냥 텍스트지만, DOM은 브라우저가 그걸 &lt;b&gt;조작 가능한 객체 트리&lt;/b&gt;로 바꿔놓은 거예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue/React가 결국 하는 일도 이 DOM 객체들을 효율적으로 업데이트하는 거고요.&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;DOM = HTML 태그들을 JS로 조작 가능한 객체로 변환한 트리 구조&lt;/b&gt;&lt;/p&gt;</description>
      <category>Front-End/네트워크(브라우저)</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/141</guid>
      <comments>https://true-false.tistory.com/141#entry141comment</comments>
      <pubDate>Wed, 1 Apr 2026 23:34:49 +0900</pubDate>
    </item>
    <item>
      <title>5. 서버 응답 + 브라우저 렌더링</title>
      <link>https://true-false.tistory.com/140</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;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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X03IL/dJMcaiv0WAn/sP2CQD9DtzXfMpqT3IlFKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X03IL/dJMcaiv0WAn/sP2CQD9DtzXfMpqT3IlFKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X03IL/dJMcaiv0WAn/sP2CQD9DtzXfMpqT3IlFKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX03IL%2FdJMcaiv0WAn%2FsP2CQD9DtzXfMpqT3IlFKk%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;1526&quot; height=&quot;1156&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;1156&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;응답 구조:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태줄의 숫자가 결과를 요약해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;200은 성공, 301/302는 다른 URL로 이동, 404는 없는 페이지, 500은 서버 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache-Control 헤더는 브라우저가 이 응답을 얼마나 캐싱할지 결정해요. 프론트엔드 성능 최적화에서 자주 건드리는 헤더예요.&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;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTML 파싱 &amp;rarr; DOM&lt;/b&gt; &amp;mdash; 브라우저가 HTML을 위에서 아래로 읽으면서 태그를 트리 구조로 변환해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 서버에서 HTML을 받으면, 이 문자열을 그대로 쓸 수 없어요. JS로 조작하고 CSS를 입히려면 &lt;b&gt;트리 구조&lt;/b&gt;로 변환해야 해요. 이 과정이 파싱이에요.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;1224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x36VE/dJMcaiCNkjK/IX0i2KcKZtIrQOxFb2mkB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x36VE/dJMcaiCNkjK/IX0i2KcKZtIrQOxFb2mkB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x36VE/dJMcaiCNkjK/IX0i2KcKZtIrQOxFb2mkB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx36VE%2FdJMcaiCNkjK%2FIX0i2KcKZtIrQOxFb2mkB0%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;1426&quot; height=&quot;1224&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;1224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파싱 5단계:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;① 바이트 수신 &amp;mdash; 서버에서 HTML이 바이트(0101...)로 날아와요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;② 문자 변환 &amp;mdash; Content-Type: charset=UTF-8 같은 인코딩 정보를 보고 바이트를 사람이 읽을 수 있는 문자열로 변환해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;③ 토큰화 &amp;mdash; 문자열을 &amp;lt;html&amp;gt;, &amp;lt;/div&amp;gt;, &quot;텍스트&quot; 같은 의미 단위(토큰)로 잘라요. HTML 스펙에 정의된 규칙대로 파서가 문자를 하나씩 읽으면서 분류해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;④ 노드 생성 &amp;mdash; 토큰 하나하나를 객체(노드)로 만들어요. 요소 노드(&amp;lt;h1&amp;gt;), 텍스트 노드(&quot;Hello&quot;), 주석 노드(&amp;lt;!-- --&amp;gt;) 등 종류가 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⑤ DOM 트리 완성 &amp;mdash; 노드들을 부모-자식 관계로 연결해서 트리를 만들어요. 이게 JS에서 document.querySelector()로 접근하는 그 DOM이에요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프론트엔드 개발에서 실제로 연결되는 포인트:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;script 태그 위치가 성능에 영향을 주는 이유가 바로 ③~④ 단계에서 파싱이 멈추기 때문이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue/React 앱에서 번들 스크립트를 &amp;lt;body&amp;gt; 맨 아래나 defer로 넣는 게 이 때문이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;innerHTML로 DOM을 바꾸면 해당 부분을 다시 파싱해서 노드를 새로 만들어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 루프 안에서 innerHTML 남발하면 느려져요. DocumentFragment나 createElement가 더 효율적인 이유예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue의 Virtual DOM도 이 실제 DOM 트리를 직접 건드리는 비용을 줄이기 위한 구조예요. 변경사항을 메모리상의 가상 트리에서 먼저 계산하고, 최소한의 실제 DOM 조작만 하는 거예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;&amp;lt;script&amp;gt;&lt;/b&gt; 태그를 만나면 파싱을 멈추고 JS를 먼저 실행해요. -&amp;gt; 이게 렌더링 블로킹&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CSS 파싱 &amp;rarr; CSSOM&lt;/b&gt; &amp;mdash; CSS도 별도로 파싱해서 스타일 트리를 만들어요. CSS가 다 로드되기 전까지는 렌더 트리를 못 만들어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;렌더 트리&lt;/b&gt; &amp;mdash; DOM + CSSOM을 합쳐서 실제로 화면에 보일 요소들만 추려요. display: none 요소는 여기서 제외돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레이아웃 (Reflow)&lt;/b&gt; &amp;mdash; 각 요소의 정확한 위치와 크기를 계산해요. JS로 DOM을 자주 건드리면 이 단계가 반복 실행되면서 성능이 떨어져요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;페인트 + 합성&lt;/b&gt; &amp;mdash; 계산된 레이아웃대로 실제 픽셀을 그려요. GPU를 활용하는 합성(Composite) 단계까지 끝나면 드디어 화면에 보여요.&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;defer와 async의 차이가 바로 이 렌더링 파이프라인에서 나와요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defer는 HTML 파싱 완료 후 실행, async는 로드되는 즉시 파싱 중단하고 실행이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue.js나 React 번들 스크립트에 defer를 붙이는 이유가 여기 있어요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL&amp;nbsp;입력&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;DNS로&amp;nbsp;IP&amp;nbsp;조회&amp;nbsp;(캐시&amp;nbsp;계층적&amp;nbsp;확인)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;TCP&amp;nbsp;3-way&amp;nbsp;handshake&amp;nbsp;(연결&amp;nbsp;수립)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;TLS&amp;nbsp;handshake&amp;nbsp;(암호화&amp;nbsp;채널,&amp;nbsp;HTTPS)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;HTTP&amp;nbsp;요청&amp;nbsp;전송&amp;nbsp;(메서드&amp;nbsp;+&amp;nbsp;헤더&amp;nbsp;+&amp;nbsp;바디)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;서버&amp;nbsp;응답&amp;nbsp;수신&amp;nbsp;(상태코드&amp;nbsp;+&amp;nbsp;헤더&amp;nbsp;+&amp;nbsp;바디)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;브라우저&amp;nbsp;렌더링&amp;nbsp;(DOM&amp;nbsp;&amp;rarr;&amp;nbsp;CSSOM&amp;nbsp;&amp;rarr;&amp;nbsp;렌더트리&amp;nbsp;&amp;rarr;&amp;nbsp;레이아웃&amp;nbsp;&amp;rarr;&amp;nbsp;페인트)&lt;/p&gt;</description>
      <category>Front-End/네트워크(브라우저)</category>
      <category>네트워크핵심</category>
      <author>isTrue</author>
      <guid isPermaLink="true">https://true-false.tistory.com/140</guid>
      <comments>https://true-false.tistory.com/140#entry140comment</comments>
      <pubDate>Wed, 1 Apr 2026 23:33:48 +0900</pubDate>
    </item>
  </channel>
</rss>