<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Duck's Tech Blog</title>
    <link>https://rvbear7.tistory.com/</link>
    <description>해충(Bug) 잡는 오리 개발자</description>
    <language>ko</language>
    <pubDate>Tue, 28 Jul 2026 09:35:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>개발자 오리</managingEditor>
    <image>
      <title>Duck's Tech Blog</title>
      <url>https://tistory1.daumcdn.net/tistory/5733103/attach/6d5543076a5943d1a48e7f1f0c3adb46</url>
      <link>https://rvbear7.tistory.com</link>
    </image>
    <item>
      <title>대규모 시스템 설계와 RabbitMQ</title>
      <link>https://rvbear7.tistory.com/60</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대규모 시스템 설계&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RabbitMQ&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서비스를 운영하다 보면 사용자가 증가하면서 동시에 수많은 요청이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 하나의 서버와 하나의 데이터베이스만으로도 충분하지만, 서비스 규모가 커질수록 단순한 구조만으로는 성능과 안정성을 보장하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 시스템에서는 많은 요청을 안정적으로 처리하면서도 장애에 강하고, 확장 가능한 구조를 설계하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 이러한 환경에서 비동기 처리와 서비스 간 결합도를 낮추기 위해 가장 많이 사용하는 메시지 브로커 중 하나이다.&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;대규모 시스템이라고 해서 단순히 사용자가 많은 시스템만 의미하는 것은 아니다.&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;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;TPS(Transactions Per Second)&lt;/li&gt;
&lt;li&gt;응답 속도&lt;/li&gt;
&lt;li&gt;확장성(Scalability)&lt;/li&gt;
&lt;li&gt;장애 대응(Fault Tolerance)&lt;/li&gt;
&lt;li&gt;데이터 일관성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 성장할수록 가장 먼저 확인해야 하는 것은 &lt;b&gt;최대 동시 요청량(TPS)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 특정 시간에 초당 200건의 요청이 발생한다면 최소 200TPS 이상을 처리할 수 있도록 설계해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 예상치 못한 트래픽 증가를 고려하여 약 1.5배 정도의 여유 용량을 확보하는 경우가 많다.&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;서버 증설(Scale-Out)&lt;/li&gt;
&lt;li&gt;Auto Scaling&lt;/li&gt;
&lt;li&gt;Queue를 이용한 요청 대기&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;size16&quot;&gt;모든 요청은 동일하게 처리하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기(Read) 요청과 쓰기(Write) 요청은 병목이 발생하는 위치가 다르므로 각각 다른 방식으로 최적화한다.&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) 읽기(Read) 요청 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 조회 요청은 데이터베이스 조회 시간이 가장 오래 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 줄이기 위해 다양한 방법을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cache&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많이 사용하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Client
   &amp;darr;
Redis Cache
   &amp;darr; (Miss)
Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 데이터를 반복 조회하는 경우 DB 대신 Redis에서 데이터를 가져오기 때문에 응답 속도가 매우 빨라지고 DB 부하도 크게 감소한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Database Index&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;하지만 인덱스가 너무 많아지면 INSERT, UPDATE 성능은 저하될 수 있다.&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;Read Replica&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 전용 데이터베이스를 별도로 운영한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Master DB
    │
Replication
    │
Read Replica&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 요청은 Replica가 처리하고 쓰기 요청은 Master가 처리하여 부하를 분산한다.&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;Database Sharding&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 여러 DB에 분산 저장하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 DB에 모든 요청이 집중되는 것을 방지할 수 있다.&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;User ID 기준 샤딩&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Query Optimization&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 자체를 효율적으로 작성하는 것도 중요하다.&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;불필요한 JOIN 제거&lt;/li&gt;
&lt;li&gt;실행 계획(EXPLAIN) 확인&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;size16&quot;&gt;&lt;b&gt;2) 쓰기(Write) 요청 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 요청은 대부분 Database 저장 시간이 병목이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 다양한 방법을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대표적인 방법이 메시지 큐를 이용한 비동기 처리이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client
    &amp;darr;
Application
    &amp;darr;
RabbitMQ
    &amp;darr;
Consumer
    &amp;darr;
Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 즉시 응답을 받고 실제 DB 저장은 Consumer가 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 대량의 요청이 발생해도 애플리케이션 응답 속도를 유지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Batch 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 처리가 필요하지 않은 데이터는 일정 시간마다 한꺼번에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예시는&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분산 Database&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 DB만으로 쓰기 성능을 감당하기 어렵다면 여러 DB에 데이터를 분산 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 처리량과 가용성을 높일 수 있다.&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;MSA에서는 하나의 요청이 여러 서비스에 걸쳐 수행되는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;주문 생성
  &amp;darr;
결제
  &amp;darr;
재고 차감&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스가 독립적인 DB를 사용한다면 데이터 일관성을 유지하는 것이 매우 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 해결 방법은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 분산 트랜잭션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 시스템에서 하나의 트랜잭션처럼 동작하도록 만드는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2PC(Two Phase Commit)&lt;/li&gt;
&lt;li&gt;Saga Pattern&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;Saga Pattern은 각 단계를 개별 트랜잭션으로 수행하고 실패하면 보상 트랜잭션을 실행하는 방식으로, MSA 환경에서 가장 많이 사용된다.&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) Event Sourcing&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상태를 저장하는 것이 아니라 상태 변경 이벤트를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;주문 생성
결제 완료
배송 시작
배송 완료
&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;데이터 변경 이력을 모두 확인할 수 있다는 장점이 있다.&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;3) CQRS&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Command와 Query를 분리하는 설계 방식이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Command
(쓰기)
  &amp;darr;
Database
  &amp;darr;
Projection
  &amp;darr;
Read Database
  &amp;darr;
Query
(조회)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기와 쓰기를 각각 최적화할 수 있으며 Event Sourcing과 함께 자주 사용된다.&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모니터링(Monitoring)&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;TPS&lt;/li&gt;
&lt;li&gt;응답 시간&lt;/li&gt;
&lt;li&gt;CPU&lt;/li&gt;
&lt;li&gt;Memory&lt;/li&gt;
&lt;li&gt;Error Rate&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prometheus&lt;/li&gt;
&lt;li&gt;Grafana&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;/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;로깅(Logging)&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;ERROR&lt;/li&gt;
&lt;li&gt;WARN&lt;/li&gt;
&lt;li&gt;INFO&lt;/li&gt;
&lt;li&gt;DEBUG&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Elasticsearch&lt;/li&gt;
&lt;li&gt;Logstash&lt;/li&gt;
&lt;li&gt;Kibana(ELK)&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;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;Unit Test&lt;/li&gt;
&lt;li&gt;Integration Test&lt;/li&gt;
&lt;li&gt;Load Test&lt;/li&gt;
&lt;li&gt;Regression Test&lt;/li&gt;
&lt;li&gt;UAT(User Acceptance Test)&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI/CD&lt;/li&gt;
&lt;li&gt;Rolling Deployment&lt;/li&gt;
&lt;li&gt;Canary Deployment&lt;/li&gt;
&lt;li&gt;Blue-Green Deployment&lt;/li&gt;
&lt;/ul&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;&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. RabbitMQ&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RabbitMQ&lt;/b&gt;는 대표적인 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;메시지 브로커(Message Broker)&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 사이에서 메시지를 전달하는 중간 역할을 수행한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Producer
  &amp;darr;
Exchange
  &amp;darr;
Queue
  &amp;darr;
Consumer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ를 사용하면 요청을 비동기로 처리할 수 있어 응답 속도를 높이고 시스템 간 결합도를 낮출 수 있다.&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;RabbitMQ의 역할은 다음과 같다.&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) 비동기 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 오래 걸리는 작업을 Queue에 저장하고 나중에 처리한다.&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;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 부하 분산&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 Consumer가 하나의 Queue를 함께 소비하여 작업을 분산 처리한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Queue
  &amp;darr;
Consumer A
Consumer B
Consumer C&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 내결함성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 Queue에 저장하기 때문에 Consumer 장애가 발생해도 메시지가 즉시 사라지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer가 복구되면 다시 처리할 수 있다.&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;RabbitMQ는 다음 요소들로 구성된다.&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;Message&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue를 통해 전달되는 데이터이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문 정보나 회원 가입 정보가 메시지가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Producer&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 생성하여 RabbitMQ에 전달하는 역할이다.&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;Queue&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;기본적으로 FIFO(First In First Out) 방식으로 처리된다.&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;Consumer&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue에서 메시지를 가져와 실제 작업을 수행한다.&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;Exchange&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer가 보낸 메시지를 적절한 Queue로 전달하는 라우터 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer는 Queue가 아니라 Exchange에 메시지를 전송한다.&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;RabbitMQ는 &lt;b&gt;AMQP(Advanced Message Queuing Protocol)를 기반으로 동작&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AMQP&lt;/b&gt;는 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;메시지를 송수신하기 위한 표준 프로토콜&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMQP의 주요 구성 요소는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Message&lt;/li&gt;
&lt;li&gt;Exchange&lt;/li&gt;
&lt;li&gt;Queue&lt;/li&gt;
&lt;li&gt;Binding&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Binding은 Exchange와 Queue를 연결하는 규칙이다.&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;RabbitMQ는 여러 방식으로 메시지를 라우팅할 수 있다.&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) Direct Exchange&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Routing Key가 정확히 일치하는 Queue로 전달한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Routing Key : order.created
  &amp;darr;
order.created Queue&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Topic Exchange&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴을 이용하여 메시지를 전달한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;order.*

payment.#&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;* : 한 단어&lt;/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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) Fanout Exchange&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Routing Key를 무시하고 연결된 모든 Queue에 메시지를 전달한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Exchange
  &amp;darr;
Queue A
Queue B
Queue C&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) Headers Exchange&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Routing Key 대신 Header 값을 기준으로 Queue를 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 메타데이터를 기준으로 라우팅해야 하는 경우 활용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;대규모 시스템은 단순히 서버를 여러 대 추가하는 것으로 해결되지 않는다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사용자 수와 TPS를 분석하고, 읽기와 쓰기의 특성을 분리하며, 캐시와 메시지 큐를 적절하게 활용해야 한다. 또한 데이터 일관성을 유지하기 위해 분산 트랜잭션, Saga Pattern, Event Sourcing, CQRS 등을 상황에 맞게 선택해야 하며, 모니터링과 테스트, 무중단 배포 체계까지 함께 구축해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;RabbitMQ는 이러한 대규모 시스템에서 비동기 처리와 부하 분산을 담당하는 핵심 컴포넌트 중 하나이며, 올바르게 활용하면 시스템의 성능과 안정성을 크게 향상시킬 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 대규모 시스템은 읽기&amp;middot;쓰기 요청을 각각 최적화하고 데이터 일관성과 모니터링을 고려해 설계해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. RabbitMQ는 비동기 처리와 메시지 라우팅을 통해 높은 처리량과 안정성을 제공하는 핵심 메시지 브로커이다.&lt;/p&gt;</description>
      <category>Bootcamp/Fundamentals</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/60</guid>
      <comments>https://rvbear7.tistory.com/60#entry60comment</comments>
      <pubDate>Mon, 27 Jul 2026 15:40:57 +0900</pubDate>
    </item>
    <item>
      <title>Redis를 활용한 Session Clustering, Leaderboard 그리고 Cache</title>
      <link>https://rvbear7.tistory.com/59</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Session Clustering&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Leaderboard&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cache&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&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;Redis는 이러한 문제를 해결하기 위해 가장 많이 사용하는 인메모리 데이터베이스이다.&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. Session&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 &lt;b&gt;Stateless Protocol&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 하나의 요청이 끝나면 서버는 이전 요청에 대한 상태를 기억하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 로그인했다고 가정해 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;로그인 요청
  &amp;darr;
로그인 성공
  &amp;darr;
상품 조회 요청&lt;/code&gt;&lt;/pre&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;따라서 클라이언트는 매 요청마다 자신을 식별할 수 있는 정보를 함께 보내야 한다.&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;Cookie와 Session&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 과정은 다음과 같이 동작한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Client
  &amp;darr;  Login
Server
  &amp;darr;
세션 생성
  &amp;darr;
Session ID 생성
  &amp;darr;
Cookie(JSESSIONID) 발급
  &amp;darr;
