망했다 미래의 악마 배에 머리 넣었으면 나도 딱히 큰 대가없이 계약 할 수 있었을듯? 회사도 망했고 6년넘게 거의 올인한 관계도 망했고 그랬다 미래 최고 제로부터 시작하는건 이번 분기였는데 리제로를 저번 분기에 써버렸네 제로부터 시작하니까 홀가분한건 맞는데. 그렇다고 좋은건 아니다. 어쩔 수 없이 제로부터 시작하는거다. 하지만? 그 덕분에 객관적으로 현재 상태를 점검하고 계속 이 길로 나아갈지-여러 시도를 더욱 적극적으로 할지~ 어쩔지~ 좀 더 편안한 관점에서 생각해볼 수 있을 것 같다. 내가 좋아했던, 자진해서 꽁꽁 묶고 있던 것들에게서 반강제 탈출했으니까 뭘 하고싶은지 뭘 해야할지 나눠서 생각해보고 이것저것 해보자. 아 그리고 추천인받은 토증 가고싶었는데, 좋은 등가교환이었다. 해낸것 회사망하기 재취업? << 이거 해야하는지가 제일 고민이긴 한데 시도는 하기 베스타 관련 베스타 전업화 전면 무료화(알림톡 bm 부분유료) 알림톡 출시 마케팅 컨택(레뷰 ... etc) 안드로이드 출시 예약페이지 서비스 제공 네이버예약 실패하기 > 링크드인, 스레드 등지에 도움 요청도 해보는 중이긴 함 잔디밭 거의 1년 다 되어가는중 해파랑길 50km 넘게 걸어보기 구마노고도 순례길 찍먹하기(50~60km) 구마노고도 순례길 영상 촬영 + 유튜브화 > 하는중... 서로 갈길 가기 알아간것과 느낀것 많은 것들이 끊어졌다 내가 믿던 것들이 전반적으로 다 끊어졌다 이런 상태일수록 처음부터 다 쌓아나가고 크게 성장하기 좋긴 한데 굳이? 그래야하나? 이런 생각이었지 사실. 큰 상실에서 많은 것들을 얻어갈 수 있는건 맞지만 결국 트레이드오프라서 손에 쥐고 있던 것들을 놓기가 싫었다. 그리고 성장하겠다고 항상 손에 있는걸 던지는 사람보다는 내가 원하는걸 손에 잘 쥐고 있는것도 능력이라고 생각했고, 나는 그런 사람이라고 생각했기에. 하지만 뭐 어쩔수있나 강도가 총들고와서 다 내놓고 가라해서 빤스도 내놓고 나왔다. 구추 덜렁거리고 있는 상태니까 입던 빤스부터 다시 사서 입을지 훈도시 입을지 치마 입을지 노팬티로 다닐지 뭐 등등 어떻게 쌓아나갈지 고민좀 해봐야겠다. 아무튼 지금의 상태가 새롭게 쌓아나가긴 좋긴 하니까- 똑같은 일을 해도 나에게 온전히 집중하는거니까. 뭘 해야할지 생각도 많이 하고 행동도 더 많이 해보자. 간단한 것부터 대단한 것까지 아무렇지않게 해보자. 아 생각해보니까 내가 좀 게을러지고 현재에 안주하려고 했었나? 왜 이런 자극들을 주는거지? 신이시여 도시테... 나 분명 슬슬 이직 준비하려고 포폴 업데이트에 내용도 좀 채워넣고 있었고 직업적으로도 발전하고 있었고 개인 플젝도 시간 써가면서 이것저것 눈에 보이게 발전시키고 있었는데 내가 게을렀나? 분명 라시사처럼 달리지는 않긴 했는데 난 그런 스타일이 아니었는데 그냥 이런 생각 안하고 열심히 한건데 적당히 열심히 해선 안되는건가? 왜 안되는거지 적당히 적당히 중용을 지키면서 살고싶은데 아 모르겠다 적당히가 좋긴 한데 억지로 모든 세상이 달리라고 하는 시점인거같긴 하다. 지원한곳 CJ올리브영(Domestic) > 서탈 토스 > 서탈 토스뱅크 > 서탈 토스플랫폼 > 서탈 토스증권 > 추천인 지원 > 면접 > 사업자 관련 논의 후 탈 토스플레이스 > 서탈 보이저엑스 > 서류합격 > 과제탈락 라프텔 > 서탈 그린카 > 서탈 하이브 > 서탈 누아 > 서탈 빌라모자이크(PO) > 서탈 원프레딕트 > 서탈 고이(PO) > 서탈 헤렌(공비서) > 서탈 빗썸 > 서탈 엔에프타임 > 서류합격 > 면접 > 면탈 비마이프렌즈 > 서탈 DSRV > 서탈 아정당 QA > 서탈 CJ올리브영(Global) > 서탈 해빗팩토리 > 서탈 플래티어 > 서탈 큐피스트 > 서류합격 > 과제탈락 스냅컴퍼니 > 크리마 > 서탈 패스오더 > 서류합격 > 면접 다이렉트클라우드랩 > 서탈 퓨쳐위즈 > 서탈 아정당 AI builder > 서탈 CJ Ment QA > 서탈 윌로그 코코지 > 서탈 팬딩 콜로세움컴퍼니 > 서탈 테슬라 필드 엔지니어 > 서탈 앤서스랩코리아 핀다 취팡 tobe(26년 3분기) 모르겠다. 베스타 네이버 제휴까지 되기 베스타 디자인/UX 전반 개편해서 토스급으로 만들기 베스타 링크 말고도 bm 추가하기(그 신규 bm을 구독 자체에 포함시키기 > 베스타 링크 말고도 여러 유료 기능 해금 시키는 형태로) 4분기 마일스톤대로 잘 살기 안죽고 잘 살기 전체적인 코멘트 몇번 죽을뻔하고(타의적으로 말고) 정신과 약좀 챙겨먹고 걸으러도 다니고 새로운 중심이 좀 잡히니까 좀 살거같다. 그나마 좀 살거같은 상황에서 드는 고민은 다음과 같다. 재취업을 해야할까? 베스타가 막 크게 잘되는것도 아니고, 유튜브도 이제 첫 영상이고, 모든게 사실 다 처음인 상황이다. 되게 안정적이고 원하는것도 되게 단순했던 삶이라서, 단순하게 살고 있었는데 다 개박살나고나니까 뭘 해야할까? 싶어서. 되게 진부한 표현이긴 한데, AI시대다. 유료로 결제하는 사람은 3%고 그 안에서도 이정도로 적극적으로 쓰는 사람은 되게 적겠지. 그렇다고 내가 선두주자라는건 아니고... 딱 롤로치면 처음했는데 골드나오는 정도의 애매한 그런 포지셔닝일듯. 잘쳐주면 다이아? 오버워치 처음할때 그느낌으로다가. 새로운 도구들로 서비스도 만들고 사업도 하고 유튜브도 하고 여행도 다니고 사실상 개인으로서 AI를 참 잘 활용하고 있는 중이긴한데. 회사의 일원으로 안정적인 월급 추구하며 살아야 할까 뭐 SK하이닉스나 삼전 뭐 로펌 이정도 대기업 가면 모르겠는데. 못가잖아(한남톤으로) 갈수있었으면 가서 이제 결혼준비하고있었겠지 근데 나는 그럴 사람이(좋은쪽으로 + 안좋은쪽으로) 아니었던거고 운명이 아니었던거지 뭐 사실 갈 수 있었어도 나중에 후회할 사람이었을까? 요즘 제일 궁금한게 '나라는 사람은 진짜 뭘 하고싶어하는건지?'다. 지금까지는 그냥 내가 어떤 사람인지는 잘 모르는 상태로 내가 좋아하는 사람과 평생을 보내겠다는 일념에만 좀 집중했던 나날들이 아닐까 싶다. 뭐 그게 나쁘다는건 전~~혀 아니지만, 이렇게 된 이상 내 자신에 대해 잘 알아가야 하니까. 이제부터라도 하나씩 해보고 싶은걸 해야지. 다만... 좀 무서운건 이렇게 하고싶은걸 지금 했을 때, 내가 다시 좋아하는 사람이 생기고 뭐 그러면 그때가서 직업을 어떻게 얻지 아 이러나 저러나 고민이니까 남한테 손 안벌리고 살 수 있도록 잘 세팅해서 꼴리는대로 다 해보고 열심히도 해봐야지. 그것말고는 별 수가 없다. 열심히 하자.
이 포스트에서는 이제까지 작성하였던 GEO에 관한 내용을 간단하고 직관적으로 정리해볼 예정이다. 정리할 논문 리스트는 아래와 같다. PoisonedRAG : Knowledge Corruption Attacks to Retrieval-Augmented Generation of Large Language Models GEO : Generative Engine Optimization Ranking Manipulation for Conversational Search Engines (EMNLP2024) What Generative Search Engines Like and How to Optimize Web Content Cooperatively - AutoGEO Agentic GEO From Experience to Skill- Multi-Agent Generative Engine Optimization via Reusable Strategy Learning — MAGEO Structural Feature Engineering for Generative Engine Optimization: How Content Structure Shapes Citation Behavior — GEO-SFE E-GEO : A Testbed for Generative Engine Optimization in E-Commerce SAGEO Arena : A Realistic Environment for Evaluating Search-Augmented Generative Engine Optimization Poisoned RAG 요약 RAG는 LLM이 최신 데이터에 대한 한계를 갖는걸 완화해주는 기법이다. RAG는 Knowledge DB, Retriever, LLM으로 구성된다. 공격자는 공격자가 원하는 답을 얻기 위해 공격 표면으로써 Knowledge DB에 malicious texts을 주입한다. 본 논문에서는 malicious text를 optimization problem으로써 수식화 하였으며, 아래 두 조건으로 나눠진다. retrieval condition: malicious text가 target question에 대해 검색되어질수 있는것을 의미한다. generation condition: malicious text가 LLM이 target question에 대해 target answer을 생성할수 있게 하는 것을 의미한다. 주의할점은 본 논문은 RAG 시스템의 접근 권한을 탈취하거나 악성 문서의 유입 경로를 확보하는 방법을 다루지 않는다. 대신, knowledge DB에 악성 문서를 삽입할 수 있다고 가정하고, 해당 문서가 검색되면서(retrieval condition) LLM이 공격자가 의도한 답변을 생성하도록(generation condition) 악성 문서를 구성하는 방법을 제안한다. Method $$ \max_{\Gamma}\ \frac{1}{M}\cdot \sum_{i=1}^{M} \mathbb{I}\left( \mathrm{LLM}\left(Q_i; E(Q_i; D \cup \Gamma)\right) = R_i \right) \tag{2} $$ 위 수식은 질문 Q로부터 얻은 대답이 우리가 원하는 대답인지를 수치적으로 본다. $\Gamma$: Q에대한 malicious text 5개 $D \cup \Gamma$: corrupted knowledge database (부패된 지식 DB) $E(Q_i; D \cup \Gamma)$: 질문 Q에 대해 부패된 지식 DB로부터 얻은 k개의 검색된 문서 $LLM(*)$: 최종 응답 $\mathbb{I}$: 대답이 target answer에 속하면 1, 아니면 0 $max_{\Gamma}$: 공격 성공률이 최대가 되도록 악성 문서 집합 $\Gamma$를 찾는다는 뜻, 즉 악성 문서 개수는 5개로 고정된 상태로 이 5개의 악성 문서 구성이 공격 성공률이 최대가 되도록 하도록 구성된다는 뜻. 위 공격 성공률이 최대가 되려면 malicious text가 retrieval condition, generation condition을 만족하여야 한다. 그래야 최종 응답에 포함되기 때문이다. 재미있는건 malicious text P의 S을 만드는 방식인데, 블랙박스 방식에서는 $P = S \oplus I$이며, S는 악성 문서의 제목, I는 악성 문서의 본문처럼 생각하면 된다. (실제로는 S는 검색을 유도하는 앞부분, I는 답변을 유도하는 뒷부분이다) 블랙박스 방식에서는 S를 Q로 상정한다. 하지만 화이트박스에서는 retrieval 내부를 알기에 다음과 같은 gradient descent 수식으로 S을 최적화 한다. $$ S = \underset{S'}{\arg\max}; \operatorname{Sim}\left(f_Q(Q),, f_T(S' \oplus I)\right) \tag{5} $$ $f_Q(Q)$: 질문 인코더가 $Q$를 변환한 문서 임베딩 $f_T(S' \oplus I)$: 문서 인코더가 악성 문서 전체를 변환한 문서 임베딩 $Sim$: 두 임베딩의 유사도 $S'$: 검색 유사도를 높이기 위해 최적화하는 후보 텍스트 $\arg\max_{S'}$: 유사도를 가장 크게 만드는 후보 텍스트 $S'$ 자체를 반환 $S$: 최적화 결과로 얻은 텍스트 참고로 I는 GPT4와 프롬프트를 통해 만들어진다. 전체 흐름 1. Knowledge DB 구성 NQ 기본 실험에서는 위키피디아 기반 문서 약 268만 개로 Knowledge DB $D$를 구성. 2. 타깃 질문과 답변 설정 데이터셋에서 타깃 질문 $Q$를 선택하고, 공격자가 유도하려는 오답 $R$을 설정. 3. 수식 (2)로 전체 공격 목표 정의 악성 문서 집합 $\Gamma$ (한 쿼리당 악성 문서 5개)를 삽입했을 때, 타깃 질문들에 대해 LLM이 공격자가 원하는 답변을 생성하는 비율을 최대화하는 것이 목표. 이를 위해 악성 문서는 다음 두 조건을 충족하도록 구성. - Retrieval condition: 타깃 질문에 대한 검색 결과에 악성 문서가 포함되어야 함. - Generation condition: 악성 문서가 컨텍스트로 제공됐을 때 LLM이 타깃 답변을 생성하도록 유도해야 함. 4. 목표를 달성하기 위한 악성 문서 생성 기본 설정에서는 질문마다 $P=S\oplus I$ 형태의 악성 문서를 5개 생성. - 먼저 GPT-4와 프롬프트로 generation condition을 충족하도록 $I$를 생성하고, 공격자 측 LLM에서 타깃 답변이 나오는지 확인하며 정해진 횟수 내에서 재시도. - 이어서 retrieval condition을 충족하도록 $S$를 구성. 블랙박스에서는 $S=Q$로 설정하고, 화이트박스에서는 수식 (5)를 통해 질문 $Q$와 전체 문서 $S\oplus I$의 임베딩 유사도를 최대화하도록 $S$를 최적화. 5. 악성 문서 삽입 및 RAG 실행 생성한 악성 문서 집합 $\Gamma$를 DB에 삽입하여 $D\cup\Gamma$를 구성. 타깃 질문마다 유사도가 높은 상위 5개 문서를 검색하고, 이를 질문과 함께 대상 LLM에 제공해 답변을 생성. 6. 검색 성능 및 최종 공격 성공률 평가 악성 문서가 얼마나 검색되는지를 Precision, Recall, F1으로 평가하고, 최종 답변이 타깃 답변에 해당하는 비율을 공격 성공률(ASR) 로 평가. 이 ASR이 수식 (2)에서 최대화하려는 목적값에 해당
타이타닉 데이터로 PyTorch 모델 만들기 오늘은 타이타닉 승객 데이터를 이용해 생존 여부와 성별을 예측하는 모델 을 만들었다. 아직 세부 코드를 모두 이해한 것은 아니지만, 데이터 준비부터 학습과 검증까지의 전체 흐름을 익혔다. 1. 데이터 확인과 feature 선택 각 열의 의미를 확인하고, 모델에 넣을 입력 정보와 예측할 정답을 구분했다. 입력(X) : 승객의 나이, 객실 등급, 운임, 동반 가족 수 등의 정보 정답(y) : survived 와 sex 정답으로 사용하는 열은 입력에서 제외해야 한다는 점도 배웠다. 2. 데이터 분리와 전처리 데이터를 train, validation, test 로 나눴다. 각각 학습, 모델 선택, 최종 평가에 사용하는 데이터다. 모델이 계산할 수 있도록 boolean과 문자 데이터를 숫자로 변환하고, 나이의 결측값은 train에서 구한 중앙값 으로 채웠다. 결측값이 많은 deck 은 제외하기로 했다. 3. Dataset과 DataLoader 생성 전처리한 데이터를 PyTorch의 Tensor 로 변환했다. TensorDataset 으로 입력과 정답을 묶고, DataLoader 로 한 번에 32명씩 학습하도록 구성했다. 학습 데이터는 순서를 섞어서 사용했다. 4. 모델 구축과 학습 여러 Linear 층과 ReLU 를 연결한 신경망을 만들었다. 생존 여부와 성별을 함께 예측하므로 마지막 출력은 2개로 설정했다. 학습은 다음 과정을 반복한다. 예측 → 정답과 비교해 오차 계산 → 기울기 계산 → 가중치 수정 손실함수는 BCEWithLogitsLoss , 최적화 도구는 Adam 을 사용했다. 5. 결과 확인과 기록 마지막 학습 기록에서 두 예측을 합친 정확도는 약 73.7% 였다. 검증 데이터에서는 다음 결과가 나왔다. 생존 예측 정확도: 65.17% 성별 예측 정확도: 71.91% 학습 정확도만으로 성능을 판단할 수 없고, 별도 데이터에서 검증해야 한다는 점을 배웠다. 또한 epoch마다 Loss와 Accuracy를 저장하고 그래프로 확인하는 방법도 살펴봤다. 오늘 이해한 핵심 흐름은 데이터 확인 → 전처리·분리 → Dataset·DataLoader 생성 → 모델 구축 → 학습 → 검증 이다. 앞으로는 각 코드의 역할을 더 자세히 이해하고, 전처리와 모델 설정에 따라 성능이 어떻게 달라지는지 확인해 보고 싶다.
1. 오늘의 한 줄 요약 Spring Cloud Netflix Eureka를 이용해 Service Discovery 서버를 구성하고, User Service를 여러 인스턴스로 실행해 Eureka에 등록하는 과정 을 실습했다. 어제 MSA의 전체적인 개념을 알아보았다면, 오늘은 여러 서비스의 위치를 관리하는 Service Discovery를 직접 구성해 보았다. 2. 배운 내용 Service Discovery란? MSA에서는 하나의 서비스를 여러 인스턴스로 실행할 수 있다. 예를 들어 User Service가 다음과 같이 서로 다른 포트에서 실행될 수 있다. USER-SERVICE ├─ Instance A : localhost:60000 ├─ Instance B : localhost:60001 └─ Instance C : localhost:60002 인스턴스의 개수가 많아지거나 실행 위치가 변경되면 다른 서비스가 각각의 주소를 직접 관리하기 어려워진다. Service Discovery는 실행 중인 서비스의 이름과 위치를 등록하고, 필요한 서비스가 해당 정보를 검색할 수 있도록 관리하는 역할을 한다. Service Instance │ │ 자신의 이름과 위치 등록 ▼ Eureka Server │ │ 사용 가능한 인스턴스 정보 제공 ▼ 다른 서비스 또는 Load Balancer 오늘은 Service Discovery 구현체로 Spring Cloud Netflix Eureka 를 사용했다. Eureka Server와 Eureka Client Eureka는 크게 Server와 Client로 구분할 수 있다. Eureka Server : 등록된 서비스와 인스턴스 정보를 관리한다. Eureka Client : 자신의 애플리케이션 이름과 주소를 Eureka Server에 등록한다. Eureka Dashboard : 현재 등록된 서비스와 실행 상태를 확인한다. Eureka에 등록된 서비스를 이용하면 호출하는 쪽에서 특정 IP와 포트를 직접 기억하는 대신, USER-SERVICE 와 같은 서비스 이름을 기준으로 인스턴스를 찾을 수 있다. 3. 실습 / 적용 Eureka Server 구성하기 먼저 Eureka Server 역할을 담당할 service-discovery 프로젝트를 생성했다. 메인 클래스에 @EnableEurekaServer 를 추가해 Eureka Server 기능을 활성화했다. @SpringBootApplication @EnableEurekaServer public class ServiceDiscoveryApplication { public static void main(String[] args) { SpringApplication.run( ServiceDiscoveryApplication.class, args ); } } application.yml 에는 서버 포트와 Eureka 설정을 작성했다. server: port: 8761 spring: application: name: service-discovery eureka: client: register-with-eureka: false fetch-registry: false service-discovery 는 다른 서비스가 등록되는 Eureka Server이므로 자기 자신을 다시 Eureka에 등록하거나 서비스 목록을 가져올 필요가 없다. register-with-eureka: false : 자기 자신을 Eureka에 등록하지 않는다. fetch-registry: false : 다른 서비스의 등록 정보를 가져오지 않는다. 애플리케이션 실행 후 다음 주소에서 Eureka Dashboard를 확인했다. http://localhost:8761 User Service 등록하기 다음으로 Eureka Client 역할을 하는 user-service 프로젝트를 생성했다. application.yml 에 서비스 이름과 Eureka Server 주소를 설정했다. server: port: 60000 spring: application: name: user-service eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://127.0.0.1:8761/eureka User Service를 실행하면 Eureka Server에 USER-SERVICE 라는 이름으로 인스턴스가 등록된다. 여기서 실제 등록 기준이 되는 중요한 값은 다음과 같다. spring: application: name: user-service 같은 이름으로 실행한 여러 애플리케이션은 Eureka에서 하나의 서비스에 속한 여러 인스턴스로 관리된다. 같은 서비스를 여러 인스턴스로 실행하기 같은 User Service를 여러 개 실행하려면 각 인스턴스가 서로 다른 포트를 사용해야 한다. 포트를 직접 지정해 실행할 수도 있다. 60000 60001 60002 60003 또는 다음과 같이 포트를 0 으로 지정할 수 있다. server: port: 0 server.port: 0 으로 설정하면 애
1. 한 줄 요약 Service Discovery는 계속 변하는 서비스 인스턴스의 위치를 등록하고 검색하는 기능이며, Spring Cloud Netflix Eureka를 사용하면 마이크로서비스가 자신의 정보를 등록하고 다른 서비스가 논리적인 서비스 이름으로 인스턴스를 찾을 수 있다. 2. 배운 내용 Service Discovery가 필요한 이유 모놀리식 애플리케이션은 대부분 하나의 주소로 요청을 전달하므로 호출할 서버의 위치를 파악하기 어렵지 않다. 반면 MSA에서는 같은 서비스를 여러 인스턴스로 실행할 수 있고, 확장이나 장애 복구 과정에서 인스턴스의 주소와 포트가 계속 바뀔 수 있다. 예를 들어 user-service 가 다음과 같이 실행될 수 있다. user-service:60000 user-service:60001 user-service:60002 호출하는 서비스가 이 주소들을 코드나 설정에 직접 작성하면 인스턴스가 추가되거나 제거될 때마다 설정을 변경해야 한다. Service Discovery를 사용하면 각 인스턴스가 중앙의 Service Registry에 자신의 위치를 등록한다. 다른 서비스나 Load Balancer는 Registry를 조회해 현재 요청을 처리할 수 있는 인스턴스를 찾는다. 서비스 인스턴스 ↓ 등록 Service Registry ↑ 조회 호출 서비스 또는 Load Balancer 내가 이해한 Service Discovery는 전화번호를 모두 외우는 대신 이름과 현재 전화번호를 관리하는 연락처를 조회하는 것과 비슷하다. Eureka Server와 Eureka Client Spring Cloud Netflix Eureka는 Service Discovery를 구현하기 위한 Server와 Client를 제공한다. Eureka Server Eureka Server는 서비스 인스턴스 정보를 저장하는 Service Registry 역할을 한다. 서비스 이름 호스트 주소 포트 상태 정보 서비스의 메타데이터 Eureka Client 각 마이크로서비스는 Eureka Client가 되어 자신의 정보를 Eureka Server에 등록한다. 등록된 서비스는 다른 서비스의 위치를 조회할 수도 있다. 같은 서비스가 여러 인스턴스로 실행되면 Load Balancer는 Eureka에 등록된 인스턴스 목록을 바탕으로 요청을 분산할 수 있다. Eureka Server 프로젝트 생성 강의에서는 다음 환경으로 Service Discovery 프로젝트를 생성했다. Java 21 Spring Boot 3.5.0 Spring Cloud 2025.0.0 Eureka Server Spring Boot와 Spring Cloud는 아무 버전이나 조합할 수 있는 것이 아니다. Spring Cloud의 Release Train마다 호환되는 Spring Boot 세대가 정해져 있으므로 프로젝트를 생성할 때 버전 호환성을 확인해야 한다. Eureka Server를 사용하기 위해 다음 의존성을 추가한다. <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> 메인 클래스에는 @EnableEurekaServer 를 선언한다. @SpringBootApplication @EnableEurekaServer public class ServiceDiscoveryApplication { public static void main(String[] args) { SpringApplication.run( ServiceDiscoveryApplication.class, args ); } } @EnableEurekaServer 를 사용하면 해당 Spring Boot 애플리케이션이 Eureka Server로 동작한다. Eureka Server 설정 Eureka Server는 기본적으로 8761 번 포트를 사용하도록 설정했다. server: port: 8761 spring: application: name: service-discovery eureka: client: register-with-eureka: false fetch-registry: false 각 설정의 의미는 다음과 같다. register-with-eureka: false 현재 애플리케이션은 서비스를 등록받는 Eureka Server이므로 자기 자신을 Eureka Registry에 등록하지 않도록 설정한다. fetch-registry: false Eureka Client가 다른 서비스 목록을 가져오는 기능을 사용하지 않도록 설정한다. 단일 Eureka Server 실습에서는 다른 Eureka Server의 Registry 정보를 가져올 필요가 없으므로 두 값을 모두 false 로 설정했다. 애플리케이션을 실행한 뒤 다음 주소에 접속하면 Eureka 대시보드를 확인할 수 있다. http://localhost:8761 처음에는 등록된 서비스가 없기 때문에 인스턴스 목록이 비어 있다. User Service를 Eureka Client로 등록하기 User Service에는 Eureka Client 의존성을 추가했다. <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> 강의에서는 메인 클래스에 @EnableDiscoveryClient 를 선언했다. @SpringBootApplication @EnableDiscoveryClient public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run( UserServiceApplication.class, args ); } } 현재 Spring Cloud에서는 Eureka Client Starter가 클래스 경로에 있으면 자동 설정을 통해 Eureka Client로 동작할 수 있어 @EnableDiscoveryClient 가 필수는 아니다. 다만 강의에서는 Discovery Client임을 명시적으로 표현하기 위해 사용했다. User Service의 설정은 다음과 같다. server: port: 60000 spring: application: name: user-service eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka fetch-registry: true register-with-eureka: true spring.application.name 은 Eureka에 등록되는 서비스의 논리적인 이름으로 사용된다. defaultZone 에는 Eureka Server의 주소를 지정한다. 현재 공식 문서에서는 defaultZone 의 대소문자가 구분되므로 정확한 이름을 사용해야 한다. User Service를 실행하면 Eureka 대시보드에 다음과 같이 등록된다. Application: USER-SERVICE Status: UP Port: 60000 같은 서비스의 인스턴스 확장하기 MSA에서는 하나의 서비스를 여러 인스턴스로 실행해 트래픽을 분산하거나 가용성을 높일 수 있다. 하지만 동일한 애플리케이션을 그대로 다시 실행하면 이미 사용 중인 포트 때문에 실행에 실패한다. Web server failed to start. Port 60000 was already in use. 하나의 IP와 포트 조합에는 하나의 서버만 바인딩할 수 있기 때문에 추가 인스턴스는 다른 포트를 사용해야 한다. IntelliJ VM Options 사용 두 번째 인스턴스의 실행 설정에 다른 포트를 지정한다. -Dserver.port=60001 Maven으로 실행 mvn spring-boot:run \ -Dspring-boot.run.jvmArguments="-Dserver.port=60002" 빌드된 JAR 파일 실행 mvn clean package java -Dserver.port=60003 -jar ./target/user-service-1.0.jar 각 인스턴스가 실행되면 Eureka에는 하나의 USER-SERVICE 아래 여러 인스턴스가 등록된다. USER-SERVICE ├── user-service:60000 ├── user-service:60001 ├── user-service:60002 └── user-service:60003 서비스를 호출하는 쪽은 물리적인 포트를 직접 선택하는 대신 USER-SERVICE 라는 논리적인 이름을 사용할 수 있다. Random Port 사용하기 인스턴스를 실행할 때마다 포트를 직접 지정하지 않고 운영체제가 사용 가능한 포트를 자동으로 할당하도록 설정할 수도 있다. server: port: 0 spring: application: name: user-service server.port 를 0 으로 설정하면 애플리케이션이 실행될 때 사용 가능한 임의의 포트가 할당된다. Tomcat started on port 53308 Tomcat started on port 53317 이 방식은 여러 인스턴스를 반복해서 실행할 때 포트 충돌을 피할 수 있다는 장점이 있다. Random Port 사용 시 Instance ID 문제 Random Port를 사용해 같은 서비스를 여러 번 실행했지만, Eureka 대시보드에서 인스턴스가 하나만 등록되는 문제가 발생했다. 두 애플리케이션의 서비스 이름과 기본 Instance ID가 같아 Eureka가 서로 다른 인스턴스로 구분하지 못했기 때문이다. 각 인스턴스에 고유한 ID를 설정해 문제를 해결할 수 있다. server: port: 0 spring: application: name: user-service eureka: instance: instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} client: service-url: defaultZone: http://127.0.0.1:8761/eureka 설정값은 다음 순서로 Instance ID를 만든다. spring.application.name 을 사용한다. spring.application.instance_id 가 있으면 해당 값을 사용한다. 값이 없으면 random.value 로 고유한 값을 만든다. 따라서 모든 인스턴스가 같은 user-service 라는 서비스 이름을 사용하면서도 각각 고유한 인스턴스로 등록될 수 있다. user-service:고유한 값 1 user-service:고유한 값 2 서비스 이름은 같은 기능을 제공하는 인스턴스를 묶는 기준이고, Instance ID는 그 안에서 각각의 실행 인스턴스를 구별하는 값이라고 이해했다. Service Discovery와 Load Balancing의 관계 Service Discovery와 Load Balancing은 비슷하게 느껴지지만 역할이 다르다. Service Discovery 현재 실행 중인 서비스 인스턴스가 어디에 있는지 관리하고 검색한다. Load Balancing Service Discovery에서 얻은 여러 인스턴스 중 하나를 선택해 요청을 분산한다. 클라이언트 요청 ↓ Load Balancer ↓ Eureka에서 인스턴스 목록 조회 ┌──────────┬──────────┬──────────┐ │ 60000번 │ 60001번 │ 60002번 │ └──────────┴──────────┴──────────┘ Eureka가 직접 모든 요청을 중계하는 것이 아니라 서비스의 위치 정보를 관리하고, 실제 요청 분산은 Spring Cloud LoadBalancer 같은 별도의 구성 요소가 담당한다. 3. 실습 / 적용 오늘 실습에서는 다음 순서로 Eureka 기반 Service Discovery 환경을 구성했다. Eureka Server 프로젝트를 생성했다. @EnableEurekaServer 로 Eureka Server를 활성화했다. 8761 번 포트에서 Eureka 대시보드를 실행했다. User Service에 Eureka Client 의존성을 추가했다. User Service를 Eureka Server에 등록했다. 서로 다른 포트를 사용해 여러 인스턴스를 실행했다. Eureka 대시보드에서 인스턴스가 함께 등록되는 것을 확인했다. server.port: 0 으로 Random Port를 사용했다. 고유한 instance-id 를 지정해 각 인스턴스를 구분했다. 4. 문제와 해결 문제 1: 추가 인스턴스 실행 시 포트 충돌 발생한 문제 같은 User Service를 추가로 실행했지만 기존 인스턴스가 사용 중인 60000 번 포트와 충돌했다. Port 60000 was already in use. 원인 같은 컴퓨터에서 동일한 IP와 포트를 사용하는 서버를 동시에 실행하려고 했다. 해결 방법 각 인스턴스에 서로 다른 포트를 지정했다. -Dserver.port=60001 -Dserver.port=60002 -Dserver.port=60003 또는 server.port: 0 으로 설정해 실행할 때마다 사용 가능한 포트를 자동으로 할당받았다. 문제 2: Random Port 인스턴스가 하나만 등록됨 발생한 문제 Random Port를 사용해 여러 인스턴스를 실행했지만 Eureka에서는 하나의 인스턴스만 보였다. 원인 포트는 달랐지만 Eureka가 인스턴스를 구분하는 Instance ID가 동일했다. 해결 방법 eureka.instance.instance-id 에 임의의 값을 포함해 각 인스턴스가 고유한 ID를 갖도록 설정했다. eureka: instance: instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} 문제 3: Spring Boot와 Spring Cloud 버전 선택 Spring Boot와 Spring Cloud의 버전을 독립적으로 선택하면 호환성 문제가 발생할 수 있다. 강의에서는 Spring Boot 3.5.0 과 Spring Cloud 2025.0.0 을 함께 사용했다. 공식 호환표에서도 Spring Cloud 2025.0.x 는 Spring Boot 3.5.x 세대와 대응한다. 실제 프로젝트에서는 강의의 초기 버전을 그대로 고정하기보다, 같은 Release Train 안에서 지원되는 최신 서비스 릴리스와 공식 호환표를 확인해야 한다. 5. 다음에 할 일 Eureka에 등록된 서비스 이름으로 다른 서비스를 호출해보기 Spring Cloud LoadBalancer의 요청 분산 방식 알아보기 Eureka Client의 Heartbeat와 인스턴스 제거 과정 공부하기 여러 Eureka Server를 구성하는 고가용성 방식 알아보기 API Gateway와 Eureka를 연결해 동적 라우팅 구현하기 참고 자료 Spring Cloud Netflix 공식 문서 Spring Cloud 공식 페이지 및 버전 호환표 Spring Initializr
Túi zip trong là dòng bao bì mềm có khóa zipper, sử dụng màng nhựa trong suốt để đóng gói và bảo quản nhiều loại sản phẩm như thực phẩm khô, trà, bánh kẹo, hạt dinh dưỡng, phụ kiện và hàng tiêu dùng. Ưu điểm nổi bật của túi là khả năng nhìn thấy sản phẩm bên trong, đóng mở nhiều lần và thuận tiện khi sử dụng. Tùy sản phẩm và yêu cầu bảo quản, túi có thể được sản xuất từ PE/LDPE hoặc cấu trúc màng ghép phù hợp. Túi zip trong là gì? Túi zip trong là loại túi nhựa được thiết kế với khóa zipper chạy ngang miệng túi, cho phép người dùng đóng và mở túi nhiều lần mà không cần sử dụng phương pháp hàn kín cố định. Phần thân túi sử dụng vật liệu trong suốt nên sản phẩm bên trong có thể được quan sát trực tiếp. Đây là đặc điểm phù hợp với những mặt hàng cần trưng bày màu sắc, hình dạng và trạng thái sản phẩm. Túi zip trong thường được sản xuất theo dạng túi phẳng, túi đáy đứng hoặc các thiết kế có kích thước và kiểu dáng khác tùy mục đích sử dụng. Đặc điểm của túi zip trong Một số đặc điểm khiến dòng bao bì này được sử dụng phổ biến: Màng trong suốt: dễ quan sát sản phẩm bên trong. Khóa zipper: thuận tiện đóng mở nhiều lần. Nhiều kích thước: có thể sản xuất theo quy cách sản phẩm. Trọng lượng nhẹ: thuận tiện đóng gói, vận chuyển và lưu kho. Tính linh hoạt cao: phù hợp với nhiều nhóm sản phẩm khác nhau. Có thể in ấn: túi zip trong dạng màng ghép có thể in logo, thông tin sản phẩm và nhận diện thương hiệu theo yêu cầu. Đối với sản phẩm cần khả năng bảo quản cao hơn, không nên chỉ dựa vào đặc điểm “túi trong” mà cần lựa chọn cấu trúc màng phù hợp với tính chất sản phẩm và thời gian bảo quản. Túi zip trong thường dùng để đựng sản phẩm gì? Túi zip trong có thể sử dụng cho nhiều nhóm sản phẩm, đặc biệt là các mặt hàng cần nhìn thấy sản phẩm khi trưng bày. Thực phẩm khô: Các loại hạt, trái cây sấy, khô gà, khô bò, khô mực, bánh kẹo và nhiều sản phẩm khô khác. Trà và nguyên liệu: Trà khô, trà hoa, trà chanh, thảo mộc và một số nguyên liệu dạng khô. Hạt dinh dưỡng: Hạnh nhân, hạt điều, óc chó, hạt bí, đậu và các loại hạt đóng gói theo khẩu phần. Túi zip trong có những loại nào? Tùy vật liệu và yêu cầu sử dụng, có thể phân chia thành một số nhóm phổ biến. 1. Túi zip trong PE Túi sử dụng màng PE, có độ mềm và khả năng chịu va chạm tương đối tốt. Loại này phù hợp với các sản phẩm không yêu cầu mức cản khí, cản sáng quá cao. 2. Túi zip trong màng ghép Màng ghép có thể kết hợp lớp ngoài, lớp cản và lớp hàn tùy yêu cầu. Cấu trúc này phù hợp khi sản phẩm cần tăng khả năng bảo vệ trước hơi ẩm, oxy hoặc các yếu tố môi trường. 3. Túi zip trong có in Bề mặt túi được in logo, hình ảnh, thông tin sản phẩm hoặc nhận diện thương hiệu. Với sản phẩm thương mại, đây là lựa chọn giúp bao bì vừa có chức năng đóng gói vừa hỗ trợ nhận diện. Có nên chọn túi zip trong cho thực phẩm? Điều này phụ thuộc vào đặc tính của sản phẩm và yêu cầu bảo quản. Nếu sản phẩm khô, thời gian sử dụng ngắn hoặc ưu tiên khả năng quan sát sản phẩm, túi zip trong có thể là lựa chọn phù hợp. Ngược lại, với sản phẩm nhạy với oxy, hơi ẩm hoặc ánh sáng, nên cân nhắc cấu trúc màng có khả năng cản phù hợp thay vì chỉ lựa chọn theo tiêu chí túi trong. Đặc biệt, đối với các loại hạt chứa hàm lượng chất béo cao như hạnh nhân, óc chó hoặc hạt điều, việc lựa chọn bao bì cần xem xét thêm yêu cầu hạn chế oxy hóa trong suốt thời gian bảo quản. Túi zip trong bán theo kg hay theo số lượng? Tùy nhà cung cấp và quy cách sản phẩm, túi zip trong có thể được cung cấp theo kg hoặc số lượng. Với các size phổ biến có sẵn, khách hàng thường có thể lựa chọn số lượng nhỏ hơn so với đặt sản xuất túi in theo yêu cầu. Đối với túi được sản xuất riêng về kích thước, vật liệu, thiết kế hoặc in ấn, số lượng đặt hàng sẽ phụ thuộc vào quy cách sản xuất. Khi yêu cầu báo giá, nên cung cấp tối thiểu: Kích thước túi. Loại sản phẩm cần đóng gói. Khối lượng sản phẩm/túi. Số lượng cần đặt. Có hoặc không in ấn. Yêu cầu về độ trong và khả năng bảo quản. Những thông tin này giúp nhà sản xuất lựa chọn cấu trúc vật liệu và quy cách túi phù hợp hơn. Sản xuất túi zip trong theo yêu cầu tại Thành Tiến Bao Bì Màng Ghép Thành Tiến cung cấp các dòng túi zip trong phục vụ nhu cầu đóng gói thực phẩm, hạt dinh dưỡng, trà, bánh kẹo và nhiều sản phẩm tiêu dùng khác. Tùy yêu cầu, túi có thể được tư vấn về kích thước, vật liệu, kiểu dáng, khóa zipper và phương án in ấn. Với những sản phẩm yêu cầu khả năng bảo quản cao, cấu trúc màng cũng cần được lựa chọn dựa trên đặc tính thực tế của sản phẩm thay vì sử dụng một cấu trúc chung cho mọi trường hợp. Khách hàng có thể liên hệ Bao Bì Màng Ghép Thành Tiến để được tư vấn quy cách và báo giá túi zip trong phù hợp với sản phẩm. Website: baobimangghep.vn Hotline: 0387 721 477 FAQ về túi zip trong Túi zip trong có thể tái sử dụng không? Có thể đóng mở nhiều lần nhờ khóa zipper. Tuy nhiên, khả năng tái sử dụng phụ thuộc vào vật liệu, độ bền khóa và mục đích sử dụng. Túi zip trong có in logo được không? Có. Với dòng túi sản xuất theo yêu cầu, có thể lựa chọn phương án in phù hợp với vật liệu và số lượng đặt hàng. Túi zip trong có đựng được hạt dinh dưỡng không? Có thể sử dụng, nhưng với các loại hạt chứa nhiều dầu và yêu cầu bảo quản dài ngày, nên lựa chọn cấu trúc màng dựa trên yêu cầu cản oxy, hơi ẩm và ánh sáng. Túi zip trong có những kích thước nào? Có nhiều kích thước khác nhau từ túi nhỏ đến túi lớn. Kích thước phù hợp nên được xác định dựa trên khối lượng, kích thước và cách đóng gói sản phẩm.
면접관이 질문을 던집니다. 「해시 테이블에 원소가 가득 차면 크기를 두 배로 늘려야 합니다. 1억 개의 키가 들어 있는 해시 테이블을 두 배로 늘리려면 모든 키를 다시 해시해야 해서 수백 밀리초 동안 멈추게 됩니다. 그런데 싱글 스레드로 동작하는 Redis 는 어떻게 멈추지 않고 키를 늘릴까요?」 질문을 받고 머릿속으로 'Redis 는 메모리 DB 라서 빠르다'라는 답변을 떠올렸다면 면접관의 의도와 멀어진 것입니다. 이 글은 해시 테이블이 거대해질 때 발생하는 정지 현상을 Redis 가 어떻게 해결했는지 기술적으로 풀어냅니다. 그릿 딥다이브 Vol.1 · Redis 2주차 「깊이」에서 다룬 내용을 바탕으로 작성했습니다. 표준 해시 테이블은 리해시할 때 왜 멈추나요? 표준 해시 테이블(Hash Table)은 배열을 기본 구조로 사용합니다. 키의 해시값을 배열 크기로 나눈 나머지를 인덱스로 삼아 데이터를 저장합니다. 키의 개수가 늘어나 배열 크기에 가까워지면 충돌이 잦아집니다. 이를 나타내는 지표가 부하 인자(load factor, 원소 수를 배열 크기로 나눈 비율)입니다. 부하 인자가 임계치(보통 0.75 또는 1.0)를 넘으면 해시 테이블은 배열 크기를 두 배로 늘립니다. 새 배열을 할당한 뒤 기존의 모든 원소를 다시 해시하여 새 배열로 옮겨 담습니다. 이 과정을 리해시(rehash)라고 부릅니다. 1억 개의 키를 가진 해시 테이블을 리해시하려면 1억 번의 해시 재계산과 메모리 쓰기가 발생합니다. 단일 CPU 기준으로 약 100ms 이상의 시간이 소요됩니다. 표준 해시 테이블 구현(예: Java의 HashMap)은 이 작업을 단 한 번의 연산 안에서 통째로 처리합니다. 리해시가 일어나는 그 한 번의 연산 동안 해당 해시 테이블에 접근하려는 모든 작업은 멈추게 됩니다. 교과서에서는 이를 분할 상환 O(1)(amortized O(1)) 분석으로 설명합니다. 1억 번의 빠른 O(1) 삽입 연산 끝에 한 번의 100ms 정지가 오더라도, 전체 비용을 1억 번으로 나누면 평균 비용은 여전히 O(1)이라는 뜻입니다. 하지만 싱글 스레드로 작동하는 Redis 에서 100ms 정지는 모든 클라이언트의 요청이 멈추는 심각한 장애로 이어집니다. Redis 는 1억 개 키를 늘릴 때 왜 멈추지 않나요? Redis 는 전체 리해시 작업을 한 번에 실행하지 않습니다. 작업 자체를 잘게 쪼개어 명령어 하나가 들어올 때마다 조금씩 진행합니다. 이 방식을 점진적 리해시(incremental rehash)라고 부릅니다. 차근차근 진행한다는 뜻의 incremental 이 붙은 점진적 리해시는 전체 옮김 작업을 한 번에 끝내지 않고, 명령어 한 번에 버킷 한 칸씩 나누어 마무리하는 방식입니다. Redis 는 이를 위해 내부 자료구조인 dict 안에 해시 테이블을 두 개 유지합니다. 평소에는 ht[0] 만 사용하다가, 리해시가 시작되면 ht[1] 에 두 배 크기의 새 해시 테이블을 할당합니다. 점진적 리해시가 시작되면 Redis 는 다음과 같이 동작합니다. 클라이언트가 GET 이나 SET 같은 명령어를 보냅니다. Redis 는 해당 명령어를 처리하는 과정에서 ht[0] 의 버킷 하나(연결 리스트 하나)에 들어 있는 키들을 ht[1] 로 옮깁니다. 옮김 위치는 rehashidx 라는 인덱스 변수로 기록합니다. 리해시가 진행되는 동안 검색은 ht[0] 을 먼저 보고, 없으면 ht[1] 을 봅니다. 새 키 삽입은 무조건 ht[1] 에만 수행하여 ht[0] 이 더 이상 자라지 않게 막습니다. 초당 10만 번의 요청이 들어오는 시스템이라면 초당 10만 개의 버킷이 자연스럽게 이동합니다. 클라이언트는 자신이 보낸 명령어에 몇 마이크로초의 리해시 비용만 얹어서 지불하므로, 서버 전체가 100ms 동안 멈추는 지연 튀어오름(latency spike)이 발생하지 않습니다. 이런 CS 질문 하나를 아침마다 같이 풀어 보는 오픈채팅방이 있습니다. 개발자: 데일리 CS 역량 강화 챌린지 들어가 보기 → 함께 보면 좋은 글: Redis 백업 중 메모리가 두 배로 찍혔다: 서버를 늘리기 전에, 면접에서 답하기 전에 볼 세 가지 점진적 리해시는 코드에서 어떻게 작동하나요? redis/src/dict.c 파일의 dictRehash 함수에 핵심 로직이 들어 있습니다. 이 함수는 옮길 버킷 수 n 을 매개변수로 받습니다. /* redis/src/dict.c 의 dictRehash 일부 */ int dictRehash(dict *d, int n) { int empty_visits = n*10; /* 빈 버킷을 너무 많이 만나면 멈춤 */ if (!dictIsRehashing(d)) return 0; while(n-- && d->ht[0].used != 0) { dictEntry *de, *nextde; while(d->ht[0].table[d->rehashidx] == NULL) { d->rehashidx++; if (--empty_visits == 0) return 1; } de = d->ht[0].table[d->rehashidx]; while(de) { uint64_t h; nextde = de->next; h = dictHashKey(d, de->key) & d->ht[1].sizemask; de->next = d->ht[1].table[h]; d->ht[1].table[h] = de; d->ht[0].used--; d->ht[1].used++; de = nextde; } d->ht[0].table[d->rehashidx] = NULL; d->rehashidx++; } return 1; } 이 코드는 두 가지 안전장치를 두고 있습니다. 첫째, 빈 버킷이 길게 이어질 때 CPU 를 계속 쓰지 않도록 empty_visits 한도를 설정합니다. 둘째, 한 버킷 안에 매달린 연결 리스트 전체를 옮긴 뒤 rehashidx 인덱스를 1 늘립니다. 이 함수는 두 곳에서 호출됩니다. 첫째는 명령어가 들어올 때 실행되는 _dictRehashStep 함수입니다. /* redis/src/dict.c 의 _dictRehashStep */ static void _dictRehashStep(dict *d) { if (d->iterators == 0) dictRehash(d,1); } dictAdd , dictFind , dictGenericDelete 등 모든 dict 접근 연산의 입구에서 _dictRehashStep 이 호출되어 딱 1 버킷( n=1 )을 옮깁니다. 둘째는 클라이언트 요청이 없는 한가한 시간(idle time)입니다. 이벤트 루프(Event Loop)가 쉴 때 dictRehashMilliseconds 함수를 호출하여 1ms 예산 동안 리해시를 최대한 가속합니다. /* redis/src/dict.c 의 dictRehashMilliseconds */ int dictRehashMilliseconds(dict *d, int ms) { long long start = timeInMilliseconds(); int rehashes = 0; while(dictRehash(d,100)) { rehashes += 100; if (timeInMilliseconds()-start > ms) break; } return rehashes; } 명령어가 들어올 때 1 버킷씩 옮기는 처리와 쉬는 시간에 1ms 씩 옮기는 처리가 협력하여 1억 개 키의 리해시를 멈춤 없이 안전하게 마칩니다. 왜 amortized O(1) 대신 worst-case O(1) 을 선택했나요? 학술적으로는 표준 해시 테이블의 분할 상환 O(1) 분석도 정직한 분석입니다. 하지만 분석 모델이 전제하는 환경과 실제 운영 환경 사이에 간극이 있습니다. 분할 상환 분석은 연산의 비용이 수많은 연산 전체에 균등하게 분산될 수 있다고 가정합니다. 멀티스레드 애플리케이션이나 개별 객체 수준에서는 한 스레드가 리해시 비용을 떠안더라도 다른 스레드가 영향을 받지 않거나, 전체 수명주기 안에서 평균을 낼 수 있습니다. 하지만 싱글 스레드로 작동하는 Redis 는 모든 클라이언트 요청을 단 하나의 스레드가 순차적으로 처리합니다. 리해시 연산이 일어나는 순간 그 단일 스레드가 멈추면, 뒤이어 들어오는 모든 클라이언트의 요청이 큐에 쌓이고 타임아웃이 발생합니다. 분할 상환 분석의 평균값은 의미를 잃고 p99 지연 시간(p99 latency)이 심각하게 상해버립니다. Redis 는 최악의 상황에서도 O(1) 시간 복잡도를 보장하는 최악 시간 O(1)(worst-case O(1)) 구조를 선택했습니다. 각 명령어마다 1 버킷을 옮기는 약간의 고정 비용을 추가하는 대신, 최악의 순간에 100ms 가 멈추는 위험을 완벽하게 제거한 것입니다. 평균 지연 시간을 아주 미세하게 올리는 대가로 최악 지연 시간을 확실하게 통제하는 설계입니다. 면접에서 이 질문을 받는다면 「Redis 는 해시 테이블을 키울 때 왜 멈추지 않나요?」라는 질문을 받는다면 다음과 같이 답변할 수 있습니다. Redis 는 전체 리해시를 한 번에 실행하지 않고 잘게 쪼개어 처리하는 점진적 리해시(incremental rehash) 방식을 사용합니다. 내부 자료구조 안에 두 개의 해시 테이블( ht[0] , ht[1] )을 두고, 평소에는 하나만 쓰다가 리해시가 시작되면 두 배 크기의 새 테이블을 할당합니다. 이후 클라이언트의 요청 명령어가 들어올 때마다 기존 테이블의 버킷을 1개씩 새 테이블로 옮깁니다. 이와 함께 이벤트 루프가 쉬는 한가한 시간에 1ms 단위로 리해시를 추가 진행합니다. 이렇게 최악 연산 비용을 시간에 분산시킴으로써 싱글 스레드 환경에서도 지연 시간이 튀어오르는 현상을 막습니다. 꼬리 질문으로 「리해시 진행 중에 데이터 조회가 들어오면 어떻게 처리하나요?」라고 물어볼 수 있습니다. rehashidx 변수를 통해 현재 리해시 진행 상태인지 확인합니다. 리해시 중이라면 먼저 기존 테이블인 ht[0] 에서 키를 찾고, 없으면 새 테이블인 ht[1] 을 추가로 조회합니다. 반면 새로운 키를 삽입하는 작업은 무조건 새 테이블인 ht[1] 에만 기록하여 기존 테이블이 더 이상 자라지 않도록 조율합니다. 개발자: 데일리 익명 면접 챌린지 들어가 보기 → 하나 더: 왜 해시 테이블을 굳이 두 개나 둘까요? 한 개의 배열 안에서 인덱스 경계선을 두고 옮긴 영역과 안 옮긴 영역을 구분하는 구현도 떠올릴 수 있습니다. 메모리를 절약할 수 있어 보이지만 Redis 는 해시 테이블 두 개( ht[0] , ht[1] )를 들고 있는 방식을 선택했습니다. 이유는 코드의 단순성과 명확성 때문입니다. 테이블 두 개를 분리하면 읽기는 ht[0] 과 ht[1] 순차 조회, 리해시 중 새 키 삽입은 ht[1] 단독 쓰기, 갱신 및 삭제는 두 테이블 검색 후 처리라는 단순한 조건문 몇 줄로 구현됩니다. 한 개 배열 안에서 비트 플래그를 관리하며 옮김 여부를 판단하면 복잡한 분기 조건이 늘어나 버그가 생기기 쉽습니다. Redis 는 리해시 동안 메모리를 2배로 사용하는 임시 비용을 지불하는 대신, 버그를 줄이고 유지보수성을 극대화하는 단순성(Simplicity)을 선택했습니다. 함께 읽기 「exactly-once 켰으니 중복 없음」 PR 한 줄: Kafka 보장이 끝나는 경계와 면접 답 Redis 백업 중 메모리가 두 배로 찍혔다: 서버를 늘리기 전에, 면접에서 답하기 전에 볼 세 가지 이 글의 출처 이 글은 팀그릿 책 「그릿 딥다이브 Vol.1 · Redis」 2주차 「깊이」의 6장 Dict 내용을 바탕으로 작성했습니다. 나머지 상세한 C 언어 소스코드 분석과 메모리 레이아웃 구조는 책에 담겨 있습니다. 그릿 딥다이브 Vol.1 · Redis 목차 보기 →