본문 바로가기

Framework

(17)
[Spring Boot] Spring WebFlux 한계 돌파 - 비동기 논블로킹(Non Blocking) JMeter로 1만 명의 부하 테스트를 때리다 보면, 평범한 Spring MVC(톰캣 기반) 서버가 어느 순간 맥없이 무너지는 것을 보게 됩니다. 이유가 뭘까요? 기존 방식은 '1 요청 = 1 쓰레드(Thread)'라는 치명적인 룰을 가집니다. 유저가 API를 호출하면 쓰레드 하나가 배정되고, 그 쓰레드는 데이터베이스에서 쿼리 결과를 가져오는 2초 동안 '대기(Blocking)' 상태에 빠집니다. 아무 일도 안 하면서 쓰레드를 꽉 쥐고 있는 거죠. 이렇게 200명의 유저가 동시에 접속해 쓰레드 200개가 전부 DB 응답을 기다리며 멈춰버리면, 유저부터는 하얀 화면만 보며 에러를 뱉게 됩니다(Thread Pool 고갈). "아니, DB 기다리는 동안 쓰레드가 놀지 말고, 돌아가서 다른 유저 요청을 받아오면..
[Spring Boot] 힙 덤프(Heap Dump) 분석 및 메모리 누수 자바(Java)의 가장 큰 장점 중 하나는 개발자가 메모리(RAM) 관리에 신경 쓰지 않아도 된다는 것입니다. 객체를 다 쓰고 나면 가비지 컬렉터(Garbage Collector, GC)라는 청소부가 돌아다니며 쓰레기를 싹 치워주기 때문이죠. 그런데 여러분이 만든 쇼핑몰 서버가 일주일만 지나면 서서히 메모리가 차오르더니 java.lang.OutOfMemoryError (OOM)을 뱉으며 장렬히 전사하는 현상이 발생했습니다. 청소부(GC)가 왜 쓰레기를 치우지 못했을까요? 개발자가 코드 어딘가에서 List나 Map 같은 저장소에 객체를 계속 쌓아두기만 하고 빼지 않으면(참조를 유지하면), GC는 "어? 이 객체는 아직 쓰고 있는 중요한 데이터인가 보네? 치우지 말아야지!" 하고 냅두게 됩니다. 이것이 바로..
[Spring Boot] 무중단 배포 전략(Blue-Green) 새벽 3시, 개발자가 버그를 고치고 새로운 스프링 부트(Spring Boot) 버전(V2)을 배포(Deploy)하기 위해 서버에 접속합니다. 기존에 돌고 있던 V1 서버 프로세스를 kill 명령어로 죽이고, 새로운 V2 .jar 파일을 java -jar 명령어로 기동 시킵니다. 스프링의 거대한 프레임워크가 톰캣(Tomcat)을 띄우고 메모리에 빈(Bean)들을 올리는 데 약 10~15초가 소요됩니다. 문제는 이 15초 동안 사이트에 들어온 사용자들은 끔찍한 "502 Bad Gateway" 또는 "연결할 수 없음" 에러 페이지를 마주하게 된다는 것입니다. (다운타임 발생) 과거에는 고객들이 잠든 새벽에 서버를 내리고 작업(점검 시간)하는 것이 당연했지만, 24시간 글로벌로 돌아가는 현대의 서비스(넷플릭스,..
[Spring Boot] Apache Kafka 연동 MSA의 이벤트 주도 비동기 통신 배달 앱에서 고객이 치킨을 '결제'했다고 상상해 봅시다. 백엔드 시스템 내부에서는 어마어마한 일들이 순차적으로 벌어집니다.1. 주문 시스템에 접수2. 라이더 배차 시스템 호출3. 고객에게 카카오톡 알림 발송4. 쿠폰 소진 처리만약 결제 서버가 이 4개의 서버에게 일일이 HTTP(REST API)로 전화를 걸어 "야, 결제됐어! 일해!"라고 동기식(Synchronous)으로 통신하면 어떻게 될까요? 만약 카카오톡 알림 서버가 장애로 3초간 멈춰있다면, 결제 서버도 그 3초 동안 응답을 기다리며 멈춰있게 되고 결국 사용자의 화면은 하얗게 굳어버립니다. 하나의 서버 장애가 전체 시스템의 장애로 퍼지는 '도미노 붕괴(강한 결합)' 현상입니다. 이 끔찍한 의존성을 칼같이 끊어내기 위해 도입된 구원자가 바로 아파치..
[Spring Boot] 파일 업로드와 AWS S3 클라우드 사용하기 게시판에 사용자가 10MB짜리 고화질 사진을 첨부하여 글을 작성합니다. 초보 시절에는 MultipartFile 객체를 받아 스프링 부트(Spring Boot)가 구동되고 있는 로컬 서버의 C:\upload 디렉토리나 리눅스 /var/www/images 폴더에 file.transferTo() 코드로 저장(Save)하는 방식을 썼습니다. 서버가 딱 1대일 때는 이 방식이 기가 막히게 잘 돌아갑니다. 하지만 대박이 터져서 서버를 3대(A, B, C)로 스케일 아웃(Scale-out)하는 순간 지옥문이 열립니다. 사용자가 A서버에 접속해 프로필 사진을 올렸는데, 내일 다시 접속할 땐 로드밸런서가 B서버로 연결해 주면? B서버 디스크에는 사진이 없으므로 화면에 끔찍한 '엑스박스(이미지 깨짐)'가 뜹니다. 게다가 ..
[Spring Boot] 트랜잭션 롤백과 전파 속성 은행 앱에서 A가 B에게 10만 원을 송금하는 상황을 상상해 봅시다. 서버는 두 가지 쿼리를 실행합니다.1. A의 통장에서 10만 원 차감 (UPDATE)2. B의 통장에 10만 원 증가 (UPDATE)그런데 1번 쿼리가 성공한 직후, 갑자기 데이터베이스 서버의 전원이 나갔습니다! A의 돈은 10만 원이 증발했는데, B는 돈을 받지 못했습니다. 뉴스에 대서특필될 만한 엄청난 사고입니다. 이런 대형 사고를 막기 위해 데이터베이스는 "1번과 2번 쿼리를 하나로 묶어라. 둘 다 성공할 때만 최종 반영(Commit)하고, 하나라도 실패하면 1번 쿼리도 아예 없었던 일로 롤백(Rollback) 시켜라!"라는 논리적 작업 단위를 제공하는데, 이것이 바로 트랜잭션(Transaction)입니다. 스프링 부트(Sprin..
[Spring Boot] OAuth 2.0 소셜 로그인 구현(카카오 네이버 구글 로그인) 인터넷 쇼핑몰이나 새로운 서비스에 접속했을 때, 여러분은 긴 회원가입 폼을 성실하게 채우시나요, 아니면 "카카오로 1초 만에 시작하기" 버튼을 누르시나요? 바쁘고 귀찮은 것을 극도로 싫어하는 현대의 유저들에게 자사몰의 독자적인 회원가입(ID/PW)을 강요하는 것은 곧 '고객 이탈'을 의미합니다. 이러한 이탈을 막기 위해 구글, 카카오, 네이버 같은 거대 IT 기업들이 유저의 신원을 대신 확인해 주고 우리 서버에 "이 사람 진짜 맞아!"라고 보증을 서주는 기술이 등장했습니다.바로 OAuth 2.0 (Open Authorization) 프로토콜입니다. 그리고 스프링 부트(Spring Boot) 진영에는 이 복잡한 프로토콜 통신을 아주 우아하게 캡슐화해 놓은 spring-boot-starter-oauth2-c..
[Spring Boot] Spring Security - JSON Web Token 구현 앞선 에서 우리는 스프링 시큐리티(Spring Security)의 위대한 필터 체인(Filter Chain)과 전통적인 폼 로그인(Session/Cookie 방식)을 마스터했습니다. 세션 기반 로그인은 보안성이 높고 구현이 편하지만, 현대의 트렌드인 '모바일 앱(iOS/Android) 통신'과 서버 수십 대를 띄우는 '마이크로서비스(MSA) 스케일 아웃' 환경을 만나면 치명적인 약점을 드러냅니다. 사용자가 서버 A에 로그인하여 세션을 만들었는데, 다음 요청이 서버 B로 향하면 서버 B는 "너 누구야?" 라며 로그아웃 시켜버리기 때문입니다(세션 불일치).이 골치 아픈 '상태 유지(Stateful)'의 사슬을 끊어내고, 서버가 사용자 정보를 기억할 필요 없이 "네가 가진 암호화된 방문증(Token)만 보여주..
[Spring Boot] Redis 캐싱(Caching)을 이용하여 응답 속도 단축하기 스타트업이 처음 런칭했을 때는 유저가 적어 데이터베이스(RDB) 하나만으로도 충분히 서비스가 잘 돌아갑니다. 하지만 마케팅이 대박 나서 '초당 1만 건'의 요청이 쏟아지기 시작하면 상황은 달라집니다. 여러분이 앞선 에서 HikariCP 커넥션 풀을 아무리 완벽하게 튜닝해 놓았더라도, 물리적인 디스크(Disk)를 긁어서 데이터를 읽어오는 관계형 데이터베이스(MySQL, Oracle)는 태생적으로 속도의 한계가 있습니다. 특히 '쇼핑몰 메인 페이지의 배너 이미지 목록'이나 '오늘의 인기 검색어'처럼 수많은 유저가 똑같이 조회하지만, 데이터는 하루에 한두 번만 변하는 정보를 매번 느린 디스크 DB까지 내려가서 읽어오는 것은 엄청난 리소스 낭비입니다.이를 해결하기 위해 백엔드 진영이 도입한 궁극의 마법, "디스..
[Spring Boot] QueryDSL 입문 / 타입 세이프(Type safe) 동적 쿼리 Spring Data JPA의 Query Method와 @Query는 훌륭했지만, 실무의 거친 요구사항을 만나면 한계가 명확하게 드러납니다. 쇼핑몰의 상품 검색창을 생각해 봅시다. 사용자는 '상품명'만 검색할 수도 있고, '가격 범위'만 필터링할 수도 있으며, '카테고리'와 '브랜드'를 동시에 선택할 수도 있습니다.즉, 조건이 있을 수도 있고 없을 수도 있는 '동적 쿼리(Dynamic Query)'가 필요합니다. 과거 MyBatis 시절에는 XML 파일에 같은 태그를 수십 개 도배하여 쿼리 문자열(String)을 조립했습니다. JPA @Query에서 문자열로 동적 쿼리를 짜다 보면 띄어쓰기 하나 잘못해서 서버가 켜진 뒤 고객이 검색 버튼을 누르는 순간 에러가 터집니다(런타임 에러). 자바 코드로 쿼리..