Client 저장&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;이후 요청에서는 브라우저가 자동으로 Cookie를 함께 보낸다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;Cookie
JSESSIONID=ABC123
&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서는 내장 Tomcat이 세션을 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서는 HttpSession을 그대로 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GetMapping(&quot;/set&quot;)
public String set(
        @RequestParam String q,
        HttpSession session
) {
    session.setAttribute(&quot;q&quot;, q);
    return &quot;saved&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;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GetMapping(&quot;/get&quot;)
public String get(HttpSession session) {
    return (String) session.getAttribute(&quot;q&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에는 JSESSIONID 쿠키가 생성되고, 서버는 해당 ID를 이용하여 세션 데이터를 관리한다.&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;이를 &lt;b&gt;Scale-Out&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;          Load Balancer
         /      |      \
   Server A  Server B  Server C&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;
&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;로그인은 Server A&lt;/li&gt;
&lt;li&gt;다음 요청은 Server B&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 전달되면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server B에는 해당 세션이 존재하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 로그인 상태가 유지되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 해결 방법은 &lt;b&gt;Sticky Session&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서가 사용자를 특정 서버에 고정한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;User A
  &amp;darr;
Server A

(항상 Server A)&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;하지만 다음과 같은 문제가 있다.&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;Scale-Out의 장점을 제대로 활용하지 못한다.&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;b&gt;Session Clustering&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 서버 내부가 아니라 외부 저장소에 저장한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;          Load Balancer
         /      |      \
   Server A  Server B  Server C
         \      |      /
              Redis&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 추가가 자유롭다.&lt;/li&gt;
&lt;li&gt;서버 제거가 자유롭다.&lt;/li&gt;
&lt;li&gt;세션이 유지된다.&lt;/li&gt;
&lt;li&gt;Load Balancing이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;은 Redis와 통신해야 하므로 약간의 네트워크 비용이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Redis는 메모리 기반이기 때문에 이러한 비용이 매우 작다.&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 Boot에서는 매우 쉽게 Session Clustering을 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 의존성을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.session:spring-session-data-redis'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것만 추가해도 Tomcat Session 대신 Redis Session을 사용하게 된다.&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 class=&quot;ebnf&quot;&gt;&lt;code&gt;JSESSIONID
&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 class=&quot;ebnf&quot;&gt;&lt;code&gt;SESSION
&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;Redis를 확인하면 세션 정보가 저장되어 있는 것을 확인할 수 있다.&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;Spring Session은 Java 직렬화를 사용&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 형태로 저장하고 싶다면 Serializer를 등록한다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Bean
public RedisSerializer&amp;lt;Object&amp;gt;
springSessionDefaultRedisSerializer() {
    return RedisSerializer.json();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Spring Security의 SecurityContext처럼 기본 생성자가 없는 객체는 JSON 직렬화 과정에서 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 Jackson 설정이나 별도의 Serializer를 추가하여 해결하는 경우가 많다.&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. Leaderboard&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 가장 강력한 자료구조 중 하나가 &lt;b&gt;Sorted Set(ZSet)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 데이터는&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Member + Score
&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;상품 A &amp;rarr; 100

상품 B &amp;rarr; 70

상품 C &amp;rarr; 30
&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB로 구현하면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 상품을 구하려면 보통 다음과 같은 SQL이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT item_id,
       SUM(count)
FROM orders
GROUP BY item_id
ORDER BY SUM(count) DESC
LIMIT 10;
&lt;/code&gt;&lt;/pre&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;Join&lt;/li&gt;
&lt;li&gt;Group By&lt;/li&gt;
&lt;li&gt;Sum&lt;/li&gt;
&lt;li&gt;Order By&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;size16&quot;&gt;Redis에서는 구매할 때마다 점수만 증가시키면 된다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;rankOps.incrementScore(
    &quot;soldRanks&quot;,
    ItemDto.fromEntity(item),
    1
);
&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 class=&quot;angelscript&quot;&gt;&lt;code&gt;ZINCRBY soldRanks 1 item
&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 class=&quot;lsl&quot;&gt;&lt;code&gt;rankOps.reverseRange(
    &quot;soldRanks&quot;,
    0,
    9
);
&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 class=&quot;angelscript&quot;&gt;&lt;code&gt;ZREVRANGE soldRanks 0 9
&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;Redis는 점수를 기준으로 자동 정렬하기 때문에 별도의 SQL이나 정렬 로직이 필요하지 않다.&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. Cache&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;자주 사용하는 데이터를 빠르게 가져오기 위한 저장소&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 상품 상세 조회가 매우 많다고 가정해 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Client
  &amp;darr;
Database
  &amp;darr;
응답&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 DB를 조회하면 부하가 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 추가하면&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Client
  &amp;darr;
Redis Cache
  &amp;darr;
(없으면)
Database&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 응답 속도와 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;캐싱에서는 두 가지 용어를 자주 사용한다.&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;b&gt;1) Cache Hit&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 데이터가 캐시에 존재한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Client
  &amp;darr;
Redis
  &amp;darr;
응답&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;b&gt;2) Cache Miss&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시에 데이터가 없다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Redis
  &amp;darr;
Database
  &amp;darr;
Redis 저장
  &amp;darr;
응답&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Cache Aside (Lazy Loading)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많이 사용하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Cache 조회
 &amp;darr;
있다 &amp;rarr; 반환
or
없다
 &amp;darr;
DB 조회
 &amp;darr;
Cache 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필요한 데이터만 저장한다.&lt;/li&gt;
&lt;li&gt;메모리 사용량이 적다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최초 조회는 느리다.&lt;/li&gt;
&lt;li&gt;캐시와 DB가 일시적으로 불일치할 수 있다.&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;b&gt;2) Write Through&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;Cache&lt;/li&gt;
&lt;li&gt;DB&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;/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;3) Write Behind&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Cache에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 일정 주기로 DB에 반영한다.&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에서는 캐시 기능을 매우 간단하게 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 캐시 기능을 활성화한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableCaching
public class CacheConfig {
}
&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;Redis를 사용할 경우 CacheManager를 등록한다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;@Bean
public RedisCacheManager cacheManager(
        RedisConnectionFactory factory
) {
    RedisCacheConfiguration configuration =
            RedisCacheConfiguration
                    .defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(10));

    return RedisCacheManager
            .builder(factory)
            .cacheDefaults(configuration)
            .build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 TTL(Time To Live)을 설정하면 캐시가 일정 시간이 지나면 자동으로 삭제된다.&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;@Cacheable&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 메서드에 적용한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Cacheable(
    cacheNames = &quot;itemCache&quot;,
    key = &quot;#id&quot;
)
public ItemDto readOne(Long 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;동작 방식은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Cache 조회
 &amp;darr;
Hit
 &amp;darr;
캐시 반환
 &amp;darr;
Miss
 &amp;darr;
메서드 실행
 &amp;darr;
캐시 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 요청은 DB를 조회하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@CachePut&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 메서드를 실행한 뒤 캐시를 갱신한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@CachePut(
    cacheNames = &quot;itemCache&quot;,
    key = &quot;#result.id&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성(Create)이나 수정(Update) 작업에서 많이 사용한다.&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;@CacheEvict&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 제거한다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;@CacheEvict(
    cacheNames = &quot;itemAllCache&quot;,
    allEntries = 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;예를 들어 상품 정보가 변경되면&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;상품 목록 캐시
&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;따라서 전체 삭제 후 다시 생성하도록 만드는 것이 일반적이다.&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;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;검색어
페이지 번호
페이지 크기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 &lt;b&gt;Key로 만들 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@Cacheable(
    cacheNames = &quot;itemSearchCache&quot;,
    key = &quot;{#query, #pageable.pageNumber, #pageable.pageSize}&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;노트북
0페이지
20개&lt;/code&gt;&lt;/pre&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;같은 요청이 들어오면 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;Redis는 단순히 캐시 서버가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 자료구조를 활용하여 여러 기능을 쉽게 구현할 수 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Redis 자료구조&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로그인 세션&lt;/td&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session Clustering&lt;/td&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh Token&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증번호&lt;/td&gt;
&lt;td&gt;String + TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조회수&lt;/td&gt;
&lt;td&gt;String(INCR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장바구니&lt;/td&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;좋아요&lt;/td&gt;
&lt;td&gt;Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인기 상품&lt;/td&gt;
&lt;td&gt;Sorted Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실시간 랭킹&lt;/td&gt;
&lt;td&gt;Sorted Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검색 결과 캐시&lt;/td&gt;
&lt;td&gt;String(JSON)&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;Redis는 단순히 빠른 데이터 저장소가 아니라 &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;확장성과 성능을 동시에 확보하기 위한 핵심 인프라&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session Clustering을 통해 여러 서버가 동일한 세션을 공유할 수 있고, Sorted Set을 활용하면 실시간 랭킹을 효율적으로 구현할 수 있다. 또한 Spring Cache를 적용하면 반복적인 데이터베이스 조회를 줄여 응답 속도를 높이고 시스템 부하를 크게 감소시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서는 spring-session-data-redis, RedisTemplate, Spring Cache를 통해 이러한 기능들을 비교적 간단하게 적용할 수 있으며, 실제 운영 환경에서도 가장 널리 사용되는 Redis 활용 방식이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 Session Clustering, 실시간 랭킹(Leaderboard), 또는 Cache를 통해 분산 환경에서 세션을 공유하고, 빠른 데이터 조회와 효율적인 캐싱을 구현할 수 있는 핵심 인메모리 데이터베이스이다.&lt;/p&gt;</description>
      <category>Bootcamp/Fundamentals</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/59</guid>
      <comments>https://rvbear7.tistory.com/59#entry59comment</comments>
      <pubDate>Fri, 24 Jul 2026 15:17:48 +0900</pubDate>
    </item>
    <item>
      <title>Redis</title>
      <link>https://rvbear7.tistory.com/58</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Redis&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;225&quot; data-start=&quot;76&quot; data-ke-size=&quot;size16&quot;&gt;서비스를 개발하다 보면 모든 데이터를 관계형 데이터베이스(RDB)에 저장하는 것이 항상 최선은 아니다. 로그인 세션, 장바구니, 인증번호, 조회수처럼 빠르게 변경되고 일정 시간이 지나면 사라져도 되는 데이터는 오히려 RDB보다 메모리에 저장하는 것이 훨씬 효율적이다.&lt;/p&gt;
&lt;p data-end=&quot;296&quot; data-start=&quot;227&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;296&quot; data-start=&quot;227&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;388&quot; data-start=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트에서는 일반적으로 MySQL, PostgreSQL과 같은 관계형 데이터베이스를 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;RDB는 데이터를 디스크(SSD/HDD)에 저장하기 때문에 서비스가 종료되어도 데이터가 유지되는 &lt;b&gt;영속성(Persistence)&lt;/b&gt;을 제공한다.&lt;/p&gt;
&lt;p data-end=&quot;498&quot; data-start=&quot;474&quot; data-ke-size=&quot;size16&quot;&gt;하지만 다음과 같은 데이터는 특성이 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;558&quot; data-start=&quot;500&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;508&quot; data-start=&quot;500&quot; data-section-id=&quot;6q9pdg&quot;&gt;로그인 세션&lt;/li&gt;
&lt;li data-end=&quot;520&quot; data-start=&quot;509&quot; data-section-id=&quot;5b6247&quot;&gt;인증번호(OTP)&lt;/li&gt;
&lt;li data-end=&quot;536&quot; data-start=&quot;521&quot; data-section-id=&quot;5yfyry&quot;&gt;Refresh Token&lt;/li&gt;
&lt;li data-end=&quot;543&quot; data-start=&quot;537&quot; data-section-id=&quot;dltsil&quot;&gt;장바구니&lt;/li&gt;
&lt;li data-end=&quot;549&quot; data-start=&quot;544&quot; data-section-id=&quot;2izoi4&quot;&gt;조회수&lt;/li&gt;
&lt;li data-end=&quot;558&quot; data-start=&quot;550&quot; data-section-id=&quot;1odewkw&quot;&gt;캐시 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;580&quot; data-start=&quot;560&quot; data-ke-size=&quot;size16&quot;&gt;이러한 데이터는 다음 특징을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;649&quot; data-start=&quot;582&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;595&quot; data-start=&quot;582&quot; data-section-id=&quot;1hk7zhn&quot;&gt;매우 자주 수정된다.&lt;/li&gt;
&lt;li data-end=&quot;613&quot; data-start=&quot;596&quot; data-section-id=&quot;zommmd&quot;&gt;빠른 읽기/쓰기가 중요하다.&lt;/li&gt;
&lt;li data-end=&quot;632&quot; data-start=&quot;614&quot; data-section-id=&quot;mrteh2&quot;&gt;일정 시간이 지나면 삭제된다.&lt;/li&gt;
&lt;li data-end=&quot;649&quot; data-start=&quot;633&quot; data-section-id=&quot;1qdcm6l&quot;&gt;영구 보관할 필요가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;695&quot; data-start=&quot;651&quot; data-ke-size=&quot;size16&quot;&gt;이러한 데이터를 매번 디스크에 저장하면 I/O 비용이 발생하여 성능이 저하된다.&lt;/p&gt;
&lt;p data-end=&quot;758&quot; data-start=&quot;697&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 사용하는 것이 &lt;b&gt;인메모리 데이터베이스(In-Memory Database)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-end=&quot;758&quot; data-start=&quot;697&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;758&quot; data-start=&quot;697&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;849&quot; data-start=&quot;776&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis&lt;/b&gt;란 &lt;b&gt;REmote DIctionary Server&lt;/b&gt;의 약자로 대표적인 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;In-Memory Key-Value Database&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;905&quot; data-start=&quot;851&quot; data-ke-size=&quot;size16&quot;&gt;Java의 Map&amp;lt;String, Object&amp;gt;처럼 Key를 이용해 데이터를 저장하고 조회한다.&lt;/p&gt;
&lt;p data-end=&quot;905&quot; data-start=&quot;851&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;936&quot; data-start=&quot;907&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SET greeting &quot;Hello Redis&quot;
GET greeting&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;
&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;/div&gt;
&lt;p data-end=&quot;1053&quot; data-start=&quot;992&quot; data-ke-size=&quot;size16&quot;&gt;SQL을 사용하는 관계형 데이터베이스와 달리 Redis는 SQL 없이 명령(Command) 기반으로 동작한다.&lt;/p&gt;
&lt;p data-end=&quot;1053&quot; data-start=&quot;992&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1053&quot; data-start=&quot;992&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1119&quot; data-start=&quot;1073&quot; data-ke-size=&quot;size16&quot;&gt;Redis의 가장 큰 특징은 &lt;b&gt;메모리(RAM)에 데이터를 저장한다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;1119&quot; data-start=&quot;1073&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1119&quot; data-start=&quot;1073&quot; data-ke-size=&quot;size16&quot;&gt;RDB는 다음과 같이 진행된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Application
  &amp;darr;
Database
  &amp;darr;
SSD / HDD&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;
&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;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1217&quot; data-start=&quot;1185&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1193&quot; data-start=&quot;1185&quot; data-section-id=&quot;1xrqoik&quot;&gt;영속성 제공&lt;/li&gt;
&lt;li data-end=&quot;1206&quot; data-start=&quot;1194&quot; data-section-id=&quot;h6owpo&quot;&gt;디스크 I/O 발생&lt;/li&gt;
&lt;li data-end=&quot;1217&quot; data-start=&quot;1207&quot; data-section-id=&quot;9nqnno&quot;&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;Redis는 다음과 같이 진행된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Application
  &amp;darr;
Redis
  &amp;darr;
RAM&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;
&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;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1320&quot; data-start=&quot;1281&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1290&quot; data-start=&quot;1281&quot; data-section-id=&quot;1oe0iwd&quot;&gt;메모리에 저장&lt;/li&gt;
&lt;li data-end=&quot;1306&quot; data-start=&quot;1291&quot; data-section-id=&quot;1o1o1m7&quot;&gt;디스크 접근이 거의 없음&lt;/li&gt;
&lt;li data-end=&quot;1320&quot; data-start=&quot;1307&quot; data-section-id=&quot;1r3wl6u&quot;&gt;매우 빠른 읽기/쓰기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1363&quot; data-start=&quot;1322&quot; data-ke-size=&quot;size16&quot;&gt;대신 메모리는 휘발성이기 때문에 서버가 종료되면 데이터가 사라질 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1400&quot; data-start=&quot;1365&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Redis는 다음과 같은 데이터를 저장하는 데 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1460&quot; data-start=&quot;1402&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1411&quot; data-start=&quot;1402&quot; data-section-id=&quot;1v3sb4m&quot;&gt;Session&lt;/li&gt;
&lt;li data-end=&quot;1419&quot; data-start=&quot;1412&quot; data-section-id=&quot;16xb1tw&quot;&gt;Cache&lt;/li&gt;
&lt;li data-end=&quot;1435&quot; data-start=&quot;1420&quot; data-section-id=&quot;5yfyry&quot;&gt;Refresh Token&lt;/li&gt;
&lt;li data-end=&quot;1442&quot; data-start=&quot;1436&quot; data-section-id=&quot;d752kd&quot;&gt;인증번호&lt;/li&gt;
&lt;li data-end=&quot;1449&quot; data-start=&quot;1443&quot; data-section-id=&quot;dltsil&quot;&gt;장바구니&lt;/li&gt;
&lt;li data-end=&quot;1455&quot; data-start=&quot;1450&quot; data-section-id=&quot;2izoi4&quot;&gt;조회수&lt;/li&gt;
&lt;li data-end=&quot;1460&quot; data-start=&quot;1456&quot; data-section-id=&quot;yi2gws&quot;&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-end=&quot;1507&quot; data-start=&quot;1485&quot; data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스는 SQL을 사용한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM users;&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;
&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;/div&gt;
&lt;p data-end=&quot;1577&quot; data-start=&quot;1542&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1577&quot; data-start=&quot;1542&quot; data-ke-size=&quot;size16&quot;&gt;반면 Redis는 SQL 대신 명령(Command)을 사용한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SET name alex
GET name&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;
&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;/div&gt;
&lt;p data-end=&quot;1649&quot; data-start=&quot;1616&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 대표적인 &lt;b&gt;NoSQL Database&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;1715&quot; data-start=&quot;1651&quot; data-ke-size=&quot;size16&quot;&gt;현재 NoSQL은 &lt;b&gt;Not Only SQL&lt;/b&gt;을 의미하며, 관계형 데이터베이스의 한계를 보완하기 위해 등장하였다.&lt;/p&gt;
&lt;p data-end=&quot;1715&quot; data-start=&quot;1651&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1739&quot; data-start=&quot;1717&quot; data-ke-size=&quot;size16&quot;&gt;대표적인 NoSQL 종류는 다음과 같다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1875&quot; data-start=&quot;1741&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;종류&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1799&quot; data-start=&quot;1770&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1782&quot; data-start=&quot;1770&quot;&gt;Key-Value&lt;/td&gt;
&lt;td data-end=&quot;1799&quot; data-start=&quot;1782&quot; data-col-size=&quot;sm&quot;&gt;Key와 Value 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1825&quot; data-start=&quot;1800&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1811&quot; data-start=&quot;1800&quot;&gt;Document&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1825&quot; data-start=&quot;1811&quot;&gt;JSON 형태 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1854&quot; data-start=&quot;1826&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1842&quot; data-start=&quot;1826&quot;&gt;Column Family&lt;/td&gt;
&lt;td data-end=&quot;1854&quot; data-start=&quot;1842&quot; data-col-size=&quot;sm&quot;&gt;컬럼 단위 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1875&quot; data-start=&quot;1855&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1863&quot; data-start=&quot;1855&quot;&gt;Graph&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1875&quot; data-start=&quot;1863&quot;&gt;관계 중심 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1914&quot; data-start=&quot;1877&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 이 중 &lt;b&gt;Key-Value Store&lt;/b&gt;에 해당한다.&lt;/p&gt;
&lt;p data-end=&quot;1914&quot; data-start=&quot;1877&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1914&quot; data-start=&quot;1877&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1958&quot; data-start=&quot;1936&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 다양한 서비스에서 활용된다.&lt;/p&gt;
&lt;p data-end=&quot;1976&quot; data-start=&quot;1960&quot; data-ke-size=&quot;size16&quot;&gt;대표적인 사례는 다음과 같다.&lt;/p&gt;
&lt;p data-end=&quot;1976&quot; data-start=&quot;1960&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1976&quot; data-start=&quot;1960&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Session Storage&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2023&quot; data-start=&quot;1999&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;User
  &amp;darr;
Server A
Server B
Server C
  &amp;darr;
Redis(Session)&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;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2109&quot; data-start=&quot;2100&quot; data-section-id=&quot;6wi416&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Cache&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2147&quot; data-start=&quot;2111&quot; data-ke-size=&quot;size16&quot;&gt;자주 조회되는 데이터를 Redis에 저장하여 DB 조회를 줄인다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Client
  &amp;darr;
Redis Cache
  &amp;darr; (없으면)
Database&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;
&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;p data-end=&quot;2224&quot; data-start=&quot;2209&quot; data-section-id=&quot;1ghk1af&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2224&quot; data-start=&quot;2209&quot; data-section-id=&quot;1ghk1af&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) Leaderboard&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2231&quot; data-start=&quot;2226&quot; data-ke-size=&quot;size16&quot;&gt;게임 점수&lt;/p&gt;
&lt;p data-end=&quot;2239&quot; data-start=&quot;2233&quot; data-ke-size=&quot;size16&quot;&gt;실시간 순위&lt;/p&gt;
&lt;p data-end=&quot;2247&quot; data-start=&quot;2241&quot; data-ke-size=&quot;size16&quot;&gt;좋아요 순위&lt;/p&gt;
&lt;p data-end=&quot;2247&quot; data-start=&quot;2241&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2247&quot; data-start=&quot;2241&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) Rate Limiter&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2298&quot; data-start=&quot;2272&quot; data-ke-size=&quot;size16&quot;&gt;짧은 시간 동안 과도한 API 요청을 제한한다.&lt;/p&gt;
&lt;p data-end=&quot;2298&quot; data-start=&quot;2272&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2298&quot; data-start=&quot;2272&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5) 방문자 수 집계&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2322&quot; data-start=&quot;2319&quot; data-ke-size=&quot;size16&quot;&gt;조회수&lt;/p&gt;
&lt;p data-end=&quot;2327&quot; data-start=&quot;2324&quot; data-ke-size=&quot;size16&quot;&gt;좋아요&lt;/p&gt;
&lt;p data-end=&quot;2336&quot; data-start=&quot;2329&quot; data-ke-size=&quot;size16&quot;&gt;실시간 인기글&lt;/p&gt;
&lt;p data-end=&quot;2336&quot; data-start=&quot;2329&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2336&quot; data-start=&quot;2329&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2380&quot; data-start=&quot;2355&quot; data-ke-size=&quot;size16&quot;&gt;Redis 설치 방법은 크게 세 가지가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2425&quot; data-start=&quot;2382&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2398&quot; data-start=&quot;2382&quot; data-section-id=&quot;20ar4l&quot;&gt;Linux / WSL 설치&lt;/li&gt;
&lt;li data-end=&quot;2416&quot; data-start=&quot;2399&quot; data-section-id=&quot;ideyhj&quot;&gt;macOS(Homebrew)&lt;/li&gt;
&lt;li data-end=&quot;2425&quot; data-start=&quot;2417&quot; data-section-id=&quot;1u6gra4&quot;&gt;Docker&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2455&quot; data-start=&quot;2427&quot; data-ke-size=&quot;size16&quot;&gt;실무에서는 Docker 사용을 가장 많이 권장한다.&lt;/p&gt;
&lt;p data-end=&quot;2455&quot; data-start=&quot;2427&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2491&quot; data-start=&quot;2457&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 Docker Compose는 다음과 같이 작성한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;services:
  redis-stack:
    image: redis/redis-stack
    container_name: redis-stack-compose
    restart: always
    environment:
      REDIS_ARGS: &quot;--requirepass systempass&quot;
    ports:
      - &quot;6379:6379&quot;
      - &quot;8001:8001&quot;&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;
&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;/div&gt;
&lt;p data-end=&quot;2744&quot; data-start=&quot;2733&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h1 data-end=&quot;2801&quot; data-start=&quot;2785&quot; data-section-id=&quot;houkf&quot;&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-end=&quot;2837&quot; data-start=&quot;2803&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2837&quot; data-start=&quot;2803&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2837&quot; data-start=&quot;2803&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 단순 문자열뿐 아니라 다양한 자료구조를 제공한다.&lt;/p&gt;
&lt;p data-end=&quot;2837&quot; data-start=&quot;2803&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2837&quot; data-start=&quot;2803&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) String&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2872&quot; data-start=&quot;2858&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SET user:name alex
GET user:name&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;
&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;/div&gt;
&lt;p data-end=&quot;2935&quot; data-start=&quot;2921&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;INCR counter
DECR counter&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;
&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;/div&gt;
&lt;p data-end=&quot;2982&quot; data-start=&quot;2977&quot; data-ke-size=&quot;size16&quot;&gt;활용 예시&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3009&quot; data-start=&quot;2984&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2989&quot; data-start=&quot;2984&quot; data-section-id=&quot;2izoi4&quot;&gt;조회수&lt;/li&gt;
&lt;li data-end=&quot;2997&quot; data-start=&quot;2990&quot; data-section-id=&quot;9gknm9&quot;&gt;방문자 수&lt;/li&gt;
&lt;li data-end=&quot;3002&quot; data-start=&quot;2998&quot; data-section-id=&quot;yijhx4&quot;&gt;토큰&lt;/li&gt;
&lt;li data-end=&quot;3009&quot; data-start=&quot;3003&quot; data-section-id=&quot;d752kd&quot;&gt;인증번호&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) List&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3054&quot; data-start=&quot;3028&quot; data-ke-size=&quot;size16&quot;&gt;Linked List 형태로 데이터를 저장한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;LPUSH users alex
RPUSH users brad

