IT/코딩

최신 기술 동향과 실무 중심의 튜토리얼을 만나보세요.

서비스 확장을 위한 세션 클러스터링(Session Clustering) 분석과 선택 가이드 (톰캣 vs Redis)

서비스 확장을 위한 세션 클러스터링(Session Clustering) 분석과 선택 가이드 (톰캣 vs Redis)

<h2>서비스&nbsp;확장을&nbsp;위한&nbsp;세션&nbsp;클러스터링(Session&nbsp;Clustering)&nbsp;분석과&nbsp;선택&nbsp;가이드</h2><p></p><p>웹&nbsp;서비스&nbsp;규모가&nbsp;커지면&nbsp;단일&nbsp;서버만으로는&nbsp;트래픽을&nbsp;감당하기&nbsp;어려워집니다.</p><p>여러&nbsp;대의&nbsp;서버로&nbsp;부하를&nbsp;분산하는&nbsp;스케일&nbsp;아웃(Scale-out)&nbsp;과정에서&nbsp;반드시&nbsp;해결해야&nbsp;할&nbsp;과제가&nbsp;바로&nbsp;<strong>세션&nbsp;관리</strong>입니다.</p><p>&nbsp;</p><p>사용자가&nbsp;A&nbsp;서버에서&nbsp;로그인한&nbsp;뒤&nbsp;다음&nbsp;요청이&nbsp;B&nbsp;서버로&nbsp;전달될&nbsp;때,&nbsp;로그인&nbsp;정보가&nbsp;공유되지&nbsp;않으면&nbsp;서비스&nbsp;연속성이&nbsp;끊기게&nbsp;됩니다.</p><p>이러한&nbsp;세션&nbsp;불일치&nbsp;문제를&nbsp;해결하여&nbsp;어떤&nbsp;서버로&nbsp;접속하더라도&nbsp;동일한&nbsp;상태를&nbsp;유지하게&nbsp;하는&nbsp;기술이&nbsp;<strong>세션&nbsp;클러스터링</strong>입니다.</p><p>&nbsp;</p><p>실무에서&nbsp;주로&nbsp;검토되는&nbsp;두&nbsp;가지&nbsp;방식인&nbsp;<strong>Tomcat&nbsp;자체&nbsp;복제</strong>와&nbsp;<strong>Redis&nbsp;공유&nbsp;저장소</strong>&nbsp;방식을&nbsp;비교하여&nbsp;최적의&nbsp;설계&nbsp;방향을&nbsp;제시합니다.</p><p>&nbsp;</p><p></p><h3>1.&nbsp;Tomcat&nbsp;세션&nbsp;복제&nbsp;방식&nbsp;(Session&nbsp;Replication)</h3><p></p><p>Tomcat의&nbsp;클러스터링&nbsp;기능을&nbsp;활용하여&nbsp;각&nbsp;WAS(Web&nbsp;Application&nbsp;Server)가&nbsp;세션&nbsp;데이터를&nbsp;서로&nbsp;복제하여&nbsp;공유하는&nbsp;방식입니다.</p><ul><li><strong>구조:</strong>&nbsp;모든&nbsp;서버가&nbsp;동일한&nbsp;세션&nbsp;정보를&nbsp;나누어&nbsp;가집니다.&nbsp;별도의&nbsp;외부&nbsp;데이터베이스가&nbsp;필요&nbsp;없습니다.</li><li><strong>장점:</strong>&nbsp;초기&nbsp;인프라&nbsp;구축&nbsp;비용이&nbsp;낮고&nbsp;설정이&nbsp;비교적&nbsp;간단하여&nbsp;소규모&nbsp;시스템에&nbsp;적합합니다.</li><li><strong>단점:</strong>&nbsp;서버&nbsp;수가&nbsp;늘어날수록&nbsp;복제에&nbsp;필요한&nbsp;네트워크&nbsp;트래픽과&nbsp;메모리&nbsp;사용량이&nbsp;기하급수적으로&nbsp;증가합니다.</li><li>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;통상적으로&nbsp;4~5대&nbsp;이상의&nbsp;서버&nbsp;구성에서는&nbsp;성능&nbsp;저하가&nbsp;뚜렷하게&nbsp;나타납니다.</li><li><strong>현재&nbsp;사용&nbsp;여부:</strong>&nbsp;확장성&nbsp;한계로&nbsp;인해&nbsp;대규모&nbsp;트래픽을&nbsp;처리하는&nbsp;현대적인&nbsp;마이크로서비스&nbsp;아키텍처(MSA)나&nbsp;클라우드&nbsp;환경에서는&nbsp;거의&nbsp;사용되지&nbsp;않습니다.</li><li>&nbsp;</li></ul><h3>2.&nbsp;Redis&nbsp;기반&nbsp;세션&nbsp;저장소&nbsp;방식&nbsp;(Session&nbsp;Storage)</h3><p></p><p>세션&nbsp;데이터를&nbsp;외부의&nbsp;고속&nbsp;메모리&nbsp;데이터베이스인&nbsp;Redis에&nbsp;저장하고,&nbsp;모든&nbsp;서버가&nbsp;이를&nbsp;공유하는&nbsp;방식입니다.</p><ul><li><strong>구조:</strong>&nbsp;애플리케이션&nbsp;서버는&nbsp;세션&nbsp;데이터를&nbsp;직접&nbsp;보유하지&nbsp;않고(Stateless),&nbsp;필요할&nbsp;때마다&nbsp;Redis에서&nbsp;읽어옵니다.</li><li><strong>장점:</strong>&nbsp;서버&nbsp;대수와&nbsp;상관없이&nbsp;일관된&nbsp;성능을&nbsp;유지하며&nbsp;확장성이&nbsp;매우&nbsp;뛰어납니다.</li></ul><p>서버&nbsp;장애&nbsp;시에도&nbsp;세션&nbsp;데이터가&nbsp;유실되지&nbsp;않아&nbsp;사용자&nbsp;경험이&nbsp;안정적입니다.</p><ul><li><strong>단점:</strong>&nbsp;Redis&nbsp;인프라를&nbsp;별도로&nbsp;운영해야&nbsp;하며,&nbsp;Redis&nbsp;자체의&nbsp;가용성(High&nbsp;Availability)을&nbsp;확보하기&nbsp;위한</li></ul><p>추가&nbsp;설정(Sentinel,&nbsp;Cluster&nbsp;등)이&nbsp;필요합니다.</p><ul><li><strong>현재&nbsp;사용&nbsp;여부:</strong>&nbsp;트래픽&nbsp;변동이&nbsp;잦고&nbsp;확장이&nbsp;빈번한&nbsp;대다수의&nbsp;현대적&nbsp;웹&nbsp;서비스에서&nbsp;표준으로&nbsp;채택하고&nbsp;있습니다.</li></ul><p></p><h3>3.&nbsp;방식별&nbsp;비교&nbsp;분석</h3><h3><strong>비교&nbsp;항목</strong>&nbsp;<strong>Tomcat&nbsp;세션&nbsp;복제</strong>&nbsp;<strong>Redis&nbsp;세션&nbsp;저장소</strong>&nbsp;<strong>확장성</strong>&nbsp;낮음&nbsp;(서버&nbsp;증가&nbsp;시&nbsp;오버헤드&nbsp;급증)&nbsp;매우&nbsp;높음&nbsp;(Scale-out에&nbsp;최적화)&nbsp;<strong>데이터&nbsp;일관성</strong>&nbsp;복제&nbsp;시차에&nbsp;따른&nbsp;불일치&nbsp;가능성&nbsp;중앙&nbsp;관리로&nbsp;즉각적인&nbsp;일관성&nbsp;보장&nbsp;<strong>자원&nbsp;효율성</strong>&nbsp;모든&nbsp;서버에&nbsp;중복&nbsp;저장&nbsp;필요할&nbsp;때만&nbsp;조회하여&nbsp;메모리&nbsp;효율적&nbsp;<strong>운영&nbsp;환경</strong>&nbsp;고정된&nbsp;수의&nbsp;소규모&nbsp;서버군&nbsp;클라우드&nbsp;및&nbsp;오토&nbsp;스케일링&nbsp;환경&nbsp;&nbsp;4.&nbsp;세션&nbsp;클러스터링&nbsp;설계의&nbsp;핵심&nbsp;선택&nbsp;기준</h3><p></p><p>성능과&nbsp;확장성&nbsp;면에서&nbsp;<strong>Redis를&nbsp;활용한&nbsp;방식이&nbsp;실무적으로&nbsp;훨씬&nbsp;유리</strong>합니다.</p><p>특히&nbsp;트래픽에&nbsp;따라&nbsp;서버가&nbsp;자동으로&nbsp;생성되고&nbsp;삭제되는&nbsp;클라우드&nbsp;환경(Auto&nbsp;Scaling)에서는</p><p>서버&nbsp;간&nbsp;직접&nbsp;복제&nbsp;방식이&nbsp;구조적으로&nbsp;불가능에&nbsp;가깝습니다.</p><p></p><p>시스템을&nbsp;&#39;상태가&nbsp;없는(Stateless)&#39;&nbsp;구조로&nbsp;설계하여&nbsp;관리&nbsp;포인트를&nbsp;외부&nbsp;저장소로&nbsp;일원화하는&nbsp;것이</p><p>장기적인&nbsp;안정성&nbsp;측면에서&nbsp;올바른&nbsp;방향입니다.</p><p>&nbsp;</p><h3>5.&nbsp;왜&nbsp;Redis인가?</h3><p></p><p>&nbsp;</p><p>최근의&nbsp;서비스&nbsp;환경은&nbsp;트래픽&nbsp;변화에&nbsp;따라&nbsp;서버&nbsp;대수를&nbsp;유동적으로&nbsp;조절하는</p><p><strong>Auto&nbsp;Scaling</strong>이&nbsp;기본입니다.</p><p>서버가&nbsp;수시로&nbsp;생성되고&nbsp;소멸되는&nbsp;환경에서,&nbsp;서버끼리&nbsp;데이터를&nbsp;주고받는&nbsp;방식은&nbsp;구조적으로</p><p>결함이&nbsp;생길&nbsp;수밖에&nbsp;없습니다.&nbsp;상태가&nbsp;없는(Stateless)&nbsp;애플리케이션&nbsp;서버를&nbsp;지향하는</p><p>현대&nbsp;설계&nbsp;철학에도&nbsp;Redis&nbsp;방식이&nbsp;완벽히&nbsp;부합합니다.</p><h3>&nbsp;6.&nbsp;도입&nbsp;시&nbsp;고려해야&nbsp;할&nbsp;위험&nbsp;요소</h3><p></p><p>Redis&nbsp;방식을&nbsp;도입할&nbsp;때&nbsp;주의할&nbsp;점은&nbsp;<strong>단일&nbsp;장애점(SPOF)</strong>&nbsp;문제입니다.</p><p>세션&nbsp;저장소인&nbsp;Redis에&nbsp;장애가&nbsp;발생하면&nbsp;전체&nbsp;서비스의&nbsp;로그인이&nbsp;마비됩니다.</p><p>따라서&nbsp;실제&nbsp;운영&nbsp;환경에서는&nbsp;반드시&nbsp;Redis를&nbsp;이중화하거나&nbsp;클러스터로&nbsp;구성하여&nbsp;물리적&nbsp;장애에&nbsp;대비해야&nbsp;합니다.</p><p>&nbsp;</p><p>반면,&nbsp;Tomcat&nbsp;복제&nbsp;방식은&nbsp;구현은&nbsp;쉬워&nbsp;보이지만&nbsp;서버가&nbsp;늘어날수록</p><p>관리가&nbsp;불가능해지는&nbsp;&#39;성능의&nbsp;덫&#39;에&nbsp;빠지기&nbsp;쉽습니다.&nbsp;따라서&nbsp;초기&nbsp;단계부터&nbsp;미래의&nbsp;확장성을&nbsp;고려한&nbsp;설계가&nbsp;필요합니다.</p><p></p><h3>결론</h3><p></p><p>안정적인&nbsp;웹&nbsp;서비스&nbsp;운영을&nbsp;위해서는&nbsp;서버&nbsp;간&nbsp;의존성을&nbsp;낮추고&nbsp;데이터&nbsp;관리&nbsp;주체를&nbsp;명확히&nbsp;분리하는&nbsp;것이&nbsp;중요합니다.</p><p>인프라&nbsp;관리&nbsp;부담이&nbsp;조금&nbsp;있더라도&nbsp;Redis&nbsp;기반의&nbsp;세션&nbsp;클러스터링을&nbsp;구축하는&nbsp;것이&nbsp;서비스&nbsp;신뢰도와&nbsp;향후&nbsp;확장성을&nbsp;보장하는&nbsp;가장&nbsp;확실한&nbsp;길입니다.</p><p>[이&nbsp;게시물은&nbsp;이재민님에&nbsp;의해&nbsp;2026-03-04&nbsp;13:53:38&nbsp;개발에서&nbsp;이동&nbsp;됨]</p>

