오늘의 핵심 오늘은 단순히 개념을 외우는 수준보다, 왜 이런 식이 나오는지 수식적으로 연결하는 것 에 집중했다. 흐름은 크게 다음과 같다. BCE → Mini-batch Gradient → 분산 → Xavier Initialization → Entropy → Cross Entropy → KL Divergence → Mutual Information 1. Binary Cross Entropy는 무엇을 보는가? 📌 Binary Classification의 출력 Binary Classification에서 실제 정답은 보통 $$ y \in {0,1} $$ 이다. 예를 들어 스팸 분류라면: $y=1$ : 스팸 $y=0$ : 스팸 아님 하지만 모델은 바로 0 또는 1 을 출력하는 것이 아니라, 보통 $$ p = P(y=1\mid x) $$ 즉 입력 $x$가 class 1일 확률 을 예측한다. 예를 들어: p = 0.8 이면 모델은 해당 입력이 class 1일 확률을 80%로 보고 있다는 뜻이다. 최종 class는 필요하다면 threshold를 적용해 결정한다. p >= 0.5 → class 1 p < 0.5 → class 0 📌 BCE의 목적 Binary Cross Entropy는 실제 정답 $y\in{0,1}$과 모델이 예측한 확률 $p\in[0,1]$이 얼마나 잘 맞는지 를 측정하는 Loss다. 수식은 다음과 같다. $$ L = -\left[ y\log p + (1-y)\log(1-p) \right] $$ 여기서: $y$ : 실제 정답 $p$ : 모델이 예측한 class 1의 확률 정답이 1이면: $$ L=-\log p $$ 정답이 0이면: $$ L=-\log(1-p) $$ 따라서 모델이 정답 클래스에 높은 확률을 줄수록 Loss가 작아진다. 2. Cross Entropy에서 BCE로 내려오기 일반적인 Cross Entropy는 $$ L = -\sum_{k=1}^{K} y_k \log p_k $$ 이다. Binary Classification은 class가 두 개뿐이므로 $K=2$라고 생각하면 된다. class 1의 확률을 $p$라고 하면 class 0의 확률은 $$ 1-p $$ 이고, 정답도 하나의 binary 값 $y$로 표현하면 $$ y_1=y,\qquad y_0=1-y $$ 가 된다. 따라서 그대로 대입하면 $$ L = -\left[ y\log p + (1-y)\log(1-p) \right] $$ 즉 BCE가 나온다. 💡 BCE는 Cross Entropy를 Binary Classification에 맞게 표현한 형태라고 볼 수 있다. 3. 왜 마지막 Activation으로 Sigmoid를 사용하는가? 📌 Linear 출력은 확률이 아니다 마지막 Linear Layer는 보통 $$ z = Wx+b $$ 를 출력한다. 이때 $z$의 범위는 $$ -\infty < z < \infty $$ 이므로 그대로는 확률로 해석할 수 없다. Sigmoid는 $$ \sigma(z) = \frac{1}{1+e^{-z}} $$ 이고 출력 범위는 $$ 0 < \sigma(z) < 1 $$ 이다. 즉: Linear ↓ logit z ↓ Sigmoid ↓ class 1일 확률 p ↓ BCELoss 의 흐름이 만들어진다. 📌 왜 ReLU는 마지막 Activation으로 부적절한가? ReLU는 $$ ReLU(z)=\max(0,z) $$ 이고 출력 범위는 $$ [0,\infty) $$ 이다. 하지만 BCE에서 $p$는 확률이므로 $$ 0 \le p \le 1 $$ 이어야 한다. 따라서 2 , 10 같은 값도 출력할 수 있는 ReLU는 BCE 앞의 최종 Activation으로 부적절하다. 🎯 BCE가 원하는 것은 class 1의 확률이고, ReLU는 확률 범위를 보장하지 않는다. 4. BCELoss와 BCEWithLogitsLoss 📌 BCELoss BCELoss 는 이미 확률로 변환된 값을 입력으로 받는다. model = nn.Sequential( nn.Linear(..., 1), nn.Sigmoid() ) criterion = nn.BCELoss() 즉: logit ↓ Sigmoid ↓ probability ↓ BCELoss 이다. 📌 BCEWithLogitsLoss PyTorch에서는 보통 다음 형태를 더 많이 사용한다. model = nn.Linear(..., 1) criterion = nn.BCEWithLogitsLoss() 이 경우 마지막에 Sigmoid() 를 직접 붙이지 않는다. BCEWithLogitsLoss 가 내부적으로 Sigmoid + BCE를 수치적으로 더 안정적인 방식으로 계산 하기 때문이다. 🚨 BCEWithLogitsLoss 를 사용할 때 마지막에 Sigmoid를 중복해서 넣으면 안 된다. 5. Mini-batch Gradient Descent 📌 GD와 SGD 복습 전체 데이터가 $N$개라고 하자. 전체 Loss는 $$ L(\theta) = \frac{1}{N} \sum_{i=1}^{N} L_i(\theta) $$ 이다. 여기서: $\theta$ : 모델의 모든 Parameter $L_i$ : i번째 데이터의 Loss Batch Gradient Descent는 전체 데이터를 사용한다. $$ g = \nabla_\theta L = \frac{1}{N} \sum_{i=1}^{N} \nabla_\theta L_i $$ Pure SGD는 데이터 하나만 사용한다. $$ g_i = \nabla_\theta L_i $$ 📌 Mini-batch는 둘의 중간 Batch Size가 $B$라면 $$ L_B = \frac{1}{B} \sum_{i\in \mathcal{B}} L_i $$ 를 계산한다. 그리고 $$ g_B = \nabla_\theta L_B $$ 를 사용한다. 미분은 선형이므로 $$ \nabla_\theta \left( \frac{1}{B} \sum_{i\in\mathcal{B}}L_i \right) = \frac{1}{B} \sum_{i\in\mathcal{B}} \nabla_\theta L_i $$ 이다. 각 데이터의 Gradient를 직접 하나씩 구해서 평균내는 것과, 평균 Loss를 한 번 Backward 하는 것은 같은 결과를 준다. 6. PyTorch에서는 평균 Loss를 통해 Gradient 평균이 만들어진다 PyTorch의 많은 Loss 함수는 기본적으로 reduction="mean" 을 사용한다. 예를 들어: criterion = nn.MSELoss() loss = criterion(y_hat, y) loss.backward() 이면: 각 데이터 Loss 계산 ↓ Batch 평균 Loss ↓ Backward ↓ 결과적으로 Batch Gradient의 평균 이 만들어진다. 7. Batch Size와 Gradient Variance Mini-batch Gradient를 $$ g_B = \frac{1}{B} \sum_{i=1}^{B}g_i $$ 라고 하자. 각 데이터의 Gradient가 서로 어느 정도 독립이라고 단순하게 가정하면 $$ Var(g_B) \approx \frac{Var(g_i)}{B} $$ 이므로 $$ Var(g_B)\propto\frac{1}{B} $$ 이다. 즉: Batch Size ↑ ↓ 더 많은 Gradient를 평균 ↓ 개별 데이터 때문에 튀는 정도 감소 ↓ Gradient Variance ↓ 💡 여기서 분산은 Mini-batch를 다르게 뽑았을 때 Gradient 추정값이 얼마나 흔들리는가 를 의미한다. 8. Mini-batch의 분산과 초기화의 분산은 다른 이야기다 Mini-batch에서는 $$ g_B = \frac{g_1+\cdots+g_B}{B} $$ 처럼 평균 을 낸다. 반면 초기화에서는 다음 Layer의 Pre-activation이 $$ z = w_1x_1+w_2x_2+\cdots+w_nx_n $$ 처럼 여러 값을 합 한다. 여기서: $x_i$ : 이전 Layer의 Activation $w_i$ : Weight $n$ : Input Feature 수, 즉 fan-in $z$ : Activation Function을 통과하기 전 값 9. 분산(Variance)이란? 분산은 값 자체가 크냐 작냐가 아니라 값들이 자신의 평균 주변에서 얼마나 퍼져 있는가 를 나타낸다. $$ Var(X) = E[(X-E[X])^2] $$ 예를 들어: [1000, 1000, 1000] 은 값은 크지만 모두 같으므로 분산은 0이다. 반면: [-1000, 0, 1000] 은 평균 주변에서 크게 퍼져 있으므로 분산이 크다. 📌 Scale과 분산 중요한 성질은 $$ Var(aX) = a^2Var(X) $$ 이다. 즉 Weight 전체의 Scale을 줄이면 Weight 분포의 분산도 자연스럽게 줄어든다. 10. 초기화에서 왜 fan-in이 커지면 Weight를 작게 해야 하는가? 다음 식을 생각하자. $$ z = \sum_{i=1}^{n}w_ix_i $$ 일반적으로는 $$ Var(z) = \sum_iVar(w_ix_i) + 2\sum_{i<j}Cov(w_ix_i,w_jx_j) $$ 이다. 초기화 이론에서는 단순화를 위해 다음과 같은 가정을 둔다. Weight들이 서로 독립 입력 Activation들도 대략 비상관 평균이 0에 가까움 $w_i$와 $x_i$가 독립 그러면 Covariance 항을 무시할 수 있고 $$ Var(z) \approx \sum_iVar(w_ix_i) $$ 가 된다. 또한 $$ Var(w_ix_i) \approx Var(w_i)Var(x_i) $$ 라고 두면 $$ \boxed{ Var(z) \approx nVar(w)Var(x) } $$ 가 된다. 즉: fan-in n ↑ ↓ 더 많은 w_i x_i 항의 분산이 더해짐 ↓ Var(z) ↑ 가능 ↓ Weight Scale / Var(w)를 줄일 필요가 있음 11. 좋은 초기화의 핵심 좋은 초기화에는 두 가지 목표가 있다. 📌 1. Symmetry Breaking 모든 Weight가 동일하면 뉴런들이 같은 Feature를 학습할 수 있다. 따라서 Weight들이 서로 다른 값을 가져야 한다. 📌 2. 적절한 Scale Weight가 너무 크면 Activation과 Gradient가 불안정해질 수 있고, 너무 작으면 Signal이 지나치게 작아질 수 있다. 🎯 좋은 초기화 = 서로 다른 Weight + 적절한 Weight Scale 12. Xavier Initialization 📌 Forward 관점 Forward에서 $$ Var(z) \approx fan_{in}Var(w)Var(x) $$ 이다. Layer를 지나도 Activation의 분산을 대략 유지하고 싶다면 $$ Var(z) \approx Var(x) $$ 를 목표로 둘 수 있다. 따라서 $$ fan_{in}Var(w)Var(x) \approx Var(x) $$ 이고 $Var(x)\neq0$라면 $$ Var(w) \approx \frac{1}{fan_{in}} $$ 이 된다. 📌 Backward 관점 Backward에서는 대략 $$ Var(\delta_{prev}) \approx fan_{out}Var(w)Var(\delta_{next}) $$ 라고 볼 수 있다. Gradient의 분산을 유지하려면 $$ Var(w) \approx \frac{1}{fan_{out}} $$ 이 필요하다. Forward와 Backward의 요구가 다를 수 있으므로 Xavier는 둘을 절충해 $$ \boxed{ Var(w) = \frac{2}{fan_{in}+fan_{out}} } $$ 를 사용한다. 13. Xavier Normal Xavier Normal은 $$ w \sim \mathcal{N} \left( 0, \frac{2}{fan_{in}+fan_{out}} \right) $$ 이다. 표준편차는 $$ std = \sqrt{ \frac{2}{fan_{in}+fan_{out}} } $$ 이다. PyTorch: nn.init.xavier_normal_(model.weight) 14. Xavier Uniform Uniform Distribution에서 $$ w\sim U(-a,a) $$ 이면 $$ Var(w)=\frac{a^2}{3} $$ 이다. Xavier의 목표 분산과 맞추면 $$ \frac{a^2}{3} = \frac{2}{fan_{in}+fan_{out}} $$ 이므로 $$ a = \sqrt{ \frac{6}{fan_{in}+fan_{out}} } $$ 이다. 즉 $$ w \sim U\left( -\sqrt{\frac{6}{fan_{in}+fan_{out}}}, +\sqrt{\frac{6}{fan_{in}+fan_{out}}} \right) $$ 이다. PyTorch: nn.init.xavier_uniform_(model.weight) 15. Xavier는 학습 내내 분산을 유지하는 방법인가? 아니다. Xavier는 학습 시작 시 Activation과 Gradient가 너무 커지거나 작아지지 않도록 안정적인 초기 조건을 만들어주는 초기화 방법 이다. 학습이 시작되면 $$ W \leftarrow W - \eta\nabla_WL $$ 에 의해 Weight가 계속 바뀐다. 따라서 Xavier가 처음 설정한 분산이 학습 내내 유지된다는 보장은 없다. 또 Xavier의 유도에는 독립성, 평균 0, 비슷한 분산 등의 가정이 들어가므로 실제 네트워크에서 정확히 분산이 보존되는 것도 아니다. 💡 Xavier의 목적은 학습 가능한 좋은 출발점 을 만드는 것이다. 16. Entropy — 정보 하나의 정보량 어떤 사건 $x$가 발생할 확률이 $p(x)$라고 하자. 그 사건의 정보량(Self-information)은 $$ I(x) = -\log_2p(x) $$ 로 정의한다. 확률이 높은 사건은 자주 발생하므로 정보량이 작고, 확률이 낮은 사건은 드물기 때문에 정보량이 크다. 예를 들어: $$ p(x)=\frac12 $$ 이면 $$ I(x) = -\log_2\frac12 = 1\text{ bit} $$ 이다. 또 $$ p(x)=\frac18 $$ 이면 $$ I(x) = 3\text{ bits} $$ 이다. 17. 왜 로그 밑이 2인가? Binary Code 관점에서 정보를 표현한다면 밑이 2인 로그를 사용한다. $$ I(x) = -\log_2p(x) $$ 이 경우 단위는 bit이다. 로그 밑을 $e$로 사용하면 단위는 nat이다. 💡 로그 밑이 2인 이유는 정보가 반드시 이진이라서가 아니라, 이진 부호화 관점에서 정보량의 단위를 bit로 표현하기 위해서 다. 18. Entropy Entropy는 각 사건의 정보량을 확률적으로 평균낸 값이다. $$ \boxed{ H(X) = -\sum_xp(x)\log_2p(x) } $$ 또는 기대값으로 $$ H(X) = E_{x\sim P} [-\log_2p(x)] $$ 라고 쓸 수 있다. 즉: Entropy는 실제 확률분포 $P$를 알고 있을 때, 그 분포에서 발생하는 데이터를 표현하는 데 필요한 평균 정보량이다. 정보이론적으로는 긴 symbol sequence를 이상적으로 부호화할 때 얻을 수 있는 최적 평균 부호 길이의 이론적 하한과 연결되는 값 이다. 🚨 Entropy와 정확히 같은 길이의 단일 symbol code가 항상 존재한다는 뜻은 아니다. 긴 sequence를 효율적으로 부호화할수록 평균 code length를 Entropy에 가깝게 만들 수 있다. 19. Cross Entropy 실제 데이터 분포를 $P$라고 하고, 우리가 모델링한 예측 분포를 $Q$라고 하자. Cross Entropy는 $$ \boxed{ H(P,Q) = -\sum_xp(x)\log_2q(x) } $$ 또는 $$ H(P,Q) = E_{x\sim P} [-\log_2q(x)] $$ 이다. 중요한 점은: 실제 데이터는 $P$에서 발생 정보량 계산에는 $Q$를 사용 한다는 것이다. 즉: 실제 데이터가 $P$를 따르는데, 우리가 $Q$라는 확률 모델을 사용해 데이터를 표현할 때 필요한 기대 정보량 이라고 볼 수 있다. 20. Entropy와 Cross Entropy의 차이 Entropy: $$ H(P) = -\sum_xp(x)\log_2p(x) $$ Cross Entropy: $$ H(P,Q) = -\sum_xp(x)\log_2q(x) $$ 즉: Entropy : 실제 분포 P를 사용 Cross Entropy : 실제 데이터는 P지만 예측 분포 Q를 사용 모델이 실제 분포를 정확히 맞히면 $$ Q=P $$ 이므로 $$ H(P,Q)=H(P) $$ 가 된다. 21. KL Divergence Cross Entropy와 Entropy의 차이가 KL Divergence다. $$ \boxed{ D_{KL}(P\parallel Q) = H(P,Q)-H(P) } $$ 이를 풀어쓰면 $$ D_{KL}(P\parallel Q) = \sum_x p(x) \log_2 \frac{p(x)}{q(x)} $$ 이다. 📌 의미 Entropy $H(P)$는 실제 분포를 알고 있을 때의 최적 기대 정보량이고, Cross Entropy $H(P,Q)$는 $Q$를 사용했을 때의 기대 정보량이다. 따라서 $$ H(P,Q)-H(P) $$ 는 잘못되거나 불완전한 분포 $Q$를 사용해서 생기는 추가적인 기대 정보 비용 으로 해석할 수 있다. 즉: 최적 기대 비용 H(P) + 분포를 잘못 가정해서 생긴 추가 비용 D_KL(P||Q) = 실제 기대 비용 H(P,Q) 이다. 📌 중요한 성질 $$ D_{KL}(P\parallel Q)\ge0 $$ 이고, 두 분포가 같으면 $$ D_{KL}(P\parallel Q)=0 $$ 이다. 하지만 일반적인 거리함수와 달리 $$ D_{KL}(P\parallel Q) \neq D_{KL}(Q\parallel P) $$ 일 수 있다. 즉 KL Divergence는 대칭적이지 않다. 22. Cross Entropy Loss와 KL Divergence의 관계 실제 데이터 분포 $P$가 고정되어 있다면 $$ H(P) $$ 는 모델 Parameter와 무관한 상수다. 그리고 $$ H(P,Q) = H(P) + D_{KL}(P\parallel Q) $$ 이므로 Cross Entropy를 최소화하는 것은 결국 $$ D_{KL}(P\parallel Q) $$ 를 최소화하는 것과 같다. 즉: Cross Entropy Loss를 줄인다는 것은 모델의 예측 분포 $Q$를 실제 데이터 분포 $P$에 가깝게 만드는 것이라고 볼 수 있다. 23. Mutual Information 이제 KL Divergence를 이용해 두 변수 $X$, $Y$가 얼마나 서로 의존하는지를 측정할 수 있다. 두 변수가 독립이라면 $$ P(X,Y) = P(X)P(Y) $$ 이다. 따라서 다음 두 분포를 비교하면 된다. 실제 Joint Distribution: $$ P_{XY}(x,y) $$ 독립이라고 가정했을 때의 분포: $$ P_X(x)P_Y(y) $$ 이 둘의 차이를 KL Divergence로 측정한 것이 Mutual Information이다. $$ \boxed{ I(X;Y) = D_{KL} \left( P_{XY} \parallel P_XP_Y \right) } $$ 24. Mutual Information의 수식 풀어쓰면 $$ \boxed{ I(X;Y) = \sum_{x,y} p(x,y) \log \frac{p(x,y)}{p(x)p(y)} } $$ 이다. 📌 독립이면? 독립이라면 $$ p(x,y)=p(x)p(y) $$ 이므로 $$ \frac{p(x,y)}{p(x)p(y)}=1 $$ 이고 $$ \log1=0 $$ 이다. 따라서 $$ I(X;Y)=0 $$ 이다. 즉: Mutual Information이 0이면 두 변수는 독립이다. 📌 의존성이 강하면? $X$를 알았을 때 $Y$에 대한 불확실성이 크게 줄어든다면 Mutual Information이 커진다. 따라서: Mutual Information은 $X$와 $Y$가 서로 공유하는 정보의 양 이라고 해석할 수 있다. 25. Mutual Information과 Entropy의 관계 Mutual Information은 다음과 같이도 쓸 수 있다. $$ \boxed{ I(X;Y) = H(X)+H(Y)-H(X,Y) } $$ 또는 $$ \boxed{ I(X;Y) = H(X)-H(X\mid Y) } $$ 이며 $$ I(X;Y) = H(Y)-H(Y\mid X) $$ 이기도 하다. 여기서 $H(X)$는 원래 $X$에 대한 불확실성이고,
브랜드의 메시지를 디지털 채널에 최적화된 비주얼로 전달할 수 있어야 합니다. 강력한 이미지 편집 툴인 포토샵(Photoshop)을 활용해 브랜드의 스토리를 담은 3장짜리 카드뉴스를 제작해 봅니다. 1. 분석 및 계획 수립 카드뉴스 주제 및 브랜드 선정 (예: 새로 출시된 텀블러 홍보, 브랜드의 친환경 가치관 소개 등) 구성 요소 기획: 1장 (표지), 2장 (내용), 3장 (엔딩) 의도 파악 및 톤앤매너 설정 [예시 참고] 2. 카드뉴스 제작의 원칙 가독성 최우선: 텍스트가 배경 이미지에 묻히지 않도록 글자 뒤에 어두운 반투명 사각형을 깔거나 이미지의 밝기를 조절 시각적 일관성: 폰트, 정렬 방식, 핵심 컬러, 레이아웃 분위기가 하나로 이어져야 "하나의 브랜드가 만든 콘텐츠"로 인식 3. 실습 포토샵을 켜고 디지털 화면(인스타그램 등)에 최적화된 정사각형 해상도 (1080*1080px, 72ppi)로 새 대지를 만듭니다. 기획한 내용에 맞는 고화질 무료 이미지(Unsplash 등 활용)를 불러와 배치하고, 텍스트 도구(T)를 사용해 타이포그래피 위계를 잡아가며 글씨를 씁니다. 1장(표지)을 완성한 뒤, 레이아웃 구조를 유지한 채 내용만 바꾸어 2장(내용)과 3장(엔딩)을 차례로 제작합니다. 완성된 이미지는 File > Export > Save for Web 을 통해 JPEG 파일로 저장합니다. 느낀 점 칼럼에는 포토샵 7일 체험판이 있다고 나와있지만 1시간 동안 찾지를 못한 관계로 모바일 무려 판을 사용했는데 역시 컴퓨터와는 차이가 있을뿐더러 터치 인식이나 여러 가지 고난을 겪었다. 본 캠프에서 제대로 된 연습이 필요할 것 같다.
[Backend] Uvicorn, FastAPI, Nginx의 역할 분담과 비동기(ASGI)의 비밀 (feat. C#과의 차이점) 웹 백엔드 아키텍처를 공부하다 보면 꼭 마주치는 조합이 있습니다. 바로 Nginx + Uvicorn + FastAPI입니다. "요청을 받아서 처리한다"는 점에서 다 비슷해 보이지만, 실제 내부를 들여다보면 각자의 역할이 명확히 나뉘어 있습니다. 이번 글에서는 이 세 가지 기술의 관계를 현실적인 비유로 알아보고, 많은 개발자들이 헷갈려하는 파이썬 비동기(ASGI)의 스레드 모델을 C#과의 비교를 통해 완벽하게 정리해 보겠습니다. 1. 한눈에 보는 역할 분담 (식당 비유 🏢) 이들의 관계는 '대형 종합 식당'의 운영 구조와 완전히 같습니다. Nginx (종합 안내 데스크 / 문지기): 세상 모든 사람(인터넷 외부)을 상대하며, 입구에서 자물쇠(HTTPS)를 풀어주고 손님을 적절한 구역으로 안내합니다. Uvicorn (고성능 서빙 로봇 / ASGI 서버): Nginx가 들여보낸 손님의 주문을 받아서 주방에 빛의 속도로 전달하고, 요리가 나오면 손님에게 배달합니다. 파이썬 코드를 읽을 줄 아는 통역사입니다. FastAPI (주방의 메인 셰프 / 웹 프레임워크): Uvicorn이 준 주문서(요청)를 바탕으로 데이터베이스를 뒤지거나 비즈니스 로직을 수행하여 진짜 요리(결과 데이터)를 만들어냅니다. 2. 현실적인 대작전 시나리오: 아이돌 콘서트 티켓팅 오픈! 🎬 사용자 1만 명이 동시에 예매 버튼을 눌렀을 때 내부에서 일어나는 실제 흐름입니다. 1단계: Nginx의 철통 방어 (HTTPS & 로드밸런싱) HTTPS 복호화: 손님들이 보낸 요청은 암호화(HTTPS)되어 있습니다. Nginx는 맨 앞에서 이 자물쇠를 번개 같은 속도로 풀어냅니다. (뒤쪽 엔진들의 연산 부담을 줄여줌) 로드밸런싱: Nginx 뒤에 Uvicorn 엔진 3대(A, B, C 포트)를 대기시켜 두고, 1만 명의 요청을 골고루 쪼개서 분배합니다. (Reverse Proxy 역할) 2단계: Uvicorn의 ASGI 비동기 마법 (초고속 접수) 구식 서버(WSGI)는 앞 손님의 요리가 끝날 때까지 뒤 손님을 대기시킵니다. 반면 Uvicorn(ASGI)은 1번 손님의 주문을 주방(FastAPI)에 던진 후, 주방이 데이터베이스를 조회하느라 멈춰 있는 시간(약 0.5초)을 가만히 기다리지 않습니다. "어, 1번 손님 조회 중이야? 오케이, 그럼 난 그 사이에 2번, 3번, 4번 주문 계속 받을게!" 하며 요청을 멈춤 없이 접수합니다. 3단계: FastAPI의 영리한 일 처리 (비동기 셰프) FastAPI 코드가 async def로 짜여 있다면, 셰프 역시 멀티태스킹의 신이 됩니다. 1번 주문의 DB 응답을 기다리는 동안 2번 주문을 처리하고, 1번 DB 결과가 도착하면 하던 걸 잠깐 멈추고 1번 요리를 완성해 Uvicorn에게 던집니다. 컴퓨터 세계에서 가장 느린 'I/O 대기 시간(DB 조회, 외부 API 호출)'을 완전히 재활용하는 구조입니다. 3. 라우팅 이후, 실제 코드는 어떤 스레드가 처리할까? 🤔 FastAPI가 내부적으로 스레드를 어떻게 쓰느냐는 개발자가 코드를 어떻게 짰느냐(async def vs def)에 따라 완전히 달라집니다. 1 async def로 짰을 때: 멀티태스킹의 신, 싱글 스레드 @app.get("/ticket")async def book_ticket(): await db.check_seat() # DB를 기다리는 동안 스레드는 다른 일을 하러 감 return {"status": "success"} 동작 방식: 별도의 프로세스나 스레드가 새로 생성되지 않습니다. Uvicorn의 단 하나의 메인 스레드 위에서 돕니다. 원리: await를 만나는 순간, 메인 스레드는 작업을 옆으로 치워두고 즉시 다음 손님의 요청을 처리합니다. 이벤트 루프(Event Loop)가 완료 신호를 주면 다시 돌아와 마무리합니다. (Context Switching 비용 최소화) 2 그냥 def로 짰을 때: 스레드 풀(Thread Pool)의 도우미들 @app.get("/heavy-job")def heavy_job(): time.sleep(2) # 메인 스레드를 얼려버리는 Blocking 코드 return {"status": "done"} 동작 방식: 파이썬에서 일반 def 코드가 멈추면 메인 스레드 전체가 얼어버립니다(Blocking). 원리: 이를 막기 위해 FastAPI는 내부적으로 '스레드 풀(Thread Pool)'이라는 서브 스레드(임시 직원)를 깨워 일을 넘깁니다. 메인 스레드는 자유 몸이 되어 다음 손님을 받고, 서브 스레드가 백그라운드에서 일을 처리합니다. 💡 참고 (Process는 언제 쓰나요?): 파이썬은 GIL(Global Interpreter Lock) 제약 때문에 하나의 프로세스 내에서 멀티 스레드를 써도 CPU 연산을 동시에 할 수 없습니다. 따라서 멀티코어 CPU를 100% 쓰려면 uvicorn main:app --workers 4처럼 Uvicorn 프로세스 자체를 여러 개 띄우고, 맨 앞의 Nginx가 이 프로세스들에게 요청을 분산해야 합니다. 4. ⚠️ 닷넷(C#) 개발자가 가장 흔하게 하는 착각! (C# vs Python 비동기) C# 개발자가 파이썬 FastAPI 생태계에 오면 엄청난 문화 충격(Culture Shock)을 겪습니다. 이름은 같은 async/await인데 스레드가 작동하는 방식이 정반대이기 때문입니다. 항목 C# (Task 기반 비동기) 🖥️ Python FastAPI (이벤트 루프) 🐍 기본 사상 멀티 스레드 기반 (스레드가 기본적으로 많음) 싱글 스레드 기반 (스레드가 기본적으로 1개) async 안 썼을 때 메인 스레드가 그대로 멈춤 (Blocking) FastAPI가 스레드 풀에 일을 넘겨 메인 스레드를 살림 async 썼을 때 await 시 스레드 풀의 다른 스레드로 작업이 넘어가서 실행됨 Microsoft Learn await 시 동일한 메인 스레드를 유지하며, 대기 시간 동안 눈알만 딴 데 돌려 다른 요청을 받음 C#에서의 비동기: 기본적으로 풍부한 멀티 스레드 풀을 활용합니다. 비동기를 써야 스레드 풀의 다른 직원에게 효율적으로 일을 넘기며(Task) 스레드를 나눠 씁니다 Microsoft Learn. FastAPI에서의 비동기: GIL 제약 때문에 "스레드 1개만 지독하게 굴리자"는 싱글 스레드 사상(Node.js와 유사)입니다. 비동기(async)를 써야 메인 스레드 하나로 광속 멀티태스킹이 일어나고, 비동기를 안 쓰면(def) 메인 스레드가 죽는 걸 막으려고 프레임워크가 억지로 스레드 풀을 켜서 유배를 보내는 구조입니다. 📝 총정리 및 결론 Nginx는 문앞에서 포트를 분배하고 암호(HTTPS)를 풀어주는 총괄 매니저입니다. Uvicorn은 Nginx에게 받은 요청을 파이썬 객체로 번역하는 비동기 엔진입니다. FastAPI에서 async def를 쓰면 단 하나의 메인 스레드가 징검다리 건너듯 비동기로 모든 요청을 광속 처리합니다. FastAPI에서 def를 쓰면 메인 스레드가 멈추는 걸 방지하기 위해 내부 스레드 풀(서브 스레드)로 작업을 토스합니다.
결론부터 말하면 이렇다. 보안 취약점 시연 사이트를 만들 때 진짜 어려운 건 XSS나 SQL Injection 코드를 짜는 게 아니었다. 진짜 벽은 두 개였다. "해킹"이라는 표현을 AI 안전 분류기가 어떻게 받아들이는지, 그리고 더블클릭으로 여는 정적 파일이 브라우저 안에서 얼마나 할 수 있는 게 없는지. 상황 사내 바이브코딩 강의에서 실습으로 보여줄 사이트를 만들어야 했다. AI로 코드를 짤 때 흔히 딸려오는 취약점 4종 — XSS, SQL Injection, 파일 업로드, Rate limit(DDoS) — 을 눈으로 보여주는 사이트다. 구성은 단순했다. 한쪽엔 "사용자가 쓰는 화면", 다른 쪽엔 "그로 인해 뭐가 새는지 보는 화면". 왼쪽에서 공격하면 오른쪽에 결과가 뜨는 연출이 핵심이었다. 첫 번째 벽 — 말 한마디에 응답이 끊긴다 안전 분류기 : AI 응답이 위험한 요청인지 판단해서 생성 자체를 막는 필터링 단계. 초기 요청에 "해킹", "SQL Injection", "악성 스크립트 업로드" 같은 말을 그대로 섞어 썼더니 두 번 연속 응답이 끊겼다. 문제는 단어가 아니라 프레이밍이었다. "공격 도구를 만들어달라"로 읽히면 막히고, "취약한 코드 때문에 뭐가 새는지 보여주는 관찰 대시보드를 만들어달라"로 읽히면 통과됐다. 목적은 똑같은데 표현만 바꿨을 뿐이다. 이후로는 이 프레임을 설계 전체에 그대로 유지했다 — 공격 재현이 아니라 관찰. 두 번째 벽 — 화면을 나누면 왜 서로 못 알아볼까 프레이밍 문제를 풀고 나니 구조 문제가 남았다. 처음엔 "사용자 화면"과 "해커 화면"을 각각 별도 HTML 파일로 만들려 했다. 그런데 배포 형태가 서버 없이 더블클릭으로 여는 file:// 방식이라는 걸 다시 확인하고 나서 멈췄다. file:// 로 열린 두 개의 HTML 파일은 브라우저 보안 정책상 서로 다른 출처(origin)로 취급된다. localStorage도 BroadcastChannel도 탭 사이에서 공유되지 않거나 불안정하게 동작한다. "왼쪽에서 입력하면 오른쪽에 바로 뜬다"는 연출 자체가 깨질 수 있는 구조였다. 해결 — 파일을 하나로 합치니 문제 자체가 사라졌다 프레임워크 없이 단일 HTML 파일 안에 좌우 분할 레이아웃을 두는 쪽으로 바꿨다. 파일을 하나로 합치면 두 화면이 같은 문서, 같은 메모리 안의 상태를 공유하니 탭 간 통신 문제 자체가 사라진다. 별도 동기화 로직을 설계할 필요가 없어졌다. 랩은 4개 — XSS, SQL Injection, 파일 업로드, Rate limit — 각각에 취약 모드/안전 모드 토글을 붙였다. 업로드 랩에는 사진처럼 보이지만 열면 스크립트가 실행되는 예시 파일을 직접 만들어서, 취약 모드에서 실제로 실행되는 걸 눈으로 보여줬다. Rate limit(DDoS)은 원래 이런 점검 항목에서 곧잘 빠지는 축에 든다. 그래도 "커맨드 한 줄로 보안 점검을 돌리면 안 보고 지나가는 자리"라는 이유로 넣기로 했다. 눈에 보이지 않는 것도 눈으로 보여줘야 실습이 된다는 판단이었다. 트레이드오프 취약/안전 토글은 코드와 연출 분량이 두 배가 된다. 그래도 "지적은 AI가 하고, 고칠지는 사람이 정한다"는 메시지의 핵심이라 그대로 뒀다. 처음엔 코드부터 보여주는 구성이었는데, 비개발자 피드백을 받고 나서 뒤집었다. "이렇게 뚫렸어요 / 이렇게 막아요" 카드를 앞에 두고, 코드는 접어서 옵션으로만 남겼다. 애니메이션(유출 빔, 공격 성공 배너, 단계별 따라하기)은 스크린샷 정적 캡처로는 타이밍 확인이 안 됐다. 화면을 눈으로 보는 대신 상태값을 프로그램적으로 확인하는 방식으로 검증 방법 자체를 바꿨다. file:// 환경에서는 브라우저 확장 권한도 막히기 때문에, 검증은 로컬 서버를 별도로 띄워서 했다. 배포는 file:// , 검증은 서버 — 이 둘을 분리해야 했다. 교훈 보안·해킹을 다루는 교육 콘텐츠는 "방법"이 아니라 "결과를 관찰하는 대시보드"로 프레이밍해야 안전 분류기도, 청중의 이해도 통과한다. 그리고 file:// 로 배포해야 하는 정적 페이지는 탭 간 상태 공유가 막힌다는 전제를 깔고, 별도 파일보다 단일 파일 내 분할 UI를 먼저 고려하는 게 맞다. 코드 문제가 아니라 구조 문제였다. 그리고 그 구조 문제는 배포 방식 하나를 먼저 확인했으면 처음부터 안 생겼을 문제였다.