LPOP users
RPOP users&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;
&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;/div&gt;
&lt;p data-end=&quot;3132&quot; data-start=&quot;3127&quot; data-ke-size=&quot;size16&quot;&gt;활용 예시&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3161&quot; data-start=&quot;3134&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3142&quot; data-start=&quot;3134&quot; data-section-id=&quot;1aydar9&quot;&gt;최근 검색어&lt;/li&gt;
&lt;li data-end=&quot;3150&quot; data-start=&quot;3143&quot; data-section-id=&quot;b10gsw&quot;&gt;메시지 큐&lt;/li&gt;
&lt;li data-end=&quot;3161&quot; data-start=&quot;3151&quot; data-section-id=&quot;cdg7sj&quot;&gt;Timeline&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;b&gt;3) Set&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3190&quot; data-start=&quot;3179&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;SADD java alex
SADD java brad

SMEMBERS java&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;
&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;/div&gt;
&lt;p data-end=&quot;3256&quot; data-start=&quot;3251&quot; data-ke-size=&quot;size16&quot;&gt;활용 예시&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3281&quot; data-start=&quot;3258&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3266&quot; data-start=&quot;3258&quot; data-section-id=&quot;xjcqh9&quot;&gt;중복 방문자&lt;/li&gt;
&lt;li data-end=&quot;3276&quot; data-start=&quot;3267&quot; data-section-id=&quot;1ugs7l2&quot;&gt;좋아요 사용자&lt;/li&gt;
&lt;li data-end=&quot;3281&quot; data-start=&quot;3277&quot; data-section-id=&quot;yijggc&quot;&gt;태그&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) Hash&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3324&quot; data-start=&quot;3300&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;HSET user:1 name alex age 25

HGET user:1 name&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;
&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;/div&gt;
&lt;p data-end=&quot;3415&quot; data-start=&quot;3387&quot; data-ke-size=&quot;size16&quot;&gt;Java 객체 하나를 저장하는 것과 매우 유사하다.&lt;/p&gt;
&lt;p data-end=&quot;3415&quot; data-start=&quot;3387&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3423&quot; data-start=&quot;3417&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;상품 &amp;rarr; 수량&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;
&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;p data-end=&quot;3460&quot; data-start=&quot;3442&quot; data-ke-size=&quot;size16&quot;&gt;형태를 저장할 때 많이 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;3460&quot; data-start=&quot;3442&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3460&quot; data-start=&quot;3442&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5) Sorted Set&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3510&quot; data-start=&quot;3485&quot; data-ke-size=&quot;size16&quot;&gt;점수(score)를 함께 저장하는 Set이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ZADD ranking 100 alex
ZADD ranking 90 brad

ZRANGE ranking 0 10&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;
&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;/div&gt;
&lt;p data-end=&quot;3595&quot; data-start=&quot;3590&quot; data-ke-size=&quot;size16&quot;&gt;활용 예시&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3624&quot; data-start=&quot;3597&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3601&quot; data-start=&quot;3597&quot; data-section-id=&quot;yi2gws&quot;&gt;랭킹&lt;/li&gt;
&lt;li data-end=&quot;3615&quot; data-start=&quot;3602&quot; data-section-id=&quot;z5ssvd&quot;&gt;Leaderboard&lt;/li&gt;
&lt;li data-end=&quot;3624&quot; data-start=&quot;3616&quot; data-section-id=&quot;i8572o&quot;&gt;인기 게시글&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 명령어도 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 삭제&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;DEL key&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;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3684&quot; data-start=&quot;3675&quot; data-section-id=&quot;17i23co&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) TTL 설정&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;EXPIRE key 30&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;
&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;/div&gt;
&lt;p data-end=&quot;3728&quot; data-start=&quot;3714&quot; data-ke-size=&quot;size16&quot;&gt;30초 후 자동 삭제된다.&lt;/p&gt;
&lt;p data-end=&quot;3728&quot; data-start=&quot;3714&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3728&quot; data-start=&quot;3714&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 모든 Key 조회&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;KEYS *&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;
&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;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 운영환경에서는 KEYS * 대신 SCAN 사용을 권장한다. KEYS는 모든 키를 한 번에 탐색하므로 데이터가 많은 환경에서는 Redis를 블로킹할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3881&quot; data-start=&quot;3873&quot; data-section-id=&quot;1bpb8as&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) 전체 삭제&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;FLUSHDB&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;
&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;/div&gt;
&lt;h1 data-end=&quot;3936&quot; data-start=&quot;3910&quot; data-section-id=&quot;100l0g9&quot;&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-end=&quot;3936&quot; data-start=&quot;3910&quot; data-section-id=&quot;100l0g9&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 Redis 사용하기 위해서 의존성을 추가해준다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-data-redis'&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;
&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;p data-end=&quot;3999&quot; data-start=&quot;3977&quot; data-ke-size=&quot;size16&quot;&gt;application.yml을 작성한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;spring:
  data:
    redis:
      host: localhost
      port: 6379
      username: default
      password: systempass&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;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4217&quot; data-start=&quot;4160&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data Redis는 Spring Data JPA와 매우 유사한 방식으로 사용할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;4231&quot; data-start=&quot;4219&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RedisHash(&quot;item&quot;)
public class Item {

    @Id
    private Long id;

    private String name;

    private String description;

    private Integer price;
}&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;
&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;/div&gt;
&lt;p data-end=&quot;4428&quot; data-start=&quot;4404&quot; data-ke-size=&quot;size16&quot;&gt;Repository는 다음과 같이 작성한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface ItemRepository
        extends CrudRepository&amp;lt;Item, Long&amp;gt; {
}&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;
&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;/div&gt;
&lt;p data-end=&quot;4540&quot; data-start=&quot;4522&quot; data-ke-size=&quot;size16&quot;&gt;사용법도 JPA와 거의 동일하다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;itemRepository.save(item);

itemRepository.findById(1L);