자세히 보기
외부 라이브러리 없이 구현하는 효율적인 파일 업로드: request.getParts() 활용법

외부 라이브러리 없이 구현하는 효율적인 파일 업로드: request.getParts() 활용법

<p>웹&nbsp;애플리케이션을&nbsp;개발하다&nbsp;보면&nbsp;프로필&nbsp;사진&nbsp;업로드나&nbsp;문서&nbsp;첨부&nbsp;같은&nbsp;파일&nbsp;업로드&nbsp;기능은</p><p>필수적으로&nbsp;들어가게&nbsp;마련입니다.</p><p></p><p>예전에는&nbsp;Apache&nbsp;Commons&nbsp;FileUpload&nbsp;같은&nbsp;외부&nbsp;라이브러리를&nbsp;사용하는&nbsp;것이&nbsp;당연시되었지만,</p><p>서블릿&nbsp;3.0&nbsp;표준이&nbsp;등장하면서&nbsp;상황이&nbsp;완전히&nbsp;바뀌었습니다.</p><p>&nbsp;</p><p>이제는&nbsp;복잡한&nbsp;설정&nbsp;없이도&nbsp;자바&nbsp;표준&nbsp;API인&nbsp;<code>HttpServletRequest.getParts()</code>를&nbsp;통해</p><p>아주&nbsp;우아하게&nbsp;파일을&nbsp;주고받을&nbsp;수&nbsp;있게&nbsp;되었죠.</p><p>오늘은&nbsp;실무에서&nbsp;바로&nbsp;적용할&nbsp;수&nbsp;있는&nbsp;<code>getParts()</code>&nbsp;사용법과&nbsp;주의사항을&nbsp;깊이&nbsp;있게&nbsp;다뤄보겠습니다.</p><p>&nbsp;</p><h3></h3><h3>1.&nbsp;왜&nbsp;request.getParts()를&nbsp;사용해야&nbsp;할까?</h3><p>가장&nbsp;큰&nbsp;이유는&nbsp;<strong>표준성</strong>과&nbsp;<strong>간결함</strong>입니다.</p><p>라이브러리&nbsp;의존성을&nbsp;줄이는&nbsp;것은&nbsp;프로젝트의&nbsp;가벼움을&nbsp;유지하고&nbsp;보안&nbsp;취약점&nbsp;관리&nbsp;측면에서도&nbsp;매우&nbsp;유리합니다.</p><ul><li><strong>의존성&nbsp;제거&nbsp;:</strong>&nbsp;<code>pom.xml</code>이나&nbsp;<code>build.gradle</code>에&nbsp;별도의&nbsp;라이브러리를&nbsp;추가할&nbsp;필요가&nbsp;없습니다.</li><li><strong>직관적인&nbsp;API&nbsp;:</strong>&nbsp;<code>Part</code>&nbsp;인터페이스를&nbsp;통해&nbsp;파일명,&nbsp;크기,&nbsp;Content-Type&nbsp;등을&nbsp;쉽게&nbsp;가져올&nbsp;수&nbsp;있습니다.</li><li><strong>성능&nbsp;최적화&nbsp;:</strong>&nbsp;서블릿&nbsp;컨테이너(Tomcat&nbsp;등)&nbsp;레벨에서&nbsp;직접&nbsp;처리하므로&nbsp;데이터&nbsp;처리&nbsp;효율이&nbsp;높습니다.</li><li><strong>기본&nbsp;API&nbsp;:&nbsp;</strong>톰캣에&nbsp;가장&nbsp;최적화된&nbsp;로직으로&nbsp;동작합니다.</li></ul><h3>&nbsp;</h3><h3>2.&nbsp;기본&nbsp;설정:&nbsp;@MultipartConfig</h3><p><code>request.getParts()</code>를&nbsp;사용하기&nbsp;위해서는&nbsp;서블릿에&nbsp;멀티파트&nbsp;데이터를&nbsp;처리할&nbsp;준비가&nbsp;되었다고&nbsp;알려줘야&nbsp;합니다.</p><p>이때&nbsp;사용하는&nbsp;것이&nbsp;<code>@MultipartConfig</code>&nbsp;어노테이션입니다.</p><p>&nbsp;</p><pre data-language="java"> @WebServlet(&quot;/upload&quot;) @MultipartConfig( fileSizeThreshold = 1024 * 1024 * 1, // 1MB (메모리에서 처리할 임계치) maxFileSize = 1024 * 1024 * 10, // 10MB (파일 하나당 최대 크기) maxRequestSize = 1024 * 1024 * 15 // 15MB (전체 요청 최대 크기) ) public class FileUploadServlet extends HttpServlet { // ... 구현부 } </pre><p><code>&nbsp;</code>이&nbsp;설정이&nbsp;없으면&nbsp;<code>getParts()</code>를&nbsp;호출했을&nbsp;때&nbsp;멀티파트&nbsp;요청이&nbsp;아니라는&nbsp;예외가&nbsp;발생할&nbsp;수&nbsp;있으니&nbsp;반드시&nbsp;체크해야&nbsp;합니다.</p><p>&nbsp;</p><p></p><h3></h3><h3>3.&nbsp;실무형&nbsp;구현&nbsp;코드:&nbsp;request.getParts()</h3><p>이제&nbsp;실제&nbsp;컨트롤러(서블릿)&nbsp;단계에서&nbsp;파일을&nbsp;어떻게&nbsp;처리하는지&nbsp;코드로&nbsp;살펴보겠습니다.</p><p>&nbsp;</p><pre data-language="java"> protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 저장 경로 설정 (실제 서버 경로 혹은 외부 저장소) String uploadPath = &quot;C:/uploads&quot;; File uploadDir = new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdir(); // 1. 모든 Part 가져오기 (파일뿐만 아니라 일반 텍스트 데이터도 Part로 전달됨) Collection&lt;Part&gt; parts = request.getParts(); for (Part part : parts) { // 2. 파일인 경우만 필터링 (파일명이 존재하는 경우) String fileName = getFileName(part); if (fileName != null &amp;&amp; !fileName.isEmpty()) { // 3. 파일 저장 part.write(uploadPath + File.separator + fileName); System.out.println(fileName + &quot; 저장 완료!&quot;); } } } // 헤더에서 파일명을 추출하는 헬퍼 메서드 private String getFileName(Part part) { String contentDisp = part.getHeader(&quot;content-disposition&quot;); for (String content : contentDisp.split(&quot;;&quot;)) { if (content.trim().startsWith(&quot;filename&quot;)) { return content.substring(content.indexOf(&quot;=&quot;) + 2, content.length() - 1); } } return null; } </pre><blockquote><code>&nbsp;</code><strong>실제&nbsp;사례&nbsp;Tip</strong>:&nbsp;실제&nbsp;상용&nbsp;서비스에서는&nbsp;위&nbsp;코드처럼&nbsp;파일명을&nbsp;그대로&nbsp;저장하면&nbsp;보안&nbsp;위험(Path&nbsp;Traversal)이&nbsp;있거나</blockquote><blockquote>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;파일명&nbsp;중복&nbsp;문제가&nbsp;발생할&nbsp;수&nbsp;있습니다.&nbsp;저&nbsp;같은&nbsp;경우&nbsp;실무에서&nbsp;<code>UUID.randomUUID()</code>를&nbsp;활용해&nbsp;파일명을&nbsp;치환하고,</blockquote><blockquote>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DB에는&nbsp;원본&nbsp;파일명과&nbsp;저장&nbsp;파일명을&nbsp;따로&nbsp;관리하는&nbsp;방식을&nbsp;선호합니다.</blockquote><blockquote>&nbsp;</blockquote><h3></h3><h3>4.&nbsp;핵심&nbsp;체크포인트</h3><p>파일&nbsp;업로드는&nbsp;보안과&nbsp;직결되는&nbsp;문제입니다.&nbsp;구글이&nbsp;강조하는&nbsp;신뢰성(Trustworthiness)을&nbsp;높이기&nbsp;위해&nbsp;다음&nbsp;사항을&nbsp;꼭&nbsp;고려하세요.</p><ol><li><strong>확장자&nbsp;검증</strong>:&nbsp;클라이언트&nbsp;측&nbsp;검증은&nbsp;필수지만,&nbsp;서버&nbsp;측에서도&nbsp;허용된&nbsp;확장자(jpg,&nbsp;png,&nbsp;pdf&nbsp;등)인지&nbsp;반드시&nbsp;재검증해야&nbsp;합니다.</li><li><strong>파일&nbsp;크기&nbsp;제한</strong>:&nbsp;무분별하게&nbsp;큰&nbsp;파일이&nbsp;업로드되면&nbsp;서버&nbsp;리소스가&nbsp;고갈됩니다.&nbsp;<code>@MultipartConfig</code>의&nbsp;설정을&nbsp;통해&nbsp;원천&nbsp;봉쇄하세요.</li><li><strong>저장&nbsp;위치&nbsp;분리</strong>:&nbsp;업로드된&nbsp;파일이&nbsp;실행&nbsp;권한을&nbsp;가지지&nbsp;않도록&nbsp;WAS의&nbsp;웹&nbsp;루트(Web&nbsp;Root)&nbsp;외부&nbsp;경로에&nbsp;저장하는&nbsp;것이&nbsp;보안의&nbsp;정석입니다.</li><li>&nbsp;</li></ol><h3></h3><h3>결론</h3><p><code>request.getParts()</code>는&nbsp;현대적인&nbsp;자바&nbsp;웹&nbsp;개발에서&nbsp;가장&nbsp;깔끔한&nbsp;파일&nbsp;업로드&nbsp;해결책입니다.</p><p>라이브러리에&nbsp;의존하지&nbsp;않고&nbsp;표준&nbsp;API를&nbsp;깊이&nbsp;있게&nbsp;이해하고&nbsp;사용하는&nbsp;것만으로도</p><p>여러분의&nbsp;코드는&nbsp;한층&nbsp;더&nbsp;견고해질&nbsp;것입니다.</p><p>오늘&nbsp;다룬&nbsp;내용을&nbsp;바탕으로&nbsp;더&nbsp;안전하고&nbsp;효율적인&nbsp;파일&nbsp;업로드&nbsp;로직을&nbsp;구축해&nbsp;보시기&nbsp;바랍니다.</p><p>&nbsp;</p><p></p><p></p><p><strong>출처&nbsp;및&nbsp;참고</strong>:</p><ul><li>Oracle&nbsp;Java&nbsp;EE&nbsp;7&nbsp;Documentation&nbsp;-&nbsp;<a href="https://docs.oracle.com/javaee/7/api/javax/servlet/http/Part.html" rel="noopener noreferrer" target="_blank">Interface&nbsp;Part</a></li></ul><p>[이&nbsp;게시물은&nbsp;이재민님에&nbsp;의해&nbsp;2026-03-04&nbsp;13:53:30&nbsp;개발에서&nbsp;이동&nbsp;됨]</p>

자세히 보기
Redis Session 개념 및 로직 설명

Redis Session 개념 및 로직 설명

<h2>Redis Session의 핵심 컴포넌트 이해: Filter, Request, Manager, Session</h2><p>서버를 여러 대 두는 분산 환경에서 '세션 관리'는 서비스의 안정성을 결정짓는 핵심 요소입니다.</p><p> 단순히 Redis를 연결하는 것을 넘어, 내부적으로 세션이 어떻게 생성되고 관리되는지</p><p> 그 메커니즘을 이해하는 것은 장애 대응과 성능 최적화에 큰 도움이 됩니다.</p><p>오늘은 Redis Session 구조의 뼈대를 이루는 4가지 핵심 컴포넌트의 역할과 상호작용 로직을 정리해 보겠습니다.</p><p>&nbsp;</p><p><br></p><h3>1. RedisSessionFilter: 표준 세션을 Redis로 전환하는 스위치</h3><p><strong>역할: 모든 HTTP 요청의 흐름을 제어하는 진입점</strong></p><p>이 컴포넌트는 서블릿 필터(Servlet Filter)로서, 애플리케이션으로 들어오는 모든 요청을 가장 먼저 처리합니다.</p><p> 기존의 톰캣(Tomcat) 같은 WAS가 관리하던 로컬 세션 메커니즘을 Redis 기반으로 갈아끼우는 역할을 수행합니다.</p><p> JSESSIONID 대신 Redis에서 사용할 SESSIONID를 쿠키에 넣는 작업을 하고,</p><p> 해당 쿠키를 기반으로 세션을 공유합니다.</p><ul><li><strong>동작 로직:</strong></li><li class="ql-indent-1"><code>HttpServletRequest</code>를 가로채&nbsp;Redis 전용 래퍼 객체로 감쌉니다.</li><li>요청이 완료되는 시점에 세션의 최종 상태를 확인하고, 변경 사항이 있다면 이를 Redis 데이터베이스에 반영(Commit)하도록 지시합니다.</li><li class="ql-indent-1">&nbsp;</li></ul><h3>2. RedisSessionRequest: 세션 조회 로직의 재정의</h3><p><strong>역할: 애플리케이션과 Redis 저장소 사이의 중개자</strong></p><p>개발자가 코드상에서 <code>request.getSession()</code>을 호출할 때, 실제로 동작하는 것은 바로 이 래핑된 요청 객체입니다.</p><ul><li><strong>동작 로직:</strong></li><li class="ql-indent-1">사용자의 요청에 포함된 세션 쿠키(Session ID)를 분석합니다.</li><li>로컬 메모리가 아닌 외부 저장소(Redis)에서 세션 데이터를 찾아오도록 로직이 설계되어 있어,</li><li> 서버가 바뀌어도 동일한 세션을 유지할 수 있게 합니다.</li><li class="ql-indent-1">&nbsp;</li></ul><h3>3. RedisSessionManager: 세션 데이터의 생명주기 관리</h3><p><strong>역할: 실질적인 Redis CRUD 및 영속성 담당</strong></p><p><code>Manager</code>는 이름 그대로 세션의 생성, 저장, 조회, 삭제를 총괄하는 컨트롤러입니다. Redis와의 직접적인 통신은 대개 이 계층을 통해 이루어집니다.</p><ul><li><strong>동작 로직:</strong></li><li class="ql-indent-1"><strong>직렬화 및 저장:</strong> 세션 객체를 Redis에 저장 가능한 형태(바이트 배열 등)로 변환하고, 적절한 만료 시간(TTL)을 부여합니다.</li><li class="ql-indent-1"><strong>조회 및 복원:</strong> Redis에 저장된 데이터를 가져와 다시 Java 객체 형태로 복구합니다.</li><li>네트워크 지연이나 Redis 연결 오류 등에 대한 예외 처리를 담당하는 핵심 지점이기도 합니다.</li><li class="ql-indent-1">&nbsp;</li></ul><h3>4. RedisSession: 분산 환경에 최적화된 데이터 모델</h3><p><strong>역할: 사용자 정보를 담는 저장소이자 변경 감지 모델</strong></p><p>표준 <code>HttpSession</code> 인터페이스를 구현한 클래스로, 실제 사용자의 로그인 정보나 상태값이 저장되는 곳입니다.</p><ul><li><strong>동작 로직:</strong></li><li class="ql-indent-1"><strong>Dirty Check:</strong> 세션 내부의 속성(Attribute)이 변경되었는지 실시간으로 추적합니다.</li><li>데이터가 실제로 변경된 경우에만 Redis에 다시 쓰는 방식을 채택하여, 불필요한 네트워크 트래픽과 Redis의 I/O 부하를 최소화합니다.</li><li class="ql-indent-1">&nbsp;</li></ul><h3>실무 운영을 위한 한 마디</h3><p>Redis 세션을 도입할 때 가장 흔히 겪는 문제는 <strong>'객체 직렬화'</strong> 관련 오류입니다.</p><p> 세션에 담기는 모든 객체는 반드시 직렬화가 가능해야 하며, 클래스 구조가 변경될 경우 세션 복원 과정에서 에러가 발생할 수 있습니다.</p><p> 따라서 세션에는 가급적 가볍고 직렬화에 안전한 데이터 위주로 담는 것이 운영 측면에서 유리합니다.</p><p>또한, 세션 만료 시간 설정 시 Redis의 TTL과 애플리케이션의 세션 타임아웃 설정이 일치하는지 반드시 확인해야 세션이 예상보다 일찍 끊기는 현상을 방지할 수 있습니다.</p><p>[이 게시물은 이재민님에 의해 2026-03-04 13:56:31 설정 가이드에서 이동 됨]</p>

자세히 보기
진수 변환 함수 (62진수 까지)

진수 변환 함수 (62진수 까지)

<pre data-language="java"> ​ public final class NumberUtil { private static final char[] DIGITS = { &#39;0&#39;,&#39;1&#39;,&#39;2&#39;,&#39;3&#39;,&#39;4&#39;,&#39;5&#39;,&#39;6&#39;,&#39;7&#39;,&#39;8&#39;,&#39;9&#39;, &#39;a&#39;,&#39;b&#39;,&#39;c&#39;,&#39;d&#39;,&#39;e&#39;,&#39;f&#39;,&#39;g&#39;,&#39;h&#39;,&#39;i&#39;,&#39;j&#39;, &#39;k&#39;,&#39;l&#39;,&#39;m&#39;,&#39;n&#39;,&#39;o&#39;,&#39;p&#39;,&#39;q&#39;,&#39;r&#39;,&#39;s&#39;,&#39;t&#39;, &#39;u&#39;,&#39;v&#39;,&#39;w&#39;,&#39;x&#39;,&#39;y&#39;,&#39;z&#39;, &#39;A&#39;,&#39;B&#39;,&#39;C&#39;,&#39;D&#39;,&#39;E&#39;,&#39;F&#39;,&#39;G&#39;,&#39;H&#39;,&#39;I&#39;,&#39;J&#39;, &#39;K&#39;,&#39;L&#39;,&#39;M&#39;,&#39;N&#39;,&#39;O&#39;,&#39;P&#39;,&#39;Q&#39;,&#39;R&#39;,&#39;S&#39;,&#39;T&#39;, &#39;U&#39;,&#39;V&#39;,&#39;W&#39;,&#39;X&#39;,&#39;Y&#39;,&#39;Z&#39; }; /** * 2 ~ 62 진법 변환 (최적화 버전) */ public static String numberToBase(long num, int radix) { if (radix &lt; 2 || radix &gt; 62) { throw new IllegalArgumentException(&quot;radix는 2 이상 62 이하만 가능합니다.&quot;); } if (num == 0L) { return &quot;0&quot;; } boolean negative = (num &lt; 0L); long value = negative ? -num : num; // long 최대 자릿수: 64bit → 2진수 기준 64자리 char[] buffer = new char[65]; int index = buffer.length; while (value != 0L) { buffer[--index] = DIGITS[(int)(value % radix)]; value /= radix; } if (negative) { buffer[--index] = &#39;-&#39;; } return new String(buffer, index, buffer.length - index); } } [이 게시물은 이재민님에 의해 2026-03-04 13:54:35 개발에서 이동 됨] </pre><p>​<br></p>

자세히 보기
Java Thread-Safe 간략 메모

Java Thread-Safe 간략 메모

<pre> <code class="language-java"> public class ThreadSafeTest { public int number = 0; public void method() { // 멤버변수는 Thread Safe 하지 않음 - 동기화 필요 { this.number = this.number+1; this.number++; // } // Thread Safe 하므로 동기화 필요 없음 { int number = 0; number++; // } // Thread Safe 하지 않음 - 동기화 필요 { method_a(); // } // Thread Safe 함 { method_b(); // } } public int method_a() { this.number++; return this.number; } public int method_b() { int number = 0; number++; return number; } } </code> </pre> <div class="content_move">[이 게시물은 이재민님에 의해 2026-03-04 13:54:05 개발에서 이동 됨]</div>

자세히 보기
개인정보 (이름) 마스킹 로직

개인정보 (이름) 마스킹 로직

<pre> <code class="language-java"> public static String maskName(String name) { if (name == null || name.isEmpty()) { return name; } int length = name.codePointCount(0, name.length()); // 한 글자 if (length == 1) { return name; } // 두 글자 if (length == 2) { int firstEnd = name.offsetByCodePoints(0, 1); return name.substring(0, firstEnd) + "*"; } // 세 글자 이상 int firstEnd = name.offsetByCodePoints(0, 1); int lastStart = name.offsetByCodePoints(0, length - 1); StringBuilder sb = new StringBuilder(name.length()); sb.append(name, 0, firstEnd); for (int i = 1; i < length - 1; i++) { sb.append('*'); } sb.append(name, lastStart, name.length()); return sb.toString(); } </code> </pre> <div class="content_move">[이 게시물은 이재민님에 의해 2026-03-04 13:54:19 개발에서 이동 됨]</div>

자세히 보기
"이미 참여한 휴대폰 번호입니다." 라고??  - 초보 개발자가 반드시 알아야 할 Thread Safe와 동기화

"이미 참여한 휴대폰 번호입니다." 라고?? - 초보 개발자가 반드시 알아야 할 Thread Safe와 동기화

<h3>1. 한 지붕 아래 여러 식구, 'Thread' 이야기</h3><p>java 언어를 다루다 보면 **'Thread(스레드)'**라는 단어를 한 번쯤 들어보셨을 거예요.&amp;nbsp;</p><p> 쉽게 비유하자면, 웹 서버라는 큰 집에서 동시에 일을 처리하는 '일꾼'들이라고 생각하면 편해요.</p><p>우리가 만든 사이트에 접속자가 많아지면, 구글이나 네이버 같은 서버는 수많은 일꾼(Thread)을 내보내 각자의 요청을 처리하게 합니다.&nbsp;</p><p>덕분에 우리는 여러 사람이 동시에 접속해도 기다리지 않고 서비스를 이용할 수 있죠.&nbsp;</p><p>하지만 이 일꾼들이 **'같은 도구(공유 자원)'**를 동시에 쓰려고 할 때 문제가 시작됩니다.</p><p>&nbsp;</p><h3>2. "분명 처음 응모한 휴대폰인데..." 나를 괴롭혔던 그 에러</h3><p>제가 예전에 이벤트 사이트를 개발했을 때의 일입니다.</p><p>이벤트 참여 버튼을 누르면 휴대폰 번호를 체크해서 중복 참여를 막는 로직이었는데,</p><p>문제가 생겨 처음 참여하는 사람인데도 자꾸 **"이미 참여한 휴대폰 번호입니다"**라는 에러가 발생하는 겁니다.</p><p>'x됐다....'</p><p>수시간을 씨름하다 찾아낸 결론은,</p><p>범인은 바로 동기화 처리가 되지 않은 **<code><strong>Map&lt;String, Object&gt; param**</strong></code> 필드였습니다.</p><ul><li><strong>사건의 전말:</strong> 1번 일꾼이 A라는 사람의 번호를 Map에 담아 검사하는 도중, 2번 일꾼이 B라는 사람의 번호를 같은 Map에 덮어써 버린 거죠.</li><li><strong>결과:</strong> 1번 일꾼은 분명 A의 정보를 처리 중이었는데, 결과는 B의 중복 체크 결과를 반환하게 됩니다.</li></ul><p>동시 접속자가 몰릴수록 이런 데이터 꼬임 현상은 심해졌고, 제 메일함은 컴플레인으로 폭발 직전이었답니다.&nbsp;</p><p>이때 필요한 개념이 바로 **Thread Safe(스레드 안전)**입니다.</p><p>다음은 해당 jsp 소스와 java 소스입니다.</p><pre class="ql-syntax" spellcheck="false"> &lt;%! MultipartRequest req; Map param; Map qparam; %&gt; &lt;% try { req = new MultipartRequest(pageContext); } catch (FileUploadException ex) { ... } catch (Throwable th) { ... } param = req.getParameter(); qparam = req.getQueryParameter(); ... </pre><p><code> %&gt;</code>위에&nbsp;&lt;%! ... %&gt; 선언부가 보이죠? 우선 여기 들어가는 변수들은 필드(멤버변수)가 되게 됩니다.</p><p>나중에 컴파일된 jspService 구조를 이해하실 수 있을 겁니다.</p><p>이 필드들은 여러 쓰레드(쉽게 말해 동시접속자) 가 접근했을 때 쓰레드에 취약하게 됩니다.</p><pre class="ql-syntax" spellcheck="false"> public class MultipartRequest extends HttpServletRequestWrapper { private final Map parameter; ... //생성자 public MultipartRequest(PageContext pageContext) { 여기서 param 세팅 (동기화처리 안해서 문제 발생!) } </pre><p><code> }</code>마찬가지로 이번엔 더 위험한 소스입니다.</p><p>필드로 선언한 parameter 이놈 정말 위험합니다.</p><p>반드시 동기화 처리를 거쳐야 합니다.&nbsp;</p><p>그렇지 않으면, 수많은 스레드들이 접근해서 구워삶아 유효하지 않은 객체가 될겁니다.</p><p>&nbsp;</p><h3>3. Thread Safe란 무엇이고, 왜 해야 할까?</h3><p><strong>Thread Safe</strong>는 여러 일꾼이 동시에 같은 데이터에 접근하더라도,&nbsp;</p><p>데이터가 오염되지 않고 계산 결과가 늘 정확하게 유지되는 상태를 말해요.</p><p>만약 동기화를 구현하지 않는다면, 여러분의 소스코드는 언제 터질지 모르는 시한폭탄과 같습니다.&nbsp;</p><p>사용자의 돈이 걸린 결제 시스템이나, 방금 제 사례처럼 중요한 개인정보를 다루는 곳에서&nbsp;</p><p>동기화 오류가 난다면... 정말 아찔하죠?</p><p>이해를 돕기 위해 아주 쉬운 코드 예제 두 가지를 살펴볼게요.</p><p>&nbsp;</p><h4>❌ 예제 1: 위험한 소스코드 (동기화 없음)</h4><pre class="ql-syntax" spellcheck="false"> public class DangerCounter { private int count = 0; public void addCount() { // 여러 스레드가 동시에 접근하면 숫자가 꼬여요! count++; } } </pre><h4><code> </code> ✅ 예제 2: 안전한 소스코드 (synchronized 사용)</h4><p><code>synchronized</code> 키워드를 붙여주면, 한 번에 한 명의 일꾼만 이 메서드를 쓸 수 있게 줄을 세워줍니다.</p><pre class="ql-syntax" spellcheck="false"> public class SafeCounter { private int count = 0; // 한 번에 하나의 스레드만 접근 가능! public synchronized void addCount() { count++; } } </pre><p><code> </code>사실 제가 겪었던 Map 문제라면 ConcurrentHashMap 같은 전용 클래스를 쓰는 것이 효율적이고 안전합니다.</p><p>이 내용은 추후에 기회가 되면 올려보겠습니다.</p><p>&nbsp;</p><h3> 4. 안전한 코딩은 선택이 아닌 필수입니다</h3><p>동기화는 단순히 "알면 좋은 기술"이 아닙니다.&nbsp;</p><p>서비스를 운영하는 개발자로서 사용자의 데이터를 보호하기 위한 <strong>최소한의 안전장치</strong>예요.</p><p>안일하고 가볍게 생각해 평소 처럼 배포했던 소스코드가&nbsp;</p><p>수많은 사용자에게 "이미 참여한 번호입니다"라는 황당한 경험을 줄 수 있습니다.</p><p>내가 작성한 코드에 여러 일꾼이 동시에 달려들어도 안전한지(Thread Safe),&nbsp;</p><p>공유 자원을 건드리는 곳에 적절한 동기화 처리가 되어 있는지 항상 의심하고 확인하세요.</p><p>여러분의 코드가 시한폭탄이 될지, 튼튼한 성벽이 될지는 바로 이 '동기화' 한 줄에 달려 있습니다!</p><p>[이 게시물은 이재민님에 의해 2026-03-04 13:53:46 개발에서 이동 됨]</p>

자세히 보기
Instagram4j - Maven Dependency

Instagram4j - Maven Dependency

<pre> <code class="language-xml">&lt;dependency&gt; &lt;groupid&gt;com.github.instagram4j&lt;/groupid&gt; &lt;artifactid&gt;instagram4j&lt;/artifactid&gt; &lt;version&gt;2.0.7&lt;/version&gt; &lt;/dependency&gt;</code> </pre> <div class="content_move">[이 게시물은 이재민님에 의해 2026-03-04 13:54:27 개발에서 이동 됨]</div>

자세히 보기
자바 람다(Lambda) 기초

자바 람다(Lambda) 기초

<p>안녕하세요! 오늘은 자바 프로그래밍의 꽃이라고 불리는 **'람다(Lambda)'**에 대해 이야기해 보려 합니다.</p><p> 아마 자바를 공부하다 보면 <code>-&gt;</code> 처럼 생긴 화살표 기호를 본 적이 있으실 텐데요.</p><p> "대체 이게 뭐야?" 싶었던 분들을 위해, 아주 쉽고 친절하게 설명해 드릴게요.</p><p>&nbsp;</p><h3><br></h3><h3> 1. 람다(Lambda)가 대체 뭔가요?</h3><p>한마디로 정의하자면, 람다는 **'이름이 없는 함수(익명 함수)'**입니다.</p><p>우리가 보통 메서드를 만들 때는 이름도 붙이고, 반환 타입도 쓰고, 복잡한 절차를 거치죠?</p><p> 하지만 람다를 사용하면 이런 번거로운 절차를 싹 생략하고, **"기능 그 자체"**를 변수처럼 다룰 수 있게 됩니다.</p><p>&nbsp;</p><p><strong>왜 쓸까요?</strong></p><ul><li><strong>가독성</strong>: 코드가 획기적으로 짧아집니다.</li><li><strong>생산성</strong>: 복잡한 익명 클래스 구현을 한 줄로 끝낼 수 있습니다.</li><li><strong>병렬 처리</strong>: 뒤에서 배울 스트림(Stream) API와 결합하면 대용량 데이터 처리도 아주 쉬워집니다.</li><li>&nbsp;</li></ul><h3><br></h3><h3> 2. 아주 쉬운 예제로 비교해보기</h3><p>백문이 불여일견! 우리가 숫자 두 개를 더하는 기능을 만든다고 가정해 봅시다.</p><p><br></p><p>&nbsp;</p><h4>기존의 방식 (익명 클래스)</h4><p>람다가 없던 시절에는 인터페이스를 구현하기 위해 아래처럼 길게 코드를 짜야 했습니다.</p><p>&nbsp;</p><pre class="ql-syntax" spellcheck="false"> // 숫자를 더하는 인터페이스가 있다고 가정 interface Calculator { int sum(int a, int b); } // 실제 사용 시 Calculator cal = new Calculator() { @Override public int sum(int a, int b) { return a + b; } }; </pre><h4><code> </code></h4><h4><br></h4><h4> 람다를 사용한 방식</h4><p>똑같은 기능을 람다로 표현하면 어떻게 될까요?</p><p>&nbsp;</p><pre class="ql-syntax" spellcheck="false"> Calculator cal = (a, b) -&gt; a + b; </pre><p><code> </code>어떤가요? 서너 줄이 넘던 코드가 단 한 줄로 줄어들었습니다.</p><p><code>(a, b)</code>는 입력받을 재료(파라미터)이고, <code>-&gt;</code> 뒤에 있는 <code>a + b</code>는 실제 수행할 행동(로직)입니다.</p><p>&nbsp;</p><h3><br></h3><h3> 3. 실무에서는 어떻게 쓰일까요? (구체적 사례)</h3><p>가장 대표적인 사례가 바로 <strong>리스트 정렬</strong>입니다.</p><p> 문자열 리스트를 글자 길이 순서대로 정렬하고 싶을 때, 예전에는 복잡한 비교기(Comparator)를 만들어야 했지만 지금은 다릅니다.</p><p>&nbsp;</p><pre class="ql-syntax" spellcheck="false"> List&lt;String&gt; names = Arrays.asList("Apple", "Banana", "Kiwi"); // 람다를 활용한 정렬 names.sort((s1, s2) -&gt; s1.length() - s2.length()); System.out.println(names); // 결과: [Kiwi, Apple, Banana] </pre><blockquote><code> </code><strong>Tip</strong>: 람다는 아무 곳에서나 쓸 수 있는 건 아닙니다.</blockquote><blockquote>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**'함수형 인터페이스'**라고 해서, 추상 메서드가 딱 하나만 있는 인터페이스에서만 사용할 수 있어요.</blockquote><blockquote>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;자바에서는 <code>@FunctionalInterface</code> 어노테이션으로 이를 보장하곤 합니다.</blockquote><blockquote>&nbsp;</blockquote><h3><br></h3><h3> 4. 람다 작성 시 주의사항</h3><p>람다가 편하다고 해서 모든 곳에 남발하는 것은 좋지 않습니다. 신뢰성 있는 코드를 위해 다음 세 가지를 기억하세요.</p><ol><li><strong>가독성이 최우선</strong>: 람다식이 너무 길어지면(예: 10줄 이상), 오히려 코드를 읽기 어렵게 만듭니다. 그럴 때는 그냥 별도의 메서드로 분리하는 것이 낫습니다.</li><li><strong>디버깅의 어려움</strong>: 이름 없는 함수이기 때문에 에러가 발생했을 때 추적이 일반 메서드보다 조금 까다로울 수 있습니다.</li><li><strong>지역 변수 제약</strong>: 람다 내부에서 외부의 지역 변수를 사용할 때는 그 변수가 사실상 수정 불가능(final)한 상태여야 합니다.</li><li>&nbsp;</li></ol><h3>람다로 변신 가능한 메서드의 '자격 조건'</h3><p><br></p><p> 람다식을 사용하려면 해당 메서드가 속한 인터페이스가 반드시 **'함수형 인터페이스(Functional Interface)'**여야 합니다.</p><p><br></p><p>&nbsp;</p><h4>[ 함수형 인터페이스란? ]</h4><p>추상 메서드(내용이 비어 있는 메서드)가 <strong>딱 하나만 존재하는 인터페이스</strong>를 말합니다.</p><p> 만약 인터페이스에 구현해야 할 메서드가 두 개 이상이라면, 컴퓨터는 람다식이 어떤 메서드를 구현한 것인지 알 수 없기 때문에 에러가 발생합니다.</p><h4><br></h4><h4><br></h4><h4> [ 어떤 구조를 줄일 수 있나요? ]</h4><p>보통 다음과 같은 흐름을 가진 코드를 람다로 대체합니다.</p><ul><li><strong>입력(파라미터)은 있고 반환(Return)은 없는 구조</strong>: 데이터를 소비만 하는 경우 (Consumer)</li><li><strong>입력은 없고 반환만 있는 구조</strong>: 데이터를 생성해서 주기만 하는 경우 (Supplier)</li><li><strong>입력과 반환이 모두 있는 구조</strong>: 데이터를 받아서 가공한 뒤 돌려주는 경우 (Function)</li><li><strong>입력을 받아 참/거짓(boolean)을 반환하는 구조</strong>: 필터링 조건으로 사용하는 경우 (Predicate)</li></ul><h4><br></h4><h4> [ 람다 변환 공식 ]</h4><p>복잡한 메서드 구조에서 딱 <strong>핵심 재료</strong>만 남긴다고 생각하면 쉽습니다.</p><ol><li><strong>메서드 이름과 반환 타입</strong>을 지웁니다. (이름이 없어도 하나뿐인 메서드라 알아서 찾아갑니다.)</li><li>&nbsp;</li><li>자바 컴파일러는 이미 인터페이스를 통해 이 메서드가 무엇인지 알고 있습니다. 따라서 중복되는 정보는 다 지워줍니다.</li><li> <strong>접근 제어자</strong> (public, private 등) → 삭제</li><li> <strong>반환 타입</strong> (void, int 등) → 삭제</li><li> <strong>메서드 이름</strong> → 삭제</li><li>&nbsp;</li><li>파라미터의 <strong>타입</strong>을 지웁니다. 파라미터가 2개 이상일 때 괄호 <code>()</code>는 유지합니다. (컴파일러가 문맥을 보고 추론합니다.)</li><li>남은 파라미터와 실행 코드 사이에 **화살표(<code>-&gt;</code>)**를 꽂아줍니다.</li></ol><p>&nbsp;</p><h3>5. 람다, 실전에서는 이렇게 씁니다! (실무 사례 보충)</h3><p><br></p><p>&nbsp;</p><h3><span style="font-size: 16px;">① list 정렬 (sort)</span></h3><p>람다식을 사용해서 리스트를 정렬할 수 있습니다.</p><p>&nbsp;</p><p><strong>[비포: 전통적인 compare 메소드 구현]</strong></p><pre class="ql-syntax" spellcheck="false"> List&lt;Integer&gt; list = new ArrayList&lt;&gt;(List.of(5,10,3,6,1,4,9,8,7,2)); list.sort(new Comparator&lt;Integer&gt;() { public int compare(Integer i1, Integer i2) { return i1.compareTo(i2); } </pre><p><code> });</code></p><p> ​<strong>[애프터: 람다 활용1]</strong></p><pre class="ql-syntax" spellcheck="false"> List&lt;Integer&gt; list = new ArrayList&lt;&gt;(List.of(5,10,3,6,1,4,9,8,7,2)); </pre><p><code> list.sort((i1,i2)-&gt;i1.compareTo(i2));</code></p><p> ​<strong>[애프터: 람다 활용2]</strong></p><pre class="ql-syntax" spellcheck="false"> List&lt;Integer&gt; list = new ArrayList&lt;&gt;(List.of(5,10,3,6,1,4,9,8,7,2)); list.sort((i1,i2)-&gt;{ return i1.compareTo(i2); </pre><ul><li><code> });</code>sort 의 파라미터는 Comparator 인데, 이 클래스틑 compare 메소드가 단 하나 존재합니다.&nbsp;</li><li> 이 때 compare의 파라미터를 위와 같이 단순하게 줄여 사용할 수 있습니다.</li></ul><p>​</p><p><br></p><h4>&nbsp;</h4><h4><span style="font-size: 16px;">② 리스트의 모든 요소 출력하기 (forEach)</span></h4><p>기존에는 리스트 안에 있는 내용을 하나씩 꺼내 보려면 <code>for</code> 문을 돌려야 했죠.</p><p> 하지만 람다를 쓰면 리스트에게 직접 "야, 이거 하나씩 출력해!"라고 명령할 수 있습니다.</p><p><br></p><p><strong>[비포: 전통적인 for-each 문]</strong></p><pre class="ql-syntax" spellcheck="false"> List&lt;String&gt; fruits = Arrays.asList("사과", "바나나", "포도"); for (String fruit : fruits) { System.out.println(fruit); } </pre><p><code> </code></p><p><strong>[애프터: 람다 활용]</strong></p><pre class="ql-syntax" spellcheck="false"> List&lt;String&gt; fruits = Arrays.asList("사과", "바나나", "포도"); fruits.forEach(fruit -&gt; System.out.println(fruit)); </pre><ul><li><code> fruit</code>라는 이름으로 요소를 하나씩 받아서(<code>-&gt;</code>), 출력해라! 라는 뜻입니다. 훨씬 직관적이죠?</li><li>&nbsp;</li></ul><h4><br></h4><h4><span style="font-size: 16px;">③ 조건에 맞는 데이터만 골라내기 (Stream Filter)</span></h4><p>리스트에서 특정 조건(예: 글자 수가 3개 이상인 것)만 뽑아낼 때도 람다는 빛을 발합니다.</p><p><br></p><p><strong>[실전 코드 예시]</strong></p><pre class="ql-syntax" spellcheck="false"> List&lt;String&gt; names = Arrays.asList("김철수", "이영희", "박지성", "제임스"); // '김'씨 성을 가진 사람만 필터링해서 출력하기 names.stream() .filter(name -&gt; name.startsWith("김")) .forEach(name -&gt; System.out.println(name)); </pre><ul><li><code> filter</code> 안에 들어간 람다식이 "이 조건에 맞는 애들만 통과시켜!"라는 문지기 역할을 합니다.</li><li>&nbsp;</li></ul><h4><br></h4><h4><span style="font-size: 16px;">④ 새로운 스레드(Thread) 만들기 (Runnable)</span></h4><p>자바에서 병렬 처리를 위해 스레드를 만들 때, 예전에는 익명 클래스라는 복잡한 구조를 써야 했습니다. 람다를 쓰면 "할 일"만 딱 전달하면 됩니다.</p><p><br></p><p><strong>[비포: 복잡한 익명 클래스]</strong></p><pre class="ql-syntax" spellcheck="false"> Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("작업시작"); } }); thread.start(); </pre><p><code> </code></p><p><strong>[애프터: 람다 활용]</strong></p><pre class="ql-syntax" spellcheck="false"> Thread thread = new Thread(() -&gt; System.out.println("작업시작")); thread.start(); </pre><ul><li><code> </code>입력값이 없을 때는 빈 괄호 <code>()</code>만 써주면 됩니다. 코드가 훨씬 가벼워졌죠?</li><li>&nbsp;</li></ul><h3><br></h3><h3><br></h3><h3><br></h3><h3> 초보자를 위한 핵심 요약</h3><p>람다를 사용할 때는 다음을 기억하세요.</p><p> 1) (파라미터)-&gt;결과</p><p> 2) (파라미터)-&gt;{return 결과}</p><p> 3) (파라미터)-&gt;{가공}</p><p><br></p><p><br></p><p><strong style="font-size: 20px;">결론</strong></p><p>람다는 현대 자바 개발자에게 선택이 아닌 필수입니다.</p><p> 처음에는 <code>-&gt;</code> 기호가 낯설 수 있지만, 몇 번 직접 타이핑해 보면 이만큼 편한 도구도 없다는 걸 깨닫게 될 거예요.</p><p>오늘 배운 <strong>"(파라미터) -&gt; {행동}"</strong> 구조만 기억해도 절반은 성공입니다!</p><p> 이제 여러분의 프로젝트에 람다를 적용해 코드를 다이어트시켜 보세요.</p><p>&nbsp;</p><p><br></p><p><strong>출처 및 참고</strong>:</p><ul><li>Oracle Java Documentation - <a href="https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html" target="_blank">Lambda Expressions</a></li><li>Baeldung - <a href="https://www.baeldung.com/java-8-lambda-expressions-tips" target="_blank">Introduction to Java 8 Lambda Expressions</a></li></ul>

자세히 보기
RedisSession 실제 구현

RedisSession 실제 구현

<h2>1.&nbsp;서론:&nbsp;왜&nbsp;Filter에서&nbsp;세션을&nbsp;직접&nbsp;다뤄야&nbsp;할까요?</h2><p></p><p>웹&nbsp;서비스를&nbsp;운영하다&nbsp;보면&nbsp;트래픽이&nbsp;늘어나면서&nbsp;자연스럽게&nbsp;서버(Tomcat&nbsp;등)를&nbsp;여러&nbsp;대로&nbsp;증설하게&nbsp;됩니다.</p><p>이때&nbsp;가장&nbsp;먼저&nbsp;마주하는&nbsp;난관이&nbsp;바로&nbsp;<strong>&#39;세션&nbsp;불일치&#39;</strong>&nbsp;문제입니다.</p><p>&nbsp;</p><p>예를&nbsp;들어,&nbsp;사용자가&nbsp;1번&nbsp;서버에서&nbsp;로그인을&nbsp;성공했는데&nbsp;로드밸런서가&nbsp;다음&nbsp;요청을&nbsp;2번&nbsp;서버로&nbsp;보내버리면&nbsp;어떻게&nbsp;될까요?</p><p>2번&nbsp;서버에는&nbsp;해당&nbsp;사용자의&nbsp;세션&nbsp;정보가&nbsp;없기&nbsp;때문에&nbsp;순식간에&nbsp;로그아웃&nbsp;처리가&nbsp;되어버립니다.</p><p>실제로&nbsp;쇼핑몰&nbsp;결제&nbsp;직전에&nbsp;서버가&nbsp;바뀌면서&nbsp;장바구니가&nbsp;모두&nbsp;날아가는&nbsp;등&nbsp;치명적인&nbsp;사용자&nbsp;경험&nbsp;저하로&nbsp;이어질&nbsp;수&nbsp;있죠.</p><p>&nbsp;</p><p>이러한&nbsp;문제를&nbsp;해결하기&nbsp;위해&nbsp;**Redis&nbsp;세션&nbsp;클러스터링(Session&nbsp;Clustering)**을&nbsp;도입하게&nbsp;됩니다.</p><p>모든&nbsp;서버가&nbsp;하나의&nbsp;Redis&nbsp;저장소를&nbsp;바라보게&nbsp;하여&nbsp;세션을&nbsp;공유하는&nbsp;방식입니다.</p><p>최근에는&nbsp;Spring&nbsp;Session&nbsp;같은&nbsp;훌륭한&nbsp;라이브러리가&nbsp;이&nbsp;과정을&nbsp;대신해주지만,&nbsp;레거시&nbsp;환경이나&nbsp;아키텍처의&nbsp;핵심&nbsp;원리를&nbsp;제대로&nbsp;이해하기&nbsp;위해서는</p><p><strong>서블릿&nbsp;필터(Filter)</strong>&nbsp;단에서&nbsp;세션&nbsp;객체를&nbsp;직접&nbsp;제어해보는&nbsp;경험이&nbsp;무척&nbsp;중요합니다.</p><p>&nbsp;</p><p>이&nbsp;글에서는&nbsp;요청을&nbsp;가로채어&nbsp;우리가&nbsp;만든&nbsp;커스텀&nbsp;세션(RedisSession)을&nbsp;적용하는&nbsp;전체&nbsp;과정을&nbsp;4개의&nbsp;핵심&nbsp;클래스&nbsp;구현과&nbsp;함께</p><p>차근차근&nbsp;논리적으로&nbsp;살펴보겠습니다.</p><p></p><h2>2.&nbsp;본론:&nbsp;RedisSession&nbsp;핵심&nbsp;클래스&nbsp;4가지&nbsp;구현하기</h2><p></p><h3>1)&nbsp;RedisSession:&nbsp;세션&nbsp;데이터의&nbsp;실체</h3><p>가장&nbsp;먼저&nbsp;만들&nbsp;클래스는&nbsp;실제&nbsp;세션&nbsp;데이터를&nbsp;담아둘&nbsp;<code>RedisSession</code>입니다.</p><p>이&nbsp;객체는&nbsp;기존&nbsp;톰캣이&nbsp;제공하는&nbsp;세션과&nbsp;완벽하게&nbsp;동일한&nbsp;역할을&nbsp;수행해야&nbsp;하므로,</p><p>반드시&nbsp;<code>HttpSession</code>&nbsp;인터페이스를&nbsp;<code>implements</code>&nbsp;하여&nbsp;모든&nbsp;메서드를&nbsp;구현해야&nbsp;합니다.</p><p></p><p>여기서&nbsp;가장&nbsp;중요한&nbsp;것은&nbsp;<strong>세션의&nbsp;상태를&nbsp;나타내는&nbsp;3가지&nbsp;플래그(Flag)&nbsp;값</strong>입니다.</p><p>이&nbsp;원리를&nbsp;이해하셔야&nbsp;불필요한&nbsp;네트워크&nbsp;I/O를&nbsp;줄일&nbsp;수&nbsp;있습니다.</p><p>&nbsp;</p><ol><li><code><strong>changed</strong></code><strong>&nbsp;(또는&nbsp;</strong><code><strong>dirty</strong></code><strong>)</strong>:&nbsp;세션&nbsp;내의&nbsp;데이터가&nbsp;하나라도&nbsp;변경되었는지를&nbsp;나타냅니다.</li><li>이&nbsp;값이&nbsp;<code>true</code>일&nbsp;때만&nbsp;Redis에&nbsp;데이터를&nbsp;덮어써서&nbsp;불필요한&nbsp;저장을&nbsp;막습니다.</li><li><code><strong>valid</strong></code>:&nbsp;세션이&nbsp;현재&nbsp;유효한지(로그아웃되거나&nbsp;만료되지&nbsp;않았는지)를&nbsp;판단합니다.</li><li><code><strong>isNew</strong></code>:&nbsp;이번&nbsp;요청에서&nbsp;새롭게&nbsp;생성된&nbsp;세션인지를&nbsp;확인합니다.</li><li>&nbsp;</li></ol><pre data-language="java"> package mvc.controller.redis; import java.io.Serializable; import java.util.Collections; import java.util.Enumeration; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpSession; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class RedisSession implements HttpSession, Serializable { private static final long serialVersionUID = -7024556060254149497L; @JsonIgnore @Getter @Setter private volatile boolean changed = false; @JsonIgnore @Getter @Setter private volatile boolean valid = true; @JsonIgnore private volatile boolean newSession; // 직렬화 대상 @Setter(AccessLevel.NONE) private String id; @Setter(AccessLevel.NONE) private long creationTime; @Setter private long lastAccessedTime; private int maxInactiveInterval = 1800; @Getter @Setter private volatile Map&lt;String, Object&gt; attributes = new ConcurrentHashMap&lt;&gt;(); @JsonIgnore private transient ServletContext servletContext; public RedisSession(final String id) { if (id == null || id.isEmpty()) { throw new IllegalArgumentException(&quot;세션 ID가 비어있습니다.&quot;); } this.id = id; this.newSession = true; this.creationTime = System.currentTimeMillis(); this.lastAccessedTime = this.creationTime; this.changed = true; } private void checkValid() { if (!this.valid) { throw new IllegalStateException(&quot;세션이 이미 무효화되었습니다.&quot;); } } /* ---------- HttpSession ---------- */ @Override public String getId() { checkValid(); return this.id; } @Override public long getCreationTime() { checkValid(); return this.creationTime; } @Override public long getLastAccessedTime() { checkValid(); return this.lastAccessedTime; } @Override public ServletContext getServletContext() { checkValid(); return this.servletContext; } @Override public void setMaxInactiveInterval(final int interval) { checkValid(); this.maxInactiveInterval = interval; this.changed = true; } @Override public int getMaxInactiveInterval() { checkValid(); return this.maxInactiveInterval; } @Override public Object getAttribute(String name) { checkValid(); if (name == null || name.isEmpty()) { throw new IllegalArgumentException(&quot;속성명이 비어있습니다.&quot;); } return this.attributes.get(name); } @Override public void setAttribute(String name, Object value) { checkValid(); if (name == null || name.isEmpty()) { throw new IllegalArgumentException(&quot;속성명이 비어있습니다.&quot;); } if (value == null) { removeAttribute(name); return; } this.attributes.put(name, value); this.changed = true; } @JsonIgnore @Override public Enumeration&lt;String&gt; getAttributeNames() { checkValid(); return Collections.enumeration(this.attributes.keySet()); } @Override public void removeAttribute(final String name) { checkValid(); if (name == null || name.isEmpty()) { throw new IllegalArgumentException(&quot;속성명이 비어있습니다.&quot;); } if (this.attributes.remove(name) != null) { this.changed = true; } } @Override public void invalidate() { checkValid(); this.valid = false; this.attributes.clear(); this.changed = true; } @Override public boolean isNew() { checkValid(); return this.newSession; } public void refreshLastAccessedTime() { checkValid(); this.lastAccessedTime = System.currentTimeMillis(); this.changed = true; } public void setNew(final boolean newSession) { this.newSession = newSession; } public void setServletContext(final ServletContext servletContext) { this.servletContext = servletContext; } } </pre><h3></h3><h3>2)&nbsp;RedisSessionManager:&nbsp;세션의&nbsp;생애주기&nbsp;관리자</h3><p>세션&nbsp;객체를&nbsp;만들었다면,&nbsp;이&nbsp;객체를&nbsp;실제&nbsp;Redis&nbsp;서버에&nbsp;넣고&nbsp;빼는&nbsp;역할을&nbsp;할&nbsp;&#39;본체&#39;가&nbsp;필요합니다.</p><p><code>RedisSessionManager</code>는&nbsp;세션의&nbsp;생성,&nbsp;저장,&nbsp;로드,&nbsp;삭제를&nbsp;전담합니다.</p><p>매번&nbsp;객체를&nbsp;생성하여&nbsp;자원을&nbsp;낭비하지&nbsp;않도록&nbsp;<strong>싱글톤(Singleton)</strong>&nbsp;패턴으로&nbsp;구현하는&nbsp;것이&nbsp;일반적입니다.</p><pre data-language="java">   package mvc.controller.redis; import common.util.xml.ConfigXml; import common.util.xml.XmlTreeNode; import lombok.extern.log4j.Log4j2; import mvc.model.DataMapper; import redis.clients.jedis.DefaultJedisClientConfig; import redis.clients.jedis.RedisClient; @Log4j2 public class RedisSessionManager { private volatile RedisClient client; private static final String KEY_PREFIX = &quot;sess:&quot;; private static final int EXPIRE_SECONDS = 1800; private static volatile RedisSessionManager instance; private RedisSessionManager() { XmlTreeNode node = ConfigXml.getNode(&quot;redis&quot;); if(node==null || node.isEmpty()) { this.client = null; return; } String url = ConfigXml.get(&quot;redis.url&quot;); Integer port = ConfigXml.getInt(&quot;redis.port&quot;); String username = ConfigXml.get(&quot;redis.username&quot;); String password = ConfigXml.get(&quot;redis.password&quot;); try { DefaultJedisClientConfig config = DefaultJedisClientConfig.builder() .connectionTimeoutMillis(1000) .socketTimeoutMillis(2000) .user(username) .password(password) .build(); RedisClient client = RedisClient.builder().hostAndPort(url, port).clientConfig(config).build(); client.ping(); this.client = client; log.info(&quot;RedisSessionManager initialized successfully.&quot;); } catch (Exception e) { throw e; } } public static synchronized RedisSessionManager getInstance() throws Exception { if (instance == null) { try { instance = new RedisSessionManager(); } catch (Exception e) { throw e; } } return instance; } public void saveSession(RedisSession session) { try { String key = KEY_PREFIX + session.getId(); String jsonValue = DataMapper.MAPPER.writeValueAsString(session); client.setex(key, EXPIRE_SECONDS, jsonValue); session.setChanged(false); } catch (Exception e) { log.error(&quot;[RedisSession] Save saveSession Error: {}&quot;, e.getMessage(), e); throw e; } } public RedisSession loadSession(String sessionId) { try { String key = KEY_PREFIX + sessionId; RedisSession session = null; String json = client.get(key); if(json!=null) { session = DataMapper.MAPPER.readValue(json, RedisSession.class); } return session; } catch (Exception e) { log.error(&quot;[RedisSession] Save loadSession Error: {}&quot;, e.getMessage(), e); throw e; } } public void deleteSession(String sessionId) { String key = KEY_PREFIX + sessionId; client.del(key); } public void updateTTL(String sessionId) { String key = KEY_PREFIX + sessionId; client.expire(key, EXPIRE_SECONDS); } public void close() { if (client != null) { client.close(); } } } </pre><h3></h3><h3>3)&nbsp;RedisSessionRequest:&nbsp;원본&nbsp;Request를&nbsp;감싸는&nbsp;래퍼(Wrapper)</h3><p>이제&nbsp;기존&nbsp;로직(컨트롤러나&nbsp;JSP)에서&nbsp;<code>request.getSession()</code>을&nbsp;호출했을&nbsp;때,&nbsp;톰캣의&nbsp;기본&nbsp;세션이&nbsp;아닌</p><p>우리가&nbsp;만든&nbsp;<code>RedisSession</code>을&nbsp;반환하도록&nbsp;속이는&nbsp;작업이&nbsp;필요합니다.</p><p>이를&nbsp;위해&nbsp;<code>HttpServletRequestWrapper</code>를&nbsp;상속받아&nbsp;<code>getSession()</code>&nbsp;메서드를&nbsp;재정의(Override)합니다.</p><pre data-language="java">   package mvc.controller.redis; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpSession; import lombok.Getter; public class RedisSessionRequest extends HttpServletRequestWrapper { @Getter private final HttpSession session; private final boolean hasSessionIdCookie; public RedisSessionRequestWrapper(HttpServletRequest request, HttpSession session, final boolean hasSessionIdCookie) { super(request); this.session = session; this.hasSessionIdCookie = hasSessionIdCookie; } @Override public HttpSession getSession(boolean create) { if (!create &amp;&amp; !this.hasSessionIdCookie) { return null; } return this.session; } } </pre><h3></h3><h3>4)&nbsp;RedisSessionFilter:&nbsp;모든&nbsp;요청을&nbsp;오케스트레이션하다</h3><p>드디어&nbsp;대망의&nbsp;필터입니다.&nbsp;이곳은&nbsp;앞서&nbsp;만든&nbsp;3개의&nbsp;클래스를&nbsp;조립하여&nbsp;클라이언트의&nbsp;요청이&nbsp;들어올&nbsp;때&nbsp;세션을&nbsp;준비하고,</p><p>응답이&nbsp;나갈&nbsp;때&nbsp;세션을&nbsp;처리하는&nbsp;<strong>이벤트&nbsp;구조의&nbsp;중심</strong>입니다.</p><pre data-language="java">   package mvc.controller.redis; import java.io.IOException; import java.util.UUID; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.log4j.Log4j2; @Log4j2 public class RedisSessionFilter implements Filter { private volatile RedisSessionManager sessionManager = null; private static final String COOKIE_NAME = &quot;RSESSIONID&quot;; @Override public void init(FilterConfig filterConfig) { try { sessionManager = RedisSessionManager.getInstance(); log.info(&quot;RedisSessionFilter initialized: REDIS_SESSION&quot;); } catch (Exception e) { sessionManager = null; } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (this.sessionManager == null) { synchronized (this) { if (this.sessionManager == null) { chain.doFilter(request, response); return; } } } if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { chain.doFilter(request, response); return; } final HttpServletRequest httpRequest = (HttpServletRequest) request; final HttpServletResponse httpResponse = (HttpServletResponse) response; String sessionId = null; final Cookie[] cookies = httpRequest.getCookies(); if (cookies != null) { for (final Cookie c : cookies) { if (COOKIE_NAME.equals(c.getName())) { sessionId = c.getValue(); break; } } } boolean issueCookie = false; String cookieHeader = null; if (sessionId == null || sessionId.isEmpty()) { sessionId = UUID.randomUUID().toString(); issueCookie = true; boolean secure = httpRequest.isSecure(); if (!secure) { final String xfProto = httpRequest.getHeader(&quot;X-Forwarded-Proto&quot;); if (xfProto != null &amp;&amp; xfProto.equalsIgnoreCase(&quot;https&quot;)) { secure = true; } } final StringBuilder sb = new StringBuilder(160); sb.append(COOKIE_NAME).append(&quot;=&quot;).append(sessionId); sb.append(&quot;; Path=/&quot;); sb.append(&quot;; HttpOnly&quot;); if (secure) { sb.append(&quot;; Secure&quot;); } sb.append(&quot;; SameSite=Lax&quot;); cookieHeader = sb.toString(); httpResponse.addHeader(&quot;Set-Cookie&quot;, cookieHeader); } RedisSession redisSession = null; final RedisSessionManager manager = sessionManager; try { redisSession = manager.loadSession(sessionId); } catch (Exception e) { this.sessionManager = null; chain.doFilter(request, response); return; } if (redisSession == null) { redisSession = new RedisSession(sessionId); } else { redisSession.setNew(false); } redisSession.setServletContext(request.getServletContext()); boolean isRedis = false; HttpServletRequestWrapper wrapRequest = null; try { wrapRequest = new RedisSessionRequest(httpRequest, redisSession, !issueCookie); isRedis = true; } catch(Exception e) { wrapRequest = new HttpServletRequestWrapper(httpRequest); } try { chain.doFilter(wrapRequest, response); } finally { if (isRedis) { try { if (redisSession != null &amp;&amp; redisSession.isValid()) { if (redisSession.isNew() || redisSession.isChanged()) { redisSession.refreshLastAccessedTime(); manager.saveSession(redisSession); } else { manager.updateTTL(sessionId); } } else { if (sessionId != null &amp;&amp; !sessionId.isEmpty()) { manager.deleteSession(sessionId); } } } catch (Exception ignore) { this.sessionManager = null; } } } } @Override public void destroy() { } } </pre><p></p><p>3.&nbsp;결론</p><p>이렇게&nbsp;4개의&nbsp;클래스를&nbsp;직접&nbsp;구현해보면&nbsp;웹&nbsp;애플리케이션이&nbsp;클라이언트의&nbsp;상태를&nbsp;어떻게&nbsp;유지하는지,</p><p>&nbsp;밑바닥&nbsp;원리를&nbsp;완벽하게&nbsp;통제하고&nbsp;이해할&nbsp;수&nbsp;있게&nbsp;됩니다.</p><p>특히&nbsp;필터의&nbsp;<code>try-finally</code>&nbsp;블록을&nbsp;통해&nbsp;예외&nbsp;발생&nbsp;시에도&nbsp;세션&nbsp;유실을&nbsp;방어하고,&nbsp;Redis&nbsp;장애&nbsp;시&nbsp;<code>chain.doFilter()</code>로&nbsp;우회하여</p><p>서비스&nbsp;중단을&nbsp;막는&nbsp;구조는&nbsp;실무에서&nbsp;고가용성(High&nbsp;Availability)&nbsp;아키텍처를&nbsp;설계할&nbsp;때&nbsp;매우&nbsp;중요한&nbsp;통찰을&nbsp;제공합니다.</p><p>&nbsp;</p><p>단순히&nbsp;라이브러리를&nbsp;가져다&nbsp;쓰는&nbsp;것을&nbsp;넘어,&nbsp;이러한&nbsp;동작&nbsp;원리를&nbsp;이해하고&nbsp;있다면&nbsp;어떠한&nbsp;프레임워크&nbsp;환경에서도</p><p>유연하게&nbsp;대처할&nbsp;수&nbsp;있는&nbsp;탄탄한&nbsp;기본기가&nbsp;될&nbsp;것입니다.</p>

자세히 보기