itemRepository.deleteById(1L);&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;
&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;/div&gt;
&lt;p data-end=&quot;4683&quot; data-start=&quot;4644&quot; data-ke-size=&quot;size16&quot;&gt;Redis에서는 내부적으로 Hash 자료형을 이용하여 객체를 저장한다.&lt;/p&gt;
&lt;p data-end=&quot;4683&quot; data-start=&quot;4644&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4683&quot; data-start=&quot;4644&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4768&quot; data-start=&quot;4712&quot; data-ke-size=&quot;size16&quot;&gt;Repository는 CRUD에는 편리하지만 Redis의 다양한 자료구조를 활용하기에는 한계가 있다.&lt;/p&gt;
&lt;p data-end=&quot;4799&quot; data-start=&quot;4770&quot; data-ke-size=&quot;size16&quot;&gt;이때 사용하는 것이 &lt;b&gt;RedisTemplate&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;4799&quot; data-start=&quot;4770&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4854&quot; data-start=&quot;4801&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 문자열을 사용하는 경우에는 StringRedisTemplate을 사용할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Autowired
private StringRedisTemplate redisTemplate;&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;
&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;/div&gt;
&lt;p data-end=&quot;4938&quot; data-start=&quot;4923&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;ValueOperations&amp;lt;String, String&amp;gt; ops =
        redisTemplate.opsForValue();

ops.set(&quot;hello&quot;, &quot;redis&quot;);

ops.get(&quot;hello&quot;);&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;
&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;/div&gt;
&lt;p data-end=&quot;5093&quot; data-start=&quot;5075&quot; data-ke-size=&quot;size16&quot;&gt;Set 자료형도 사용할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;SetOperations&amp;lt;String, String&amp;gt; ops =
        redisTemplate.opsForSet();

ops.add(&quot;hobbies&quot;, &quot;coding&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5279&quot; data-start=&quot;5239&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5279&quot; data-start=&quot;5239&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5279&quot; data-start=&quot;5239&quot; data-ke-size=&quot;size16&quot;&gt;직접 RedisTemplate을 만들어보자.&lt;/p&gt;
&lt;p data-end=&quot;5279&quot; data-start=&quot;5239&quot; data-ke-size=&quot;size16&quot;&gt;복잡한 객체를 저장하려면 직접 RedisTemplate을 등록하면 된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Bean
public RedisTemplate&amp;lt;String, ItemDto&amp;gt; itemRedisTemplate(
        RedisConnectionFactory connectionFactory) {

    RedisTemplate&amp;lt;String, ItemDto&amp;gt; template =
            new RedisTemplate&amp;lt;&amp;gt;();

    template.setConnectionFactory(connectionFactory);

    template.setKeySerializer(RedisSerializer.string());

    template.setValueSerializer(RedisSerializer.json());

    return template;
}&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;
&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;/div&gt;
&lt;p data-end=&quot;5708&quot; data-start=&quot;5686&quot; 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;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;ValueOperations&amp;lt;String, ItemDto&amp;gt; ops =
        itemRedisTemplate.opsForValue();

ops.set(&quot;item:1&quot;, itemDto);&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;
&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;/div&gt;
&lt;p data-end=&quot;5880&quot; data-start=&quot;5832&quot; data-ke-size=&quot;size16&quot;&gt;직렬화를 JSON으로 설정하면 Redis 내부에서도 사람이 읽기 쉬운 형태로 저장된다.&lt;/p&gt;
&lt;p data-end=&quot;5880&quot; data-start=&quot;5832&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5880&quot; data-start=&quot;5832&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5880&quot; data-start=&quot;5832&quot; data-ke-size=&quot;size16&quot;&gt;RedisRepository와 RedisTemplate를 비교해보면 다음과 같다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;6122&quot; data-start=&quot;5923&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;RedisRepository&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;RedisTemplate&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;6030&quot; data-start=&quot;5995&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;6012&quot; data-start=&quot;5995&quot;&gt;Spring Data 방식&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;6030&quot; data-start=&quot;6012&quot;&gt;Redis 명령 직접 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;6063&quot; data-start=&quot;6031&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;6045&quot; data-start=&quot;6031&quot;&gt;CRUD 구현이 간단&lt;/td&gt;
&lt;td data-end=&quot;6063&quot; data-start=&quot;6045&quot; data-col-size=&quot;sm&quot;&gt;다양한 자료구조 활용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;6084&quot; data-start=&quot;6064&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;6072&quot; data-start=&quot;6064&quot;&gt;객체 중심&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;6084&quot; data-start=&quot;6072&quot;&gt;Redis 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;6122&quot; data-start=&quot;6085&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;6100&quot; data-start=&quot;6085&quot;&gt;JPA와 사용법이 유사&lt;/td&gt;
&lt;td data-end=&quot;6122&quot; data-start=&quot;6100&quot; data-col-size=&quot;sm&quot;&gt;Redis의 모든 기능 활용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6159&quot; data-start=&quot;6124&quot; data-ke-size=&quot;size16&quot;&gt;간단한 객체 저장은 RedisRepository가 적합하다.&lt;/p&gt;
&lt;p data-end=&quot;6213&quot; data-start=&quot;6161&quot; data-ke-size=&quot;size16&quot;&gt;Redis의 자료구조를 적극적으로 활용해야 한다면 RedisTemplate이 더 적합하다.&lt;/p&gt;
&lt;p data-end=&quot;6213&quot; data-start=&quot;6161&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6213&quot; data-start=&quot;6161&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6290&quot; data-start=&quot;6227&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 단순히 빠른 데이터베이스가 아니라 &lt;b&gt;메모리를 활용하여 성능을 극대화하기 위한 데이터 저장소&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;6371&quot; data-start=&quot;6292&quot; data-ke-size=&quot;size16&quot;&gt;특히 캐시, 세션, 인증 토큰, 장바구니, 조회수, 랭킹처럼 &lt;b&gt;빠른 응답과 빈번한 데이터 변경이 필요한 영역&lt;/b&gt;에서 강력한 성능을 발휘한다.&lt;/p&gt;
&lt;p data-end=&quot;6531&quot; data-start=&quot;6373&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot에서는 RedisRepository를 통해 JPA처럼 간편하게 사용할 수도 있고, RedisTemplate을 통해 Redis의 다양한 자료구조를 직접 활용할 수도 있다. 프로젝트의 요구사항에 맞는 방식을 선택하면 Redis의 장점을 최대한 활용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 메모리 기반의 Key-Value NoSQL 데이터베이스로 캐시, 세션, 인증 토큰, 장바구니처럼 빠른 읽기/쓰기와 일시적인 데이터 처리가 필요한 상황에서 뛰어난 성능을 제공하며, Spring Boot에서는 RedisRepository와 RedisTemplate을 통해 손쉽게 활용할 수 있다.&lt;/p&gt;</description>
      <category>Bootcamp/Fundamentals</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/58</guid>
      <comments>https://rvbear7.tistory.com/58#entry58comment</comments>
      <pubDate>Thu, 23 Jul 2026 15:34:56 +0900</pubDate>
    </item>
    <item>
      <title>CI/CD와 GitLab CI + AWS ECS 자동배포</title>
      <link>https://rvbear7.tistory.com/57</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CI/CD&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitLab CI + AWS ECS&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. CI/CD&lt;/b&gt;&lt;/h4&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;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;CI/CD는 이러한 과정을 자동화하여 개발 생산성과 서비스 안정성을 높이는 방법이다.&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;CI&lt;/b&gt;는 &lt;b&gt;Continuous Integration의 약자&lt;/b&gt;로 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;지속적인 통합&lt;/b&gt;&lt;/span&gt;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개발자가 작업한 코드를 자주 메인 브랜치에 병합하고, 코드가 변경될 때마다 자동으로 빌드와 테스트를 수행하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 GitHub이나 GitLab에 코드를 Push하면 다음과 같은 작업이 자동으로 실행된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;코드 Push
  &amp;darr;
소스 코드 다운로드
  &amp;darr;
프로젝트 빌드
  &amp;darr;
테스트 실행
  &amp;darr;
성공 여부 확인&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI가 끝난 이후에는 &lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;생성된 결과물을 서버에 배포해야 한다. &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이 과정을 &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;자동화한 것&lt;/span&gt;&lt;/b&gt;이 &lt;b&gt;CD&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CD는 두 가지 의미로 사용된다.&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;Continuous Delivery&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드와 테스트가 완료된 결과물을 자동으로 배포 가능한 상태까지 준비하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에 실제 배포하는 단계는 사람의 승인을 거친다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Build
 &amp;darr;
Test
 &amp;darr;
Deploy Ready
 &amp;darr;
승인
 &amp;darr;
Production&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;&lt;b&gt;Continuous Deployment&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 통과하면 사람의 승인 없이 운영 서버까지 자동으로 배포하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Build
 &amp;darr;
Test
 &amp;darr;
Production&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 규모가 크거나 안정성이 매우 중요한 환경에서는 Continuous Delivery를 많이 사용하며, 스타트업이나 빠른 배포가 필요한 서비스에서는 Continuous Deployment를 적용하는 경우도 많다.&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;CI/CD를 적용하면 다음과 같은 &lt;b&gt;장점&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 변경 사항을 빠르게 검증할 수 있다.&lt;/li&gt;
&lt;li&gt;빌드와 테스트가 자동으로 수행된다.&lt;/li&gt;
&lt;li&gt;배포 과정에서 발생하는 실수를 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;모든 환경에서 동일한 방식으로 배포된다.&lt;/li&gt;
&lt;li&gt;개발 속도가 빨라지고 배포 주기가 짧아진다.&lt;/li&gt;
&lt;li&gt;코드 품질을 지속적으로 유지할 수 있다.&lt;/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;CI/CD를 구축하기 위한 도구는 다양하지만 가장 많이 사용하는 것은 다음 세 가지이다.&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) GitHub Actions&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub에서 제공하는 CI/CD 서비스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 저장소와 완전히 통합되어 있으며, .github/workflows 디렉터리의 YAML 파일만 작성하면 자동으로 파이프라인을 구성할 수 있다.&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;GitHub와 완벽한 통합&lt;/li&gt;
&lt;li&gt;Marketplace를 통한 다양한 Action 제공&lt;/li&gt;
&lt;li&gt;이벤트 기반 자동 실행&lt;/li&gt;
&lt;li&gt;GitHub 프로젝트에서 가장 많이 사용&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;b&gt;2) Jenkins&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins는 가장 오래된 오픈소스 CI/CD 도구 중 하나이다.&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;높은 확장성&lt;/li&gt;
&lt;li&gt;자유로운 커스터마이징&lt;/li&gt;
&lt;li&gt;온프레미스 환경에 적합&lt;/li&gt;
&lt;li&gt;자체 서버 운영 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) GitLab CI&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab에서 제공하는 CI/CD 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.gitlab-ci.yml 파일을 이용하여 파이프라인을 구성하며 GitLab과 자연스럽게 통합된다.&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;GitLab 저장소와 통합&lt;/li&gt;
&lt;li&gt;파이프라인 관리 기능 제공&lt;/li&gt;
&lt;li&gt;DevOps 전 과정을 하나의 플랫폼에서 관리 가능&lt;/li&gt;
&lt;li&gt;Self-hosted 환경도 지원&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;GitHub과 GitLab은 모두 Git 기반의 저장소를 제공하지만 CI/CD를 사용하는 방식에는 차이가 있다.&lt;/p&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;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GitHub&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GitLab&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD 도구&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;GitLab CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설정 파일&lt;/td&gt;
&lt;td&gt;.github/workflows/*.yml&lt;/td&gt;
&lt;td&gt;.gitlab-ci.yml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 환경&lt;/td&gt;
&lt;td&gt;GitHub Runner&lt;/td&gt;
&lt;td&gt;GitLab Runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특징&lt;/td&gt;
&lt;td&gt;Marketplace 기반의 다양한 Action 활용&lt;/td&gt;
&lt;td&gt;저장소와 CI/CD가 긴밀하게 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주요 활용&lt;/td&gt;
&lt;td&gt;오픈소스 및 협업 프로젝트&lt;/td&gt;
&lt;td&gt;기업 및 자체 DevOps 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub에서는 &lt;b&gt;GitHub Actions&lt;/b&gt;를 이용하여 Workflow를 정의하고, GitLab에서는 &lt;b&gt;GitLab CI&lt;/b&gt;를 이용하여 Pipeline을 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 플랫폼 모두 YAML 파일을 작성한다는 점은 동일하지만, 실행 환경과 문법, 제공 기능에는 차이가 있다.&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. GitLab CI + AWS ECS&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Amazon ECS&lt;/b&gt;는 &lt;b&gt;Elastic Container Service의 약자&lt;/b&gt;로 AWS에서 제공하는 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;완전관리형 컨테이너 오케스트레이션 서비스&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 컨테이너를 손쉽게 실행하고 관리할 수 있으며, Kubernetes보다 비교적 단순하게 사용할 수 있어 소규모부터 중규모 프로젝트까지 많이 활용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 EC2 기반뿐 아니라 &lt;b&gt;AWS Fargate(Serverless)&lt;/b&gt; 환경도 지원하여 인프라를 직접 관리하지 않고도 컨테이너를 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECS는 여러 구성 요소가 함께 동작하여 컨테이너를 운영한다.&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;Amazon ECR&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon ECR(Elastic Container Registry)은 Docker 이미지를 저장하는 저장소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 Docker 이미지로 빌드한 뒤 ECR에 업로드하면 ECS는 해당 이미지를 내려받아 실행한다.&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;ECS Cluster&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cluster는 컨테이너가 실행되는 논리적인 그룹이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 서비스와 Task를 관리하며, Fargate를 사용할 경우 별도의 EC2 인스턴스를 관리하지 않아도 된다.&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;ECS Service&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service는 하나 이상의 Task를 지속적으로 실행하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task가 종료되면 자동으로 다시 생성하며 원하는 개수만큼 유지한다.&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;ECS Task&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task는 실제 실행되는 Docker 컨테이너이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECS에서 컨테이너를 실행하는 최소 단위이다.&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;Task Definition&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task Definition은 Task를 실행하기 위한 설정 파일이다.&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;사용할 Docker 이미지&lt;/li&gt;
&lt;li&gt;CPU&lt;/li&gt;
&lt;li&gt;Memory&lt;/li&gt;
&lt;li&gt;Port&lt;/li&gt;
&lt;li&gt;Environment Variable&lt;/li&gt;
&lt;li&gt;Volume&lt;/li&gt;
&lt;li&gt;Logging 설정&lt;/li&gt;
&lt;/ul&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;&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;이제, GitLab과 AWS ECS를 활용하여 CI/CD 파이프라인 구축을 진행해보자.&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;0) GitLab을 이용한 CI/CD 구축 과정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab에서는 .gitlab-ci.yml 파일을 통해 파이프라인을 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Git Push
  &amp;darr;
GitLab Pipeline
  &amp;darr;
Gradle Build
  &amp;darr;
Docker Image 생성
  &amp;darr;
Amazon ECR Push
  &amp;darr;
Amazon ECS 배포&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;b&gt;1) 프로젝트 준비&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD를 구축하기 위해 먼저 Spring Boot 프로젝트를 생성한다.&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;REST API 작성&lt;/li&gt;
&lt;li&gt;Dockerfile 생성&lt;/li&gt;
&lt;li&gt;GitLab Repository 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile은 애플리케이션 이미지를 생성하기 위한 파일이다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM eclipse-temurin:17-jdk-jammy

VOLUME /tmp

ARG JAR_FILE=build/libs/*.jar

COPY ${JAR_FILE} app.jar

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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Amazon ECR 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 이미지를 저장하기 위해 Amazon ECR Repository를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI 파이프라인에서는 Docker 이미지를 빌드한 뒤 이 저장소로 Push한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Spring Boot
  &amp;darr;
docker build
  &amp;darr;
Docker Image
  &amp;darr;
docker push
  &amp;darr;
Amazon ECR&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;&lt;b&gt;3) Amazon ECS 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECR이 준비되면 ECS를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 다음 순서로 구성한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Cluster 생성&lt;/li&gt;
&lt;li&gt;Task Definition 생성&lt;/li&gt;
&lt;li&gt;Service 생성&lt;/li&gt;
&lt;li&gt;Load Balancer 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service는 Task를 실행하고 관리하며, Load Balancer를 통해 외부 요청을 분산한다.&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;4) AWS IAM 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab에서 AWS 리소스를 제어하려면 AWS 접근 권한이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 IAM 사용자와 Access Key를 생성한 뒤 GitLab CI/CD Variables에 등록한다.&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 class=&quot;nginx&quot;&gt;&lt;code&gt;AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로젝트에서는 Access Key를 코드에 직접 작성하지 않고 CI/CD 환경 변수(Variables 또는 Secrets)를 사용하여 안전하게 관리한다.&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;5) GitLab CI Pipeline 작성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab에서는 .gitlab-ci.yml 파일 하나로 전체 파이프라인을 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 파이프라인은 다음 세 단계로 구성된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Build
  &amp;darr;
Docker Build
  &amp;darr;
Deploy&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Build&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;Gradle Build&lt;/li&gt;
&lt;li&gt;테스트 수행&lt;/li&gt;
&lt;li&gt;JAR 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker Build&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;Docker Image 생성&lt;/li&gt;
&lt;li&gt;Amazon ECR Push&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Deploy&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;ECS Service Update&lt;/li&gt;
&lt;li&gt;새로운 Task 실행&lt;/li&gt;
&lt;li&gt;Rolling Update 수행&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;진행한 자동 배포 과정을 정리해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 코드를 Push하면 다음 과정이 자동으로 수행된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Git Push
  &amp;darr;
GitLab CI 실행
  &amp;darr;
Gradle Build
  &amp;darr;
Docker Image 생성
  &amp;darr;
Amazon ECR Push
  &amp;darr;
ECS Service Update
  &amp;darr;
새로운 Container 실행
  &amp;darr;
서비스 배포 완료&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD는 단순히 빌드와 배포를 자동화하는 기술이 아니라 &lt;b&gt;개발부터 운영까지의 전체 과정을 표준화하고 자동화하는 DevOps의 핵심 요소&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions와 GitLab CI는 각각 GitHub와 GitLab 환경에서 손쉽게 CI/CD를 구축할 수 있도록 지원하며, AWS의 ECR과 ECS를 함께 활용하면 Docker 이미지를 자동으로 배포하는 안정적인 파이프라인을 구축할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화된 CI/CD 파이프라인을 적용하면 반복 작업을 줄이고, 코드 품질을 지속적으로 검증하며, 빠르고 일관된 배포를 수행할 수 있어 개발 생산성과 서비스 안정성을 동시에 향상시킬 수 있다.&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CI/CD는 코드 변경부터 Docker 이미지 생성, 컨테이너 배포까지의 모든 과정을 자동화하여 빠르고 안정적인 서비스 운영을 가능하게 하는 핵심 DevOps 기술이다.&lt;/p&gt;</description>
      <category>Bootcamp/Fundamentals</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/57</guid>
      <comments>https://rvbear7.tistory.com/57#entry57comment</comments>
      <pubDate>Wed, 22 Jul 2026 15:05:37 +0900</pubDate>
    </item>
    <item>
      <title>Docker와 Docker Compose</title>
      <link>https://rvbear7.tistory.com/56</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Docker&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Docker Compose&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Docker&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다 보면 가장 자주 발생하는 문제 중 하나가 &lt;b&gt;환경 차이(Environment Difference) &lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자의 PC에서는 정상적으로 실행되던 애플리케이션이 테스트 서버나 운영 서버에서는 실행되지 않는 경우가 자주 발생한다. 운영체제, 라이브러리 버전, 런타임 환경 등이 서로 다르기 때문이다.&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;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;컨테이너 기반 가상화 플랫폼&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션과 실행에 필요한 모든 요소를 하나의 이미지(Image)로 만들고, 이를 컨테이너(Container) 형태로 실행하여 어느 환경에서든 동일한 실행 결과를 보장한다.&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;기존의 가상 머신(VM)처럼 운영체제를 통째로 올리는 것이 아니라 &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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경에 관계없이 동일하게 실행 가능&lt;/li&gt;
&lt;li&gt;빠른 실행 속도&lt;/li&gt;
&lt;li&gt;적은 리소스 사용&lt;/li&gt;
&lt;li&gt;쉬운 배포&lt;/li&gt;
&lt;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;size16&quot;&gt;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;&lt;b&gt;Image&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Image는 컨테이너를 생성하기 위한 &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;/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;Container&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Container는 이미지를 실제로 실행한 상태이다.&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;b&gt;Dockerfile&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile은 이미지를 생성하기 위한 스크립트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile 내부에는 이미지 생성 과정이 순서대로 작성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 Dockerfile은 Spring Boot 애플리케이션을 실행하는 이미지를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM eclipse-temurin:17-jre

COPY build/libs/*.jar app.jar

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;&lt;b&gt;Docker Hub&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Hub는 Docker 이미지 저장소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개 이미지(PostgreSQL, Redis, Nginx, MySQL 등)를 내려받거나 자신이 만든 이미지를 업로드하여 공유할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 PostgreSQL 이미지는 다음 명령어로 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker pull postgres
&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;&lt;b&gt;Volume&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;이를 해결하기 위해 사용하는 것이 Volume이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Volume을 사용하면 컨테이너가 삭제되어도 데이터는 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 데이터베이스(PostgreSQL, MySQL)의 데이터를 저장할 때 사용된다.&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;Network&lt;/b&gt;&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;대표적인 네트워크 종류는 다음과 같다.&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;Bridge Network&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;같은 Bridge Network에 연결된 컨테이너들은 컨테이너 이름으로 서로 통신할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;docker network create my-network
&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;&lt;b&gt;Host Network&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호스트의 네트워크를 그대로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 성능은 좋지만 컨테이너 격리가 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Overlay Network&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 Docker Host에 걸쳐 컨테이너를 연결하는 네트워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Swarm이나 Kubernetes에서 많이 사용된다.&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와 VM은 모두 애플리케이션을 실행하기 위한 가상화 기술이지만 구조가 다르다.&lt;/p&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;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Docker&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Virtual Machine&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커널 공유&lt;/td&gt;
&lt;td&gt;운영체제 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가볍다&lt;/td&gt;
&lt;td&gt;무겁다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빠르게 실행&lt;/td&gt;
&lt;td&gt;부팅 시간이 길다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리소스 사용이 적다&lt;/td&gt;
&lt;td&gt;리소스 사용이 많다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장이 쉽다&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;Docker는 운영체제를 공유하기 때문에 성능과 효율성이 뛰어나지만, VM에 비해 격리 수준은 다소 낮다.&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;Docker는 다음과 같은 상황에서 매우 효과적이다.&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;CI/CD 파이프라인을 구축할 때&lt;/li&gt;
&lt;li&gt;마이크로서비스(MSA)를 구성할 때&lt;/li&gt;
&lt;li&gt;여러 서비스를 독립적으로 실행할 때&lt;/li&gt;
&lt;li&gt;Kubernetes 환경에서 서비스를 운영할 때&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;상황별로 자주 사용하는 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;&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;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 이미지 생성
docker build -t myapp .

# 이미지 다운로드
docker pull postgres

# 이미지 조회
docker images

# 이미지 삭제
docker rmi myapp&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;b&gt;컨테이너&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 컨테이너 실행
docker run -d -p 8080:80 myapp

# 실행 중인 컨테이너 조회
docker ps

# 전체 컨테이너 조회
docker ps -a

# 컨테이너 접속
docker exec -it 컨테이너ID /bin/bash

# 컨테이너 중지
docker stop 컨테이너ID

# 컨테이너 삭제
docker rm 컨테이너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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;네트워크&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 네트워크 생성
docker network create my-network

# 네트워크 조회
docker network ls

# 네트워크 삭제
docker network rm my-network&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;b&gt;볼륨&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 볼륨 생성
docker volume create my-volume

# 볼륨 조회
docker volume ls

# 볼륨 삭제
docker volume rm my-volume&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. Docker Compose&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;예를 들어 Spring Boot 프로젝트를 운영한다고 가정하면 다음과 같은 컨테이너가 함께 실행된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Nginx&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 컨테이너를 docker run으로 하나씩 실행하는 것은 매우 번거롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 사용하는 것이 &lt;b&gt;Docker Compose&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose는 여러 컨테이너를 하나의 docker-compose.yml 파일에서 정의하고 함께 실행할 수 있도록 해준다.&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 Compose를 사용하면 다음과 같은 장점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 컨테이너를 한 번에 실행 가능&lt;/li&gt;
&lt;li&gt;네트워크 자동 생성&lt;/li&gt;
&lt;li&gt;볼륨 자동 연결&lt;/li&gt;
&lt;li&gt;환경 변수 관리 가능&lt;/li&gt;
&lt;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;p data-ke-size=&quot;size16&quot;&gt;docker-compose.yml 구조의&amp;nbsp;가장 기본적인 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;services:
  app:
    build: .
    ports:
      - &quot;8080:8080&quot;

  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: example
&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;services&lt;/td&gt;
&lt;td&gt;실행할 컨테이너 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;image&lt;/td&gt;
&lt;td&gt;사용할 이미지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;build&lt;/td&gt;
&lt;td&gt;Dockerfile을 이용해 이미지 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ports&lt;/td&gt;
&lt;td&gt;포트 매핑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;environment&lt;/td&gt;
&lt;td&gt;환경 변수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;depends_on&lt;/td&gt;
&lt;td&gt;서비스 실행 순서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;volumes&lt;/td&gt;
&lt;td&gt;데이터 저장소 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;networks&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용하는 Docker Compose 주요 명령어를 정리해보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 서비스 실행
docker compose up -d

# 서비스 종료
docker compose down

# 이미지 빌드
docker compose build

# 실행 중인 서비스 조회
docker compose ps

# 로그 확인
docker compose logs&lt;/code&gt;&lt;/pre&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;&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;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같이 두 개의 Spring Boot 서비스를 구성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;service-a:8080
  |
Feign Client
  &amp;darr;
service-b:8081&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 class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;service.b.url=http://localhost:8081&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;하지만 Docker에서는 컨테이너 이름을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;service.b.url=http://service-b:8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Docker Network에서는 DNS가 자동으로 제공되기 때문에 컨테이너 이름만으로도 통신이 가능하다.&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 Compose로 실행하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 각각 실행하는 대신 하나의 Compose 파일만 작성하면 된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  service-a:
    image: img-service-a
    ports:
      - &quot;8080:8080&quot;
    depends_on:
      - service-b

  service-b:
    image: img-service-b
    ports:
      - &quot;8081:8080&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;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose up -d
&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;Compose는 자동으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 생성&lt;/li&gt;
&lt;li&gt;컨테이너 생성&lt;/li&gt;
&lt;li&gt;서비스 실행&lt;/li&gt;
&lt;li&gt;서비스 간 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 모두 처리해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 프로젝트를 종료할 때도 다음 명령어 하나면 된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose down
&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;Docker Compose를 실행하면 별도로 네트워크를 생성하지 않아도 자동으로 Bridge Network가 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 이름은 일반적으로 다음과 같은 형태로 생성된다.&lt;/p&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;프로젝트명_default
&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;이를 통해 같은 Compose 파일에 정의된 서비스들은 모두 같은 네트워크에 연결되며, 컨테이너 이름으로 자유롭게 통신할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 service-a에서는 다음과 같이 service-b를 호출할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;service.b.url=http://service-b:8080
&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;별도의 IP 주소를 관리할 필요가 없으며, 컨테이너가 재생성되어 IP가 변경되더라도 서비스 이름은 그대로 유지되므로 안정적으로 통신할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker는 애플리케이션과 실행 환경을 하나의 컨테이너로 패키징하여 &lt;b&gt;어디서나 동일한 실행 환경을 제공하는 플랫폼&lt;/b&gt;이다. 개발 환경과 운영 환경의 차이를 줄이고, 빠른 배포와 높은 이식성을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose는 이러한 여러 컨테이너를 하나의 설정 파일로 관리하는 도구이다. 복잡한 서비스 구성을 간단하게 만들고, 네트워크와 볼륨을 자동으로 구성하며, 여러 서비스를 한 번의 명령으로 실행하고 종료할 수 있게 해준다.&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 Boot, PostgreSQL, Redis, Nginx 등을 함께 사용하는 경우가 많기 때문에 Docker와 Docker Compose는 함께 사용하는 것이 일반적이며, 이후 Kubernetes와 같은 컨테이너 오케스트레이션 환경으로 확장하는 기반 기술로도 활용된다.&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. Docker란 애플리케이션을 어디서나 동일한 환경에서 실행할 수 있도록 컨테이너로 패키징하는 플랫폼이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. Docker Compose란 여러 Docker 컨테이너를 하나의 YAML 파일로 정의하고 함께 관리하는 도구이다.&lt;/p&gt;</description>
      <category>Bootcamp/Fundamentals</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/56</guid>
      <comments>https://rvbear7.tistory.com/56#entry56comment</comments>
      <pubDate>Tue, 21 Jul 2026 15:31:54 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security에서 흔히 발생하는 문제와 해결 사례</title>
      <link>https://rvbear7.tistory.com/55</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring Security에서 흔히 발생하는 문제 &amp;amp; 해결 사례&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 처음 적용하면 기능 구현보다 오류를 해결하는 데 더 많은 시간을 보내는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT를 정상적으로 발급했는데도 401 Unauthorized가 반환되거나, 인증이 완료되었는데 403 Forbidden이 발생하기도 한다. 또한 SecurityContextHolder에서 Authentication이 null로 조회되거나 hasRole()이 정상적으로 동작하지 않는 문제도 자주 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제들은 대부분 Spring Security의 동작 원리를 충분히 이해하지 못한 상태에서 발생한다. 따라서 단순히 오류를 해결하는 것보다 &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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 1. JWT가 있는데도 401 Unauthorized가 발생&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;클라이언트는 JWT를 전송했지만 서버는 인증되지 않은 사용자로 판단한다.&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;Authorization Header 누락&lt;/li&gt;
&lt;li&gt;Bearer Prefix 누락&lt;/li&gt;
&lt;li&gt;JWT 만료&lt;/li&gt;
&lt;li&gt;JWT 서명(Signature) 오류&lt;/li&gt;
&lt;li&gt;JwtAuthenticationFilter가 등록되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Header가 다음과 같다면&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Authorization: eyJhbGc...
&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;JWT Filter는 토큰을 찾지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 형식은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Authorization: Bearer eyJhbGc...
&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;b&gt;해결 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT Filter에서는 반드시 Header를 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String bearerToken =
        request.getHeader(&quot;Authorization&quot;);

if (bearerToken == null ||
        !bearerToken.startsWith(&quot;Bearer &quot;)) {

    filterChain.doFilter(request, response);
    return;
}
&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;또한 JWT 검증이 성공한 경우에만 Authentication을 생성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;if (jwtProvider.validateToken(token)) {

    Authentication authentication =
            jwtProvider.getAuthentication(token);

    SecurityContextHolder.getContext()
            .setAuthentication(authentication);
}
&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. 인증은 되었는데 403 Forbidden이 발생&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 정상적으로 검증되었지만 권한이 부족한 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@PreAuthorize(&quot;hasRole('ADMIN')&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 사용자가&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;ROLE_USER
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면 403이 발생한다.&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;현재 Authentication이 가지고 있는 권한을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Authentication authentication =
        SecurityContextHolder.getContext()
                .getAuthentication();

authentication.getAuthorities()
        .forEach(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 저장된 권한과 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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 3. hasRole()이 동작하지 않음&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 있다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@PreAuthorize(&quot;hasRole('ADMIN')&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;그런데 Authentication에는 다음 권한이 저장되어 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ADMIN
&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 Security는 내부적으로&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;ROLE_ADMIN
&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;따라서 항상 권한이 없다고 판단한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법 1&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한을 ROLE_PREFIX와 함께 저장한다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;new SimpleGrantedAuthority(
        &quot;ROLE_ADMIN&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;b&gt;방법 2&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROLE Prefix를 사용하지 않는다면&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PreAuthorize(
        &quot;hasAuthority('ADMIN')&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;
&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. Authentication이 null&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드를 실행했는데&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Authentication authentication =
        SecurityContextHolder
                .getContext()
                .getAuthentication();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;null이 반환되는 경우가 있다.&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;JwtAuthenticationFilter가 실행되지 않음&lt;/li&gt;
&lt;li&gt;Authentication을 저장하지 않음&lt;/li&gt;
&lt;li&gt;permitAll() 요청&lt;/li&gt;
&lt;li&gt;Filter 등록 순서 오류&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;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication을 생성한 뒤 반드시 저장해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Authentication authentication =
        jwtProvider.getAuthentication(token);

SecurityContextHolder
        .getContext()
        .setAuthentication(authentication);
&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;문제 5. Controller에서 로그인한 사용자를 가져오지 못함&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 자주 보인다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GetMapping(&quot;/me&quot;)
public UserResponse me(
        @RequestHeader(&quot;Authorization&quot;)
        String token
) {

    Long userId =
            jwtProvider.getUserId(token);

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서 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;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;Authentication 또는 @AuthenticationPrincipal을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(&quot;/me&quot;)
public UserResponse me(
        @AuthenticationPrincipal
        CustomUserDetails user
) {

    return userService.findById(
            user.getUserId()
    );
}
&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;문제 6. JwtAuthenticationFilter가 실행되지 않음&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityConfig에 Filter를 등록하지 않으면 JWT 인증은 수행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 예시이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.build();
}&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;b&gt;해결 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter를 등록한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&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;문제 7. permitAll()인데도 JwtAuthenticationFilter가 실행&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;다음과 같은 설정이 있다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;.requestMatchers(&quot;/login&quot;)
.permitAll()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT Filter는 여전히 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;permitAll은 &lt;b&gt;인가를 허용하는 설정&lt;/b&gt;이지 Filter 실행을 생략하는 설정이 아니다.&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;JWT 검증이 필요 없는 URL은 Filter 내부에서 제외한다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Override
protected boolean shouldNotFilter(
        HttpServletRequest request
) {

    return request.getServletPath()
            .startsWith(&quot;/auth&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 8. SecurityContext가 다음 요청에도 유지된다고 생각함&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication은 요청마다 새로 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 끝나면 SecurityContext는 제거된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SecurityContextHolder.clearContext();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 요청에서는 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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 9. Refresh Token으로 API 인증을 수행&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token은 Access Token을 재발급하기 위한 토큰이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 인증에는 사용해서는 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Access Token
  &amp;darr;
API 요청
----------------
Refresh Token
  &amp;darr;
재발급 API&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;문제 10. 401과 403을 구분하지 않음&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 프로젝트가 다음과 같이 처리한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;모든 인증 오류
  &amp;darr;
403&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;상태 코드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;인증 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403&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;이를 명확히 구분해야 한다.&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 문제를 해결할 때는 다음 순서대로 확인하면 대부분의 원인을 빠르게 찾을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 디버깅 체크리스트를 작성해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Authorization Header가 정상적으로 전달되는가?&lt;/li&gt;
&lt;li&gt;Bearer Prefix가 포함되어 있는가?&lt;/li&gt;
&lt;li&gt;JWT가 만료되지 않았는가?&lt;/li&gt;
&lt;li&gt;JwtAuthenticationFilter가 실행되는가?&lt;/li&gt;
&lt;li&gt;SecurityContextHolder.setAuthentication()이 호출되는가?&lt;/li&gt;
&lt;li&gt;Authentication에 권한이 정상적으로 저장되는가?&lt;/li&gt;
&lt;li&gt;hasRole()과 hasAuthority()를 올바르게 사용했는가?&lt;/li&gt;
&lt;li&gt;Filter 등록 순서가 올바른가?&lt;/li&gt;
&lt;li&gt;AuthenticationEntryPoint와 AccessDeniedHandler가 정상 등록되어 있는가?&lt;/li&gt;
&lt;li&gt;permitAll()과 shouldNotFilter()의 역할을 혼동하지 않았는가?&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 발생하는 대부분의 문제는 인증과 인가의 흐름을 이해하면 원인을 빠르게 파악할 수 있다. 401 Unauthorized는 인증 정보가 없거나 유효하지 않은 경우 발생하며, 403 Forbidden은 인증은 완료되었지만 필요한 권한이 없는 경우 발생한다. 또한 hasRole()과 hasAuthority()의 차이, ROLE_ Prefix 처리 방식, SecurityContextHolder의 생명주기, JwtAuthenticationFilter의 등록 위치와 실행 순서를 이해하는 것은 안정적인 인증 시스템을 구현하는 데 필수적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 발생했을 때는 단순히 설정을 수정하기보다 &lt;b&gt;요청이 Security Filter Chain을 통과하면서 Authentication이 언제 생성되고, 어디에 저장되며, 어떤 시점에 권한 검사가 수행되는지&lt;/b&gt;를 추적하는 것이 가장 효과적인 디버깅 방법이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security 문제의 대부분은 Authentication이 생성되고 SecurityContextHolder에 저장되어 권한 검사를 거쳐 Controller까지 전달되는 흐름을 이해하면 원인을 빠르게 찾고 해결할 수 있다.&lt;/p&gt;</description>
      <category>Bootcamp/Implementation</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/55</guid>
      <comments>https://rvbear7.tistory.com/55#entry55comment</comments>
      <pubDate>Fri, 17 Jul 2026 21:28:21 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 전체 과정 흐름 추적</title>
      <link>https://rvbear7.tistory.com/54</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring Security 전체 과정 흐름 추적&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 처음 학습하면 SecurityFilterChain, JwtAuthenticationFilter, Authentication, SecurityContextHolder, AuthorizationFilter 등 다양한 구성 요소를 개별적으로 배우게 된다. 하지만 실제 요청이 들어왔을 때 이들이 어떤 순서로 동작하는지 한 번에 이해하기는 쉽지 않다.&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;JWT는 언제 검증될까?&lt;/li&gt;
&lt;li&gt;Authentication은 언제 생성될까?&lt;/li&gt;
&lt;li&gt;SecurityContextHolder에는 누가 저장할까?&lt;/li&gt;
&lt;li&gt;@PreAuthorize는 언제 실행될까?&lt;/li&gt;
&lt;li&gt;Controller에서는 어떻게 로그인한 사용자를 조회할 수 있을까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이번에는 하나의 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;HTTP 요청이 들어온 순간부터 Controller가 실행될 때까지 Spring Security 내부에서 발생하는 모든 과정을 시간 순서대로 추적&lt;/b&gt;&lt;/span&gt;해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 인증 기반 요청은 다음과 같은 순서로 처리된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Client
  &amp;darr;
HTTP Request
  &amp;darr;
DelegatingFilterProxy
  &amp;darr;
FilterChainProxy
  &amp;darr;
SecurityFilterChain
  &amp;darr;
JwtAuthenticationFilter
  &amp;darr;
AuthorizationFilter
  &amp;darr;
DispatcherServlet
  &amp;darr;
HandlerMapping
  &amp;darr;
Controller
  &amp;darr;
Service
  &amp;darr;
Response&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 Controller보다 먼저 모든 인증과 권한 검사를 수행한다.&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;1단계. HTTP 요청이 서버에 도착&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 자신의 정보를 조회하는 API를 호출한다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /users/me

Authorization: Bearer eyJhbGc...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 Controller는 실행되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat은 먼저 등록된 Filter들을 실행하기 시작한다.&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;2단계. DelegatingFilterProxy가 요청을 전달&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 Servlet Filter를 직접 등록하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat에는 다음 Filter 하나만 등록된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;DelegatingFilterProxy
&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;이 Filter가 Spring Bean인 FilterChainProxy를 찾아 요청을 위임한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Tomcat
  &amp;darr;
DelegatingFilterProxy
  &amp;darr;
FilterChainProxy&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;b&gt;3단계. FilterChainProxy가 SecurityFilterChain을 선택&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에는 여러 개의 SecurityFilterChain이 존재할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) {
    ...
}

@Bean
SecurityFilterChain adminSecurity(HttpSecurity http) {
    ...
}
&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;FilterChainProxy는 현재 요청 URL과 가장 일치하는 SecurityFilterChain을 선택한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Request
  &amp;darr;
FilterChainProxy
  &amp;darr;
Matching SecurityFilterChain&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;b&gt;4단계. JwtAuthenticationFilter가 JWT를 검증&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Security Filter들이 순서대로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 JWT Filter가 Authorization Header를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;String bearerToken =
        request.getHeader(&quot;Authorization&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;Bearer Token이라면 JWT를 추출한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;String token =
        bearerToken.substring(7);
&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;JWT를 검증한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;jwtProvider.validateToken(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;b&gt;5단계. Authentication을 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT가 정상이라면 Claims에서 사용자 정보를 읽는다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Authentication authentication =
        jwtProvider.getAuthentication(token);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication 안에는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Principal&lt;/li&gt;
&lt;li&gt;Authorities&lt;/li&gt;
&lt;li&gt;authenticated=true&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;size16&quot;&gt;&lt;b&gt;6단계. SecurityContextHolder에 저장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication만 생성해서는 인증이 완료되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 현재 요청의 SecurityContext에 저장해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SecurityContextHolder
        .getContext()
        .setAuthentication(authentication);
&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 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;&lt;b&gt;7단계. AuthorizationFilter가 권한을 검사&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증이 끝났다면 권한을 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@PreAuthorize(&quot;hasRole('ADMIN')&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;AuthorizationFilter는 Authentication의 권한을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ROLE_USER
  &amp;darr;
hasRole(&quot;ADMIN&quot;)
  &amp;darr;
false
  &amp;darr;
403&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;
&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;8단계. DispatcherServlet이 Controller를 찾음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Filter Chain을 모두 통과하면 DispatcherServlet이 실행된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DispatcherServlet
  &amp;darr;
HandlerMapping
  &amp;darr;
Controller&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;
&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;9단계. @AuthenticationPrincipal이 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller에서는 다음과 같이 현재 로그인한 사용자를 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(&quot;/me&quot;)
public UserResponse me(
        @AuthenticationPrincipal
        CustomUserDetails user
) {

    return userService.findById(
            user.getUserId()
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 SecurityContextHolder에서 Authentication을 조회한 뒤 Principal을 전달한다.&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;10단계. Service 실행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller는 일반적인 비즈니스 로직을 수행한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
public class UserService {

    public UserResponse findById(Long id) {

        ...

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서는 인증이 이미 완료되었기 때문에 Service는 인증 여부를 다시 검사할 필요가 없다.&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;11단계. 응답을 반환&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller가 응답을 반환하면&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Controller
  &amp;darr;
DispatcherServlet
  &amp;darr;
Filter Chain
  &amp;darr;
Client&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;요청이 종료되면 SecurityContext도 함께 정리된다.&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;요청이 끝나면 SecurityContext는 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 요청이 종료되면 SecurityContext를 제거한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SecurityContextHolder.clearContext();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 요청에서 이전 사용자의 Authentication이 남아 있으면 심각한 보안 문제가 발생할 수 있기 때문이다.&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Client
  &amp;darr;
DelegatingFilterProxy
  &amp;darr;
FilterChainProxy
  &amp;darr;
SecurityFilterChain
  &amp;darr;
JwtAuthenticationFilter
  &amp;darr;
Authorization Header
  &amp;darr;
JWT 검증
  &amp;darr;
Authentication 생성
  &amp;darr;
SecurityContextHolder 저장
  &amp;darr;
AuthorizationFilter
  &amp;darr;
DispatcherServlet
  &amp;darr;
HandlerMapping
  &amp;darr;
Controller
  &amp;darr;
Service
  &amp;darr;
Response
  &amp;darr;
SecurityContext 제거&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Controller에서 JWT를 직접 파싱하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 구현은 권장되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GetMapping(&quot;/me&quot;)
public UserResponse me(
        @RequestHeader(&quot;Authorization&quot;) String token
) {

    Long userId = jwtProvider.getUserId(token);

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 검증은 JwtAuthenticationFilter에서 이미 완료되므로, Controller에서는 @AuthenticationPrincipal 또는 Authentication을 사용하는 것이 적절하다.&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. Authentication을 생성하고 SecurityContextHolder에 저장하지 않는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드만으로는 인증이 완료되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Authentication authentication =
        jwtProvider.getAuthentication(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;반드시 SecurityContextHolder에 저장해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SecurityContextHolder.getContext()
        .setAuthentication(authentication);
&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;&lt;b&gt;3. JwtAuthenticationFilter에서 filterChain.doFilter()를 호출하지 않는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 Filter와 DispatcherServlet, Controller가 실행되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;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;b&gt;4. 요청 간에 Authentication이 유지된다고 생각하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityContextHolder는 요청이 끝나면 정리된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SecurityContextHolder.clearContext();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음 요청에서는 JWT를 다시 검증하여 새로운 Authentication을 생성해야 한다.&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는 HTTP 요청이 들어오면 DelegatingFilterProxy를 시작으로 FilterChainProxy와 SecurityFilterChain을 거쳐 인증과 권한 검사를 수행한다. JwtAuthenticationFilter는 JWT를 검증하여 Authentication을 생성하고 SecurityContextHolder에 저장하며, AuthorizationFilter는 이를 이용해 권한을 확인한다.&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;이후 요청은 DispatcherServlet을 거쳐 Controller와 Service로 전달되고, @AuthenticationPrincipal은 SecurityContextHolder에 저장된 인증 정보를 이용해 현재 로그인한 사용자를 주입한다. 요청이 종료되면 SecurityContext는 정리되며 다음 요청에서 다시 동일한 인증 과정을 수행한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security의 모든 인증과 권한 검사는 Controller 이전의 Security Filter Chain에서 완료되며, Authentication을 SecurityContextHolder에 저장하는 순간부터 애플리케이션은 해당 요청을 인증된 사용자로 처리한다.&lt;/p&gt;</description>
      <category>Bootcamp/Implementation</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/54</guid>
      <comments>https://rvbear7.tistory.com/54#entry54comment</comments>
      <pubDate>Thu, 16 Jul 2026 21:50:32 +0900</pubDate>
    </item>
    <item>
      <title>Refresh Token</title>
      <link>https://rvbear7.tistory.com/53</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Refresh Token&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 기반 인증을 처음 구현하면 Access Token 하나만으로 인증을 처리해도 충분해 보인다. 하지만 Access Token의 유효 시간을 길게 설정하면 토큰이 탈취되었을 때 장기간 악용될 수 있고, 반대로 유효 시간을 너무 짧게 설정하면 사용자는 자주 로그인을 다시 해야 하는 불편함을 겪게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 등장한 방식이 &lt;b&gt;Access Token과 Refresh Token을 함께 사용하는 구조&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token은 짧은 수명을 가지며 API 인증에 사용되고, Refresh Token은 긴 수명을 가지며 새로운 Access Token을 발급받는 용도로만 사용된다.&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;왜 Refresh Token이 필요할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token만 사용하는 경우 다음과 같은 문제가 발생한다.&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;Access Token의 만료 시간이 긴 경우&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;/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;b&gt;Access Token의 만료 시간이 짧은 경우&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token은 이러한 두 문제를 동시에 해결하기 위한 방법이다.&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Access Token&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Refresh Token&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적&lt;/td&gt;
&lt;td&gt;API 인증&lt;/td&gt;
&lt;td&gt;Access Token 재발급&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&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;/td&gt;
&lt;td&gt;짧음&lt;/td&gt;
&lt;td&gt;김&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;노출 가능성&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token은 API 인증에 사용해서는 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오직 새로운 Access Token을 발급받기 위한 용도로만 사용한다.&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Login
  &amp;darr;
AuthenticationManager
  &amp;darr;
인증 성공
  &amp;darr;
Access Token 생성
  &amp;darr;
Refresh Token 생성
  &amp;darr;
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 class=&quot;ebnf&quot;&gt;&lt;code&gt;Authentication authentication =
        authenticationManager.authenticate(loginToken);

String accessToken =
        jwtProvider.createAccessToken(authentication);

String refreshToken =
        jwtProvider.createRefreshToken(authentication);
&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;만약 Access Token이 만료되면 API 요청은 실패한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Client
  &amp;darr;
GET /users/me
  &amp;darr;
401 Unauthorized&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;클라이언트는 Refresh Token을 이용하여 재발급 API를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;POST /auth/reissue
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Access Token은 사용하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token만 사용한다.&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;Refresh Token으로 재발급하는 과정은 다음 순서로 이루어진다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Refresh Token
  &amp;darr;
JWT 검증
  &amp;darr;
사용자 확인
  &amp;darr;
새 Access Token 생성
  &amp;darr;
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 class=&quot;reasonml&quot;&gt;&lt;code&gt;public TokenResponse reissue(String refreshToken) {

    jwtProvider.validateToken(refreshToken);

    Long userId =
            jwtProvider.getUserId(refreshToken);

    String accessToken =
            jwtProvider.createAccessToken(userId);

    return new TokenResponse(accessToken);
}
&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;Refresh Token은 어디에 저장할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 저장 방식은 세 가지이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;저장 위치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LocalStorage&lt;/td&gt;
&lt;td&gt;구현이 간단&lt;/td&gt;
&lt;td&gt;XSS에 취약&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HttpOnly Cookie&lt;/td&gt;
&lt;td&gt;JavaScript 접근 불가&lt;/td&gt;
&lt;td&gt;CSRF 고려 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB / Redis&lt;/td&gt;
&lt;td&gt;서버에서 폐기 가능&lt;/td&gt;
&lt;td&gt;저장소 관리 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;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;Access Token &amp;rarr; Authorization Header&lt;/li&gt;
&lt;li&gt;Refresh Token &amp;rarr; HttpOnly Cookie&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;왜 Refresh Token을 저장소에 보관할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 기본적으로 Stateless이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 발급하면 서버는 토큰을 기억하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Refresh Token을 저장소(DB 또는 Redis)에 보관하면 다음 기능을 구현할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그아웃&lt;/li&gt;
&lt;li&gt;강제 만료&lt;/li&gt;
&lt;li&gt;중복 로그인 관리&lt;/li&gt;
&lt;li&gt;Token Rotation&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;userId
  &amp;darr;
Refresh Token
  &amp;darr;
Redis&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 Refresh Token을 계속 재사용하지 않는다. 재발급이 성공하면 새로운 Refresh Token도 함께 발급한다. 기존 Refresh Token은 즉시 폐기한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Refresh Token A
  &amp;darr;
재발급
  &amp;darr;
Access Token B
  +
Refresh Token B
  &amp;darr;
Refresh Token A 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;Refresh Token Rotation&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token 탈취를 어떻게 막을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 공격자가 Refresh Token을 탈취했다고 가정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rotation을 사용하지 않으면&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Refresh Token
  &amp;darr;
무한 재발급 가능&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;Rotation을 사용하면&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Refresh Token
  &amp;darr;
한 번 사용
  &amp;darr;
삭제
  &amp;darr;
재사용 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Refresh Token으로 다시 요청하면 서버는 이를 거부할 수 있다.&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;로그아웃 시에는 Refresh Token을 저장소에서 삭제한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;refreshTokenRepository.deleteByUserId(userId);
&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;브라우저의 Cookie도 함께 제거한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Cookie cookie =
        new Cookie(&quot;refreshToken&quot;, &quot;&quot;);

cookie.setMaxAge(0);
&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityConfig와는 어떤 관계가 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token은 일반적으로 재발급 API만 허용한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;http.authorizeHttpRequests(auth -&amp;gt; auth

        .requestMatchers(&quot;/auth/login&quot;).permitAll()

        .requestMatchers(&quot;/auth/reissue&quot;).permitAll()

        .anyRequest().authenticated()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재발급 API는 Access Token 없이도 호출할 수 있어야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Refresh Token 자체는 반드시 서버에서 검증해야 한다.&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Refresh Token으로 API 인증을 수행하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token은 API 인증용이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 인증에는 항상 Access Token을 사용해야 한다.&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. Rotation 없이 동일한 Refresh Token을 계속 사용하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token이 탈취되면 만료 시점까지 계속 재사용될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하다면 Rotation을 적용하는 것이 안전하다.&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;3. 로그아웃 시 Cookie만 삭제하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 Cookie만 삭제하면 서버는 해당 Refresh Token의 존재를 알지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 또는 Redis에 저장하는 구조라면 서버 저장소에서도 함께 삭제해야 한다.&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;4. Refresh Token을 LocalStorage에 저장하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token은 장기간 유지되는 토큰이므로 XSS 공격의 영향을 최소화할 수 있는 HttpOnly Cookie에 저장하는 것이 일반적이다.&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;5. 재발급 시 기존 Refresh Token을 그대로 유지하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rotation을 적용하지 않으면 동일한 Refresh Token이 반복적으로 사용될 수 있다. 재발급 성공 시 새로운 Refresh Token을 발급하고 기존 토큰을 폐기하는 것이 보안 측면에서 유리하다.&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Login
  &amp;darr;
Access Token 발급
  &amp;darr;
Refresh Token 발급
  &amp;darr;
API 요청
  &amp;darr;
Access Token 만료
  &amp;darr;
401
  &amp;darr;
Refresh API
  &amp;darr;
Refresh Token 검증
  &amp;darr;
새 Access Token
  &amp;darr;
(선택)
새 Refresh Token
  &amp;darr;
기존 Refresh 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;Access Token과 Refresh Token을 함께 사용하는 구조는 보안성과 사용자 경험을 모두 고려한 인증 전략이다. Access Token은 짧은 수명을 통해 토큰 탈취 위험을 줄이고, Refresh Token은 새로운 Access Token을 발급하는 역할만 수행하여 사용자가 반복해서 로그인하지 않아도 되도록 한다.&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;또한 Refresh Token을 저장소에 관리하면 로그아웃, 강제 만료, 중복 로그인 제어와 같은 기능을 구현할 수 있으며, Refresh Token Rotation을 적용하면 탈취된 토큰의 재사용 위험도 줄일 수 있다. 따라서 실무에서는 단순히 토큰을 발급하는 것을 넘어, 저장 방식과 재발급 전략까지 함께 설계하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Refresh Token은 로그인을 연장하기 위한 토큰이 아니라, 짧은 수명의 Access Token을 안전하게 재발급하기 위한 장기 자격 증명이며, Rotation과 서버 저장소를 함께 활용할 때 가장 안전한 인증 구조를 구성할 수 있다.&lt;/p&gt;</description>
      <category>Bootcamp/Implementation</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/53</guid>
      <comments>https://rvbear7.tistory.com/53#entry53comment</comments>
      <pubDate>Wed, 15 Jul 2026 23:08:15 +0900</pubDate>
    </item>
    <item>
      <title>CORS와 CSRF</title>
      <link>https://rvbear7.tistory.com/52</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CORS&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CSRF&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 처음 적용하면 가장 먼저 작성하는 설정 중 하나가 다음 코드이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;http
    .csrf(AbstractHttpConfigurer::disable)
    .cors(Customizer.withDefaults());
&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;특히 JWT 기반 인증에서는 대부분 CSRF를 비활성화하는데, 이를 &quot;JWT를 사용하니까 무조건 CSRF를 끄면 된다.&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;b&gt;CSRF&lt;/b&gt;는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;인증 정보를 브라우저가 자동으로 전송하는 방식&lt;/b&gt;&lt;/span&gt;과 관련된 공격이고, &lt;b&gt;CORS&lt;/b&gt;는 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;브라우저의 동일 출처 정책(Same-Origin Policy)&lt;/b&gt;&lt;/span&gt; 으로 인해 발생하는 제약을 해결하기 위한 정책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS와 CSRF는 서로 다른 문제를 해결한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름이 비슷하여 함께 설명되는 경우가 많지만 목적은 전혀 다르다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;CORS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;CSRF&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적&lt;/td&gt;
&lt;td&gt;다른 Origin 요청 허용 여부&lt;/td&gt;
&lt;td&gt;사용자의 의도하지 않은 요청 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;발생 원인&lt;/td&gt;
&lt;td&gt;브라우저의 Same-Origin Policy&lt;/td&gt;
&lt;td&gt;브라우저의 자동 인증 정보 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;해결 방법&lt;/td&gt;
&lt;td&gt;Access-Control-Allow-Origin&lt;/td&gt;
&lt;td&gt;CSRF Token 검증&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CORS는 &lt;b&gt;브라우저가 요청을 막는 문제&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;CSRF는 &lt;b&gt;브라우저가 요청을 대신 보내는 문제&lt;/b&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;&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. CORS&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 기본적으로 &lt;b&gt;동일 출처 정책(Same-Origin Policy)&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;&lt;b&gt;Frontend&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:3000
&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;b&gt;Backend&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 서로 다른 Origin이다.&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;Frontend에서 다음과 같이 API를 호출하면&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;fetch(&quot;http://localhost:8080/users&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;이는 서버가 요청을 거부하는 것이 아니라 &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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS는 어떻게 동작할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 요청을 보내기 전에 서버가 해당 Origin을 허용하는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 다음과 같은 Header를 응답에 포함한다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;Access-Control-Allow-Origin: http://localhost:3000
&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 Security에서는 다음과 같이 설정한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
CorsConfigurationSource corsConfigurationSource() {

    CorsConfiguration configuration =
            new CorsConfiguration();

    configuration.setAllowedOrigins(
            List.of(&quot;http://localhost:3000&quot;)
    );

    configuration.setAllowedMethods(
            List.of(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;)
    );

    configuration.setAllowedHeaders(
            List.of(&quot;*&quot;)
    );

    configuration.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();

    source.registerCorsConfiguration(&quot;/**&quot;, configuration);

    return source;
}&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;그리고 SecurityConfig에서 활성화한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;http.cors(Customizer.withDefaults());
&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;p data-ke-size=&quot;size16&quot;&gt;모든 요청이 바로 서버로 전달되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PUT, DELETE, Authorization Header 등을 사용하는 경우 브라우저는 먼저 OPTIONS 요청을 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;Preflight Request&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;OPTIONS /users

Origin: http://localhost:3000

Access-Control-Request-Method: PUT
&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 class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;OPTIONS
  &amp;darr;
200 OK
  &amp;darr;
PUT 요청&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. CSRF&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CSRF&lt;/b&gt;는 &lt;b&gt;Cross Site Request Forgery의 약자&lt;/b&gt;로 &lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;사용자가 의도하지 않은 요청을 보내도록 만드는 공격&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 쇼핑몰에 로그인한 상태라고 가정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에는 Session Cookie가 저장되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 악성 사이트를 방문하면&lt;/p&gt;
&lt;form action=&quot;&amp;lt;a href=https://shop.com/pay&amp;gt;https://shop.com/pay&amp;lt;/a&amp;gt;&quot;&gt;&lt;input type=&quot;hidden&quot; /&gt;&lt;/form&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;form action=&quot;https://shop.com/pay&quot;&amp;gt;

&amp;lt;input type=&quot;hidden&quot;&amp;gt;

&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 Session Cookie를 자동으로 함께 전송한다.&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는 CSRF를 어떻게 막을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 CSRF Token을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 랜덤 Token을 발급하고 요청마다 함께 전송하도록 한다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;Client
  &amp;darr;
CSRF Token
  &amp;darr;
POST 요청
  &amp;darr;
Server
  &amp;darr;
Token 검증&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Token이 일치하지 않으면 요청을 거부한다.&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;그렇다면 JWT에서는 왜 CSRF를 비활성화할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 &lt;b&gt;JWT를 사용한다고 해서 무조건 CSRF가 발생하지 않는 것은 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSRF의 핵심 조건은 &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;예를 들어 Access Token을 Authorization Header에 저장하는 경우&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Authorization: Bearer xxx
&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;브라우저는 Header를 자동으로 전송하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript가 직접 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 일반적인 CSRF 공격이 성립하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조에서는 다음과 같이 설정하는 것이 일반적이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;http.csrf(AbstractHttpConfigurer::disable);&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;Refresh Token을 Cookie에 저장하면 어떻게 될까?&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;Access Token &amp;rarr; Authorization Header&lt;/li&gt;
&lt;li&gt;Refresh Token &amp;rarr; HttpOnly Cookie&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Refresh Token Cookie는 브라우저가 자동으로 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Refresh Token 재발급 API는 CSRF 공격 대상이 될 수 있다.&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;SameSite=Lax 또는 Strict&lt;/li&gt;
&lt;li&gt;Origin 검증&lt;/li&gt;
&lt;li&gt;Refresh Token Rotation&lt;/li&gt;
&lt;li&gt;CSRF Token 적용(필요한 경우)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;JWT를 사용하더라도 Cookie 기반 인증 정보가 있다면 CSRF를 다시 고려해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 기반 인증에서 자주 사용하는 SecurityConfig 설정은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http)
        throws Exception {

    http
            .csrf(AbstractHttpConfigurer::disable)

            .cors(Customizer.withDefaults())

            .sessionManagement(session -&amp;gt;
                    session.sessionCreationPolicy(
                            SessionCreationPolicy.STATELESS
                    )
            );

    return http.build();
}&lt;/code&gt;&lt;/pre&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;Session 사용 안 함&lt;/li&gt;
&lt;li&gt;CSRF 비활성화&lt;/li&gt;
&lt;li&gt;CORS 활성화&lt;/li&gt;
&lt;li&gt;Stateless 인증&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;실무에서 자주 하는 실수를 알아보자.&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. CORS와 CSRF를 같은 개념으로 이해하는 경우&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;CORS &amp;rarr; 브라우저의 보안 정책&lt;/li&gt;
&lt;li&gt;CSRF &amp;rarr; 인증 정보를 악용하는 공격&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;b&gt;2. allowedOrigins(&quot;*&quot;)와 allowCredentials(true)를 함께 사용하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 설정은 허용되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;configuration.setAllowedOrigins(List.of(&quot;*&quot;));
configuration.setAllowCredentials(true);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 명세상 &lt;b&gt;Access-Control-Allow-Origin: * &lt;/b&gt;와 &lt;b&gt;Access-Control-Allow-Credentials: true &lt;/b&gt;는 함께 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Credential(쿠키, 인증 정보)을 허용한다면 &lt;b&gt;구체적인 Origin&lt;/b&gt;을 지정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. JWT를 사용하니까 무조건 CSRF를 꺼도 된다고 생각하는 경우&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;판단 기준은 JWT 사용 여부가 아니라 &lt;b&gt;브라우저가 인증 정보를 자동으로 전송하는지 여부&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token을 Authorization Header로 전달하는 경우에는 일반적으로 CSRF 위험이 낮지만, Refresh Token을 Cookie에 저장하는 경우에는 CSRF 방어 전략을 함께 고려해야 한다.&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;4. OPTIONS 요청을 허용하지 않는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Preflight Request가 차단되면 실제 API 요청도 수행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 OPTIONS 요청을 허용하거나 CORS 설정이 정상적으로 적용되었는지 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;http.authorizeHttpRequests(auth -&amp;gt; auth
        .requestMatchers(HttpMethod.OPTIONS, &quot;/**&quot;).permitAll()
        .anyRequest().authenticated()
);&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;p data-ke-size=&quot;size16&quot;&gt;전체적인 흐름을 정리해보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Browser
  &amp;darr;
Origin 확인
  &amp;darr;
CORS 검사
  &amp;darr;
JWT Filter
  &amp;darr;
Authentication
  &amp;darr;
Authorization
  &amp;darr;
Controller&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;CSRF가 필요한 경우&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;POST
  &amp;darr;
CSRF Token
  &amp;darr;
Token 검증
  &amp;darr;
Controller&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;CORS와 CSRF는 이름은 비슷하지만 서로 다른 문제를 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS는 브라우저의 동일 출처 정책으로 인해 발생하는 제약을 해결하기 위한 설정이며, CSRF는 브라우저가 인증 정보를 자동으로 전송하는 특성을 악용한 공격을 방어하기 위한 메커니즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 기반 인증에서 Access Token을 Authorization Header로 전달하는 경우에는 일반적으로 CSRF 위험이 낮기 때문에 csrf().disable()을 사용하는 경우가 많다. 하지만 Refresh Token을 HttpOnly Cookie에 저장하는 구조라면 브라우저가 쿠키를 자동으로 전송하므로 CSRF 방어 전략을 함께 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 CORS는 클라이언트와 서버 간의 통신을 허용하기 위한 정책이므로, 프런트엔드와 백엔드가 서로 다른 Origin에서 동작하는 환경이라면 반드시 적절한 CORS 설정을 적용해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CORS는 브라우저의 요청을 허용하기 위한 정책이고, CSRF는 브라우저의 자동 인증 전송을 악용한 공격을 방어하는 메커니즘이며, JWT 환경에서는 토큰의 종류보다 인증 정보를 전달하는 방식이 CSRF 적용 여부를 결정한다.&lt;/p&gt;</description>
      <category>Bootcamp/Implementation</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/52</guid>
      <comments>https://rvbear7.tistory.com/52#entry52comment</comments>
      <pubDate>Tue, 14 Jul 2026 23:13:57 +0900</pubDate>
    </item>
    <item>
      <title>AuthenticationEntryPoint와 AccessDeniedHandler 예외 처리</title>
      <link>https://rvbear7.tistory.com/51</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 오늘 학습 키워드&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AuthenticationEntryPoint&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AccessDeniedHandler&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt; &lt;/b&gt;&amp;nbsp; 학습 내용 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 적용하면 인증 또는 권한과 관련된 문제가 발생했을 때 자동으로 HTTP 상태 코드를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증되지 않은 사용자가 보호된 API에 접근하면 401 Unauthorized가 반환되고, 인증은 되었지만 권한이 부족한 사용자가 접근하면 403 Forbidden이 반환된다.&lt;/p&gt;
&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;예외 처리 방식을 이해하기 위해서는 먼저 인증과 인가를 구분해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인증(Authentication)&lt;/b&gt;은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;사용자가 누구인지 확인하는 과정&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인가(Authorization)&lt;/b&gt;는 &lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;인증된 사용자가 특정 자원에 접근할 권한이 있는지 확인하는 과정&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 API가 있다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(&quot;/admin/users&quot;)
@PreAuthorize(&quot;hasRole('ADMIN')&quot;)
public List&amp;lt;UserResponse&amp;gt; getUsers() {
    ...
}
&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;table style=&quot;border-collapse: collapse; width: 100%; height: 55px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JWT 없음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;401 Unauthorized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JWT 있음 + ROLE_USER&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;403 Forbidden&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 예외는 어디에서 발생할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 인증이 완료되면 Spring Security는 AuthorizationFilter에서 권한을 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 현재 사용자의 권한이 ROLE_USER이고 다음 API를 요청한다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PreAuthorize(&quot;hasRole('ADMIN')&quot;)
@GetMapping(&quot;/admin&quot;)
public String admin() {
    return &quot;admin&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;AuthorizationFilter는 현재 Authentication의 권한을 확인한 뒤 접근 가능 여부를 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한이 없다면 AccessDeniedException을 발생시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 인증 정보 자체가 없다면 AuthenticationException 계열 예외가 발생한다.&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에는 인증 및 권한 관련 예외를 처리하는 전용 Filter가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 &lt;b&gt;ExceptionTranslationFilter&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Filter는 직접 인증을 수행하지 않는다.&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;AuthenticationException 처리&lt;/li&gt;
&lt;li&gt;AccessDeniedException 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Security 계층에서 발생한 예외를 적절한 HTTP 응답으로 변환하는 역할을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. AuthenticationEntryPoint&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증되지 않은 사용자가 보호된 리소스에 접근하면 Spring Security는 AuthenticationEntryPoint를 호출한다.&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;JWT 없음&lt;/li&gt;
&lt;li&gt;JWT 만료&lt;/li&gt;
&lt;li&gt;JWT 위변조&lt;/li&gt;
&lt;li&gt;Authentication 생성 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로는 401 응답을 반환한다.&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;AuthenticationEntryPoint 구현하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class JwtAuthenticationEntryPoint
        implements AuthenticationEntryPoint {

    @Override
    public void commence(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException exception
    ) throws IOException {

        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);

        response.getWriter().write(
            &quot;&quot;&quot;
                {
                  &quot;code&quot;: &quot;AUTH_UNAUTHORIZED&quot;,
                  &quot;message&quot;: &quot;인증이 필요합니다.&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;&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. AccessDeniedHandler&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;대표적인 상황은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ROLE_USER가 관리자 API 접근&lt;/li&gt;
&lt;li&gt;OWNER가 MASTER API 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Spring Security는 AccessDeniedHandler를 호출한다.&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;AccessDeniedHandler 구현하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class JwtAccessDeniedHandler
        implements AccessDeniedHandler {

    @Override
    public void handle(
            HttpServletRequest request,
            HttpServletResponse response,
            AccessDeniedException exception
    ) throws IOException {

        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);

        response.getWriter().write(
            &quot;&quot;&quot;
                {
                  &quot;code&quot;:&quot;ACCESS_DENIED&quot;,
                  &quot;message&quot;:&quot;접근 권한이 없습니다.&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;&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;구현한 EntryPoint와 Handler는 exceptionHandling()을 통해 등록한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationEntryPoint authenticationEntryPoint;
    private final JwtAccessDeniedHandler accessDeniedHandler;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception {

        http
                .exceptionHandling(exception -&amp;gt; exception
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler)
                );

        return http.build();
    }
}&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;401과 403은 어떻게 결정될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 현재 Authentication 존재 여부를 기준으로 판단한다.&lt;/p&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;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8605%;&quot;&gt;&lt;b&gt;Authentication&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.4651%;&quot;&gt;&lt;b&gt;권한&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8605%;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;width: 35.4651%;&quot;&gt;-&lt;/td&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;401&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8605%;&quot;&gt;있음&lt;/td&gt;
&lt;td style=&quot;width: 35.4651%;&quot;&gt;부족&lt;/td&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8605%;&quot;&gt;있음&lt;/td&gt;
&lt;td style=&quot;width: 35.4651%;&quot;&gt;충분&lt;/td&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;200&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;401은 &lt;b&gt;인증 실패&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;403은 &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;&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;&lt;b&gt;1. AuthenticationEntryPoint에서 권한 예외까지 처리하려는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthenticationEntryPoint는 인증 실패만 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 부족은 AccessDeniedHandler가 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 역할을 혼동하면 올바른 상태 코드를 반환하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. ControllerAdvice에서 처리하려는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드는 Security 예외를 처리하지 못한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity&amp;lt;Void&amp;gt; handle() {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증과 권한 예외는 Controller에 도달하기 전에 &lt;b&gt;Filter Chain에서 발생&lt;/b&gt;하므로 @RestControllerAdvice의 처리 대상이 아니다.&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;3. JWT 검증 실패 시 RuntimeException을 던지는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 검증 실패는 AuthenticationException 흐름으로 처리하는 것이 바람직하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 RuntimeException을 던지면 ExceptionTranslationFilter가 처리하지 못해 의도한 응답을 반환하지 못할 수 있다.&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;4. 모든 인증 실패를 403으로 반환하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;401과 403은 의미가 명확하게 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증되지 않음 &amp;rarr; 401&lt;/li&gt;
&lt;li&gt;인증되었지만 권한 없음 &amp;rarr; 403&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;size16&quot;&gt;전체 예외 처리 흐름을 정리해보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Request
  &amp;darr;
JwtAuthenticationFilter
  &amp;darr;
Authentication
  &amp;darr;
AuthorizationFilter
  &amp;darr;
예외 발생
  &amp;darr;
ExceptionTranslationFilter
  ├── AuthenticationEntryPoint &amp;rarr; 401
  └── AccessDeniedHandler &amp;rarr; 403&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;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 인증 실패와 권한 부족을 서로 다른 예외로 구분하여 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 정보가 없거나 유효하지 않은 경우에는 AuthenticationException이 발생하고 AuthenticationEntryPoint가 이를 처리하여 401 응답을 반환한다. 반면 인증은 완료되었지만 필요한 권한이 없는 경우에는 AccessDeniedException이 발생하며 AccessDeniedHandler가 이를 처리하여 403 응답을 반환한다.&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;이처럼 ExceptionTranslationFilter를 중심으로 인증과 인가 예외가 분리되어 처리되기 때문에, 클라이언트는 인증이 필요한 상황과 권한이 부족한 상황을 명확하게 구분할 수 있으며, 개발자는 일관된 예외 응답 형식을 제공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &amp;nbsp; 한줄 정리&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AuthenticationEntryPoint는 인증 실패(401)를, AccessDeniedHandler는 권한 부족(403)을 처리하며, ExceptionTranslactionFilter는 이 두 예외를 적절한 HTTP 응답으로 변환하는 핵심 컴포넌트이다.&lt;/p&gt;</description>
      <category>Bootcamp/Implementation</category>
      <category>Til</category>
      <category>내일배움캠프</category>
      <author>개발자 오리</author>
      <guid isPermaLink="true">https://rvbear7.tistory.com/51</guid>
      <comments>https://rvbear7.tistory.com/51#entry51comment</comments>
      <pubDate>Mon, 13 Jul 2026 23:50:03 +0900</pubDate>
    </item>
  </channel>
</rss>