<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>slowgrid 님의 블로그</title>
    <link>https://slowgrid.tistory.com/</link>
    <description>slowgrid 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 2 Nov 2026 13:32:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>slowgrid</managingEditor>
    <item>
      <title>[YOLO 기초 9] 손실이 낮아지면 좋은 모델일까?</title>
      <link>https://slowgrid.tistory.com/entry/YOLO-Basics-9-Is-a-lower-loss-a-good-model</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;학습 손실, 검증, 테스트, 과적합, 과소적합, 일반화&lt;br&gt;&lt;strong&gt;버전 메모&lt;/strong&gt;&lt;br&gt;이 글은 &lt;strong&gt;2026-09-09 최종 검증 기준 Ultralytics 공식 문서와 공개 구현&lt;/strong&gt;을 직접 확인하면서 정리했다. &lt;code&gt;optimizer=auto&lt;/code&gt;, &lt;code&gt;cls_remap&lt;/code&gt;, 체크포인트 저장 형식, YOLO26의 &lt;code&gt;nms&lt;/code&gt;/손실 이름처럼 구현에 가까운 부분은 버전에 따라 달라질 수 있다. 그래서 실제 실험에서는 &lt;code&gt;ultralytics&lt;/code&gt; 버전, &lt;code&gt;args.yaml&lt;/code&gt;, 학습 로그를 함께 확인해 두는 편이 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;YOLO 학습이 오류 없이 끝나고 학습 손실이 계속 내려갔다고 하자.&lt;/p&gt;
&lt;p&gt;그렇다면 모델이 잘 학습됐다고 말해도 될까?&lt;/p&gt;
&lt;p&gt;꼭 그런 것은 아니다. 객체 탐지에서 중요한 것은 &lt;strong&gt;학습 데이터에 얼마나 잘 맞았는지&lt;/strong&gt;만이 아니라, &lt;strong&gt;학습에 사용하지 않은 새로운 데이터에서도 잘 동작하는지&lt;/strong&gt;이기 때문이다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 loss 자체가 무엇을 보여주는지부터 다시 짚고, train/validation/test의 역할과 과적합 및 과소적합을 어떻게 해석해야 하는지 정리해 보고, Precision, Recall, AP, mAP 같은 탐지 지표의 계산과 해석은 다음 글에서 집중적으로 다룰 예정이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 학습 손실(Training Loss)&lt;/h2&gt;
&lt;p&gt;학습 과정에서는 &lt;strong&gt;배치(batch)&lt;/strong&gt;마다 모델의 &lt;strong&gt;예측(prediction)&lt;/strong&gt;과 &lt;strong&gt;정답(ground truth)&lt;/strong&gt;을 비교해 &lt;strong&gt;손실(loss)&lt;/strong&gt;을 계산한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;입력 배치
   ↓
순전파(Forward)
   ↓
예측
   ↓
정답과 비교
   ↓
손실 계산
   ↓
역전파(Backpropagation)
   ↓
가중치 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정상적으로 &lt;strong&gt;최적화(optimization)&lt;/strong&gt;가 진행된다면 학습 손실은 전체적으로 감소하는 경향을 기대할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;손실

높음
│\
│ \
│  \
│   \____
└──────── 에포크&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다만 실제 그래프가 항상 부드럽게 감소하는 것은 아니며 배치에 포함된 이미지의 난이도, 데이터 증강(augmentation), 학습률, 무작위성 등의 영향으로 중간에 손실이 올라갈 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;한 배치나 한 스텝의 값보다 여러 에포크에 걸친 전체 추세&lt;/strong&gt;를 보는 게 중요하다.&lt;/p&gt;
&lt;p&gt;또 하나 주의할 점은 손실의 절대값이다. 손실 항목마다 계산 방식과 가중치가 다르기 때문에&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;box_loss = 0.8
cls_loss = 0.5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만 보고 &lt;code&gt;박스 문제가 분류 문제보다 더 심각하다&lt;/code&gt;고 단정할 수 없다. &lt;/p&gt;
&lt;p&gt;따라서 &lt;strong&gt;같은 모델, 같은 데이터, 같은 손실 설정에서 시간에 따른 변화&lt;/strong&gt;를 보는 용도로 해석하는 편이 더 정확할 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 손실이 낮으면 좋은 모델인가?&lt;/h2&gt;
&lt;p&gt;그렇다고 꼭 그런 것은 아니다.&lt;/p&gt;
&lt;p&gt;학습 손실은 주로&lt;/p&gt;
&lt;p&gt;&lt;code&gt;현재 모델이 학습 데이터에 맞도록 얼마나 잘 최적화되고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 보여준다.&lt;/p&gt;
&lt;p&gt;하지만 우리가 원하는 모델은 학습 이미지 자체를 잘 외우는 모델이 아니라, &lt;strong&gt;처음 보는 새로운 이미지에서도 객체를 잘 찾는 모델&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;그래서 좋은 모델을 판단하려면 최소한 두 가지를 구분해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 손실
→ 최적화가 진행되는 상태를 확인

검증 성능
→ 새로운 데이터에 일반화되는 정도를 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;손실이 매우 낮더라도 검증 성능이 나쁘다면 과적합, 데이터 편향, 잘못된 데이터 분할 등을 의심해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 학습 데이터셋(Train Dataset)&lt;/h2&gt;
&lt;p&gt;학습 데이터셋은 실제 &lt;strong&gt;가중치(weight)&lt;/strong&gt;를 업데이트하는 데 사용된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 이미지
   ↓
순전파
   ↓
예측
   ↓
손실
   ↓
역전파
   ↓
옵티마이저(Optimizer)
   ↓
가중치 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;쉽게 말하면 모델이 직접 문제를 풀고 답을 확인하면서 파라미터를 바꾸는 &lt;strong&gt;공부용 문제&lt;/strong&gt;이고,&lt;/p&gt;
&lt;p&gt;학습 데이터에는 일반적으로 데이터 증강이 적용될 수 있다. 따라서 같은 원본 이미지라도 에포크마다 조금 다른 형태로 모델에 들어갈 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 검증 데이터셋(Validation Dataset)&lt;/h2&gt;
&lt;p&gt;검증 데이터셋은 학습 중 모델의 &lt;strong&gt;일반화(generalization)&lt;/strong&gt; 상태를 확인하는 데 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;검증 이미지
   ↓
예측
   ↓
정답과 비교
   ↓
손실 및 평가 지표 계산

가중치 업데이트 X&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;검증 단계에서는 일반적으로 역전파를 수행하지 않고 가중치를 수정하지 않으며&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 데이터
→ 공부하는 문제

검증 데이터
→ 공부 중간에 보는 모의시험&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 생각해 두면 이해가 편하다.&lt;/p&gt;
&lt;p&gt;다만 검증 데이터도 완전히 &lt;code&gt;손대지 않은 최종 시험&lt;/code&gt;은 아니다. 검증 결과를 보면서 에포크 수, 데이터 증강, 모델 크기, 신뢰도 임계값 등을 바꾸면 개발자가 간접적으로 검증 데이터에 맞춰 의사결정을 하게 된다.&lt;/p&gt;
&lt;p&gt;또 &lt;code&gt;results.csv&lt;/code&gt;에는 검증 손실이 함께 기록될 수 있지만, 학습과 검증은 데이터 증강 여부와 실행 모드 등이 다르므로 &lt;strong&gt;학습 손실과 검증 손실의 숫자를 1:1로 단순 비교하기보다 각각의 추세와 검증 지표를 함께 보는 편이 좋다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 테스트 데이터셋(Test Dataset)&lt;/h2&gt;
&lt;p&gt;최종 모델을 객관적으로 평가하려면 검증 데이터와 별도로 &lt;strong&gt;테스트 데이터셋&lt;/strong&gt;을 두는 것이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습(Train)
→ 가중치 학습

검증(Validation)
→ 하이퍼파라미터 선택
→ 모델 및 체크포인트 선택
→ 학습 상태 확인
→ 운영 임계값 후보 결정

테스트(Test)
→ 최종 선택된 모델의 일반화 성능 평가&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;왜 테스트 데이터를 따로 둘까?&lt;/p&gt;
&lt;p&gt;검증 결과를 반복해서 보면서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;에포크 수 조정
데이터 증강 변경
모델 변경
전처리 변경
신뢰도 임계값 조정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 하다 보면 검증 데이터가 사실상 개발 과정의 일부가 된다.&lt;/p&gt;
&lt;p&gt;이 때문에 테스트 데이터는 가능하면 &lt;strong&gt;최종 설계가 끝난 뒤에만 사용&lt;/strong&gt;해야 한다. 테스트 결과를 보고 다시 임계값이나 모델을 계속 바꾸면 테스트 데이터도 다시 검증 데이터처럼 오염된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 과적합(Overfitting)과 과소적합(Underfitting)&lt;/h2&gt;
&lt;p&gt;학습 데이터의 성능은 계속 좋아지는데 새로운 데이터에서 성능이 나빠지는 현상을 &lt;strong&gt;과적합(overfitting)&lt;/strong&gt;이라고 한다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 손실
→ 계속 감소

검증 mAP / 재현율
→ 어느 시점까지 좋아짐
→ 이후 정체하거나 악화&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한다면 과적합 가능성을 생각할 수 있다.&lt;/p&gt;
&lt;p&gt;단순화하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;데이터에 존재하는 일반적인 규칙을 학습
→ 좋은 일반화

학습 데이터의 세부 패턴까지 지나치게 맞춤
→ 과적합&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;반대로 학습 손실 자체가 충분히 내려가지 않고 학습 및 검증 성능이 모두 낮다면 &lt;strong&gt;과소적합(underfitting)&lt;/strong&gt;일 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습도 나쁨 + 검증도 나쁨
→ 모델 용량 부족, 학습 부족, 학습률 문제, 라벨 문제 등을 점검

학습은 매우 좋음 + 검증이 상대적으로 나쁨
→ 과적합, 데이터 누수/분포 차이 등을 점검&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 과적합 여부는 손실 하나만 보지 말고 &lt;strong&gt;학습-검증 간 격차와 검증 지표의 변화&lt;/strong&gt;를 함께 봐야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 손실만 보면 안 되는 이유&lt;/h2&gt;
&lt;p&gt;객체 탐지(Object Detection)의 최종 목적은 손실 숫자 자체를 낮추는 것이 아니고&lt;/p&gt;
&lt;p&gt;&lt;code&gt;실제 객체의 위치와 종류를 정확하게 찾아내는 것&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 목적이다.&lt;/p&gt;
&lt;p&gt;그래서 검증에서는 이런 평가 지표(metric)를 함께 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;정밀도(Precision)
재현율(Recall)
mAP50
mAP50-95&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정리하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 손실
→ 최적화가 제대로 진행되는지 확인

검증 평가 지표
→ 실제 탐지 성능과 일반화 상태 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;손실이 줄어도 mAP나 재현율이 좋아지지 않는 구간이 있을 수 있으며 손실 함수는 모델을 학습시키기 위한 목적 함수이고, mAP는 최종 탐지 품질을 평가하는 지표이기 때문에 두 값이 항상 완전히 같은 방향으로 움직이지는 않는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. YOLO26에서 보이는 손실 항목은 어떻게 읽어야 할까?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;results.csv&lt;/code&gt;나 학습 로그를 보면 손실이 하나의 숫자로만 나오지 않고 여러 항목으로 나뉘어 보인다. &lt;strong&gt;현재 공개 Ultralytics &lt;code&gt;main&lt;/code&gt;에서 YOLO26처럼 &lt;code&gt;reg_max=1&lt;/code&gt;인 검출 모델은&lt;/strong&gt; 다음 이름을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;box_loss
cls_loss
l1_loss&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YOLO26의 검출 손실은 개념적으로 다음 세 축으로 보면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;box_loss
→ 예측 박스와 GT 박스의 IoU / 형상 차이를 줄이는 항

cls_loss
→ 올바른 클래스 점수를 높이고 잘못된 클래스 점수를 낮추는 항

l1_loss
→ anchor point 기준 box distance를 GT distance에 맞추는 L1 계열 항&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 이름이 중요한 이유는 YOLO26이 &lt;code&gt;reg_max=1&lt;/code&gt;이기 때문이다. 현재 &lt;code&gt;v8DetectionLoss&lt;/code&gt; 구현은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;reg_max &amp;gt; 1
→ use_dfl = True
→ loss 이름: dfl_loss
→ 여러 bin의 분포를 이용하는 DFL 계열 계산

reg_max = 1
→ use_dfl = False
→ loss 이름: l1_loss
→ DFL-free box-distance L1 계열 계산&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로 구분한다.&lt;/p&gt;
&lt;p&gt;그런데 설정에는 여전히&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dfl: &amp;lt;gain&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;키가 남아 있고, 코드에서도 세 번째 loss 항에 이 gain을 곱한다. 따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;설정에 dfl gain이 존재한다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;와&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;YOLO26은 DFL-Free다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;는 서로 모순이 아니다. 현재 YOLO26에서는 &lt;strong&gt;&lt;code&gt;dfl&lt;/code&gt;이라는 설정 이름을 세 번째 distance loss의 gain으로 재사용&lt;/strong&gt;하고 있을 뿐이다. 과거 버전 로그나 checkpoint metadata에서는 &lt;code&gt;dfl_loss&lt;/code&gt;라는 이름이 남아 있을 수도 있으므로, 실제 &lt;code&gt;reg_max&lt;/code&gt;와 코드 버전을 같이 확인해야한다.&lt;/p&gt;
&lt;h3&gt;Dual Head에서는 화면의 개별 loss와 backward total도 구분해야 한다&lt;/h3&gt;
&lt;p&gt;YOLO26은 one-to-many와 one-to-one 두 branch를 함께 학습한다. 현재 공개 &lt;code&gt;main&lt;/code&gt;의 &lt;code&gt;E2ELoss&lt;/code&gt;에서 실제 backward에 들어가는 total loss는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;L_total
= w_o2m(t) × L_one-to-many
+ w_o2o(t) × L_one-to-one&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 두 branch의 loss를 가중해서 합친 값이다. 그런데 criterion이 Trainer에 같이 반환하는 &lt;strong&gt;개별 loss dictionary는 현재 구현에서 one-to-one branch의 &lt;code&gt;box_loss / cls_loss / l1_loss&lt;/code&gt; 항목&lt;/strong&gt;이다. Trainer는 이 dictionary의 key를 학습 loss 이름으로 사용한다.&lt;/p&gt;
&lt;p&gt;따라서 &lt;code&gt;results.csv&lt;/code&gt;에서 보이는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;train/box_loss
train/cls_loss
train/l1_loss&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 보고&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이 세 숫자를 더하면 실제 backward에 사용된 dual-head total loss가 된다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 해석하면 안 되며, 실제 gradient를 만드는 total에는 그 시점의 Progressive Loss weight가 적용된 one-to-many와 one-to-one branch가 함께 들어간다.&lt;/p&gt;
&lt;p&gt;또 &lt;code&gt;box_loss&lt;/code&gt;, &lt;code&gt;cls_loss&lt;/code&gt;, &lt;code&gt;l1_loss&lt;/code&gt;는 계산 방식과 gain이 서로 다르다. 예를 들어 &lt;code&gt;box_loss=0.8&lt;/code&gt;, &lt;code&gt;cls_loss=0.5&lt;/code&gt;라는 숫자만 보고 box 문제가 cls보다 1.6배 심각하다고 해석할 수도 없기에 &lt;strong&gt;같은 설정 안에서 각 항목의 시간에 따른 추세와 검증 지표를 함께 보는 것&lt;/strong&gt;이 더 중요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. 손실이 계속 내려가도 &lt;code&gt;best.pt&lt;/code&gt;가 마지막 모델이 아닐 수 있다&lt;/h2&gt;
&lt;p&gt;이 부분은 &amp;quot;손실이 낮으면 좋은 모델인가?&amp;quot;라는 질문을 가장 직접적으로 보여준다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음처럼 학습됐다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;에포크 35
→ 검증 mAP50-95 최고

에포크 36 이후
→ 학습 손실은 계속 감소
→ 검증 성능은 조금씩 하락

에포크 50
→ 학습 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;best.pt
→ 에포크 35에서 선택될 수 있음

last.pt
→ 마지막으로 수행한 에포크 50&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 서로 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;즉 &lt;strong&gt;학습 데이터에 대한 최적화는 계속 진행되고 있어도 일반화 성능은 이미 나빠지기 시작할 수 있다.&lt;/strong&gt; 그래서 loss curve와 validation metric을 함께 봐야 하고, 마지막 epoch의 가중치가 무조건 가장 좋은 모델이라고 생각하면 안 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. 현재 프로젝트에서는 무엇을 같이 볼까?&lt;/h2&gt;
&lt;p&gt;기준 실험(Baseline), CLAHE, &lt;strong&gt;양방향 필터(Bilateral Filter) + CLAHE&lt;/strong&gt; 실험이 끝나면 다음 항목을 비교할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 손실 곡선
검증 손실 곡선
정밀도
재현율
mAP50
mAP50-95
best.pt가 선택된 에포크&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;단순히 마지막 숫자 하나만 보는 것이 아니라&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;에포크가 증가할 때
학습 손실은 어떻게 변했는가?

검증 지표는 어떻게 변했는가?

가장 좋은 성능은 어느 에포크에서 나왔는가?

학습과 검증 사이의 격차가 커졌는가?

과적합 또는 과소적합 징후가 있었는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 함께 확인해 두는 편이 좋다.&lt;/p&gt;
&lt;p&gt;또한 전처리 실험끼리 비교할 때는 모델, 데이터 분할, 시드, 입력 크기, 학습 에포크, 평가 설정을 가능한 한 동일하게 유지해야 전처리의 효과를 더 공정하게 비교할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 손실 감소
→ 최적화가 진행되고 있다는 신호

검증 성능 향상
→ 학습하지 않은 데이터에 더 잘 일반화되고 있다는 신호

학습은 계속 좋아지는데 검증이 나빠짐
→ 과적합 가능성

학습과 검증이 모두 낮음
→ 과소적합, 학습 설정, 라벨, 모델 용량 등을 점검&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;손실 함수는 &lt;strong&gt;학습을 위한 목적 함수&lt;/strong&gt;이고, 실제 객체 탐지 성능을 평가하는 지표와 완전히 같은 값이 아니다. 그래서 손실이 계속 내려가도 mAP나 재현율이 더 이상 좋아지지 않을 수 있고, 마지막 에포크가 가장 좋은 체크포인트라는 보장도 없다.&lt;/p&gt;
&lt;p&gt;다음 글에서는 객체 탐지 성능을 실제로 판단할 때 사용하는 &lt;strong&gt;Precision, Recall, AP, mAP50, mAP50-95, confidence threshold&lt;/strong&gt;를 서로 연결해서 살펴볼 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고한 Ultralytics 공식 문서&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;YOLO 학습 모드: &lt;a href=&quot;https://docs.ultralytics.com/modes/train/&quot;&gt;https://docs.ultralytics.com/modes/train/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;객체 탐지 평가: &lt;a href=&quot;https://docs.ultralytics.com/tasks/detect/&quot;&gt;https://docs.ultralytics.com/tasks/detect/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics Loss 구현: &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/loss.py&quot;&gt;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/loss.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics Trainer 구현: &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/trainer.py&quot;&gt;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/trainer.py&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>검증</category>
      <category>과소적합</category>
      <category>과적합</category>
      <category>일반화</category>
      <category>테스트</category>
      <category>학습 손실</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/104</guid>
      <comments>https://slowgrid.tistory.com/entry/YOLO-Basics-9-Is-a-lower-loss-a-good-model#entry104comment</comments>
      <pubDate>Thu, 1 Oct 2026 18:45:06 +0900</pubDate>
    </item>
    <item>
      <title>구글, Gemini 4 Argon 공개&amp;hellip;19개 평가 중 14개 1위, 실무&amp;middot;장문 처리에 강점</title>
      <link>https://slowgrid.tistory.com/entry/Google-Unveils-Gemini-4-Argon-Ranked-1st-in-14-Out-of-19-Reviews-Strong-in-Practical-Long-Text-Processing</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C9suE/dJMb99VzpVd/4YPlvDpaL7oqC2JzTQkRR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C9suE/dJMb99VzpVd/4YPlvDpaL7oqC2JzTQkRR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C9suE/dJMb99VzpVd/4YPlvDpaL7oqC2JzTQkRR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC9suE%2FdJMb99VzpVd%2F4YPlvDpaL7oqC2JzTQkRR1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;구글이 9월 30일 차세대 AI 모델 Gemini 4 Argon을 공개했다. 금융·법률 등 전문 업무와 장시간에 걸친 소프트웨어 개발, 보안 방어를 주요 활용 분야로 내세운 모델이다.&lt;/p&gt;

&lt;p&gt;이번에 공개된 성적표에는 GPT-6 Astra, Claude Fable 5.1, Claude Opus 5.5와의 비교 결과가 담겼다. Argon은 총 19개 평가 가운데 14개에서 가장 높은 점수를 기록했다. 보안 취약점 수정 평가인 CWE-bench v1에서 Astra와 기록한 공동 1위도 여기에 포함된다.&lt;/p&gt;

&lt;p&gt;다만 이번 결과를 모든 분야에서 경쟁 모델을 앞섰다는 의미로 받아들이기는 어렵다. 실무 업무와 긴 입력 처리에서는 강점을 보였지만, 터미널 작업과 일부 코딩·컴퓨터 조작 평가에서는 다른 모델이 더 높은 점수를 기록했다.&lt;/p&gt;

&lt;h2&gt;실무 업무 평가 4개 모두 1위…금융·법률 분야에서 강점&lt;/h2&gt;

&lt;p&gt;가장 먼저 눈에 들어오는 부분은 실무 업무 평가다. 금융·코딩·법률·세무 업무를 종합한 Vals 지수, 업무 자동화를 평가하는 AutomationBench, 금융 리서치와 법률 업무를 다루는 평가에서 Argon이 모두 1위를 기록했다.&lt;/p&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PJuvy/dJMcaikKl7O/5YDnIoa5BwElKAuAMkV47K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PJuvy/dJMcaikKl7O/5YDnIoa5BwElKAuAMkV47K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PJuvy/dJMcaikKl7O/5YDnIoa5BwElKAuAMkV47K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPJuvy%2FdJMcaikKl7O%2F5YDnIoa5BwElKAuAMkV47K%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;&lt;small&gt;▲ 실무 업무 평가 비교. 금융·코딩·법률·세무를 종합한 Vals 지수와 업무 자동화, 금융 리서치, 법률 업무 평가에서 Argon이 가장 높은 점수를 기록했다.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Vals 지수에서 Argon은 68.9%를 기록했다. 같은 표에서 Claude Opus 5.5는 67.0%, Claude Fable 5.1은 65.8%, GPT-6 Astra는 63.1%로 제시됐다. 회사의 핵심 업무를 처음부터 끝까지 자동으로 처리하는 능력을 측정하는 AutomationBench에서는 Argon이 51.3%를 기록했다.&lt;/p&gt;

&lt;p&gt;여러 단계를 거치는 금융 리서치를 평가하는 Vals Finance Agent v2에서는 65.4%를 기록했다. 법률 조사와 문서 작성을 평가하는 Harvey 법률 에이전트 시험에서는 19.6%로, 나머지 모델의 3.8~6.7%보다 높은 점수를 보였다.&lt;/p&gt;

&lt;p&gt;특히 법률 평가에서는 비교 대상 중 두 번째로 높은 점수의 약 3배에 가까운 결과가 나왔다. 다만 이를 실제 법률 업무 전반을 3배 더 잘 수행한다는 뜻으로 해석해서는 안 되며 해당 평가가 정한 과제와 채점 방식 안에서 나타난 점수 차이로 보는 것이 적절하다.&lt;/p&gt;

&lt;h2&gt;긴 입력에서는 격차 확대…25만6,000~100만 토큰 구간 84.2%&lt;/h2&gt;

&lt;p&gt;긴 입력을 읽고 그 안의 연결 관계를 따라가는 GraphWalks에서도 Argon이 앞섰다. 12만8,000토큰까지의 평가에서는 99.7%를 기록했고, 25만6,000~100만 토큰 구간에서는 84.2%를 기록했다.&lt;/p&gt;

&lt;p&gt;특히 더 긴 입력 구간에서 GPT-6 Astra의 점수는 71.8%였다. Argon과의 차이는 12.4%포인트다. 공개된 두 구간의 결과를 비교하면, 긴 입력 구간에서 두 모델 사이의 격차가 더 크게 나타난다.&lt;/p&gt;

&lt;p&gt;이미지와 영상 이해에서도 높은 점수가 나왔다. 도구 없이 차트를 읽고 답하는 Chartography에서는 71.6%, 긴 영상의 내용을 이해하는 LVBench에서는 91.7%로 비교 대상 모델 가운데 가장 높은 점수를 기록했다.&lt;/p&gt;

&lt;p&gt;이러한 결과는 장문의 입력뿐 아니라 차트와 영상 같은 시각 자료를 다루는 업무에서도 Argon의 활용 가능성을 보여준다. 다만 GraphWalks는 긴 입력 속 연결 관계를 추적하는 특정 평가이므로, 그 점수를 모든 장문 문서 분석의 정확성과 동일시할 수는 없다.&lt;/p&gt;

&lt;h2&gt;코딩은 DeepSWE 77.9%…터미널 작업은 Opus 5.5 우세&lt;/h2&gt;

&lt;p&gt;코딩에서는 평가 종류에 따라 결과가 갈렸다. 장시간에 걸친 실제 소프트웨어 개발 과제를 평가하는 DeepSWE v1.1에서 Argon은 77.9%를 기록했다. 바이브 코딩을 평가하는 Vibe Code Bench에서도 91.9%로 가장 높은 점수를 보였다.&lt;/p&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbp0Hj/dJMcagUK1VH/ah01SdkZeUpNDDnyVUbeSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbp0Hj/dJMcagUK1VH/ah01SdkZeUpNDDnyVUbeSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbp0Hj/dJMcagUK1VH/ah01SdkZeUpNDDnyVUbeSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcbp0Hj%2FdJMcagUK1VH%2Fah01SdkZeUpNDDnyVUbeSK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;&lt;small&gt;▲ 에이전트 코딩 평가 비교. Argon은 DeepSWE와 Vibe Code Bench에서 앞섰지만, FrontierSWE와 Terminal-Bench에서는 다른 모델이 더 높은 점수를 기록했다.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;반면 코딩 에이전트 평가인 FrontierSWE v2에서는 GPT-6 Astra가 65.5%로 1위였고, Argon은 55.0%를 기록했다. 터미널 명령으로 개발 작업을 수행하는 Terminal-Bench 4.0에서는 Claude Opus 5.5가 66.4%로 앞섰다. Argon은 57.4%로 네 모델 가운데 가장 낮았다.&lt;/p&gt;

&lt;p&gt;즉 코딩 성능을 하나의 순위로 정리하기는 어렵다. 긴 개발 과제와 바이브 코딩에서는 Argon이 강점을 보였지만, 터미널 환경에서 명령을 실행하며 작업하는 평가에서는 Opus 5.5가 더 좋은 결과를 냈다. 실제 개발에 사용할 모델을 고를 때도 어떤 종류의 작업을 맡길 것인지부터 구분할 필요가 있다.&lt;/p&gt;

&lt;h2&gt;컴퓨터 조작과 과학 평가도 과제에 따라 차이&lt;/h2&gt;

&lt;p&gt;컴퓨터 조작 능력을 평가하는 Agent&amp;#x27;s Last Exam에서 Argon은 39.5%를 기록했다. 5시간 안에 과제를 통과했는지를 측정하는 이 평가에서는 점수가 공개된 비교 대상 가운데 가장 높은 결과였다. 반면 화면을 보고 컴퓨터를 조작하는 OSWorld-2.0에서는 Argon이 69.2%, GPT-6 Astra가 72.6%를 기록했다.&lt;/p&gt;

&lt;p&gt;두 평가는 점수를 매기는 방식도 다르다. Agent&amp;#x27;s Last Exam은 과제 통과 여부를 세고, 이번 성적표의 OSWorld-2.0은 부분 점수를 사용한다. 따라서 서로 다른 평가의 백분율을 그대로 비교해 어느 작업을 더 잘한다고 판단하기는 어렵다.&lt;/p&gt;

&lt;p&gt;과학·수학 분야에서는 생물정보 도구와 인터넷을 활용하는 LABBench 2에서 88.8%, 수학 평가인 RiemannBench에서 76.0%로 Argon이 앞섰다. 하지만 터미널로 과학 과제를 해결하는 Terminal-Bench Science 0.1에서는 Astra가, 제한된 GPU와 시간으로 AI 모델을 추가 학습시키는 PostTrainBench에서는 Opus 5.5가 1위를 기록했다.&lt;/p&gt;

&lt;h2&gt;19개 성적표, 다만 모두 같은 조건의 비교는 아니다&lt;/h2&gt;

&lt;p&gt;전체 성적표를 보면 Argon의 강점과 약점이 비교적 명확하게 드러난다. 하지만 순위를 읽기 전에 확인해야 할 부분은 각 점수가 어떤 조건에서 측정됐는지다.&lt;/p&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR6XlO/dJMcagUK1WQ/l1faO3k6FxT0uHGhAjskT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR6XlO/dJMcagUK1WQ/l1faO3k6FxT0uHGhAjskT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR6XlO/dJMcagUK1WQ/l1faO3k6FxT0uHGhAjskT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR6XlO%2FdJMcagUK1WQ%2Fl1faO3k6FxT0uHGhAjskT0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;&lt;small&gt;▲ 19개 평가 전체 성적표. 파란색은 Argon이 1위인 항목, 회색은 다른 모델이 1위인 항목이다. CWE-bench v1은 Astra와 공동 1위이며, 가로줄은 점수가 공개되지 않아 비교에서 제외된 항목을 뜻한다.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;한국어판에 정리된 평가 조건에 따르면 Argon의 점수는 가장 높은 추론 설정을 기준으로 한다. 다른 모델의 점수는 대부분 각 회사가 발표한 결과를 가져왔으며, 일부 평가는 구글이 직접 측정했다. 모든 수치가 하나의 동일한 실험 환경에서 나온 것은 아니라는 의미다.&lt;/p&gt;

&lt;p&gt;예를 들어 PostTrainBench는 모든 모델을 같은 도구와 H100 GPU 1장, 10시간의 예산으로 평가했다. 반면 Terminal-Bench Science 0.1의 Argon 점수는 검증 시간 제한을 6배로 늘려 자체 측정한 값이며, OSWorld-2.0의 Argon 점수는 세 번 실행한 결과 가운데 가장 높은 값이다.&lt;/p&gt;

&lt;p&gt;LVBench에서도 API 제한 때문에 모델별로 입력한 영상 프레임 수가 달랐다. Gemini는 초당 1장, Astra는 800장, Fable 5.1은 300장, Opus 5.5는 600장을 사용한 것으로 정리돼 있다. 긴 영상 이해 점수 역시 이러한 조건 차이를 함께 고려해야 한다.&lt;/p&gt;

&lt;p&gt;성적표의 빈칸도 0점을 의미하지 않는다. Fable 5.1은 Agent&amp;#x27;s Last Exam 공개 순위표에 점수가 없었고, OSWorld-2.0에서는 앤트로픽이 부분 점수를 따로 공개하지 않아 비교에서 빠졌다.&lt;/p&gt;

&lt;p&gt;따라서 작은 점수 차이까지 곧바로 모델 자체의 우열로 연결하기보다는 추론 설정, 실행 시간, 입력 정보와 채점 방식을 함께 살펴보는 것이 적절할 것이다.&lt;/p&gt;

&lt;h2&gt;출력 한도 100만 토큰…긴 입력 평가와는 다른 변화&lt;/h2&gt;

&lt;p&gt;벤치마크와 별개로 눈에 띄는 변화는 출력 한도다. 구글은 Gemini 4 Argon의 최대 출력 토큰 수를 기존 6만4,000토큰에서 100만 토큰으로 확대했다고 밝혔다.&lt;/p&gt;

&lt;p&gt;여기서 말하는 100만 토큰은 모델이 읽을 수 있는 입력 길이가 아니라 한 번의 생성 과정에서 사용할 수 있는 출력의 상한이다. 앞서 GraphWalks에서 제시된 최대 100만 토큰의 입력 구간과는 구분해야 한다.&lt;/p&gt;

&lt;p&gt;구글은 이를 복잡한 문제를 여러 단계에 걸쳐 풀고 긴 결과를 생성할 수 있도록 여유를 늘린 변화로 설명한다. 다만 출력 한도가 커졌다는 것만으로 실제 작업의 성공률이나 비용 효율이 보장되는 것은 아니다.&lt;/p&gt;

&lt;h2&gt;보안 취약점 수정 68%…검증된 방어 파트너부터 공개&lt;/h2&gt;

&lt;p&gt;보안 취약점을 수정하는 능력을 평가하는 CWE-bench v1에서는 Argon이 68.0%로 GPT-6 Astra와 공동 1위를 기록했다.&lt;/p&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnCS8F/dJMcajjGVSl/I4gakaAegoItLuvsUriKdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnCS8F/dJMcajjGVSl/I4gakaAegoItLuvsUriKdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnCS8F/dJMcajjGVSl/I4gakaAegoItLuvsUriKdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnCS8F%2FdJMcajjGVSl%2FI4gakaAegoItLuvsUriKdk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;&lt;small&gt;▲ CWE-bench v1 결과. Argon과 Astra가 68.0%로 공동 1위를 기록했다. 취약점 수정 능력을 평가하며, 점수가 높을수록 좋다.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;구글은 보안 기업 Wiz가 Argon을 활용해 전 세계 병원에서 사용하는 의료 소프트웨어의 심각한 개인정보 노출 취약점을 발견했다고 밝혔다. 구글의 설명에 따르면 이전의 최고 성능 모델들이 놓쳤던 문제다. 다만 이는 구글이 소개한 활용 사례이며, 모든 소프트웨어에서 동일한 성과를 보장하는 결과는 아니다.&lt;/p&gt;

&lt;p&gt;공개 범위도 이러한 보안 활용과 연결돼 있다. 발표 시점 기준 Argon은 Fairwind 프로그램을 통해 검증된 보안 방어 파트너에게 먼저 제공된다. 이후 유료 API 고객과 Google AI Ultra 구독자부터 이용 범위를 넓힐 예정이지만, 구체적인 날짜는 공개되지 않았다.&lt;/p&gt;

&lt;p&gt;구글은 공개 확대에 앞서 악용 방지, 프롬프트 인젝션 방어, 사용자 의도를 벗어나는 행동 감시, 실행 환경 격리를 강화한다고 설명했다. 문서나 웹페이지에 숨겨진 지시에 모델이 휘둘리지 않도록 하고, 작업 중 의도를 벗어나는 행동이 감지되면 실행을 중단하는 방식이다. 프롬프트 인젝션 방어에서는 Gray Swan 평가에서 가장 높은 결과를 기록했다고 밝혔다.&lt;/p&gt;

&lt;p&gt;한편 검증된 보안 파트너와 구글 내부 팀에는 방어 역량을 충분히 활용할 수 있도록 사이버 보안 제한을 푼 모델을 제공한다는 방침이다. 일반 공개와 검증된 방어 목적의 접근을 구분한 셈이다.&lt;/p&gt;

&lt;h2&gt;API 출시 초기가 입력 2달러·출력 10달러&lt;/h2&gt;

&lt;p&gt;API 출시 초기가는 100만 토큰당 입력 2달러, 출력 10달러다. 초기 기간이 끝나면 입력 4달러, 출력 20달러가 적용된다.&lt;/p&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cevoDm/dJMcafhgbUL/D0ejir1k9wXK3T7OptQV0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cevoDm/dJMcafhgbUL/D0ejir1k9wXK3T7OptQV0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cevoDm/dJMcafhgbUL/D0ejir1k9wXK3T7OptQV0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcevoDm%2FdJMcafhgbUL%2FD0ejir1k9wXK3T7OptQV0k%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;&lt;small&gt;▲ Gemini 4 Argon API 가격. 100만 토큰 기준이며, 출시 초기가와 초기 기간 이후 가격을 구분해야 한다.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;초기 가격 안내에는 반복되는 입력을 캐시에서 다시 불러올 때 일반 입력 가격보다 95% 할인한다는 내용도 포함됐다. 다만 초기 가격의 적용 종료일은 공개되지 않았다.&lt;/p&gt;

&lt;p&gt;가격이 공개됐다고 해서 누구나 곧바로 API를 이용할 수 있다는 뜻은 아니다. 실제 접근 가능 여부는 단계적 공개 일정과 함께 확인해야 한다. 또한 서비스를 운영할 때는 토큰 단가뿐 아니라 한 작업을 완료하는 데 소비하는 입력·출력 토큰과 재시도 횟수까지 함께 고려할 필요가 있다.&lt;/p&gt;

&lt;h2&gt;구글 내부에서는 메모리 절감과 코드 전환에 활용&lt;/h2&gt;

&lt;p&gt;구글은 벤치마크 외에 내부 활용 사례도 공개했다. 데이터센터에서는 여러 Argon 에이전트가 서버 전반의 사용 기록을 분석하고 메모리 최적화를 적용해 300TiB 이상을 확보했다고 밝혔다. 전체 절감량은 500TiB~1PiB로 예상했지만, 이는 이미 확보한 양이 아니라 추가 적용을 포함한 추정치다.&lt;/p&gt;

&lt;p&gt;코드 작업에서는 C/C++ 코드를 Rust로 옮기는 데 활용하고 있다. re2와 libgav1 같은 라이브러리부터 80만 줄이 넘는 Fuchsia 운영체제의 Zircon 커널까지 대상에 포함된다. 구글은 이러한 변경을 자동·수동 감사와 에뮬레이션 테스트 등을 거친 뒤 반영한다고 설명했다.&lt;/p&gt;

&lt;p&gt;영상 디코더 libgav1에서는 기존 Rust 이식본의 SIMD 코드 3만2,000줄을 교체해 같은 영상을 출력하면서 실행 속도를 2.7배 높였다고 밝혔다. 비교 기준은 기존 Rust 이식본이기에, 모든 영상 디코더나 기존 C++ 구현보다 2.7배 빠르다는 뜻은 아니다.&lt;/p&gt;

&lt;p&gt;양자 컴퓨팅 연구에서는 한 알고리즘 최적화 과제에서 공개된 기존 기준보다 40% 나은 결과를 몇 분 만에 얻었다고 소개했다. 이 역시 구글이 제시한 특정 내부 사례로, 범용적인 성능 향상률로 확대해 해석할 수는 없다.&lt;/p&gt;

&lt;h2&gt;결국 중요한 것은 1위 개수보다 맡길 업무의 종류&lt;/h2&gt;

&lt;p&gt;Gemini 4 Argon의 성적표는 실무 업무와 긴 입력 처리에서 강점을 보여준다. 금융 리서치와 법률 문서 작성, 긴 소프트웨어 개발 과제와 시각 자료 이해에서도 높은 점수를 기록했다. 반면 터미널 작업과 일부 코딩·컴퓨터 조작 평가에서는 경쟁 모델이 앞섰다.&lt;/p&gt;

&lt;p&gt;따라서 이번 발표의 핵심은 단순히 19개 중 14개에서 1위를 했다는 숫자보다 어떤 종류의 업무에서 그 결과가 나왔는지에 있고, 여러 평가를 합친 1위 개수만으로 모든 작업에 가장 적합한 모델을 정할 수는 없다.&lt;/p&gt;

&lt;p&gt;실제 도입 단계에서는 자신이 맡길 작업이 어느 평가와 가까운지 살펴보고, 동일한 프롬프트와 도구 환경에서 성공률과 비용을 직접 확인하는 과정이 필요하다. 성적표는 모델 선택의 출발점이지만, 실제 업무에서의 성능과 운영 비용까지 대신해 주는 답은 아니기 때문이다.&lt;/p&gt;

&lt;h2&gt;참고 자료&lt;/h2&gt;

&lt;ul&gt;

&lt;li&gt;&lt;a href=&quot;https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-4-argon/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Google: Gemini 4 Argon: our next era of frontier intelligence&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href=&quot;https://deepmind.google/models/evals-methodology/gemini-4-argon&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Google DeepMind: Gemini 4 Argon Model evaluation&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;</description>
      <category>AI News</category>
      <category>Gemini 4 Argon</category>
      <category>구글</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/103</guid>
      <comments>https://slowgrid.tistory.com/entry/Google-Unveils-Gemini-4-Argon-Ranked-1st-in-14-Out-of-19-Reviews-Strong-in-Practical-Long-Text-Processing#entry103comment</comments>
      <pubDate>Thu, 1 Oct 2026 14:05:25 +0900</pubDate>
    </item>
    <item>
      <title>나는 어떤 행동을 배우며 살아가고 있을까?: 에이커스의 사회학습이론</title>
      <link>https://slowgrid.tistory.com/entry/What-behaviors-am-I-learning-as-I-live-Akers-Social-Learning-Theory</link>
      <description>&lt;p&gt;우리는 어떤 행동을 왜 하게 되는 것일까?&lt;/p&gt;
&lt;p&gt;생각해보면 나는 지금까지 어떤 행동을 설명할 때 개인의 성격이나 의지를 먼저 떠올렸던 것 같다.&lt;/p&gt;
&lt;p&gt;공부를 열심히 하는 사람을 보면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;원래 성실한 사람이니까.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하고,&lt;/p&gt;
&lt;p&gt;규칙을 자주 어기는 사람을 보면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;원래 그런 성격을 가진 사람이니까.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하는 식이다.&lt;/p&gt;
&lt;p&gt;그런데 조금 다르게 생각해볼 수도 있다.&lt;/p&gt;
&lt;p&gt;우리가 하는 행동이 처음부터 내 안에 있었던 것이 아니라면 어떨까?&lt;/p&gt;
&lt;p&gt;주변 사람들의 행동을 보고,&lt;br&gt;그들의 생각을 듣고,&lt;br&gt;어떤 행동을 했을 때 돌아오는 반응을 경험하면서&lt;/p&gt;
&lt;p&gt;조금씩 지금의 행동을 배우게 된 것이라면 말이다.&lt;/p&gt;
&lt;p&gt;이런 질문을 거듭해서 생각하다 보면 에이커스(Ronald L. Akers)의 사회학습이론과 만나게 된다.&lt;/p&gt;
&lt;p&gt;에이커스의 사회학습이론은 범죄나 일탈행동을 설명하기 위해 발전한 이론이지만, 그 내용을 살펴보면 우리가 일상에서 어떤 행동을 배우고 반복하게 되는지를 생각해보는 데에도 흥미로운 관점을 제공해준다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;먼저, 에이커스의 사회학습이론이란 무엇일까?&lt;/h1&gt;
&lt;p&gt;에이커스의 사회학습이론을 아주 단순하게 표현하면 이렇게 말할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;사람의 행동은 다른 사람들과 관계를 맺고 그 행동의 결과를 경험하는 과정에서 학습된다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;에이커스는 로버트 버지스(Robert Burgess)와 함께 서덜랜드의 차별적 접촉이론에 행동주의의 강화 개념을 결합했고, 이후 이를 사회학습이론으로 발전시켰다. 이 이론에서 행동은 단순히 다른 사람에게서 전달되는 것이 아니라, 주변 사람과의 관계, 행동에 대한 생각, 보상과 처벌, 다른 사람의 행동을 관찰하는 과정이 서로 영향을 주면서 형성된다.&lt;/p&gt;
&lt;p&gt;이 과정을 설명하는 핵심 개념은 네 가지다.&lt;/p&gt;
&lt;p&gt;차별적 접촉,&lt;br&gt;정의,&lt;br&gt;차별적 강화,&lt;br&gt;그리고 모방이다.&lt;/p&gt;
&lt;p&gt;처음에는 각각 다른 개념처럼 보이지만 하나씩 살펴보면 서로 연결되어 있다는 것을 알 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;우리는 누구와 함께 있는가?&lt;/h1&gt;
&lt;p&gt;첫 번째는 &lt;strong&gt;차별적 접촉(differential association)&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;사람은 혼자 행동을 배우지 않는다.&lt;/p&gt;
&lt;p&gt;가족과 친구가 있고,&lt;br&gt;학교와 직장이 있고,&lt;br&gt;온라인에서는 수많은 사람과 콘텐츠를 접한다.&lt;/p&gt;
&lt;p&gt;그리고 그 관계 속에서 자연스럽게 특정한 행동과 생각을 반복해서 보게 된다.&lt;/p&gt;
&lt;p&gt;예를 들어 주변 친구들이 모두 시험에서 부정행위를 매우 심각한 문제라고 생각한다고 해보자.&lt;/p&gt;
&lt;p&gt;그 환경에서는 나 역시 부정행위를 쉽게 선택하기 어려울 가능성이 높다.&lt;/p&gt;
&lt;p&gt;반대로 주변 사람들이&lt;/p&gt;
&lt;p&gt;&lt;code&gt;다들 이 정도는 하는데 뭐.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 이야기한다면 어떨까?&lt;/p&gt;
&lt;p&gt;처음에는 잘못된 행동이라고 생각했던 사람도 계속 그런 이야기를 듣다 보면 자신의 기준을 조금씩 바꿀 수 있다.&lt;/p&gt;
&lt;p&gt;에이커스의 관점에서 중요한 것은 단순히 ‘나쁜 친구와 어울리면 나쁜 행동을 한다’는 식의 설명은 아니다.&lt;/p&gt;
&lt;p&gt;누구와 얼마나 자주 접촉하는지,&lt;br&gt;얼마나 오랫동안 관계를 유지하는지,&lt;br&gt;그 사람이 나에게 얼마나 중요한 사람인지와 같은 관계의 특성이 행동의 학습에 영향을 준다고 본다. 가족과 친구 같은 가까운 집단은 특히 중요한 사회적 학습의 환경이 된다.&lt;/p&gt;
&lt;p&gt;생각해보면 나 역시 주변 사람에게 영향을 받지 않고 살아간다고 말하기는 어려운 것 같다.&lt;/p&gt;
&lt;p&gt;내가 자주 사용하는 말,&lt;br&gt;당연하다고 생각하는 행동,&lt;br&gt;무엇을 중요하다고 생각하는지까지&lt;/p&gt;
&lt;p&gt;어쩌면 상당 부분은 내가 반복해서 만난 사람들과 연결되어 있을지도 모른다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;행동보다 먼저 배우는 것은 행동에 대한 생각&lt;/h1&gt;
&lt;p&gt;두 번째는 &lt;strong&gt;정의(definitions)&lt;/strong&gt;dl다.&lt;/p&gt;
&lt;p&gt;여기서 정의는 사전적인 뜻을 말하는 것이 아니다.&lt;/p&gt;
&lt;p&gt;어떤 행동을&lt;/p&gt;
&lt;p&gt;&lt;code&gt;괜찮은 행동이라고 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;잘못된 행동이라고 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;혹은&lt;/p&gt;
&lt;p&gt;&lt;code&gt;상황에 따라서는 해도 된다고 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;와 같이 행동에 대해 가지고 있는 태도와 의미에 가깝다. 에이커스의 이론에서는 이러한 정의가 특정 행동을 지지하거나 억제하는 방향으로 작용할 수 있다고 본다.&lt;/p&gt;
&lt;p&gt;이 부분이 흥미로운 이유는 사람의 행동이 갑자기 시작되는 것이 아닐 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;처음에는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;저 행동은 하면 안 된다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각했던 사람이&lt;/p&gt;
&lt;p&gt;&lt;code&gt;한 번 정도는 괜찮지 않을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하게 되고,&lt;/p&gt;
&lt;p&gt;시간이 지나&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이 정도는 문제가 아니다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하게 된다면 행동에 대한 심리적 장벽도 낮아질 수 있다.&lt;/p&gt;
&lt;p&gt;행동이 바뀌기 전에 그 행동을 바라보는 해석이 먼저 바뀌는 셈이다.&lt;/p&gt;
&lt;p&gt;그래서 어떤 행동을 이해하려면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;왜 그런 행동을 했을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;만 묻는 것보다&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 사람은 그 행동을 어떤 행동이라고 생각하고 있었을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 질문하는 것이 중요할 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;우리는 보상받은 행동을 다시 하게 된다&lt;/h1&gt;
&lt;p&gt;세 번째는 &lt;strong&gt;차별적 강화(differential reinforcement)&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;개인적으로 에이커스의 사회학습이론에서 가장 직관적으로 이해되는 부분이었다.&lt;/p&gt;
&lt;p&gt;어떤 행동을 했는데 좋은 결과가 돌아온다면 그 행동을 다시 할 가능성이 높아진다.&lt;/p&gt;
&lt;p&gt;반대로 좋지 않은 결과가 반복된다면 그 행동을 줄일 가능성이 높아진다.&lt;/p&gt;
&lt;p&gt;여기서 보상은 반드시 돈이나 물건처럼 눈에 보이는 것일 필요가 없다.&lt;/p&gt;
&lt;p&gt;친구들의 웃음,&lt;br&gt;칭찬,&lt;br&gt;인정,&lt;br&gt;소속감,&lt;br&gt;관심 역시 보상이 될 수 있다.&lt;/p&gt;
&lt;p&gt;반대로 비난을 받거나,&lt;br&gt;친구들에게 배제되거나,&lt;br&gt;중요한 무언가를 잃는 경험은 행동을 억제하는 결과가 될 수 있다.&lt;/p&gt;
&lt;p&gt;에이커스는 특정 행동 뒤에 따라오는 사회적이나 비사회적 보상과 처벌의 균형에 따라 그 행동이 유지되거나 약화될 수 있다고 설명했다.&lt;/p&gt;
&lt;p&gt;이 개념을 생각하면서 SNS가 떠올랐다.&lt;/p&gt;
&lt;p&gt;어떤 게시물을 올렸는데 평소보다 많은 관심을 받았다고 해보자.&lt;/p&gt;
&lt;p&gt;좋아요가 많아지고,&lt;br&gt;댓글이 달리고,&lt;br&gt;사람들이 나를 이야기한다.&lt;/p&gt;
&lt;p&gt;그러면 자연스럽게 비슷한 게시물을 다시 올리고 싶어질 수 있다.&lt;/p&gt;
&lt;p&gt;처음에는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 좋아서 하는 행동&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이라고 생각했는데,&lt;/p&gt;
&lt;p&gt;조금 더 자세히 보면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 행동 이후에 받았던 반응&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 행동을 계속 유지하게 만드는 이유 중 하나일 수도 있다.&lt;/p&gt;
&lt;p&gt;그렇다면 이런 질문도 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 반복하고 있는 행동 중에는 보상 때문에 계속하고 있는 것이 얼마나 있을까?&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;직접 경험하지 않아도 배울 수 있다&lt;/h1&gt;
&lt;p&gt;네 번째는 &lt;strong&gt;모방(imitation)&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;사람은 모든 행동을 직접 경험한 뒤 배우는 것은 아니다.&lt;/p&gt;
&lt;p&gt;다른 사람이 어떻게 행동하는지 관찰하고,&lt;br&gt;그 행동이 어떤 결과로 이어지는지를 본 뒤,&lt;br&gt;비슷한 행동을 따라 할 수도 있다.&lt;/p&gt;
&lt;p&gt;특히 자신이 중요하게 생각하거나 닮고 싶어 하는 사람의 행동은 더 강한 모델이 될 수 있다. 에이커스의 사회학습이론에서 모방은 다른 사람의 행동을 관찰한 뒤 이를 재현하는 과정이며, 그 사람이 행동을 통해 보상을 받는 모습 역시 모방 가능성에 영향을 줄 수 있다.&lt;/p&gt;
&lt;p&gt;생각해보면 우리는 생각보다 많은 것을 모방하면서 살아가는 것 같다.&lt;/p&gt;
&lt;p&gt;말하는 방식,&lt;br&gt;옷을 입는 방식,&lt;br&gt;공부하는 방식,&lt;br&gt;사람을 대하는 방식,&lt;br&gt;문제를 해결하는 방식까지 그렇다.&lt;/p&gt;
&lt;p&gt;처음에는 다른 사람에게서 본 행동이었는데 계속 반복하다 보면 어느 순간부터는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;원래 내가 이렇게 하는 사람&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이라고 생각하게 되기도 한다.&lt;/p&gt;
&lt;p&gt;그렇다면 지금 내가 자연스럽게 하고 있는 행동 가운데 얼마나 많은 행동이 누군가를 관찰하면서 시작된 것일까?&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;그렇다면 모든 행동은 주변 사람 때문에 만들어지는 것일까?&lt;/h1&gt;
&lt;p&gt;여기서 한 가지 의문이 생긴다.&lt;/p&gt;
&lt;p&gt;사회학습이론을 그대로 받아들이면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;결국 사람의 행동은 주변 환경이 결정하는 것 아닌가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각할 수도 있다.&lt;/p&gt;
&lt;p&gt;하지만 그렇게 단순하게 이해하는 것은 조금 다른 것 같다.&lt;/p&gt;
&lt;p&gt;같은 친구를 만나고,&lt;br&gt;같은 학교에 다니고,&lt;br&gt;비슷한 경험을 한 사람이라고 해서 모두 같은 행동을 하는 것은 아니기 때문이다.&lt;/p&gt;
&lt;p&gt;사회학습이론에서 중요한 것은 하나의 원인이 사람의 행동을 기계적으로 결정한다는 것이 아니라 여러 학습 과정이 특정 행동의 가능성을 높이거나 낮춘다는 점이다.&lt;/p&gt;
&lt;p&gt;어떤 사람들과 관계를 맺고 있는지,&lt;/p&gt;
&lt;p&gt;그 행동을 어떻게 생각하고 있는지,&lt;/p&gt;
&lt;p&gt;행동 뒤에 어떤 보상과 처벌을 경험하는지,&lt;/p&gt;
&lt;p&gt;그리고 어떤 행동을 모델로 보고 있는지가 함께 작용한다. 에이커스의 사회학습이론은 이러한 네 요소를 통해 일탈행동뿐 아니라 순응적 행동의 습득과 유지, 변화까지 설명하려는 일반적 학습 과정으로 제시되어 왔다.&lt;/p&gt;
&lt;p&gt;그래서 이 이론을 보고 나니 행동을 조금 다르게 바라보게 된다.&lt;/p&gt;
&lt;p&gt;문제가 되는 행동을 보고&lt;/p&gt;
&lt;p&gt;&lt;code&gt;왜 저 사람은 저럴까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 판단하는 것에서 끝나는 것이 아니라,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;저 행동을 계속 가능하게 만드는 환경은 무엇일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 질문할 수 있게 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;좋은 행동 역시 학습될 수 있다&lt;/h1&gt;
&lt;p&gt;사회학습이론은 주로 범죄와 일탈행동을 설명하는 맥락에서 등장했기 때문에 처음에는 부정적인 행동만 설명하는 이론처럼 느껴질 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 이 이론의 학습 원리는 반드시 일탈에만 적용되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;공부를 꾸준히 하는 친구들과 함께 지내고,&lt;/p&gt;
&lt;p&gt;노력하는 것이 중요하다는 생각을 공유하고,&lt;/p&gt;
&lt;p&gt;공부한 뒤 성취감을 경험하고,&lt;/p&gt;
&lt;p&gt;효율적으로 공부하는 사람의 방법을 따라 해본다면&lt;/p&gt;
&lt;p&gt;공부하는 행동 역시 강화될 수 있다.&lt;/p&gt;
&lt;p&gt;운동도 비슷하다.&lt;/p&gt;
&lt;p&gt;주변 사람들이 운동을 자연스러운 일상의 일부로 받아들이고,&lt;br&gt;운동을 했을 때 기분이 좋아지는 경험을 하고,&lt;br&gt;꾸준히 운동하는 사람의 모습을 관찰한다면&lt;/p&gt;
&lt;p&gt;운동을 지속할 가능성이 높아질 수 있다.&lt;/p&gt;
&lt;p&gt;결국 사회학습이론이 말하는 핵심은&lt;/p&gt;
&lt;p&gt;&lt;code&gt;사람은 나쁜 행동을 배운다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라기보다&lt;/p&gt;
&lt;p&gt;&lt;code&gt;사람은 행동을 배운다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;에 더 가까운 것 같다.&lt;/p&gt;
&lt;p&gt;어떤 행동을 배우게 되는지는 우리가 어떤 관계와 환경 속에 놓여 있는지에 따라 달라질 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;그렇다면 환경을 바꾸면 행동도 달라질 수 있을까?&lt;/h1&gt;
&lt;p&gt;이론을 공부하면서 가장 흥미로웠던 부분은 여기였다.&lt;/p&gt;
&lt;p&gt;행동이 학습된 것이라면,&lt;/p&gt;
&lt;p&gt;반대로 새로운 행동도 학습할 수 있지 않을까?&lt;/p&gt;
&lt;p&gt;어떤 습관을 바꾸고 싶을 때 우리는 흔히 의지부터 생각한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이번에는 반드시 해야지.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이제부터 절대 하지 말아야지.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그런데 사회학습이론의 관점에서 보면 의지만 바라보는 것은 충분하지 않을 수도 있다.&lt;/p&gt;
&lt;p&gt;내가 계속 같은 사람들과 같은 상황에 있고,&lt;br&gt;기존 행동이 계속해서 보상을 받고 있으며,&lt;br&gt;주변에서 같은 행동을 반복해서 보고 있다면&lt;/p&gt;
&lt;p&gt;혼자 의지만으로 행동을 바꾸는 것은 어려울 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 오히려 이런 질문들이 필요할지도 모른다.&lt;/p&gt;
&lt;p&gt;내 주변에는 어떤 행동을 하는 사람들이 많은가?&lt;/p&gt;
&lt;p&gt;나는 그 행동을 어떻게 정당화하고 있는가?&lt;/p&gt;
&lt;p&gt;그 행동을 했을 때 나는 무엇을 얻고 있는가?&lt;/p&gt;
&lt;p&gt;그리고 내가 자주 관찰하고 따라 하는 사람은 누구인가?&lt;/p&gt;
&lt;p&gt;행동을 바꾼다는 것은 결국 행동 하나만 고치는 것이 아닌 그 행동을 둘러싼 학습의 환경을 조금씩 바꾸는 일일 수도 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;나에게 적용해보기&lt;/h1&gt;
&lt;p&gt;에이커스의 사회학습이론을 나에게 적용해보면 몇 가지 질문을 던져볼 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;최근 내가 반복하고 있는 행동은 무엇인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 행동은 누구에게서 영향을 받은 것일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 그 행동을 어떤 행동이라고 생각하고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 행동을 했을 때 내가 얻는 보상은 무엇인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 무의식적으로 따라 하고 있는 사람은 누구인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그리고 마지막에는 이런 질문까지 이어진다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;지금의 환경은 내가 원하는 행동을 배우기 좋은 환경인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 질문은 생각보다 중요할 수 있다.&lt;/p&gt;
&lt;p&gt;나는 내 행동을 전부 내가 결정한다고 생각하지만,&lt;br&gt;실제로는 내가 자주 만나는 사람과 반복해서 경험하는 반응이 나의 선택에 계속 영향을 주고 있기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;내가 느낀 점&lt;/h1&gt;
&lt;p&gt;에이커스의 사회학습이론을 공부하면서 가장 크게 느낀 것은 행동을 개인의 성격만으로 설명하기 어렵다는 점이었다.&lt;/p&gt;
&lt;p&gt;누군가의 행동을 보면 우리는 쉽게 그 사람 자체를 평가한다.&lt;/p&gt;
&lt;p&gt;성실한 사람,&lt;br&gt;게으른 사람,&lt;br&gt;착한 사람,&lt;br&gt;문제가 있는 사람처럼 말이다.&lt;/p&gt;
&lt;p&gt;하지만 행동이 관계와 경험 속에서 학습된다는 관점으로 바라보면 조금 다른 질문을 할 수 있다.&lt;/p&gt;
&lt;p&gt;그 사람은 지금까지 어떤 행동을 보면서 살아왔을까?&lt;/p&gt;
&lt;p&gt;어떤 행동을 했을 때 인정받았을까?&lt;/p&gt;
&lt;p&gt;주변 사람들은 무엇을 당연하다고 이야기했을까?&lt;/p&gt;
&lt;p&gt;그리고 어떤 행동이 반복해서 보상받았을까?&lt;/p&gt;
&lt;p&gt;이렇게 생각하면 행동은 단순히 개인 안에서 만들어진 결과가 아니라 사람과 사람 사이에서 만들어진 결과이기도 하다는 생각이 든다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;오늘의 결론&lt;/h1&gt;
&lt;p&gt;나는 내가 스스로 판단하고 행동한다고 생각한다.&lt;/p&gt;
&lt;p&gt;물론 실제로 내가 선택하는 부분도 있다.&lt;/p&gt;
&lt;p&gt;하지만 그 선택을 만드는 재료까지 모두 내가 만든 것은 아닐 수 있다.&lt;/p&gt;
&lt;p&gt;내가 만난 사람,&lt;br&gt;반복해서 들은 말,&lt;br&gt;칭찬받았던 행동,&lt;br&gt;불이익을 받았던 행동,&lt;br&gt;그리고 내가 오랫동안 바라본 사람들의 모습이&lt;/p&gt;
&lt;p&gt;조금씩 나의 행동을 만들어왔을지도 모른다.&lt;/p&gt;
&lt;p&gt;그런 의미에서 에이커스의 사회학습이론은 단순히&lt;/p&gt;
&lt;p&gt;&lt;code&gt;왜 사람은 일탈행동을 하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 설명하는 이론을 넘어&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 어떤 환경 속에서 어떤 행동을 배우고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 질문을 던지게 만드는 이론이라고 생각한다.&lt;/p&gt;
&lt;p&gt;그리고 지금의 나에게 더 중요한 질문은 이것인 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 지금 어떤 사람들에게서, 어떤 행동을 배우며 살아가고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;내가 어떤 사람이 될지는 나의 의지만으로 결정되는 것은 아닐지도 모르지만,&lt;/p&gt;
&lt;p&gt;그래서 어쩌면 원하는 방향으로 살아가기 위해서는 나 자신만 바라보는 것만큼이나&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;내가 어떤 사람들과 함께하고, 어떤 행동을 보고, 어떤 행동에 보상을 주고 있는지를 살펴보는 일도 중요할 것 같다.&lt;/strong&gt;&lt;/p&gt;</description>
      <category>etc/자아의 바다</category>
      <category>모방</category>
      <category>사회학습이론</category>
      <category>에이커스</category>
      <category>정의</category>
      <category>차별적 강화</category>
      <category>차별적 접촉</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/102</guid>
      <comments>https://slowgrid.tistory.com/entry/What-behaviors-am-I-learning-as-I-live-Akers-Social-Learning-Theory#entry102comment</comments>
      <pubDate>Wed, 30 Sep 2026 18:24:13 +0900</pubDate>
    </item>
    <item>
      <title>OpenAI, dots 공개&amp;hellip;대화창을 닫아도 일을 이어가는 AI 비서와 GPT-6.1 Sol</title>
      <link>https://slowgrid.tistory.com/entry/OpenAI-dots-release-AI-assistant-that-continues-work-even-when-chat-window-closed-and-GPT-61-Sol</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;대화가 끝난 뒤에도 맡긴 일을 계속하는 AI 비서, dots가 ChatGPT에 새로 들어왔다.&lt;br&gt;이번 글에서는 dots가 기존 ChatGPT와 어떻게 다른지, 처음 설정할 때 무엇을 확인해야 하는지 정리해 보았다.&lt;br&gt;추가로 같은 날 공개된 GPT-6.1 Sol과 DevDay 2026의 주요 발표도 함께 살펴볼 예정이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;이번 발표에서 먼저 볼 것&lt;/h2&gt;
&lt;p&gt;이번 발표에서 가장 눈에 들어온 것은 &lt;strong&gt;대화를 마친 뒤에도 작업이 이어진다는 점&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;우리가 ChatGPT에 질문하고 답을 받는 흐름은 이미 익숙하지만 dots는 여기에 목표를 맡기고, 진행 상황을 확인하고, 필요한 순간에 판단을 내려주는 흐름을 더해준다.&lt;/p&gt;
&lt;p&gt;이를 가능하게 하는 작업 공간도 따로 존재하며 각 dot은 전용 클라우드 컴퓨터와 브라우저에서 움직이고, GPT-6 Astra를 사용한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;dots의 역할&lt;/td&gt;
&lt;td&gt;대화가 끝난 뒤에도 맡긴 일을 이어가는 AI 비서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기반 모델&lt;/td&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;작업 환경&lt;/td&gt;
&lt;td&gt;dot마다 전용 클라우드 컴퓨터와 브라우저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;앱 연결&lt;/td&gt;
&lt;td&gt;플러그인을 통해 4,000개 넘는 앱 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대화 채널&lt;/td&gt;
&lt;td&gt;ChatGPT 웹·모바일·데스크톱, Slack, Teams, 문자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성 방법&lt;/td&gt;
&lt;td&gt;ChatGPT 데스크톱 앱 또는 PC 웹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;함께 발표된 모델&lt;/td&gt;
&lt;td&gt;GPT-6.1 Sol&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;1. dots는 일반 ChatGPT 대화와 무엇이 다른가&lt;/h2&gt;
&lt;p&gt;dots는 목표를 받으면 다음에 할 일을 정하고, 연결한 앱을 확인하면서 작업을 이어가며, 사용자의 판단이 필요한 순간에는 다시 확인을 요청하는 방식으로 작동한다.&lt;/p&gt;
&lt;p&gt;여러 일을 동시에 맡길 수 있다는 점도 기존과의 차이점 중에 하나이다. &lt;/p&gt;
&lt;p&gt;하나의 대화에서 답이 끝날 때까지 기다리는 대신, 새 작업을 계속 추가하고 진행 상태를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;함께 일하면서 사용자의 선호와 결과물 기준을 기억하고, ChatGPT 메모리도 이어받는다. &lt;/p&gt;
&lt;p&gt;반복해서 비슷한 일을 맡길 때 매번 같은 설명을 줄이는 데 도움이 될 수 있는 부분이다.&lt;/p&gt;
&lt;h3&gt;연결한 앱을 보고 먼저 도울 일을 찾는다&lt;/h3&gt;
&lt;p&gt;dots는 백그라운드에서 연결한 앱을 읽고 도움이 필요한 일을 찾을 수 있다.&lt;/p&gt;
&lt;p&gt;여기에는 범위가 있다. 자료에 따르면 이 선제적 리서치 단계에서는 기능이 제한된 도구를 사용하며, &lt;strong&gt;메시지 전송, 앱 내용 변경, 컴퓨터 조작은 할 수 없다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;앱을 읽고 필요한 작업을 발견하는 것과 실제로 외부에 결과를 보내는 것은 서로 다른 단계로 이해하면 된다.&lt;/p&gt;
&lt;p&gt;자료에 소개된 초기 테스터 사례도 이 흐름을 보여준다. dot이 청구서 발송을 놓친 사실을 발견하고, 청구서를 작성한 뒤 사용자의 승인을 받아 발송했다.&lt;/p&gt;
&lt;h3&gt;어떤 일에 활용할 수 있나&lt;/h3&gt;
&lt;p&gt;공개된 활용 예시는 개발부터 콘텐츠 제작까지 이어진다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분야&lt;/th&gt;
&lt;th&gt;활용 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;개발&lt;/td&gt;
&lt;td&gt;사용자 피드백을 바탕으로 버그를 수정하고 테스트한 결과의 검토 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제품 출시&lt;/td&gt;
&lt;td&gt;출시 범위가 바뀌면 관련 자료 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;과학 연구&lt;/td&gt;
&lt;td&gt;새 데이터가 들어오면 분석 다시 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;영업&lt;/td&gt;
&lt;td&gt;고객 요구사항에 맞춰 제안서 갱신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;콘텐츠 제작&lt;/td&gt;
&lt;td&gt;녹취록에서 클립 구간, 소개 자료, SNS 초안 준비&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;공통점은 한 번의 답변으로 끝나지 않는 작업이라는 것이다. 자료가 바뀌거나 새 요청이 들어왔을 때, 기존 작업을 이어서 수정하는 흐름에 가깝다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 첫 dot 만들기&lt;/h2&gt;
&lt;p&gt;처음 설정은 PC에서 시작한다. ChatGPT 데스크톱 앱이나 PC 브라우저에서 dots를 열면 되며 윈도우 데스크톱 앱도 포함된다.&lt;/p&gt;
&lt;h3&gt;이름을 정하고 앱을 연결한다&lt;/h3&gt;
&lt;p&gt;안내에 따라 이름을 붙인다. 기본 주소는 &lt;code&gt;@내이름-dot&lt;/code&gt;이며, 이름을 지정하면 &lt;code&gt;@내이름-지은이름&lt;/code&gt; 형태로 바뀐다. 캐릭터나 펫을 선택할 수 있고, dot에 펫 제작을 맡길 수도 있다.&lt;/p&gt;
&lt;p&gt;이후 프로필의 &lt;code&gt;Customize → Plugins&lt;/code&gt;에서 사용할 앱을 선택한다. 앱이 요청하는 권한을 확인한 뒤 연결하면 된다.&lt;/p&gt;
&lt;p&gt;dot이 활용할 수 있는 정보는 연결한 범위까지이다. 그래서 처음부터 모든 앱을 붙이기보다, 맡기려는 일에 필요한 앱부터 연결하는 방식이라 생각하면 된다.&lt;/p&gt;
&lt;h3&gt;일을 맡기고 진행 상태를 확인한다&lt;/h3&gt;
&lt;p&gt;할 일과 필요한 정보를 대화창에 적는다. 파일이나 사진은 &lt;code&gt;+&lt;/code&gt; 버튼으로 첨부할 수 있다.&lt;/p&gt;
&lt;p&gt;작업 상태는 프로필에서 확인한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메뉴&lt;/th&gt;
&lt;th&gt;확인하는 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;In progress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;진행 중인 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Scheduled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;예약된 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Completed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;완료된 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;PC에서 설정을 마치면 ChatGPT 모바일 앱에서도 대화할 수 있으며, Slack이나 문자 연결 설정은 PC에서 진행한다.&lt;/p&gt;
&lt;p&gt;문자는 &lt;strong&gt;미국 Pro 사용자 대상 제한 베타&lt;/strong&gt;이며, 한국에서는 ChatGPT 앱, Slack, Teams를 통한 대화가 안내되어 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 사용할 수 있는 요금제와 비용&lt;/h2&gt;
&lt;p&gt;자료의 세부 제공 대상은 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;요금제&lt;/th&gt;
&lt;th&gt;제공 조건&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;유럽경제지역, 영국, 스위스 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business Premium&lt;/td&gt;
&lt;td&gt;제공 대상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;워크스페이스 관리자가 베타를 활성화하면 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plus&lt;/td&gt;
&lt;td&gt;공개 시점에는 대상에 포함되지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;순차 제공이므로 대상 요금제여도 계정에 메뉴가 나타나기까지 며칠 걸릴 수 있다. Enterprise의 베타는 처음에는 꺼져 있어 관리자 설정을 확인해야 한다.&lt;/p&gt;
&lt;p&gt;비용에서는 세 가지를 나누어 볼 필요가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;첫 dot은 추가 비용 없이 요금제에 포함된다.&lt;/li&gt;
&lt;li&gt;dot과의 대화는 ChatGPT 사용량 한도에 포함되지 않는다.&lt;/li&gt;
&lt;li&gt;출시 후 첫 한 달은 dots 사용량을 요금제 한도에서 차감하지 않는다고 안내되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다만 공식 발표에서는 dot에 Codex나 ChatGPT Work 작업을 시작하거나 관리하도록 요청하면, 해당 작업은 평소처럼 사용 한도에 포함된다고 구분한다. &lt;/p&gt;
&lt;p&gt;그렇기에 dot과의 대화가 한도에 들어가지 않는다는 설명을 모든 작업이 무제한이라는 의미로 받아들이면 안 된다.&lt;/p&gt;
&lt;p&gt;첫 한 달 이후의 요금제별 사용 조건은 별도로 공지할 예정이라고 한다. &lt;/p&gt;
&lt;p&gt;따라서 초기 제공 조건만 보고 이후에도 같은 방식으로 사용할 수 있다고 생각하지 않는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 어디까지 혼자 실행하게 할 것인가&lt;/h2&gt;
&lt;p&gt;dots를 설정할 때 가장 먼저 생각할 부분은 &lt;strong&gt;어떤 행동을 자동으로 실행하게 할지&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;자료 조사와 초안 작성은 비교적 쉽게 맡길 수 있으나 공유, 구매, 접근 권한과 관련된 행동은 확인이 필요한 순간이 많다.&lt;/p&gt;
&lt;p&gt;dots에는 기본 규칙이 들어 있고, &lt;code&gt;Customize → Custom rules&lt;/code&gt;에서 행동별 규칙을 추가할 수 있다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선택지&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Take action without asking&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;묻지 않고 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Take action if pre-approved&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;요청문에서 해당 행동을 직접 승인했을 때 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ask before taking action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실행 전에 확인 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Hand off to you&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용자에게 작업을 넘김&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;예를 들어 콘텐츠 작업이라면 초안 작성은 자동으로 진행하고, 실제 게시 전에는 승인을 받도록 정할 수 있다.&lt;/p&gt;
&lt;p&gt;자료에서도 돈이 나가거나 외부로 결과를 보내는 행동은 처음 몇 주 동안 &lt;code&gt;Ask before taking action&lt;/code&gt;으로 두고, 결과를 확인하면서 범위를 넓히는 방식을 권한다. 규칙을 설정하더라도 dot이 실수할 수 있기 때문이다.&lt;/p&gt;
&lt;h3&gt;내 컴퓨터 접근은 별도로 허용한다&lt;/h3&gt;
&lt;p&gt;dot의 클라우드 컴퓨터와 사용자의 컴퓨터는 구분되며, 사용자 컴퓨터 연결은 기본적으로 꺼져 있다.&lt;/p&gt;
&lt;p&gt;연결하려면 해당 컴퓨터의 ChatGPT 데스크톱 앱에서 &lt;code&gt;Allow access&lt;/code&gt;를 확인해야 한다. 연결을 끊을 때는 &lt;code&gt;Revoke access&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;p&gt;비밀번호 변경처럼 민감한 작업은 규칙과 관계없이 사용자가 직접 해야 한다. 지원되는 로그인 방식에서는 저장된 비밀번호를 모델에 노출하지 않고 사용한다고 안내되어 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 반복 업무에 어떻게 적용할 수 있을까&lt;/h2&gt;
&lt;p&gt;dots의 특징은 반복되는 작업에서 더 쉽게 이해할 수 있다.&lt;/p&gt;
&lt;h3&gt;아침 일정 브리핑&lt;/h3&gt;
&lt;p&gt;“매일 아침 캘린더를 보고 오늘 일정을 알려줘”처럼 반복 확인을 요청하면 &lt;code&gt;Scheduled&lt;/code&gt;에 등록된다.&lt;/p&gt;
&lt;p&gt;등록된 작업을 열어 반복 주기, 시간, 완료 알림을 수정할 수 있다. 나중에 알려 달라는 리마인더도 같은 메뉴에서 관리한다.&lt;/p&gt;
&lt;h3&gt;코딩 작업&lt;/h3&gt;
&lt;p&gt;Codex에 클라우드 환경을 먼저 만들어두면 dot이 그 환경에서 작업을 만들 수 있다.&lt;/p&gt;
&lt;p&gt;사용자 피드백을 받아 수정하고 검토를 요청하는 공개 예시처럼, 코딩 작업과 결과 확인을 연결하는 흐름이다.&lt;/p&gt;
&lt;h3&gt;콘텐츠 초안 준비&lt;/h3&gt;
&lt;p&gt;녹취록이나 자료를 바탕으로 게시물 초안을 준비하는 작업도 활용 예시 중 하나이다.&lt;/p&gt;
&lt;p&gt;블로그 작업에 적용한다면 자료 정리와 초안 작성까지 맡기고, 게시 전 확인은 직접 하는 방식으로 시작할 수 있다. 이때 결과물의 길이, 독자, 문체, 승인할 행동을 함께 적어두면 원하는 작업 범위를 전달하기 쉽다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 함께 공개된 GPT-6.1 Sol&lt;/h2&gt;
&lt;p&gt;같은 날 발표된 GPT-6.1 Sol은 GPT-6 Astra에 가까운 성능을 더 낮은 비용으로 제공하는 모델로 소개되었다.&lt;/p&gt;
&lt;p&gt;9월 22일 공개된 GPT-6 Sol의 일주일 만의 후속 모델이며, 표준 입출력 토큰 가격은 Astra의 5분의 1이다.&lt;/p&gt;
&lt;h3&gt;토큰 가격 비교&lt;/h3&gt;
&lt;p&gt;아래는 100만 토큰 기준 표준 요금이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;GPT-6.1 Sol&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;GPT-6 Astra&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;입력&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;2달러&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;10달러&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출력&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;10달러&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;50달러&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;캐시 입력&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.1달러&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1달러&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;입출력 가격과 캐시 입력 가격의 비율은 다르다. 입출력은 Astra의 5분의 1이고, 캐시 입력은 10분의 1이다.&lt;/p&gt;
&lt;h3&gt;성능과 실제 작업 비용&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시험&lt;/th&gt;
&lt;th&gt;주요 결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DeepSWE v1.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;약 5분의 1 비용으로 Astra와 대등한 코드 작업 성능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OSWorld 2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최대 추론 강도에서 Astra와 2.1%포인트 차이, 작업당 비용은 약 7분의 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Terminal-Bench Science 0.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작업당 평균 비용 Sol 5.47달러, Astra 23.80달러&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;과학 연구 시험에서는 Sol의 평균 작업 비용이 75% 넘게 낮았지만, 최고 점수는 Astra의 68.1%였다.&lt;/p&gt;
&lt;p&gt;따라서 모델을 고를 때는 토큰 단가뿐 아니라, 해당 작업에서 필요한 성능과 완료까지 드는 비용을 함께 보는 것이 좋다.&lt;/p&gt;
&lt;h3&gt;어디에서 사용할 수 있나&lt;/h3&gt;
&lt;p&gt;공개 시점 기준으로 Plus·Pro·Business·Enterprise·Edu 사용자는 &lt;strong&gt;ChatGPT Work와 Codex&lt;/strong&gt;에서 선택할 수 있으며 일반 채팅 화면에는 아직 제공되지 않는다고 안내되어 있다.&lt;/p&gt;
&lt;p&gt;API 모델 이름은 &lt;code&gt;gpt-6.1-sol&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;Astra를 최대 8배 빠르게 실행하는 Ultrafast가 먼저 공개됐으며, Sol용 Ultrafast도 예정되어 있다.&lt;/p&gt;
&lt;h3&gt;API 사용 시 확인할 조건&lt;/h3&gt;
&lt;p&gt;개발자라면 모델 이름 외에도 아래 조건을 확인할 필요가 있다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;자료에 정리된 조건&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;컨텍스트&lt;/td&gt;
&lt;td&gt;최대 105만 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대 출력&lt;/td&gt;
&lt;td&gt;12만 8천 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추론 강도&lt;/td&gt;
&lt;td&gt;낮음·중간·높음·매우 높음·최대, 기본은 중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추론 끄기&lt;/td&gt;
&lt;td&gt;지원하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;도구 호출&lt;/td&gt;
&lt;td&gt;Responses API 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat Completions&lt;/td&gt;
&lt;td&gt;도구 없이 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch·Flex&lt;/td&gt;
&lt;td&gt;표준 요금의 절반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;표준 요금의 2배&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;긴 입력에는 별도 요금 조건이 있다. 입력이 &lt;strong&gt;27만 2천 토큰을 넘으면 요청 전체에 입력 및 캐시 입력은 2배, 출력은 1.5배 요금&lt;/strong&gt;이 적용된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. DevDay 2026에서 함께 발표된 기능&lt;/h2&gt;
&lt;p&gt;dots와 GPT-6.1 Sol 외에도 팀 협업, 문서, 회의, 코딩과 관련된 발표가 이어졌다.&lt;/p&gt;
&lt;h3&gt;일반 사용자와 팀이 볼 만한 소식&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;발표&lt;/th&gt;
&lt;th&gt;주요 내용&lt;/th&gt;
&lt;th&gt;대상 및 조건&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;ChatGPT Space&lt;/td&gt;
&lt;td&gt;팀원, ChatGPT, dot이 함께 일하고 대화를 페이지나 슬라이드로 전환&lt;/td&gt;
&lt;td&gt;Pro·Business·Enterprise, 모바일은 예정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;살아 있는 페이지&lt;/td&gt;
&lt;td&gt;차트, 대시보드, 이미지를 담은 문서, dot을 태그해 수정하고 연결 도구로 최신 상태 유지&lt;/td&gt;
&lt;td&gt;Pro·Business·Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;함께 만드는 슬라이드&lt;/td&gt;
&lt;td&gt;동시 편집, ChatGPT에서 발표, 서식을 유지하며 PowerPoint·Google Slides로 내보내기&lt;/td&gt;
&lt;td&gt;Pro·Business·Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meetings 플러그인&lt;/td&gt;
&lt;td&gt;회의 녹음에서 핵심, 결정 사항, 후속 작업을 정리해 Space에 저장&lt;/td&gt;
&lt;td&gt;Pro·Business 베타&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack·Teams의 &lt;code&gt;@ChatGPT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;채널 멘션으로 조사, 요약, 수정안 준비, 개별 라이선스 없는 팀원도 참여&lt;/td&gt;
&lt;td&gt;Business·Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;팀 작업&lt;/td&gt;
&lt;td&gt;주간 보고 등 반복 업무를 일정이나 새 메시지에 맞춰 실행&lt;/td&gt;
&lt;td&gt;Business·Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;월 500달러 Pro&lt;/td&gt;
&lt;td&gt;키노트 기준 Plus의 25배 사용량과 Astra Ultrafast 독점 제공&lt;/td&gt;
&lt;td&gt;새 Pro 요금제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex 클라우드, CLI, 코드 리뷰&lt;/td&gt;
&lt;td&gt;노트북을 닫아도 코딩 작업 지속, CLI 자연어 지시, 자리를 비운 동안 1차 리뷰&lt;/td&gt;
&lt;td&gt;클라우드는 Plus 이상, CLI·리뷰는 전 요금제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT로 로그인&lt;/td&gt;
&lt;td&gt;다른 서비스에 ChatGPT 계정으로 로그인&lt;/td&gt;
&lt;td&gt;조직 설정에 따라 전 사용자&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;월 200달러 Pro의 신규 가입도 다시 열렸다고 안내되어 있다.&lt;/p&gt;
&lt;p&gt;ChatGPT로 로그인할 때 다른 서비스에 전달되는 정보는 이름, 이메일, 프로필 사진이다. 대화와 메모리는 공유되지 않는다고 정리되어 있다.&lt;/p&gt;
&lt;h3&gt;개발자 및 기업용 발표&lt;/h3&gt;
&lt;p&gt;개발자와 기업을 위한 소식도 함께 나왔다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Decisions API&lt;/strong&gt;: Luna가 미리 정해진 선택지 안에서 판단을 반환한다. 1초 미만 응답을 내세운 제한 프리뷰이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agents API 컴퓨터 사용&lt;/strong&gt;: Codex를 움직이는 틀을 관리형 서비스로 제공하며, 컴퓨터 조작까지 맡길 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;플러그인 확장 및 MCP 이벤트&lt;/strong&gt;: ChatGPT 안에 사이드바, 패널, 파일 뷰어를 만들고 연결 앱의 이벤트로 자동화를 시작한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codex Security Cloud&lt;/strong&gt;: 저장소와 새 커밋을 계속 점검하고 수정안까지 클라우드에서 준비한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private Intelligence&lt;/strong&gt;: 고객 데이터를 보관하지 않는 안전 검토 방식과 기밀 추론 프리뷰이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAI Marketplace&lt;/strong&gt;: 기존 OpenAI 약정 일부로 Adobe·Figma 등 승인된 파트너 32곳의 제품을 구매할 수 있는 기업용 베타이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amazon Bedrock Managed Agents&lt;/strong&gt;: AWS에서 OpenAI 에이전트를 운영하는 관리형 서비스이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;발표 범위를 보면 대화, 문서 작성, 앱 연결, 반복 업무 실행이 같은 작업 환경으로 이어지는 방향을 확인할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. dots 메뉴가 보이지 않는다면&lt;/h2&gt;
&lt;p&gt;먼저 요금제와 접속 환경을 확인하면 된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;공개 시점의 대상 요금제인지 확인한다. Plus는 현재 대상이 아니다.&lt;/li&gt;
&lt;li&gt;모바일이 아니라 ChatGPT 데스크톱 앱이나 PC 웹에서 확인한다.&lt;/li&gt;
&lt;li&gt;Enterprise라면 관리자가 베타를 활성화했는지 확인한다.&lt;/li&gt;
&lt;li&gt;조건을 충족해도 순차 제공으로 며칠 걸릴 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;이번 발표를 보며 생각한 점&lt;/h2&gt;
&lt;p&gt;dots에서 관심이 가는 부분은 작업을 계속 이어갈 수 있다는 점이다. 사용자가 자리를 비운 동안 자료를 확인하고, 바뀐 내용을 반영하고, 결과를 준비하는 흐름이 가능해진다.&lt;/p&gt;
&lt;p&gt;그만큼 처음 설정할 때 정해야 할 것도 있다. 어떤 앱에 접근하게 할지, 결과물을 어떤 기준으로 만들지, 어느 행동부터 승인을 받을지.&lt;/p&gt;
&lt;p&gt;처음에는 자료 조사, 일정 브리핑, 콘텐츠 초안처럼 결과를 확인하기 쉬운 작업부터 맡겨볼 만할 것 같고, 이후 작업이 원하는 방식으로 진행되는지 확인하면서 실행 범위를 넓히면 될 것 같다.&lt;/p&gt;
&lt;p&gt;GPT-6.1 Sol도 같은 맥락에서 볼 수 있다. 작업이 길어지고 반복 횟수가 늘어나면 모델 성능과 함께 비용이 중요해진다. Astra와 Sol을 작업 난도에 맞춰 선택하는 방식이 앞으로 더 자주 쓰일 것으로 보인다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://openai.com/ko-KR/index/introducing-dots/&quot;&gt;OpenAI dots 공식 발표&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI News</category>
      <category>dots</category>
      <category>gpt-6.1 Sol</category>
      <category>openai</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/101</guid>
      <comments>https://slowgrid.tistory.com/entry/OpenAI-dots-release-AI-assistant-that-continues-work-even-when-chat-window-closed-and-GPT-61-Sol#entry101comment</comments>
      <pubDate>Wed, 30 Sep 2026 09:38:52 +0900</pubDate>
    </item>
    <item>
      <title>나는 누구를 보며 배우고 있는가: 반두라의 모방학습이론과 관찰학습</title>
      <link>https://slowgrid.tistory.com/entry/Whom-Am-I-Learning-From-Banduras-Theory-of-Imitation-Learning-and-Observational-Learning</link>
      <description>&lt;p&gt;우리가 무언가를 배운다고 하면 보통 직접 해보는 모습을 떠올린다.&lt;/p&gt;
&lt;p&gt;문제를 풀어보고,&lt;br&gt;실수를 해보고,&lt;br&gt;그 결과를 통해 조금씩 방법을 알아가는 것이다.&lt;/p&gt;
&lt;p&gt;그런데 가만히 생각해보면 우리가 배운 것 중에는 직접 경험하지 않은 것도 꽤 많다.&lt;/p&gt;
&lt;p&gt;어른에게 인사하는 방법도 그렇고,&lt;br&gt;처음 가본 장소에서 어떻게 행동해야 하는지도 그렇다.&lt;/p&gt;
&lt;p&gt;누군가 발표하는 모습을 보고 발표하는 방법을 배우기도 하고,&lt;br&gt;다른 사람이 실수하는 모습을 보면서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 저렇게 하지 말아야겠다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하기도 한다.&lt;/p&gt;
&lt;p&gt;그렇다면 이런 질문을 해볼 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;사람은 직접 경험하지 않고도 배울 수 있을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그리고 조금 더 나아가면,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 지금까지 누구의 행동을 보면서 지금의 내가 되었을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 질문을 생각하다 보면 앨버트 반두라(Albert Bandura)의 사회학습이론, 그중에서도 관찰학습과 모방이라는 개념을 만나게 된다.&lt;/p&gt;
&lt;p&gt;반두라는 사람이 반드시 직접 보상이나 처벌을 경험해야만 행동을 배우는 것은 아니라고 보았다.&lt;/p&gt;
&lt;p&gt;다른 사람의 행동과 그 행동 뒤에 따라오는 결과를 보는 것만으로도 우리는 많은 것을 배울 수 있다. 그의 연구는 행동을 단순히 외부 자극에 대한 반응으로만 바라보던 설명에서 벗어나, 관찰과 인지 과정이 학습에서 중요하다는 점을 보여주었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;먼저, 반두라의 모방학습이론이란 무엇일까?&lt;/h1&gt;
&lt;p&gt;흔히 반두라의 이론을 ‘모방학습이론’이라고 부르지만 조금 더 정확하게 말하면 핵심 개념은 &lt;strong&gt;관찰학습(observational learning)&lt;/strong&gt;과 &lt;strong&gt;모델링(modeling)&lt;/strong&gt;에 가깝다.&lt;/p&gt;
&lt;p&gt;모방은 다른 사람의 행동을 비슷하게 따라 하는 것이다.&lt;/p&gt;
&lt;p&gt;반면 관찰학습은 조금 더 넓다.&lt;/p&gt;
&lt;p&gt;다른 사람이 무엇을 하는지 보고,&lt;br&gt;그 행동이 어떤 결과를 가져오는지 살펴보고,&lt;br&gt;그 정보를 기억한 다음,&lt;br&gt;필요한 상황에서 자신의 행동에 사용하는 과정까지 포함한다.&lt;/p&gt;
&lt;p&gt;예를 들어 친구가 수업 시간에 질문했는데 선생님이&lt;/p&gt;
&lt;p&gt;&lt;code&gt;좋은 질문이네.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 칭찬했다고 생각해볼 수 있다.&lt;/p&gt;
&lt;p&gt;나는 직접 칭찬을 받은 것이 아니다.&lt;/p&gt;
&lt;p&gt;하지만 그 장면을 보면서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;질문을 하면 긍정적인 반응을 얻을 수도 있구나.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 것을 배울 수 있다.&lt;/p&gt;
&lt;p&gt;반대로 누군가 규칙을 어겼다가 좋지 않은 결과를 경험하는 모습을 본다면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;저 행동은 하지 않는 것이 좋겠다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각할 수도 있다.&lt;/p&gt;
&lt;p&gt;즉 우리는 자신의 경험만으로 배우는 것이 아니라 다른 사람의 경험을 어느 정도 빌려서 배우기도 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;그렇다면 우리는 본 것을 모두 따라 할까?&lt;/h1&gt;
&lt;p&gt;처음에는 관찰학습이라는 말을 들으면서 이런 생각이 들었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그렇다면 우리는 다른 사람의 행동을 보면 자동으로 따라 하는 것일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;하지만 실제로는 그렇지 않다.&lt;/p&gt;
&lt;p&gt;하루 동안 우리는 수많은 사람을 본다.&lt;/p&gt;
&lt;p&gt;길에서 지나치는 사람도 보고,&lt;br&gt;친구의 행동도 보고,&lt;br&gt;유튜브나 SNS에서 처음 보는 사람의 행동도 본다.&lt;/p&gt;
&lt;p&gt;그렇다고 그 모든 행동을 따라 하지는 않는다.&lt;/p&gt;
&lt;p&gt;어떤 행동은 기억조차 하지 못한다.&lt;/p&gt;
&lt;p&gt;반두라는 관찰이 실제 행동으로 이어지기 위해 몇 가지 과정이 필요하다고 설명했다.&lt;/p&gt;
&lt;p&gt;대표적으로 &lt;strong&gt;주의, 파지, 행동 재생, 동기&lt;/strong&gt;라는 네 가지 과정이 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;관찰한 행동이 내 행동이 되기까지&lt;/h1&gt;
&lt;h3&gt;1. 먼저 그 행동을 보아야 한다 — 주의&lt;/h3&gt;
&lt;p&gt;당연해 보이지만 학습의 시작은 관심을 가지는 것이다.&lt;/p&gt;
&lt;p&gt;아무리 좋은 행동이 눈앞에서 일어나더라도 내가 그것을 제대로 보지 않았다면 배우기 어렵다.&lt;/p&gt;
&lt;p&gt;예를 들어 발표를 잘하는 사람의 모습을 보고 있다고 해보자.&lt;/p&gt;
&lt;p&gt;단순히&lt;/p&gt;
&lt;p&gt;&lt;code&gt;발표를 잘한다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하는 것과&lt;/p&gt;
&lt;p&gt;&lt;code&gt;말을 시작할 때 결론부터 말하는구나.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;슬라이드를 그대로 읽지 않는구나.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;질문을 받으면 바로 대답하기보다 잠깐 생각하는구나.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 관찰하는 것은 다르다.&lt;/p&gt;
&lt;p&gt;관찰학습의 시작은 단순히 사람을 보는 것이 아니라 &lt;strong&gt;행동의 특징을 발견하는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 본 것을 기억해야 한다 — 파지&lt;/h3&gt;
&lt;p&gt;주의 깊게 보았다고 해서 모두 배운 것은 아니다.&lt;/p&gt;
&lt;p&gt;나중에 사용할 수 있도록 어느 정도 기억되어야 한다.&lt;/p&gt;
&lt;p&gt;누군가 코딩하는 모습을 옆에서 한 시간 동안 보았는데 며칠 뒤&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 사람이 어떻게 했지?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 물었을 때 아무것도 기억나지 않는다면 실제 행동으로 이어지기 어렵다.&lt;/p&gt;
&lt;p&gt;그래서 관찰할 때는 단순히 보는 것보다&lt;/p&gt;
&lt;p&gt;&lt;code&gt;왜 저렇게 했을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;순서가 어떻게 되었지?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하면서 보는 것이 더 중요하다.&lt;/p&gt;
&lt;p&gt;생각해보면 나도 무언가를 잘 배웠던 순간에도 비슷한 과정이 있었던 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. 기억한 것을 실제로 해볼 수 있어야 한다 — 행동 재생&lt;/h3&gt;
&lt;p&gt;아무리 정확하게 기억하고 있어도 내가 그것을 실행할 능력이 없다면 바로 행동으로 이어지지는 않는다.&lt;/p&gt;
&lt;p&gt;축구 선수의 경기를 수없이 본다고 해서 똑같은 기술을 바로 사용할 수 없는 것과 비슷하다.&lt;/p&gt;
&lt;p&gt;우리는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;어떻게 하는지 안다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;와&lt;/p&gt;
&lt;p&gt;&lt;code&gt;실제로 할 수 있다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 종종 같은 것으로 생각한다.&lt;/p&gt;
&lt;p&gt;하지만 둘 사이에는 꽤 큰 차이가 있다.&lt;/p&gt;
&lt;p&gt;관찰은 행동의 방향을 알려줄 수 있지만 실제 능력을 만들기 위해서는 연습이 필요하다.&lt;/p&gt;
&lt;p&gt;이 부분을 생각하면서 관찰학습이라는 것이 단순히 남을 따라 하는 이론은 아니라는 생각이 들었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4. 할 수 있어도 하고 싶어야 한다 — 동기&lt;/h3&gt;
&lt;p&gt;마지막으로 행동할 이유가 필요하다.&lt;/p&gt;
&lt;p&gt;방법을 알고,&lt;br&gt;기억하고,&lt;br&gt;실제로 할 수 있어도&lt;/p&gt;
&lt;p&gt;&lt;code&gt;굳이 저 행동을 할 이유가 없다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각한다면 행동하지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;여기에서 중요한 것이 보상과 처벌이다.&lt;/p&gt;
&lt;p&gt;특히 반두라는 내가 직접 보상이나 처벌을 받지 않더라도 다른 사람이 어떤 결과를 경험하는지 보는 것이 나의 행동에 영향을 줄 수 있다고 보았다.&lt;/p&gt;
&lt;p&gt;이를 &lt;strong&gt;대리강화(vicarious reinforcement)&lt;/strong&gt;라고 한다.&lt;/p&gt;
&lt;p&gt;다른 사람이 어떤 행동을 하고 좋은 결과를 얻는 모습을 보면 그 행동을 해보고 싶은 마음이 커질 수 있다.&lt;/p&gt;
&lt;p&gt;반대로 좋지 않은 결과를 경험하는 모습을 보면 이미 행동을 배웠더라도 실제로 하지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;반두라의 1965년 연구에서는 모델이 보상받거나 처벌받는 모습을 본 아이들의 즉각적인 모방 행동에는 차이가 있었지만, 이후 모든 아이에게 행동을 따라 할 보상을 제공하자 집단 간 차이가 크게 줄어들었다. 이는 어떤 행동을 &lt;strong&gt;배웠는가&lt;/strong&gt;와 그것을 실제로 &lt;strong&gt;실행하는가&lt;/strong&gt;가 반드시 같은 문제는 아니라는 점을 보여주었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;보보인형 실험은 무엇을 보여주었을까?&lt;/h1&gt;
&lt;p&gt;반두라의 이름과 함께 가장 많이 언급되는 연구가 보보인형(Bobo doll) 실험이다.&lt;/p&gt;
&lt;p&gt;아이들은 성인이 보보인형을 공격적으로 대하는 모습을 관찰했고, 이후 같은 인형이 있는 공간에서 놀게 되었다.&lt;/p&gt;
&lt;p&gt;공격적인 성인의 행동을 본 아이들은 그렇지 않은 행동을 본 아이들보다 모델이 보여준 공격적인 행동을 더 많이 나타냈다.&lt;/p&gt;
&lt;p&gt;이 연구는 아이들이 반드시 직접 보상을 받아야만 행동을 배우는 것이 아니라 &lt;strong&gt;다른 사람의 행동을 관찰하는 것 자체가 학습에 영향을 줄 수 있다&lt;/strong&gt;는 점을 보여준 대표적인 연구가 되었다.&lt;/p&gt;
&lt;p&gt;물론 이 실험 하나만으로 현실에서 나타나는 모든 공격 행동을 설명할 수 있는 것은 아니다.&lt;/p&gt;
&lt;p&gt;사람의 행동에는 성격도 영향을 주고,&lt;br&gt;환경도 영향을 주고,&lt;br&gt;과거 경험이나 상황도 영향을 준다.&lt;/p&gt;
&lt;p&gt;그럼에도 이 실험이 흥미로운 이유는 한 가지인 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;우리는 생각보다 다른 사람의 행동을 많이 보고 배우고 있다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;는 사실을 눈에 보이는 형태로 보여주었기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;모방은 단순히 따라 하는 것일까?&lt;/h1&gt;
&lt;p&gt;모방이라는 말을 들으면 흔히 그대로 복사하는 모습을 생각한다.&lt;/p&gt;
&lt;p&gt;누군가 이렇게 행동했으니 나도 똑같이 행동하는 것이다.&lt;/p&gt;
&lt;p&gt;하지만 관찰학습은 조금 다르게 생각해볼 수 있다.&lt;/p&gt;
&lt;p&gt;우리는 누군가의 행동을 그대로 가져오기보다 그 행동에서 하나의 규칙을 발견하기도 한다.&lt;/p&gt;
&lt;p&gt;예를 들어 발표를 잘하는 여러 사람을 보면서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;발표할 때는 천천히 말해야 한다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 것을 배울 수도 있고,&lt;/p&gt;
&lt;p&gt;문제를 잘 해결하는 사람을 보면서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;문제가 생기면 바로 답을 찾기보다 원인을 먼저 나누어보는구나.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 방식을 배울 수도 있다.&lt;/p&gt;
&lt;p&gt;그렇게 생각하면 내가 다른 사람을 따라 한다는 표현보다&lt;/p&gt;
&lt;p&gt;&lt;code&gt;다른 사람을 통해 행동의 가능성을 배운다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 표현이 더 적절하게 느껴진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;우리는 지금 누구를 관찰하고 있을까?&lt;/h1&gt;
&lt;p&gt;반두라의 관찰학습을 공부하면서 가장 먼저 떠오른 것은 SNS와 유튜브였다.&lt;/p&gt;
&lt;p&gt;예전보다 지금은 다른 사람의 행동을 관찰할 기회가 훨씬 많다.&lt;/p&gt;
&lt;p&gt;어떤 사람이 무엇을 사고,&lt;br&gt;어디에 가고,&lt;br&gt;어떻게 공부하고,&lt;br&gt;어떻게 일하며,&lt;br&gt;어떤 방식으로 성공했다고 말하는지를 매일 볼 수 있다.&lt;/p&gt;
&lt;p&gt;그중 일부는 나에게 도움이 되는 모델이 된다.&lt;/p&gt;
&lt;p&gt;좋은 공부 방법을 배우기도 하고,&lt;br&gt;새로운 기술을 배우기도 하고,&lt;br&gt;몰랐던 삶의 방식을 발견하기도 한다.&lt;/p&gt;
&lt;p&gt;하지만 한편으로는 이런 질문도 생긴다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 무엇을 배우고 있는지 의식하면서 보고 있을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;우리가 반복해서 보는 행동은 생각보다 자연스럽게 정상적인 행동처럼 느껴질 수 있다.&lt;/p&gt;
&lt;p&gt;모두가 무엇인가를 끊임없이 사고 있다면 소비가 자연스럽게 느껴지고,&lt;/p&gt;
&lt;p&gt;모두가 쉬지 않고 일하는 모습만 보여준다면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;열심히 산다는 것은 항상 바쁘게 사는 것이다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각할 수도 있다.&lt;/p&gt;
&lt;p&gt;관찰학습의 관점에서 보면 무엇을 보는가는 단순한 콘텐츠 선택의 문제가 아닐 수도 있다.&lt;/p&gt;
&lt;p&gt;어쩌면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 어떤 행동을 계속 학습하고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;의 문제일 수도 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;그렇다면 좋은 사람만 따라 하면 되는 것일까?&lt;/h1&gt;
&lt;p&gt;그렇다고 모든 문제를&lt;/p&gt;
&lt;p&gt;&lt;code&gt;좋은 사람을 찾아서 따라 하면 된다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 정리하기도 어려운 것 같다.&lt;/p&gt;
&lt;p&gt;아무리 좋은 모델이라도 그 사람과 나의 상황은 다를 수 있다.&lt;/p&gt;
&lt;p&gt;누군가에게 효과가 있었던 공부 방법이 나에게는 맞지 않을 수 있고,&lt;/p&gt;
&lt;p&gt;어떤 사람에게 좋은 선택이었던 것이 나에게도 같은 결과를 가져온다는 보장도 없다.&lt;/p&gt;
&lt;p&gt;그래서 중요한 것은 모델을 그대로 복사하는 것이 아니라 관찰한 행동을 한 번 해석하는 과정인 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;저 사람은 왜 저 행동을 했을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 행동은 어떤 결과를 만들었을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;저 사람의 상황과 나의 상황은 무엇이 다를까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그중에서 내가 가져올 부분은 무엇일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이런 질문을 거치면 모방은 단순한 복사가 아니라 하나의 학습 도구가 될 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;나에게 적용해보기&lt;/h1&gt;
&lt;p&gt;반두라의 이론을 공부하면서 앞으로는 내가 무엇을 배우고 있는지를 조금 더 의식해보려고 한다.&lt;/p&gt;
&lt;p&gt;가끔 다음과 같은 질문을 스스로에게 던져볼 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;요즘 내가 가장 자주 보고 있는 사람은 누구인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 그 사람에게서 어떤 행동을 자연스럽게 배우고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 부러워하는 것은 그 사람의 결과일까, 행동 방식일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;좋아 보인다는 이유만으로 따라 하고 있는 행동은 없는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;반대로 다른 사람의 실패를 보면서 배운 것은 무엇인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그리고 한 가지 질문은 조금 다른 방향에서 생각해볼 필요가 있을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;누군가는 나를 보면서 무엇을 배우고 있을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;관찰학습에서는 나만 다른 사람을 관찰하는 것이 아니다.&lt;/p&gt;
&lt;p&gt;나 역시 누군가에게는 하나의 모델이 될 수 있다.&lt;/p&gt;
&lt;p&gt;부모에게는 아이가,&lt;br&gt;아이에게는 부모가,&lt;br&gt;후배에게는 선배가,&lt;br&gt;친구에게는 친구가 서로의 행동을 보고 배운다.&lt;/p&gt;
&lt;p&gt;내가 아무 생각 없이 반복하고 있는 행동이 누군가에게는 하나의 학습 자료가 될 수도 있다.&lt;/p&gt;
&lt;p&gt;이렇게 생각하면 행동을 조금 다른 시선으로 바라보게 되는 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;내가 느낀 점&lt;/h1&gt;
&lt;p&gt;반두라의 모방학습이론을 공부하면서 가장 흥미로웠던 것은 사람이 반드시 직접 경험해야만 배울 수 있는 것은 아니라는 점이었다.&lt;/p&gt;
&lt;p&gt;우리는 다른 사람이 성공하는 모습을 보면서 배우고,&lt;/p&gt;
&lt;p&gt;다른 사람이 실패하는 모습을 보면서도 배운다.&lt;/p&gt;
&lt;p&gt;누군가 칭찬받는 모습을 보고 행동을 선택하기도 하고,&lt;/p&gt;
&lt;p&gt;누군가 좋지 않은 결과를 경험하는 모습을 보면서 행동하지 않기로 결정하기도 한다.&lt;/p&gt;
&lt;p&gt;결국 우리의 경험에는 내가 직접 겪은 경험뿐 아니라 &lt;strong&gt;다른 사람을 통해 간접적으로 경험한 것들&lt;/strong&gt;도 포함되어 있는 셈이다.&lt;/p&gt;
&lt;p&gt;그렇게 생각하면 지금의 나를 만든 것은 내가 해온 행동만은 아닐 것이며,&lt;/p&gt;
&lt;p&gt;지금까지 내가 지켜본 사람들,&lt;/p&gt;
&lt;p&gt;내가 반복해서 본 행동들,&lt;/p&gt;
&lt;p&gt;그리고 그 행동의 결과를 보며 내렸던 수많은 판단 역시 지금의 나를 만드는 데 조금씩 영향을 주었을 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;오늘의 결론&lt;/h1&gt;
&lt;p&gt;반두라의 관찰학습이론을 단순하게 정리하면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;사람은 다른 사람을 보면서도 배운다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 말할 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 조금 더 생각해보면 중요한 것은 단순히 다른 사람을 따라 한다는 사실이 아니다.&lt;/p&gt;
&lt;p&gt;우리는 누군가의 행동을 보고,&lt;/p&gt;
&lt;p&gt;그 행동을 기억하고,&lt;/p&gt;
&lt;p&gt;그 결과를 판단하고,&lt;/p&gt;
&lt;p&gt;필요하다면 자신의 행동으로 옮긴다.&lt;/p&gt;
&lt;p&gt;그리고 그 과정은 우리가 의식하지 않는 순간에도 계속 일어나고 있을 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 관찰학습에 대해 생각하면서 마지막으로 이런 질문을 남기게 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 요즘 누구를 보고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그리고&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 사람에게서 나는 무엇을 배우고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;어쩌면 우리가 어떤 사람이 되어가는지를 이해하기 위해서는&lt;/p&gt;
&lt;p&gt;내가 무엇을 직접 경험했는지만큼이나,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 지금까지 누구를 지켜보며 살아왔는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 생각해볼 필요가 있는 것 같다.&lt;/p&gt;</description>
      <category>etc/자아의 바다</category>
      <category>관찰학습</category>
      <category>모방학습이론</category>
      <category>반두라</category>
      <category>보보인형</category>
      <category>자아의 바다</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/100</guid>
      <comments>https://slowgrid.tistory.com/entry/Whom-Am-I-Learning-From-Banduras-Theory-of-Imitation-Learning-and-Observational-Learning#entry100comment</comments>
      <pubDate>Tue, 29 Sep 2026 16:16:56 +0900</pubDate>
    </item>
    <item>
      <title>왜 컬러맵 대신 그레이스케일을 선택했을까?</title>
      <link>https://slowgrid.tistory.com/entry/Why-was-grayscale-chosen-instead-of-a-color-map</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Lepton 3.5와 YOLO로 실내 재실 여부를 판단할 때, 열영상을 어떤 형태로 입력해야 할까?&lt;br&gt;컬러맵, 합성곱, 사전 학습, 입력 일관성의 관점에서 그레이스케일을 선택한 이유를 정리해 보았다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;앞선 &lt;a href=&quot;https://slowgrid.tistory.com/entry/YOLO-Basics-5-How-are-pre-trained-weights-passed-to-the-new-model&quot;&gt;YOLO 글&lt;/a&gt;에서는 열화상 영상을 같은 값의 3채널로 복제하는 방법과, 모델 자체를 실제 1채널 입력 구조로 바꾸는 방법을 구분했다. 이번에는 그보다 앞선 질문으로 돌아가 보려고 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;열영상에 컬러맵을 적용하면, 모델도 사람처럼 대상을 더 쉽게 구분할 수 있을까?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 질문에 답하려면 두 가지를 나누어 생각해야 한다.&lt;/p&gt;
&lt;p&gt;하나는 입력에 담긴 &lt;strong&gt;측정 정보가 늘어나는가&lt;/strong&gt;이고, 다른 하나는 그 정보를 &lt;strong&gt;주어진 모델이 더 쉽게 학습하는가&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;두 질문은  얼핏 보기에 비슷해 보이지만 같은 질문은 아니다.&lt;/p&gt;
&lt;p&gt;이번 Lepton 3.5 기반 실내 재실 감지에서는 색상화 전 데이터를 정규화하고, 같은 그레이스케일 값을 3채널에 복제하는 입력 방식을 선택했다. 선택 기준은 원본 강도 정보를 가능한 한 유지하고, 불필요한 변환을 제외하며, 학습과 추론의 입력을 일관되게 만드는 것이었다.&lt;/p&gt;
&lt;p&gt;다만 이 글은 &lt;strong&gt;그레이스케일이 컬러맵보다 정확하다는 실험 결과를 제시하는 글은 아니다.&lt;/strong&gt; 어떤 근거로 기본 입력을 정했으며, 어디까지가 수학적으로 설명되는 부분이고 어디부터가 실험으로 확인해야 하는 부분인지를 정리하는 글이라고 생각하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 먼저, 무엇을 판단하려는 문제인가?&lt;/h2&gt;
&lt;p&gt;이번 문제의 센서는 Lepton 3.5이다. 이 센서는 160 × 120 해상도의 열영상을 제공하며 세부적인 센서 사양은 &lt;a href=&quot;https://oem.flir.com/products/lepton/&quot;&gt;FLIR의 Lepton 공식 제품 정보&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;프로젝트의 입력과 최종 목표는 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;센서&lt;/td&gt;
&lt;td&gt;Lepton 3.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;원본 영상 해상도&lt;/td&gt;
&lt;td&gt;160 × 120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최종 출력&lt;/td&gt;
&lt;td&gt;재실 = 1, 공실 = 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선택한 입력 표현&lt;/td&gt;
&lt;td&gt;그레이스케일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델에 전달하는 채널 구성&lt;/td&gt;
&lt;td&gt;같은 그레이스케일 값을 3채널에 복제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;여기서 이번에 결정한 것은 모델의 전체 구조가 아니라 &lt;strong&gt;열영상의 입력 표현&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;비교하려는 두 방식을 단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;방식 A
색상화 전 열영상 → 정규화 → 같은 값을 3채널에 복제 → YOLO

방식 B
색상화 전 열영상 → 정규화 → 컬러맵 적용 → YOLO&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;두 방식이 동일한 정규화 결과에서 출발한다고 놓으면, 핵심 차이는 마지막에 색을 입히는 변환을 추가하느냐에 있다.&lt;/p&gt;
&lt;p&gt;여기서 말하는 컬러맵 비교는 Lepton이 출력하는 24-bit RGB 영상과의 직접 비교가 아니라, &lt;strong&gt;동일한 색상화 전 입력과 동일한 정규화 결과에 고정 컬러맵만 추가한 조건&lt;/strong&gt;을 의미한다. Lepton의 24-bit RGB 출력에는 AGC와 색상화가 함께 적용되므로, 이를 그대로 사용할 경우에는 단순한 그레이스케일과 컬러맵의 비교가 아니라 별도의 전처리 조건까지 포함한 비교로 보아야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 열영상의 색은 어디에서 오는가?&lt;/h2&gt;
&lt;p&gt;컬러맵은 그레이스케일의 값에 색을 대응시켜 시각화를 돕는 방법이다. &lt;a href=&quot;https://docs.opencv.org/4.x/d3/d50/group__imgproc__colormap.html&quot;&gt;OpenCV 공식 문서&lt;/a&gt;도 컬러맵을 영상의 작은 차이를 사람이 살펴보기 쉽게 만드는 시각화 기능으로 설명하고 있다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 중요한 점이 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;색을 입혔다고 해서 새로운 센서 측정값이 생기는 것은 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;정규화한 한 픽셀의 열강도를 다음처럼 정의해 보자.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;g \in [0,1]&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;여기서 $g$는 모델 입력을 위해 정규화한 값이며, 그 자체를 섭씨온도라고 가정하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;이 값을 그레이스케일 3채널로 복제하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;X_{\text{gray}}=(g,g,g)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;반면 고정된 컬러맵을 적용하면 다음처럼 표현할 수 있다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;X_{\text{color}}&lt;br&gt;=&lt;br&gt;\Phi(g)&lt;br&gt;=&lt;br&gt;\bigl(f_R(g),f_G(g),f_B(g)\bigr)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;그레이스케일은 같은 값을 세 번 넣고, 컬러맵은 같은 값에 서로 다른 변환을 적용해 세 채널을 만든다.&lt;/p&gt;
&lt;p&gt;두 경우 모두 출발점은 하나의 측정값 $g$이다. 컬러맵의 세 채널은 별도로 측정한 값이 아니라, 그 측정값으로부터 계산된 값이다. 따라서 이는 가시광 RGB 센서를 추가한 것과 다르다.&lt;/p&gt;
&lt;p&gt;즉, 다음 두 문장을 구분해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;채널마다 서로 다른 숫자가 들어 있다.&lt;br&gt;새로운 측정 정보가 추가되었다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;컬러맵에서는 첫 번째가 성립할 수 있지만, 그것만으로 두 번째가 성립하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;컬러맵은 정보의 추가라기보다 정보의 표현 방식을 바꾸는 변환이기 때문이다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 세 채널이 같아도 YOLO는 특징을 추출할 수 있을까?&lt;/h2&gt;
&lt;p&gt;여기서 자연스럽게 다음 의문이 생긴다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;R, G, B에 같은 값을 넣으면 모델이 볼 수 있는 특징도 하나로 줄어드는 것 아닐까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;첫 합성곱을 펼쳐 보면 그렇지 않다는 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;YOLOv8 계열에서 사용하는 기본 Conv 블록은 다음 흐름으로 이해할 수 있다. &lt;a href=&quot;https://docs.ultralytics.com/reference/nn/modules/conv/&quot;&gt;Ultralytics의 &lt;code&gt;Conv&lt;/code&gt; 구현&lt;/a&gt;도 합성곱, 배치 정규화, 기본 활성화 함수인 SiLU를 순서대로 적용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Conv2d → BatchNorm → SiLU&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정규화한 그레이스케일 영상 전체를 $G$라고 하자. 첫 합성곱에서 $k$번째 출력 채널의 값은 다음처럼 표현할 수 있다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;z_k&lt;br&gt;=&lt;br&gt;W_{k,R}\ast X_R&lt;br&gt;+&lt;br&gt;W_{k,G}\ast X_G&lt;br&gt;+&lt;br&gt;W_{k,B}\ast X_B&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;여기서 $\ast$는 합성곱 연산을 뜻하며, $z_k$는 배치 정규화와 활성화 함수가 적용되기 전의 값이다.&lt;/p&gt;
&lt;p&gt;세 입력 채널이 모두 같은 영상이라면,&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;X_R=X_G=X_B=G&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이므로 다음과 같이 정리된다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\begin{aligned}&lt;br&gt;z_k&lt;br&gt;&amp;amp;=&lt;br&gt;W_{k,R}\ast G&lt;br&gt;+&lt;br&gt;W_{k,G}\ast G&lt;br&gt;+&lt;br&gt;W_{k,B}\ast G\&lt;br&gt;&amp;amp;=&lt;br&gt;\left(W_{k,R}+W_{k,G}+W_{k,B}\right)\ast G\&lt;br&gt;&amp;amp;=&lt;br&gt;W_{k,\text{eff}}\ast G&lt;br&gt;\end{aligned}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;즉, 세 채널의 필터를 더한 &lt;strong&gt;유효 필터&lt;/strong&gt;가 하나의 그레이스케일 영상에 적용되는 형태가 된다. 출력 채널마다 유효 필터가 다를 수 있으므로, 서로 다른 공간 특징을 추출할 수 있다.&lt;/p&gt;
&lt;p&gt;이를 단순한 예로 생각하면, 같은 영상에 가로 방향의 차이를 보는 필터와 세로 방향의 차이를 보는 필터를 적용했을 때 결과가 달라지는 것과 같다.&lt;/p&gt;
&lt;p&gt;입력 영상이 하나라는 사실과, 그 영상에서 추출할 수 있는 특징이 하나라는 것은 전혀 다른 이야기이기에&lt;/p&gt;
&lt;p&gt;따라서 여기서 얻을 수 있는 결론은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;색 채널 사이의 차이는 합성곱 특징 추출의 필수 조건이 아니라는 것&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;다만 이 식이 보여 주는 것은 그레이스케일 입력으로도 합성곱이 동작한다는 사실이기에, 이것만으로 컬러맵과의 정확도 우열까지 결정되지는 않는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 비선형 컬러맵은 무엇을 바꾸는가?&lt;/h2&gt;
&lt;p&gt;컬러맵이 새로운 측정 정보를 만들지 않는다고 해서, 모델 입장에서 아무 변화도 없는 것은 아니다.&lt;/p&gt;
&lt;p&gt;컬러맵은 &lt;strong&gt;강도 차이가 숫자로 표현되는 방식&lt;/strong&gt;을 바꿀 수 있다.&lt;/p&gt;
&lt;p&gt;인접한 두 픽셀의 차이를 구하는 필터를 생각해 보자.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;K=[-1,1]&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;그레이스케일에서 두 픽셀의 값이 $g_1,g_2$라면 응답은 다음과 같다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;r_{\text{gray}}=g_2-g_1&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;컬러맵을 적용한 뒤에는 각 채널에서 다음 차이를 계산하게 된다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;r_{\text{color},c}&lt;br&gt;=&lt;br&gt;f_c(g_2)-f_c(g_1)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;실제 컬러맵은 이산적인 LUT로 구현될 수 있지만, 강도 변환이 지역적인 차이에 미치는 영향을 설명하기 위해 여기서는 컬러맵을 연속 함수 $f_c(g)$로 근사해서 생각해 보자.&lt;/p&gt;
&lt;p&gt;이 근사 함수가 매끄럽고 두 값이 가까운 구간에서는 다음처럼 표현할 수 있다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;r_{\text{color},c}&lt;br&gt;\approx&lt;br&gt;f&amp;#39;_c(\bar g)(g_2-g_1)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;즉, 원래 강도 차이에 해당 구간의 변환 함수 기울기가 곱해지는 형태이며, 같은 강도 차이라도 팔레트의 어느 구간에 있는지에 따라 다른 크기로 표현될 수 있다.&lt;/p&gt;
&lt;p&gt;실제 팔레트 대신, 원리를 보여 주는 간단한 변환을 사용해 보자.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;f(g)=g^2&lt;br&gt;$$&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;원래 강도 쌍&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;원래 차이&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;$g^2$ 적용 후 차이&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;$0.2 \rightarrow 0.3$&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;$0.10$&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;$0.09-0.04=0.05$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$0.7 \rightarrow 0.8$&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;$0.10$&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;$0.64-0.49=0.15$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;원래 차이는 둘 다 $0.10$이다. 하지만 변환 이후의 차이는 각각 $0.05$, $0.15$가 된다.&lt;/p&gt;
&lt;p&gt;이 계산은 비선형 변환이 특정 강도 구간의 차이는 작게, 다른 구간의 차이는 크게 표현할 수 있음을 보여 준다.&lt;/p&gt;
&lt;p&gt;여기서도 해석을 조심해야 한다.&lt;/p&gt;
&lt;p&gt;이 예시는 컬러맵이 항상 정보를 잃거나 성능을 떨어뜨린다는 증명이 아니기에, 실제로 $g^2$는 $[0,1]$에서 역변환할 수 있다.&lt;/p&gt;
&lt;p&gt;다만 &lt;strong&gt;정보가 남아 있다는 것과, 그 정보의 수치적 구조가 그대로라는 것은 다르다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이번 설계에서는 성능 개선이 확인되지 않은 상태에서 이러한 추가 변환을 기본 입력에 넣기보다, 먼저 정규화한 강도 자체를 사용하는 쪽을 선택한 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 정보가 늘지 않는다면 그레이스케일이 항상 더 정확할까?&lt;/h2&gt;
&lt;p&gt;이 부분이 가장 중요하다. 결론부터 말하자면&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;그렇지는 않다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이를 이해하려면 이상적인 판정 함수와 실제로 학습하는 모델을 구분해야 한다.&lt;/p&gt;
&lt;p&gt;그레이스케일 영상 전체를 $G$, 고정된 컬러맵을 영상 전체에 적용한 결과를 $\Phi(G)$라고 하자.&lt;/p&gt;
&lt;p&gt;컬러맵 영상으로 판단하는 어떤 함수 $h$가 있다면, 다음과 같은 함수를 정의할 수 있다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;q(G)=h(\Phi(G))&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;그레이스케일 영상을 받은 뒤 내부적으로 컬러맵을 계산하고, 기존 판정 함수 $h$를 적용하면 된다.&lt;/p&gt;
&lt;p&gt;따라서 가능한 모든 판정 함수를 허용할 때, 원본 입력으로 달성할 수 있는 최저 오류율은 그 변환 결과만으로 달성할 수 있는 최저 오류율보다 나쁠 수 없다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;R^{\ast}(G)\leq R^{\ast}(\Phi(G))&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;여기서 $R^{\ast}$는 특정 YOLO 모델의 오류율이 아니라, &lt;strong&gt;가능한 모든 판정 함수 중에서 달성할 수 있는 최저 오류율&lt;/strong&gt;을 뜻한다. &lt;/p&gt;
&lt;p&gt;컬러맵이 원래 값을 완전히 복원할 수 있는 변환이고 저장 과정에서도 정보가 사라지지 않는다면, 두 최저 오류율은 같을 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 실제 학습에서는 가능한 모든 함수를 자유롭게 사용할 수 없다.&lt;/p&gt;
&lt;p&gt;우리가 사용하는 것은 정해진 구조의 모델이며, 데이터와 학습 시간도 제한되어 있다. 출발점이 되는 사전 학습 가중치 역시 정해져 있다. 이런 조건에서는 입력 표현과 최적화 과정에 따라 실제 정확도가 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 다음처럼 구분하는 편이 더 낫다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;이론적으로 설명되는 부분:&lt;/strong&gt; 고정된 컬러맵은 새로운 측정 정보를 만들지 않는다.&lt;br&gt;&lt;strong&gt;실험으로 확인할 부분:&lt;/strong&gt; 특정 데이터와 YOLO 설정에서 어떤 입력 표현이 더 잘 학습되는가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;그래서 이번 선택의 의미는 “그레이스케일의 성능 우위를 증명했다”가 아닌&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;추가적인 표현 변환이 필요하다는 근거가 아직 제시되지 않았으므로, 더 직접적인 입력을 기본값으로 삼았다&lt;/strong&gt;는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. RGB 사전 학습 가중치를 사용하는데도 괜찮을까?&lt;/h2&gt;
&lt;p&gt;여기서는 &lt;strong&gt;가중치의 형상이 맞는 것&lt;/strong&gt;과 &lt;strong&gt;학습된 특징이 그대로 유용한 것&lt;/strong&gt;을 나누어 보아야 한다.&lt;/p&gt;
&lt;p&gt;기존 3채널 합성곱에 $(G,G,G)$를 넣으면 입력 채널 수를 바꾸지 않고 계산할 수 있다. 하지만 색 채널 사이의 차이에 반응하는 필터는 그레이스케일 입력에서 반응이 작아지거나 사라질 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 어떤 필터가 다음과 같다고 가정해 보자.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;W_R=K,\qquad W_G=-K,\qquad W_B=0&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;세 채널이 모두 같은 영상이면,&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;K\ast G-K\ast G=0&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;색 채널의 차이를 보는 필터가 동일한 채널에서는 반응하지 않는 것이다.&lt;/p&gt;
&lt;p&gt;그렇다고 사전 학습 가중치 전체가 무의미하다는 결론이 나오는 것은 아니다. 앞에서 보았듯 채널별 필터의 합이 0이 아닌 경우에는 그레이스케일의 공간적 패턴에 대한 응답이 남는다.&lt;/p&gt;
&lt;p&gt;따라서 사전 학습 모델은 열영상 데이터에 맞추어 미세 조정해야 하며, 컬러맵을 적용했을 때 전이 학습상의 이득이 생기는지는 별도로 검증할 필요가 있다.&lt;/p&gt;
&lt;h3&gt;3채널 복제와 실제 1채널 모델은 별개의 선택이다&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/YOLO-Basics-5-How-are-pre-trained-weights-passed-to-the-new-model&quot;&gt;앞선 글&lt;/a&gt;에서 구분했던 것처럼, 같은 값을 3채널에 복제하는 방식과 첫 합성곱 자체를 1채널 구조로 바꾸는 방식은 서로 다른 실험 조건이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;그레이스케일 정보 사용
        +
기존 3채널 입력 구조 유지&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러므로 “그레이스케일을 사용한다”는 말만으로 입력 텐서의 채널 수가 1이라고 생각하면 안 되며&lt;/p&gt;
&lt;p&gt;또한 3채널 복제를 그대로 사용하는 한, 입력 채널 수를 줄인 데 따른 연산량 감소를 주장할 수도 없다. &lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 실제로 채택한 입력 파이프라인&lt;/h2&gt;
&lt;p&gt;선택한 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;색상화 전 데이터
      ↓
정규화
      ↓
그레이스케일 영상 G
      ↓
동일한 값을 3채널에 복제
      ↓
YOLO&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정규화의 한 예는 다음과 같다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;g=&lt;br&gt;\operatorname{clip}&lt;br&gt;\left(&lt;br&gt;\frac{r-a}{b-a},&lt;br&gt;0,&lt;br&gt;1&lt;br&gt;\right),&lt;br&gt;\qquad a&amp;lt;b&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;$r$은 색상화 전 수신값이며, $a,b$는 학습 데이터와 운용 범위를 고려해 정할 기준이다.&lt;/p&gt;
&lt;p&gt;여기서 $g\in[0,1]$은 개념적인 모델 입력 범위를 나타낸다. 표준 Ultralytics 학습 파이프라인은 일반적인 8-bit 이미지 입력을 학습 직전에 다시 255로 나누어 &lt;code&gt;[0,1]&lt;/code&gt; 범위로 변환하므로, 전처리 단계에서 이미 &lt;code&gt;[0,1]&lt;/code&gt;의 float tensor를 만든다면 &lt;strong&gt;중복 정규화가 발생하지 않도록 실제 데이터 로딩 과정을 확인해야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 파이프라인을 구현할 때는 몇 가지를 명확히 해야 한다.&lt;/p&gt;
&lt;h3&gt;정규화도 입력 표현의 일부라는 것&lt;/h3&gt;
&lt;p&gt;같은 $r$이라도 $a,b$가 바뀌면 정규화된 값 $g$는 달라진다.&lt;/p&gt;
&lt;p&gt;따라서 학습에서는 한 기준으로 정규화하고, 실제 추론에서는 다른 기준으로 정규화하면 동일한 수신값이 모델에 다른 숫자로 들어갈 수 있다.&lt;/p&gt;
&lt;p&gt;프레임마다 최솟값과 최댓값을 사용하는 방법도 같은 관점에서 생각할 수 있으나 장면의 최솟값이나 최댓값이 달라지면, 동일한 수신값의 정규화 결과도 달라진다.&lt;/p&gt;
&lt;p&gt;어떤 정규화가 더 적합한지는 검증할 문제이지만, 최소한 &lt;strong&gt;학습과 추론에서 어떤 규칙을 적용하는지는 같아야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;그레이스케일이라고 원본이 무손실로 보존되는 것은 아니라는 것&lt;/h3&gt;
&lt;p&gt;위 식에서 $r&amp;lt;a$인 값들은 모두 0이 되고, $r&amp;gt;b$인 값들은 모두 1이 된다. 서로 다른 원래 값이 같은 값으로 합쳐지는 것이다.&lt;/p&gt;
&lt;p&gt;저장 과정에서 정밀도를 줄여 양자화한다면, 그 과정에서도 값의 구분이 사라질 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 여기서 말하는 원본 강도 정보의 유지는 모든 처리 과정이 무손실이다라는 의미가 아닌&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;정규화와 저장 형식을 명시적으로 관리하면서, 별도의 색상 변환은 추가하지 않는다&lt;/strong&gt;는 의미에 가깝다.&lt;/p&gt;
&lt;h3&gt;컬러 영상을 다시 흑백으로 만드는 것은 다른 과정이라는 것&lt;/h3&gt;
&lt;p&gt;다음 두 흐름도 구분해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;색상화 전 데이터 → 정규화 → 그레이스케일&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;색상화 전 데이터 → 컬러맵 → 컬러 영상 → 흑백 변환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;두 번째 방식은 이미 변환된 색 채널을 다시 조합하는 과정이기에 그 결과가 처음의 정규화 값 $g$와 같다는 보장은 없다.&lt;/p&gt;
&lt;p&gt;따라서 이번 파이프라인의 출발점은 &lt;strong&gt;색상화 전 데이터&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;마지막으로, $(G,G,G)$라는 조건을 유지하려면 채널 복제 이후의 처리도 확인해야 한다. 채널마다 다른 변환을 적용하면 세 채널이 다시 달라질 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;결과적으로 전처리 코드를 확인하는 것과 함께, 실제 모델에 들어가기 직전 텐서의 값 범위와 채널 구성을 확인하는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 밝기 변화에 대한 설명에도 조건이 필요하다&lt;/h2&gt;
&lt;p&gt;그레이스케일 입력의 성질을 설명할 때, 일정한 밝기 변화에 대한 차이 필터의 반응도 살펴볼 수 있다.&lt;/p&gt;
&lt;p&gt;영상 전체에 동일한 값 $d$가 더해졌다고 가정하자.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;G&amp;#39;=G+d&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;필터 계수의 합이 0이라면,&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\sum K=0&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;클리핑이나 경계 패딩의 영향을 제외한 영역에서는 다음 관계가 성립한다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\begin{aligned}&lt;br&gt;K\ast(G+d)&lt;br&gt;&amp;amp;=K\ast G+d\sum K\&lt;br&gt;&amp;amp;=K\ast G&lt;br&gt;\end{aligned}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이런 필터는 일정한 오프셋보다 주변 값 사이의 차이에 반응한다.&lt;/p&gt;
&lt;p&gt;하지만 비선형 컬러맵을 거친 값에 대해서는 동일한 성질이 일반적으로 자동 보장되지 않는다. 원래 값에 $d$를 더한 결과가 변환 후에도 일정한 크기의 변화로 나타나는 것은 아니기 때문이다.&lt;/p&gt;
&lt;p&gt;여기서 다음과 같은 결론으로  단정짓고 넘어가면 안 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;그레이스케일 YOLO는 환경 변화에 자동으로 강하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;위 계산은 &lt;strong&gt;계수 합이 0인 필터&lt;/strong&gt;와 &lt;strong&gt;일정한 오프셋 변화&lt;/strong&gt;라는 가정 아래의 성질이다. 모든 YOLO 필터가 0합이라는 뜻도 아니고, 실제 열영상의 환경 변화가 모두 일정한 오프셋이라는 뜻도 아니다.&lt;/p&gt;
&lt;p&gt;설계의 성질을 설명하는 근거와, 실제 환경에서의 강건성을 입증하는 실험은 구분해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. YOLO의 손실함수는 무엇을 학습시키는가?&lt;/h2&gt;
&lt;p&gt;입력 표현을 이야기하다 보면 손실함수와도 연결하게 된다.&lt;/p&gt;
&lt;p&gt;설명용으로 YOLOv8 탐지 손실을 보면, 주요 구성은 다음과 같이 정리할 수 있다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;L&lt;br&gt;=&lt;br&gt;\lambda_{\text{box}}L_{\text{box}}&lt;br&gt;+&lt;br&gt;\lambda_{\text{cls}}L_{\text{cls}}&lt;br&gt;+&lt;br&gt;\lambda_{\text{dfl}}L_{\text{dfl}}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이는 박스 위치, 클래스 예측, 박스 경계의 분포 표현과 관련된 오차를 줄이는 구성이다. &lt;a href=&quot;https://docs.ultralytics.com/reference/utils/loss/&quot;&gt;Ultralytics의 &lt;code&gt;v8DetectionLoss&lt;/code&gt; 구현&lt;/a&gt;에서도 &lt;code&gt;box&lt;/code&gt;, &lt;code&gt;cls&lt;/code&gt;, &lt;code&gt;dfl&lt;/code&gt; 항에 각각 가중치를 적용한다.&lt;/p&gt;
&lt;p&gt;클래스 예측에 사용하는 BCE의 한 항은 다음과 같다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\ell(p,t)&lt;br&gt;=&lt;br&gt;-t\log p-(1-t)\log(1-p)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;p=\sigma(z)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;가중치와 집계를 제외한 이 한 항을 $z$에 대해 미분하면,&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{\partial \ell}{\partial z}=p-t&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;가 된다. YOLOv8의 &lt;a href=&quot;https://docs.ultralytics.com/reference/utils/loss/&quot;&gt;해당 구현&lt;/a&gt;은 수치적으로 안정적인 형태인 &lt;code&gt;BCEWithLogitsLoss&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;p&gt;중요한 것은 손실함수가 영상의 색이 보기 좋은지를 평가하는 것이 아니라는 점이며&lt;/p&gt;
&lt;p&gt;모델은 입력으로부터 만들어 낸 예측이 학습 목표에 얼마나 가까운지를 기준으로 가중치를 조정한다. 따라서 사람이 보기 좋은 컬러맵이라는 이유만으로 탐지에도 유리하다고 결론 내릴 수는 없다.&lt;/p&gt;
&lt;p&gt;반대로 손실함수가 같다는 이유만으로 두 입력의 학습 결과가 같다고 볼 수도 없다. 입력이 달라지면 예측값과 그에 따른 기울기도 달라질 수 있기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. 실제 성능은 어떤 비교로 확인해야 할까?&lt;/h2&gt;
&lt;p&gt;지금까지의 논리는 기본 입력을 선정하는 근거인데 이제 그 선택이 실제 데이터에서 더 나은지는 별도의 비교가 필요하다.&lt;/p&gt;
&lt;p&gt;이를 검증한다면, 먼저 &lt;strong&gt;정규화한 그레이스케일의 3채널 복제&lt;/strong&gt;와 &lt;strong&gt;동일한 정규화 결과에 고정 컬러맵을 적용한 3채널 입력&lt;/strong&gt;을 비교하는 편이 좋다.&lt;/p&gt;
&lt;p&gt;이때 원본 데이터, 학습 및 검증 분할, 모델 구조, 초기 가중치, 학습 조건, 평가 절차를 가능한 한 동일하게 맞추어야 한다. 정규화나 입력 채널 구조까지 동시에 바꾸면 결과 차이를 컬러맵의 효과로 해석하기 어려워지기 때문이다.&lt;/p&gt;
&lt;p&gt;실제 1채널 모델을 비교하려면 별도의 실험 조건으로 분리하는 것이 명확하며&lt;/p&gt;
&lt;p&gt;평가도 최종 목표와 연결해야 한다.&lt;/p&gt;
&lt;p&gt;이번 문제의 최종 출력은 재실과 공실이기에 따라서 사람 탐지 결과를 재실 판정으로 연결한다면, 탐지 지표만이 아니라 &lt;strong&gt;사람이 있는데 공실로 판단한 경우&lt;/strong&gt;와 &lt;strong&gt;사람이 없는데 재실로 판단한 경우&lt;/strong&gt;를 함께 확인하도록 평가를 설계할 필요가 있다.&lt;/p&gt;
&lt;p&gt;재실 판정 임계값이나 시간에 따른 후처리를 사용한다면, 그 선택 절차 역시 비교 조건에 포함해야 한다.&lt;/p&gt;
&lt;p&gt;이런 검증을 거쳐야 “이러한 입력을 선택한 이유”에서 한 단계 더 나아가, “어떤 조건에서 이 입력이 유리했는가”를 이야기할 수 있기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p&gt;이번 Lepton 3.5 기반 실내 재실 감지에서는 다음 입력을 선택했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;색상화 전 데이터 → 정규화 → 그레이스케일 3채널 복제 → YOLO&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;선정 근거는 크게 세 가지이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;컬러맵은 새로운 측정 정보를 추가하지 않으며&lt;/strong&gt; 같은 열강도 값을 다른 채널 표현으로 바꾸는 변환이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;그레이스케일만으로도 합성곱 특징 추출은 가능하며&lt;/strong&gt; 입력 채널이 같아도 출력 채널별 유효 필터는 서로 다른 공간 특징을 추출할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;입력 변환을 단순하게 정의하고 일관되게 적용할 수 있다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그렇다고 컬러맵을 사용한 모델이 더 좋은 결과를 낼 가능성을 배제하는 것은 아니다. 제한된 모델과 학습 조건에서는 표현 방식이 성능에 영향을 줄 수 있으므로, 그 부분은 실험으로 결론 내려야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/YOLO-Basics-5-How-are-pre-trained-weights-passed-to-the-new-model&quot;&gt;관련 글: YOLO 사전 학습 가중치와 입력 채널&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oem.flir.com/products/lepton/&quot;&gt;FLIR Lepton 공식 제품 정보&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.opencv.org/4.x/d3/d50/group__imgproc__colormap.html&quot;&gt;OpenCV: ColorMaps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ultralytics.com/reference/nn/modules/conv/&quot;&gt;Ultralytics: Conv 모듈&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ultralytics.com/reference/utils/loss/&quot;&gt;Ultralytics: 탐지 손실함수&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>Lepton3.5</category>
      <category>YoLO</category>
      <category>그레이스케일</category>
      <category>데이터전처리</category>
      <category>열영상</category>
      <category>열화상</category>
      <category>재실감지</category>
      <category>전이학습</category>
      <category>컬러맵</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/99</guid>
      <comments>https://slowgrid.tistory.com/entry/Why-was-grayscale-chosen-instead-of-a-color-map#entry99comment</comments>
      <pubDate>Mon, 28 Sep 2026 12:00:44 +0900</pubDate>
    </item>
    <item>
      <title>1/&amp;pi;를 향한 2000년 (完) &amp;mdash; 검증과 그 이후: 고스퍼에서 추드노프스키까지</title>
      <link>https://slowgrid.tistory.com/entry/2000-Years-Toward-1pi-Complete-Verification-and-Beyond-From-Gosper-to-Chudnovsky</link>
      <description>&lt;p&gt;&lt;em&gt;시리즈 「1/π를 향한 2000년」: 1편 원과 무한 / 2편 AGM과 타원적분 / 3편 모듈러의 세계 / 4편 1103의 탄생 / *&lt;/em&gt;5편 검증과 그 이후***&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;지난 편 요약.&lt;/strong&gt; 르장드르 관계식에 특이 모듈러스 $k_N$ 과 특이값 함수 $\alpha(N)$ 을 넣으면, 계수가 모두 대수적 수인 1/π 급수가 나온다. $N$이 클수록 급수가 빠르지만 계수가 복잡해지므로, 유수 2인 판별식 중 가장 큰 $N = 58$ 이 최선이었고, 이때 베버 불변량 $g_{58}^{12} = 9801 + 1820\sqrt{29}$ 에서 $9801 = 99^2$ 과 $z_{58} = 1/99^4$ 이 나오고, $396^4 = e^{\pi\sqrt{58}} + 104 + 4372\thinspace e^{-\pi\sqrt{58}} + \cdots$ 라는 거의-정수 관계가 생긴다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;마지막 편에서는 공식을 수치로 검증하고, 증명과 그 이후의 이야기로 시리즈를 마무리할 것이다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{1}{\pi} = \frac{2\sqrt{2}}{99^2}\sum_{k=0}^{\infty}\frac{(4k)!}{(k!)^4}\thinspace \frac{26390k+1103}{396^{4k}}&lt;br&gt;$$&lt;/p&gt;
&lt;h2&gt;수렴 속도와 수치 검증&lt;/h2&gt;
&lt;p&gt;연속한 항의 비는 $k\to\infty$ 에서 다음 값으로 수렴한다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{T_{k+1}}{T_k} \to \frac{4^4}{396^4} = \frac{1}{99^4} \approx 1.04\times 10^{-8}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;따라서 항 하나당 $\log_{10}(99^4) \approx 7.98$ 자리씩 정확도가 늘어나며, 아래는 부분합 $S_K = \sum_{k=0}^{K}$ 로 계산한 $\pi \approx 9801/(2\sqrt2\thinspace S_K)$ 의 오차이다(mpmath, 80자리 정밀도).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;사용한 항 수&lt;/th&gt;
&lt;th&gt;π 근삿값&lt;/th&gt;
&lt;th&gt;오차&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1 ($k = 0$)&lt;/td&gt;
&lt;td&gt;3.14159273001…&lt;/td&gt;
&lt;td&gt;$7.6\times10^{-8}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 ($k \le 1$)&lt;/td&gt;
&lt;td&gt;3.141592653589793878…&lt;/td&gt;
&lt;td&gt;$6.4\times10^{-16}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 ($k \le 2$)&lt;/td&gt;
&lt;td&gt;3.14159265358979323846264906…&lt;/td&gt;
&lt;td&gt;$5.7\times10^{-24}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 ($k \le 3$)&lt;/td&gt;
&lt;td&gt;(소수점 아래 31자리 일치)&lt;/td&gt;
&lt;td&gt;$5.2\times10^{-32}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 ($k \le 4$)&lt;/td&gt;
&lt;td&gt;(소수점 아래 39자리 일치)&lt;/td&gt;
&lt;td&gt;$4.9\times10^{-40}$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mSMVZ/dJMcadXZLrD/qzAGoXtkHizCCP7leFrC5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mSMVZ/dJMcadXZLrD/qzAGoXtkHizCCP7leFrC5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mSMVZ/dJMcadXZLrD/qzAGoXtkHizCCP7leFrC5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmSMVZ%2FdJMcadXZLrD%2FqzAGoXtkHizCCP7leFrC5K%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;그림 1. 항을 하나씩 더할 때 정확한 자릿수. 점선은 기울기 $\log_{10}(99^4) \approx 7.98$ 인 직선으로, 실제 값과 거의 완벽히 겹친다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;오차가 항마다 정확히 약 $10^{-8}$ 배씩 줄어드는 것을 볼 수 있으며 이것은 4편에서 본 근사 관계도 같은 계산으로 확인된다. $396^4 - 104 - e^{\pi\sqrt{58}} = 1.777867585\times10^{-7}$ 이고, 전개식이 예측한 $4372\thinspace e^{-\pi\sqrt{58}} = 1.777867584\times10^{-7}$ 과 9자리까지 일치한다.&lt;/p&gt;
&lt;p&gt;비교를 위해 시리즈 전체에서 본 방법들을 정리하면 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;단계당 얻는 자릿수&lt;/th&gt;
&lt;th&gt;다룬 편&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;아르키메데스 다각형 이중화&lt;/td&gt;
&lt;td&gt;약 0.6&lt;/td&gt;
&lt;td&gt;1편&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라이프니츠 급수&lt;/td&gt;
&lt;td&gt;로그적으로 느림&lt;/td&gt;
&lt;td&gt;1편&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마친 공식 (arctan 1/5 항)&lt;/td&gt;
&lt;td&gt;약 1.4&lt;/td&gt;
&lt;td&gt;1편&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라마누잔 $N = 3$ 급수&lt;/td&gt;
&lt;td&gt;약 0.6&lt;/td&gt;
&lt;td&gt;4편&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라마누잔 $N = 58$ 급수&lt;/td&gt;
&lt;td&gt;약 8&lt;/td&gt;
&lt;td&gt;4편&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추드노프스키 급수 ($N = 163$)&lt;/td&gt;
&lt;td&gt;약 14&lt;/td&gt;
&lt;td&gt;5편&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가우스–르장드르 AGM&lt;/td&gt;
&lt;td&gt;매 단계 두 배&lt;/td&gt;
&lt;td&gt;2편&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bivVLN/dJMcacxYuCz/2eNpzkZw9dJK5MLaRiJZu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bivVLN/dJMcacxYuCz/2eNpzkZw9dJK5MLaRiJZu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bivVLN/dJMcacxYuCz/2eNpzkZw9dJK5MLaRiJZu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbivVLN%2FdJMcacxYuCz%2F2eNpzkZw9dJK5MLaRiJZu0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;그림 2. 같은 횟수의 항(또는 반복)으로 얻는 정확한 자릿수 비교(로그 축, 1600자리 정밀도로 실측). 급수들은 직선적으로 늘어나고, AGM만 기하급수적으로 늘어난다. 다만 AGM은 매 단계 제곱근을 전체 정밀도로 계산해야 하므로, 실제 기록 계산에서는 이진 분할로 구현한 급수가 더 자주 쓰인다.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;증명과 그 이후: 1985–현재&lt;/h2&gt;
&lt;p&gt;라마누잔은 1920년 32세로 세상을 떠났고, 1/π 급수들은 70년 넘게 증명되지 않은 채 남아 있었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1985년, 고스퍼.&lt;/strong&gt; 빌 고스퍼는 $N = 58$ 급수로 π를 17,526,100자리까지 계산했다. 당시 이 공식은 증명되지 않은 추측이었지만, 기존에 알려진 자릿수와 완벽히 일치했다. 계수가 차수가 정해진 대수적 수라는 구조를 알면, 이런 수치적 일치는 공식이 옳다는 강력한 근거가 된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1987년, 보르와인 형제.&lt;/strong&gt; 조너선 보르와인과 피터 보르와인은 《Pi and the AGM》에서 이 시리즈의 3–4편과 같은 틀(특이값 함수 $\alpha(N)$, 모듈러 방정식, 클라우젠 항등식)로 라마누잔의 17개 급수를 모두 증명했으며 그 과정에서 매 단계 자릿수가 네 배로 늘어나는 알고리즘도 얻을 수 있었다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1987–1989년, 추드노프스키 형제.&lt;/strong&gt; 데이비드와 그레고리 추드노프스키는 같은 아이디어를 유수 1인 가장 큰 판별식 $-163$ 과 $j$-불변량에 적용했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$&lt;br&gt;\frac{1}{\pi} = 12\sum_{k=0}^{\infty}\frac{(-1)^k\thinspace (6k)!\thinspace (13591409 + 545140134\thinspace k)}{(3k)!\thinspace (k!)^3\thinspace 640320^{3k+3/2}}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;여기서 $j\negthinspace \left(\frac{1+\sqrt{-163}}{2}\right) = -640320^3$ 이고, 4편에서 본 $N = 58$ 의 경우와 똑같은 원리로 $e^{\pi\sqrt{163}} \approx 640320^3 + 744 - 7.5\times10^{-13}$ 이라는 유명한 거의-정수가 생긴다(4편 그림 2의 가장 높은 막대). 항마다 약 14자리를 얻으며, 1989년 10억 자리 계산에 쓰였고 오늘날 π 자릿수 세계 기록 계산도 대부분 이 급수를 이진 분할(binary splitting)로 구현한 프로그램으로 이루어진다.&lt;/p&gt;
&lt;p&gt;결과적으로 라마누잔의 58과 추드노프스키의 163은 같은 나무의 두 가지이며, 유수가 작은 허수 이차체의 특이값이 π를 가장 효율적으로 붙잡는다.&lt;/p&gt;
&lt;h2&gt;맺음말: 한 줄의 공식에 담긴 2000년&lt;/h2&gt;
&lt;p&gt;라마누잔 공식은 π에 대한 서로 다른 세 가지 얼굴을 하나로 묶는다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;기하학의 π&lt;/strong&gt; — 아르키메데스의 다각형, 그리고 그 안에 이미 있던 &amp;quot;평균의 반복&amp;quot;(1편).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;해석학의 π&lt;/strong&gt; — 비에트, 월리스, 오일러의 무한곱과 급수, 감마함수와 포흐하머 기호(1편).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정수론의 π&lt;/strong&gt; — 가우스의 AGM에서 시작해 르장드르 관계식, 야코비의 모듈러 방정식, 허수곱셈과 베버 불변량으로 이어지는 흐름(2–4편).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이제 공식을 다시 읽어 보면 모든 숫자에 출처가 있음이 보일 것이다. $(4k)!/(k!)^4$ 는 시그니처 4 초기하급수의 계수이고(3편), $396^4 = 256\times99^4$ 은 $g_{58}$ 의 대칭식이며, $99^2 = 9801$ 은 $\mathbb Q(\sqrt{29})$ 의 기본 단수의 여섯제곱 $9801 + 1820\sqrt{29}$ 의 유리수 부분이며(4편), π 자체는 르장드르 관계식을 통해 들어온다(2편).&lt;/p&gt;
&lt;p&gt;라마누잔이 이 모든 것을 어떻게 한 줄로 압축해 냈는지는 여전히 완전히 알 수 없다. 다만 그 한 줄을 이해하는 데 인류가 2000년 동안 쌓아온 수학이 모두 필요하다는 것은 분명할 것이다.&lt;/p&gt;
&lt;h3&gt;더 읽을거리&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;J. M. Borwein, P. B. Borwein, &lt;em&gt;Pi and the AGM&lt;/em&gt; (Wiley, 1987)&lt;/li&gt;
&lt;li&gt;S. Ramanujan, &amp;quot;Modular equations and approximations to π&amp;quot;, &lt;em&gt;Quarterly Journal of Mathematics&lt;/em&gt; 45 (1914), 350–372.&lt;/li&gt;
&lt;li&gt;B. C. Berndt, &lt;em&gt;Ramanujan&amp;#39;s Notebooks&lt;/em&gt;, Part V (Springer, 1998)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;</description>
      <category>CS/수학</category>
      <category>640320</category>
      <category>Chudnovsky</category>
      <category>N163</category>
      <category>PiCalculation</category>
      <category>고스퍼</category>
      <category>라마누잔공식</category>
      <category>보르와인</category>
      <category>원주율</category>
      <category>추드노프스키</category>
      <category>파이</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/98</guid>
      <comments>https://slowgrid.tistory.com/entry/2000-Years-Toward-1pi-Complete-Verification-and-Beyond-From-Gosper-to-Chudnovsky#entry98comment</comments>
      <pubDate>Sun, 27 Sep 2026 16:22:25 +0900</pubDate>
    </item>
    <item>
      <title>1/&amp;pi;를 향한 2000년 (4) &amp;mdash; 1103의 탄생: 라마누잔의 주정리와 58의 비밀</title>
      <link>https://slowgrid.tistory.com/entry/2000-Years-Toward-1pi-4-The-Birth-of-1103-Ramanujans-Theorem-and-the-Secret-of-58</link>
      <description>&lt;p&gt;&lt;em&gt;시리즈 「1/π를 향한 2000년」: 1편 원과 무한 / 2편 AGM과 타원적분 / 3편 모듈러의 세계 / &lt;strong&gt;4편 1103의 탄생&lt;/strong&gt; / 5편 검증과 그 이후&lt;/em&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;지난 편 요약.&lt;/strong&gt; $K&amp;#39;(k)/K(k) = \sqrt N$ 이 되는 특이 모듈러스 $k_N$ 은 대수적 수이고, 그 점에서 특이값 함수 $\alpha(N)$ 도 대수적 수다. 클라우젠 항등식은 $K^2$ 를 하나의 급수 $(2K/\pi)^2 = \sum \frac{(1/2)_ n^3}{(n!)^3}(2kk&amp;#39;)^{2n}$ 로 바꿔 준다. 라마누잔의 &amp;quot;시그니처 4&amp;quot; 이론에서는 계수가 $(\frac14)_ n(\frac12)_ n(\frac34)_ n/(n!)^3$ 이 되는데, 이것이 곧 $(4n)!/(n!)^4$ 를 $256^n$ 으로 나눈 것이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이번 편에서는 이 재료를 르장드르 관계식에 넣어 1/π 급수를 만들고, 공식 속 숫자 99와 396이 어디서 왔는지 확인할 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SbhHT/dJMcaaUqpgN/UAO0J9pfui9j9vW2UBqUkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SbhHT/dJMcaaUqpgN/UAO0J9pfui9j9vW2UBqUkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SbhHT/dJMcaaUqpgN/UAO0J9pfui9j9vW2UBqUkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSbhHT%2FdJMcaaUqpgN%2FUAO0J9pfui9j9vW2UBqUkK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;그림 1. 1편에서 본 공식의 해부도. 이번 편은 파란 상자와 보라 상자(4편), 주황 상자의 앞부분을 다룬다.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;라마누잔(1914): 1/π 급수의 탄생&lt;/h2&gt;
&lt;p&gt;1913년 인도 마드라스의 회계 사무원이던 라마누잔은 케임브리지의 하디에게 공식으로 가득한 편지를 보냈고, 1914년 케임브리지로 건너갔으며 같은 해 발표한 논문에는 1/π 에 대한 급수 17개가 실려 있었다. 라마누잔은 유도 과정을 거의 적지 않았으며, 여기서는 앞 편들의 재료로 그 구조를 재구성한다.&lt;/p&gt;
&lt;h3&gt;주정리: 특이 모듈러스에서의 1/π 급수&lt;/h3&gt;
&lt;p&gt;$x = (2kk&amp;#39;)^2$, $A_n = (\frac12)_ n^3/(n!)^3$, $F(x) = \sum A_n x^n$ 으로 두자. 3편의 클라우젠 항등식($s = 1/2$)에 의해 $F(x) = 4K^2/\pi^2$ 이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1단계 (미분).&lt;/strong&gt; $dx/dk = 8k(k&amp;#39;^2 - k^2)$ 이고, 2편 르장드르 관계식의 증명에 나온 $\dfrac{dK}{dk} = \dfrac{E - k&amp;#39;^2K}{k\thinspace k&amp;#39;^2}$ 에서 $\dfrac{d}{dk}\dfrac{4K^2}{\pi^2} = \dfrac{8K(E - k&amp;#39;^2K)}{\pi^2 k k&amp;#39;^2}$. 둘을 나누면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\sum_{n=0}^{\infty} n A_n x^n = x\thinspace F&amp;#39;(x) = \frac{4}{\pi^2}\cdot\frac{KE - k&amp;#39;^2K^2}{k&amp;#39;^2 - k^2}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2단계 (특이값 대입).&lt;/strong&gt; $k = k_N$ 에서 3편의 특이값 함수 $\alpha(N) = \dfrac{\pi}{4K^2} - \sqrt N\left(\dfrac{E}{K} - 1\right)$ 을 $E$에 대해 풀면 $KE = K^2 + (\pi/4 - \alpha(N)K^2)/\sqrt N$. 따라서&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;KE - k&amp;#39;^2K^2 = k^2K^2 + \frac{\pi}{4\sqrt N} - \frac{\alpha(N)}{\sqrt N}K^2&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3단계 (정리).&lt;/strong&gt; 이것을 1단계에 넣고 $4K^2/\pi^2 = F$ 를 쓰면 $(k&amp;#39;^2 - k^2)\sum nA_nx^n = (k^2 - \alpha/\sqrt N)F + \dfrac{1}{\pi\sqrt N}$. $\sqrt N$ 을 곱해 정리하면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\boxed{\ \frac{1}{\pi} = \sum_{n=0}^{\infty}\frac{\left(\tfrac12\right)_ n^{3}}{(n!)^3}\Bigl[\bigl(\alpha(N) - \sqrt N\thinspace k_N^2\bigr) + n\sqrt N\bigl(k_N&amp;#39;^2 - k_N^2\bigr)\Bigr]\thinspace x_N^{\thinspace n}\ }, \qquad x_N = (2k_Nk_N&amp;#39;)^2&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;계수 $\alpha(N) - \sqrt N k_N^2$, $\sqrt N(k_N&amp;#39;^2 - k_N^2)$ 와 $x_N$ 은 모두 &lt;strong&gt;대수적 수&lt;/strong&gt;이기에, 초월수 π가 대수적 데이터만으로 된 급수로 표현되는 이유가 바로 이것이다. π는 르장드르 관계식을 통해 들어왔고, 나머지는 모두 모듈러 방정식이 대수적으로 고정해 준다.&lt;/p&gt;
&lt;h3&gt;검산: $N = 3$&lt;/h3&gt;
&lt;p&gt;$k_3^2 = (2-\sqrt3)/4$, $k_3&amp;#39;^2 = (2+\sqrt3)/4$ 이므로 $x_3 = 4\times(4-3)/16 = 1/4$, $\sqrt3(k&amp;#39;^2-k^2) = \sqrt3\times\sqrt3/2 = 3/2$. $\alpha(3) = (\sqrt3-1)/2$ 를 쓰면 상수항은 $(\sqrt3-1)/2 - (2\sqrt3-3)/4 = 1/4$. 따라서&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{4}{\pi} = \sum_{n=0}^{\infty}\frac{\left(\tfrac12\right)_ n^{3}}{(n!)^3}\cdot\frac{6n+1}{4^n}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이것은 라마누잔 논문에 실린 가장 단순한 급수 중 하나이며, 항마다 약 $\log_{10}4 \approx 0.6$ 자리를 얻는다.&lt;/p&gt;
&lt;h3&gt;빠른 급수를 원한다면: $N$을 키워라&lt;/h3&gt;
&lt;p&gt;$q = e^{-\pi\sqrt N}$ 이 작을 때 $k \approx 4\sqrt q$ 이므로 $x_N \approx 64q$ 이다. 항마다 얻는 자릿수는 대략 $\log_{10}(e^{\pi\sqrt N}/64)$ 로, &lt;strong&gt;$N$이 클수록 급수가 빨라진다.&lt;/strong&gt; 다만 $N$이 크면 $k_N$, $\alpha(N)$ 이 복잡한 대수적 수가 되어 계산이 어렵다. 따라서 &amp;quot;$k_N$ 이 간단하면서 $N$은 큰&amp;quot; 특별한 $N$을 찾아야 한다.&lt;/p&gt;
&lt;h3&gt;시그니처 4 버전&lt;/h3&gt;
&lt;p&gt;3편의 $s = 1/4$ 이론에서 같은 세 단계(미분 → 특이값 대입 → 정리)를 밟으면, 대수적 수 $a_N, b_N, z_N$ 에 대해 다음 꼴을 얻는다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{1}{\pi} = \sum_{n=0}^{\infty}\frac{\left(\tfrac14\right)_ n\left(\tfrac12\right)_ n\left(\tfrac34\right)_ n}{(n!)^3}\thinspace (a_N + b_N n)\thinspace z_N^{\thinspace n}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;라마누잔이 고른 $N$은 58이었고, 이때 $z_{58} = 1/99^4$, $a_{58} = 2\sqrt2\times1103/9801$, $b_{58} = 2\sqrt2\times26390/9801$ 이다. 왜 하필 58인지, 99는 어디서 왔는지가 이 편 후반부의 주제다.&lt;/p&gt;
&lt;h2&gt;58의 비밀: 99와 396은 어디서 왔나&lt;/h2&gt;
&lt;p&gt;$99^2 = 9801$ 과 $396^4$ 은 모두 정수 58에 대한 &lt;strong&gt;베버 불변량&lt;/strong&gt; $g_{58}$ 에서 나온다. 계산은 전부 손으로 확인할 수 있다.&lt;/p&gt;
&lt;h3&gt;베버 불변량&lt;/h3&gt;
&lt;p&gt;$q = e^{-\pi\sqrt N}$ 에 대해 라마누잔의 표기로 다음과 같이 정의한다($k_N$ 으로는 $g_N^{12} = k_N&amp;#39;^2/(2k_N)$).&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;g_N = 2^{-1/4}\thinspace q^{-1/24}\prod_{n=1}^{\infty}\left(1 - q^{2n-1}\right)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;시그니처 4 이론에서 급수의 변수는 이 불변량으로 다음과 같이 쓰인다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;z_N = \frac{4}{\left(g_N^{12} + g_N^{-12}\right)^2}&lt;br&gt;$$&lt;/p&gt;
&lt;h3&gt;$N = 58$: 모든 것이 $\mathbb Q(\sqrt{29})$ 안에서 일어난다&lt;/h3&gt;
&lt;p&gt;허수 이차체 $\mathbb Q(\sqrt{-58})$ 의 판별식은 $-232$ 이고 &lt;strong&gt;유수(class number)가 2&lt;/strong&gt;다. 유수가 작을수록 특이값이 단순해지는데, 유수 2인 경우 $g_{58}^2$ 이 실이차체 $\mathbb Q(\sqrt{29})$ 의 원소가 된다. 그리고 라마누잔은 다음 값을 알고 있었다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;g_{58}^2 = \frac{5 + \sqrt{29}}{2}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이 수 $u = (5+\sqrt{29})/2$ 는 $\mathbb Q(\sqrt{29})$ 의 기본 단수(fundamental unit)이며, 노름은 $(25-29)/4 = -1$ 이다. 이제 거듭제곱을 계산해보자.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;u^2 = \frac{27 + 5\sqrt{29}}{2}, \qquad u^3 = 70 + 13\sqrt{29}, \qquad u^6 = (70+13\sqrt{29})^2 = 9801 + 1820\sqrt{29}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;$g_{58}^{12} = u^6$ 이고, 노름이 $(-1)^6 = 1$ 이므로 $g_{58}^{-12}$ 은 켤레 $9801 - 1820\sqrt{29}$ 이다($9801^2 - 29\times1820^2 = 96059601 - 96059600 = 1$ 로 확인된다). 따라서&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;g_{58}^{12} + g_{58}^{-12} = 2 \times 9801 = 2 \times 99^2, \qquad z_{58} = \frac{4}{(2\cdot 99^2)^2} = \frac{1}{99^4}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;공식 앞의 $9801 = 99^2$ 과 급수의 분모 $99^4$ 의 정체가 이것이다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;$396^4$ 과 $e^{\pi\sqrt{58}}$ 의 거의-정수 관계&lt;/h3&gt;
&lt;p&gt;곱 표현을 전개하면 $\prod(1-q^{2n-1})^{24} = 1 - 24q + 276q^2 - \cdots$ ($q^2$ 의 계수는 $\binom{24}{2} = 276$) 이므로&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;64\thinspace g_N^{24} = q^{-1} - 24 + 276\thinspace q - \cdots, \qquad 64\thinspace g_N^{-24} = 4096\thinspace q + O(q^2)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;한편 $396^4 = 256\times99^4 = 64\thinspace (g^{12} + g^{-12})^2 = 64g^{24} + 128 + 64g^{-24}$ 이다. 대입하면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;396^4 = e^{\pi\sqrt{58}} + 104 + 4372\thinspace e^{-\pi\sqrt{58}} + \cdots&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;즉 $e^{\pi\sqrt{58}} = 24591257751.99999982\ldots$ 로, 정수 $396^4 - 104 = 24591257752$ 와 약 $1.8\times10^{-7}$ 차이밖에 나지 않고 여기서 오차항 $4372\thinspace e^{-\pi\sqrt{58}} \approx 1.78\times10^{-7}$ 이 이 차이를 정확히 설명해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdQiM2/dJMcagN2G4A/9kOqFs7EJD6YhkOJUFrag1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdQiM2/dJMcagN2G4A/9kOqFs7EJD6YhkOJUFrag1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdQiM2/dJMcagN2G4A/9kOqFs7EJD6YhkOJUFrag1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdQiM2%2FdJMcagN2G4A%2F9kOqFs7EJD6YhkOJUFrag1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;그림 2. $N = 1, \ldots, 200$ 에 대해 $e^{\pi\sqrt N}$ 이 가장 가까운 정수에서 얼마나 떨어져 있는지 보여주며 막대가 높을수록 정수에 가깝다. 43, 67, 163 은 유수 1, 37 과 58 은 유수 2인 경우이며, 무작위라면 대부분 0–2 사이에 머문다.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;왜 하필 58인가&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;급수의 속도는 $1/z_N \approx e^{\pi\sqrt N}/256$ 으로 정해지므로 $N$이 클수록 좋다.&lt;/li&gt;
&lt;li&gt;계수가 단순하려면 유수가 작아야 한다. 판별식 $-4N$ 꼴의 유수 2인 기본판별식은 $-20, -24, -40, -52, -88, -148, -232$ 뿐이고, 58은 그중 가장 크다. (유수 2인 판별식의 완전한 목록은 1971년 베이커와 스타크가 확정했다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;라마누잔은 유수 2 목록이 확정되기 반세기 이상 전에, 사실상 가능한 최선의 $N$을 골라냈다. 나머지 계수 1103 과 $26390 = 2\times5\times7\times13\times29$ 는 이 편 앞부분에서 본 $\alpha(N)$ 에 해당하는 시그니처 4 이론의 특이값에서 나오며, 29가 인수로 등장하는 것도 $\mathbb Q(\sqrt{29})$ 와 관련이 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;다음 편 예고 — 5편: 검증과 그 이후.&lt;/strong&gt; 공식이 정말 항마다 8자리씩 맞추는지 수치로 확인하고, 다른 방법들과 속도를 비교할 것이다. 그리고 70년 넘게 증명되지 않았던 이 급수가 어떻게 증명되었는지, 같은 원리가 $N = 163$ 에서 어떻게 오늘날의 π 세계 기록으로 이어졌는지 따라가볼 예정이다.&lt;/p&gt;</description>
      <category>CS/수학</category>
      <category>1103</category>
      <category>26390</category>
      <category>396</category>
      <category>9801</category>
      <category>99</category>
      <category>N58</category>
      <category>라마누잔</category>
      <category>베버불변량</category>
      <category>원주율</category>
      <category>파이</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/97</guid>
      <comments>https://slowgrid.tistory.com/entry/2000-Years-Toward-1pi-4-The-Birth-of-1103-Ramanujans-Theorem-and-the-Secret-of-58#entry97comment</comments>
      <pubDate>Sat, 26 Sep 2026 20:25:51 +0900</pubDate>
    </item>
    <item>
      <title>1/&amp;pi;를 향한 2000년 (3) &amp;mdash; 모듈러의 세계: 야코비, 특이 모듈러스, 초기하급수</title>
      <link>https://slowgrid.tistory.com/entry/2000-Toward-1pi-3-The-Modular-World-Jacobi-Singular-Modulus-Hypergeometric-Series</link>
      <description>&lt;p&gt;&lt;em&gt;시리즈 「1/π를 향한 2000년」: 1편 원과 무한 / 2편 AGM과 타원적분 / &lt;strong&gt;3편 모듈러의 세계&lt;/strong&gt; / 4편 1103의 탄생 / 5편 검증과 그 이후&lt;/em&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;지난 편 요약.&lt;/strong&gt; 가우스의 AGM은 한 단계마다 자릿수를 두 배로 늘리며, 완전타원적분 $K(k) = \pi/(2M(1,k&amp;#39;))$ 를 계산한다. 이항전개하면 $K = \frac{\pi}{2}\sum \frac{(1/2)_ n^2}{(n!)^2}k^{2n}$ 으로, π가 급수 앞에 계수로 붙어 나온다. 르장드르 관계식 $EK&amp;#39; + E&amp;#39;K - KK&amp;#39; = \pi/2$ 는 $k$와 $k&amp;#39;$ 에서의 타원적분으로 π를 정확히 표현한다. 남은 질문은 &lt;strong&gt;$K&amp;#39;, E&amp;#39;$ 를 $K, E$로 대수적으로 표현할 수 있는 특별한 $k$가 있는가&lt;/strong&gt;였다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이번 편에서 그 답을 찾는다. 재료는 세 가지다. 특별한 $k$(특이 모듈러스), 그 점에서의 $E$에 대한 정보(특이값 함수 $\alpha(N)$), 그리고 $K^2$ 를 하나의 급수로 쓰는 방법(클라우젠 항등식)이다.&lt;/p&gt;
&lt;h2&gt;야코비(1829): 세타함수, 모듈러 방정식, 특이 모듈러스&lt;/h2&gt;
&lt;p&gt;야코비의 《Fundamenta Nova》(1829)는 타원적분을 &lt;strong&gt;세타함수&lt;/strong&gt;라는 $q$-급수로 바꿔 썼다. 노움(nome)을 $q = \exp(-\pi K&amp;#39;/K)$ 로 정의하고&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\theta_2(q) = \sum_{n\in\mathbb Z} q^{(n+\frac12)^2}, \qquad \theta_3(q) = \sum_{n\in\mathbb Z} q^{n^2}, \qquad \theta_4(q) = \sum_{n\in\mathbb Z} (-1)^n q^{n^2}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;로 두면 다음이 성립한다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;k = \frac{\theta_2^2(q)}{\theta_3^2(q)}, \qquad k&amp;#39; = \frac{\theta_4^2(q)}{\theta_3^2(q)}, \qquad K = \frac{\pi}{2}\thinspace \theta_3^2(q), \qquad \theta_3^4 = \theta_2^4 + \theta_4^4&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;여기서 $K = \frac{\pi}{2}\theta_3^2$ 는 2편에서 본 $K = \frac{\pi}{2}\cdot(\text{급수})$ 와 같은 이야기를 $q$의 언어로 한 것이며 이제 $K&amp;#39;/K$ 의 비율이 $q$라는 하나의 변수에 담긴다.&lt;/p&gt;
&lt;h3&gt;모듈러 방정식&lt;/h3&gt;
&lt;p&gt;두 모듈러스 $k, l$ 이 $L&amp;#39;/L = n\cdot K&amp;#39;/K$ 를 만족하면(즉 노움이 $q\to q^n$ 으로 바뀌면) $l$은 $k$의 &lt;strong&gt;$n$차 변환&lt;/strong&gt;이라 한다. 야코비는 $k$와 $l$ 사이에 대수적 관계식, 즉 &lt;strong&gt;$n$차 모듈러 방정식&lt;/strong&gt;이 항상 존재함을 보였다. 예를 들면 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2차: $l = \dfrac{1-k&amp;#39;}{1+k&amp;#39;}$. 이것은 랜든 변환이며, 2편에서 본 가우스의 AGM 한 단계와 정확히 같은 연산이다. &lt;/li&gt;
&lt;li&gt;3차 (르장드르): $\sqrt{kl} + \sqrt{k&amp;#39;l&amp;#39;} = 1$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;라마누잔은 이런 모듈러 방정식을 수백 개 발견해 노트에 남겼고, 그중 상당수는 당시 누구도 알지 못한 것이었다.&lt;/p&gt;
&lt;h3&gt;특이 모듈러스 $k_N$&lt;/h3&gt;
&lt;p&gt;양의 정수 $N$에 대해 $K&amp;#39;(k)/K(k) = \sqrt N$, 즉 $q = e^{-\pi\sqrt N}$ 이 되는 $k$를 &lt;strong&gt;특이 모듈러스&lt;/strong&gt; $k_N$ 이라 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;정리.&lt;/strong&gt; $k_N$ 은 대수적 수다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이유.&lt;/strong&gt; $k&amp;#39;_ N$ 의 입장에서 보면 $K/K&amp;#39; = 1/\sqrt N$ 이다. $k&amp;#39;_ N$ 에 $N$차 변환을 가하면 비율이 $N$배가 되어 $\sqrt N$ 이 되므로, 그 결과는 $k_N$ 자신이다. 따라서 $(k&amp;#39;_ N, k_N)$ 은 $N$차 모듈러 방정식을 만족하고, 여기에 $k^2 + k&amp;#39;^2 = 1$ 을 연립하면 $k_N$ 은 대수방정식의 근이 된다. ∎&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;$N$&lt;/th&gt;
&lt;th&gt;$k_N$&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;$1/\sqrt2$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$\sqrt2 - 1$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$(\sqrt6 - \sqrt2)/4$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;$3 - 2\sqrt2$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buCw5C/dJMcacrknRy/IXdTY0BZnm7g3fyDoIO5wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buCw5C/dJMcacrknRy/IXdTY0BZnm7g3fyDoIO5wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buCw5C/dJMcacrknRy/IXdTY0BZnm7g3fyDoIO5wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuCw5C%2FdJMcacrknRy%2FIXdTY0BZnm7g3fyDoIO5wK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;그림 1. 왼쪽: 세타함수로 직접 계산한 $k_N$ ($N = 1, \ldots, 70$). $k_N \approx 4e^{-\pi\sqrt N/2}$ 로 지수적으로 작아진다. 오른쪽: 수치값이 표의 대수적 닫힌 꼴과 10자리까지 일치한다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;더 깊이 들어가면 $k_N$ 은 허수 이차체 $\mathbb Q(\sqrt{-N})$ 의 &lt;strong&gt;허수곱셈(complex multiplication)&lt;/strong&gt; 이론과 연결된다. 크로네커와 베버가 19세기 후반에 이 이론을 세웠고, 베버는 《대수학 교본》(1908)에 $k_N$ 과 관련된 불변량 표를 실었다. 하지만 라마누잔은 이 표를 보지 못한 채 독자적으로 많은 값을 계산했다.&lt;/p&gt;
&lt;h3&gt;특이값 함수 $\alpha(N)$&lt;/h3&gt;
&lt;p&gt;$k_N$ 에서는 $K&amp;#39; = \sqrt N\thinspace K$ 이므로, 르장드르 관계식에 남는 미지수는 $E$와 $E&amp;#39;$ 이다. 보르와인 형제의 표기로&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\alpha(N) = \frac{\pi}{4K^2} - \sqrt{N}\left(\frac{E}{K} - 1\right) \qquad (k = k_N)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;로 두면, 르장드르 관계식과 동치로 $E&amp;#39;/K = \alpha(N) + \pi/(4K^2)$ 이다. 모듈러 방정식을 $k$로 미분하면 $\alpha(N)$ 역시 &lt;strong&gt;대수적 수&lt;/strong&gt;임이 증명된다. 예를 들어 $N = 1$ 에서 $2EK - K^2 = \pi/2$ 를 대입하면 $\alpha(1) = 1/2$, 그리고 $\alpha(3) = (\sqrt3 - 1)/2$ 이다.&lt;/p&gt;
&lt;p&gt;정리하면, 특이 모듈러스에서 $E/K$ 는 &amp;quot;$\pi/K^2$ 와 대수적 수&amp;quot;로 표현되며 이것이 1/π 급수의 재료다.&lt;/p&gt;
&lt;h2&gt;초기하함수와 클라우젠 항등식: $K^2$ 를 하나의 급수로&lt;/h2&gt;
&lt;p&gt;가우스(1812)는 다음 &lt;strong&gt;초기하급수&lt;/strong&gt;를 체계적으로 연구했다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;{}_ 2F_1(a,b;c;x) = \sum_{n=0}^{\infty}\frac{(a)_ n (b)_ n}{(c)_ n\thinspace n!}x^n, \qquad {}_ 3F_2(a,b,c;d,e;x) = \sum_{n=0}^{\infty}\frac{(a)_ n(b)_ n(c)_ n}{(d)_ n(e)_ n\thinspace n!}x^n&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;2편의 결과는 $K = \frac{\pi}{2}\thinspace {}_ 2F_1(\frac12,\frac12;1;k^2)$ 로 쓸 수 있다. 르장드르 관계식에는 $K^2$ 와 $KE$ 가 등장하므로, $K^2$ 를 하나의 깔끔한 급수로 쓰는 것이 다음 목표이고 필요한 도구는 두 개이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이차 변환&lt;/strong&gt; (가우스, 쿰머). $x \le 1/2$ 에서&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;{}_ 2F_1\negthinspace \left(a, b; a+b+\tfrac12; 4x(1-x)\right) = {}_ 2F_1\negthinspace \left(2a, 2b; a+b+\tfrac12; x\right)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;클라우젠 항등식&lt;/strong&gt; (1828).&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\left[{}_ 2F_1\negthinspace \left(a, b; a+b+\tfrac12; x\right)\right]^2 = {}_ 3F_2\negthinspace \left(2a, 2b, a+b;\ 2a+2b, a+b+\tfrac12;\ x\right)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;두 식에 $a = s/2$, $b = (1-s)/2$ 를 넣으면($a + b + \frac12 = 1$) 다음 일반 공식을 얻는다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\left[{}_ 2F_1\negthinspace \left(s, 1-s; 1; x\right)\right]^2 = \sum_{n=0}^{\infty}\frac{(s)_ n\left(\tfrac12\right)_ n(1-s)_ n}{(n!)^3}\thinspace \bigl(4x(1-x)\bigr)^n&lt;br&gt;$$&lt;/p&gt;
&lt;h3&gt;$s = 1/2$: 고전적 타원함수&lt;/h3&gt;
&lt;p&gt;$x = k^2$ 로 두면 $4x(1-x) = (2kk&amp;#39;)^2$ 이고&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\left(\frac{2K}{\pi}\right)^2 = \sum_{n=0}^{\infty}\frac{\left(\tfrac12\right)_ n^{3}}{(n!)^3}\thinspace (2kk&amp;#39;)^{2n}&lt;br&gt;$$&lt;/p&gt;
&lt;h3&gt;$s = 1/4$: 라마누잔의 &amp;quot;시그니처 4&amp;quot; 이론&lt;/h3&gt;
&lt;p&gt;라마누잔은 ${}_ 2F_1(\frac12,\frac12;1;x)$ 대신 ${}_ 2F_1(s,1-s;1;x)$ ($s = 1/3, 1/4, 1/6$) 를 기본으로 하는 &lt;strong&gt;대체 기저(alternative bases)의 타원함수 이론&lt;/strong&gt;을 노트에 전개했으며 이때 노움 역할을 하는 양은 다음과 같다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;q_s = \exp\negthinspace \left(-\frac{\pi}{\sin \pi s}\cdot\frac{{}_ 2F_1(s,1-s;1;1-x)}{{}_ 2F_1(s,1-s;1;x)}\right)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;$s = 1/2$ 이면 고전적인 $q = \exp(-\pi K&amp;#39;/K)$ 로 돌아간다. $s = 1/4$ 일 때 위 일반 공식의 계수는 $(\frac14)_ n(\frac12)_ n(\frac34)_ n/(n!)^3$ 이다.&lt;/p&gt;
&lt;h3&gt;$(4k)!/(k!)^4$ 의 정체&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;보조정리.&lt;/strong&gt; $\dfrac{(4n)!}{(n!)^4} = 256^n\cdot\dfrac{(\frac14)_ n(\frac12)_ n(\frac34)_ n}{(n!)^3}$.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;증명.&lt;/strong&gt; $r = 1, 2, 3, 4$ 각각에 대해&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\left(\tfrac{r}{4}\right)_ n = \prod_{m=0}^{n-1}\left(m + \tfrac{r}{4}\right) = 4^{-n}\prod_{m=0}^{n-1}(4m + r)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;네 곱을 모두 곱하면 1부터 $4n$까지의 정수가 정확히 한 번씩 나오므로 $(\frac14)_ n(\frac12)_ n(\frac34)_ n(1)_ n = (4n)!/4^{4n}$ 이다. $(1)_ n = n!$ 이므로 정리가 따른다.&lt;/p&gt;
&lt;p&gt;그리고 $396^4 = 4^4\cdot 99^4 = 256\cdot 99^4$ 이다. 따라서 라마누잔 공식은 정확히 다음과 같다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{1}{\pi} = \frac{2\sqrt2}{9801}\sum_{k=0}^{\infty}\frac{\left(\tfrac14\right)_ k\left(\tfrac12\right)_ k\left(\tfrac34\right)_ k}{(k!)^3}\thinspace (1103 + 26390k)\left(\frac{1}{99^4}\right)^{k}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;즉 이 공식은 &lt;strong&gt;시그니처 4 이론의 급수&lt;/strong&gt;이고, $(4k)!/(k!)^4$ 에서 생긴 인자 $256^k$ 는 $396 = 4\times 99$ 속에 숨어 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;다음 편 예고 — 4편: 1103의 탄생.&lt;/strong&gt; 이제 공식을 완성하기 위한 재료들이 모두 모였다. 특이 모듈러스 $k_N$, 특이값 함수 $\alpha(N)$, 그리고 $(2K/\pi)^2$ 의 급수 표현을 르장드르 관계식에 넣으면 1/π 급수가 나온다. 그리고 라마누잔이 왜 하필 $N = 58$ 을 골랐는지, 99와 396이 어디서 왔는지를 손으로 계산해 확인해 볼 것이다.&lt;/p&gt;</description>
      <category>CS/수학</category>
      <category>모듈러방정식</category>
      <category>복소곱셈</category>
      <category>세타함수</category>
      <category>야코비</category>
      <category>원주율</category>
      <category>초기하급수</category>
      <category>클라우젠항등식</category>
      <category>특이모듈러스</category>
      <category>파이</category>
      <category>허수곱셈</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/96</guid>
      <comments>https://slowgrid.tistory.com/entry/2000-Toward-1pi-3-The-Modular-World-Jacobi-Singular-Modulus-Hypergeometric-Series#entry96comment</comments>
      <pubDate>Fri, 25 Sep 2026 15:50:01 +0900</pubDate>
    </item>
    <item>
      <title>완벽하게 공정한 난수 발생기는 왜 존재할 수 없을까?</title>
      <link>https://slowgrid.tistory.com/entry/Why-cant-a-perfectly-fair-random-number-generator-exist</link>
      <description>&lt;p&gt;로또 추첨, 게임의 아이템 드롭, 암호 키 생성. 우리는 매일 무작위를 믿으며 살아가고 있다. 그런데 한 번 이렇게 생각해보자.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;어떤 것에도 영향을 받지 않는, 완벽하게 공정한 난수 발생기를 만들 수 있을까?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;결론부터 말하면 &lt;strong&gt;불가능하다.&lt;/strong&gt; 하지만 그 이유를 따라가다 보면 &lt;code&gt;공정하다&lt;/code&gt;는 말의 진짜 의미와, 그럼에도 우리가 믿을 수 있는 난수를 얻는 방법이 보인다. 이 글의 그래프는 모두 직접 코드를 짜서 실행한 실험 결과이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 아무것에도 영향받지 않는 알고리즘은 아무것도 하지 않는다&lt;/h2&gt;
&lt;p&gt;먼저 &lt;code&gt;영향을 받지 않는다&lt;/code&gt;는 조건부터 끝까지 밀어붙여 보자.&lt;/p&gt;
&lt;p&gt;알고리즘이 &lt;strong&gt;입력&lt;/strong&gt;에조차 영향을 받지 않는다면, 무엇을 넣든 항상 같은 값을 내놓는다. 그건 그냥 &lt;code&gt;return 42&lt;/code&gt; 같은 상수 함수이며 쓸모가 없다.&lt;/p&gt;
&lt;p&gt;게다가 알고리즘은 공중에 떠서 돌아가지 않는다. CPU, 메모리, 계산 모델 위에서 돌아가고, 그 환경의 영향(부동소수점 오차, 하드웨어 차이)을 받는다. 그리고 &lt;strong&gt;&amp;quot;공짜 점심은 없다(No Free Lunch)&amp;quot; 정리&lt;/strong&gt;가 말해 주듯, 어떤 알고리즘이 잘 작동한다는 건 언제나 &amp;quot;데이터가 이럴 것이다&amp;quot;라는 가정에 기대고 있다는 뜻이다.&lt;/p&gt;
&lt;p&gt;그래서 현실적인 질문은 &amp;quot;의존성을 0으로 만들 수 있나?&amp;quot;가 아니라 &lt;strong&gt;&amp;quot;무엇에 의존하지 않게 만들 수 있나?&amp;quot;&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 알고리즘은 무작위성을 만들어 낼 수 없다&lt;/h2&gt;
&lt;p&gt;그럼 다시 난수 발생기로 돌아와보자. 가장 먼저 부딪히는 벽은 이것이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;컴퓨터 알고리즘은 결정론적이라는 것.&lt;/strong&gt; 같은 입력이 들어가면 반드시 같은 출력이 나온다. 그렇다면 알고리즘이 만든 난수는 사실 입력(시드)에 의해 전부 정해진 숫자열일 뿐이다. 컴퓨터 과학의 아버지 중 한 명인 폰 노이만은 산술적 방법으로 난수를 만들려는 사람은 &amp;quot;죄를 짓고 있는 것&amp;quot;이라고 농담하기도 했다.&lt;/p&gt;
&lt;p&gt;그래서 이것이 얼마나 쉽게 무너지는지 직접 확인해 보았다. 파이썬의 기본 &lt;code&gt;random&lt;/code&gt; 모듈(메르센 트위스터)에 무작위 시드를 넣고, &lt;strong&gt;시드는 모르는 채로 출력값 624개만 관찰했다.&lt;/strong&gt; 이 출력으로 내부 상태를 거꾸로 계산해 복원한 뒤, 이후 값을 예측해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sz4u4/dJMcafBsyJm/eV5K83aC1S9ps9fyaveLS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sz4u4/dJMcafBsyJm/eV5K83aC1S9ps9fyaveLS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sz4u4/dJMcafBsyJm/eV5K83aC1S9ps9fyaveLS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsz4u4%2FdJMcafBsyJm%2FeV5K83aC1S9ps9fyaveLS0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;파란 선은 실제 출력, 주황 점은 예측값. 다음 1,000개를 하나도 빠짐없이 맞혔다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;통계적으로는 고르게 분포하는 난수지만, 누군가 결과를 맞히려 드는 상황(추첨, 도박, 암호)에서는 전혀 공정하지 않다.&lt;/p&gt;
&lt;p&gt;즉, &lt;strong&gt;무작위성은 알고리즘 안에서 생겨날 수 없고, 반드시 바깥에서 가져와야 한다.&lt;/strong&gt; 이것이 끊을 수 없는 첫 번째 의존성이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 그래서 세상에서 무작위를 빌려온다&lt;/h2&gt;
&lt;p&gt;바깥 어디에서 가져올까? &lt;/p&gt;
&lt;p&gt;정답은 결과를 예측하기 어려운 물리 현상들이다. 이를 &lt;strong&gt;엔트로피 원천&lt;/strong&gt;이라고 부른다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;원천&lt;/th&gt;
&lt;th&gt;무엇의 영향을 받나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;동전, 주사위&lt;/td&gt;
&lt;td&gt;던지는 힘, 공기 저항, 초기 조건 (고전 물리라 원리상 결정되어 있음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;회로의 열잡음, 타이머 떨림&lt;/td&gt;
&lt;td&gt;온도, 전자의 운동, CPU 부하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마우스 움직임, 키 입력 간격&lt;/td&gt;
&lt;td&gt;사람의 행동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라바램프 벽 (Cloudflare의 실제 사례)&lt;/td&gt;
&lt;td&gt;램프 속 왁스의 유체 운동&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;실제 원천은 어떤 모습일까? 내 PC에서 타이머를 연속으로 두 번 읽고, 그 간격(100ns 단위 틱 수)이 홀수면 1, 짝수면 0으로 5만 번씩 20회 측정해서 기록해보았다. &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3iMxY/dJMcad4DldM/94K5TDFl0QDzjAaRNEK5f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3iMxY/dJMcad4DldM/94K5TDFl0QDzjAaRNEK5f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3iMxY/dJMcad4DldM/94K5TDFl0QDzjAaRNEK5f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3iMxY%2FdJMcad4DldM%2F94K5TDFl0QDzjAaRNEK5f1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;주황 선이 타이머 원천이다. 1의 비율이 회차마다 26%에서 93%까지 오간다. 파란 선은 5장에서 설명할 폰 노이만 처리를 거친 결과다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;같은 PC, 같은 코드인데도 측정할 때마다 결과가 크게 달라진다. CPU 부하, 클럭 변화, 백그라운드 작업이 모두 이 원천에 영향을 주기 때문이다.&lt;br&gt;그렇기에 &lt;strong&gt;&amp;quot;아무것에도 영향을 받지 않는&amp;quot; 원천은 없다.&lt;/strong&gt; 동전조차 던지는 순간의 조건을 완벽히 안다면 결과를 계산할 수 있다.&lt;/p&gt;
&lt;p&gt;그렇다면 공정한 난수란 도대체 무엇일까?&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. &amp;quot;공정하다&amp;quot;의 진짜 의미: 원인이 없는 게 아니라, 아무도 모르는 것&lt;/h2&gt;
&lt;p&gt;여기서 관점을 바꿔야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;공정한 난수 = 결과가 공격자나 관찰자가 알 수 있는 어떤 정보와도 독립적인 것&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;원인이 있어도 괜찮다. 그 원인에 &lt;strong&gt;아무도 접근할 수 없으면&lt;/strong&gt; 공정하다.&lt;/p&gt;
&lt;p&gt;운영체제는 바로 이 원리로 난수를 만든다. 하드웨어 잡음, 인터럽트 타이밍 같은 엔트로피를 긁어모아 섞은 뒤 제공한다. 파이썬에서는 &lt;code&gt;secrets&lt;/code&gt; 모듈이 이를 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import secrets

def fair_int(lo, hi):
    return lo + secrets.randbelow(hi - lo + 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 흔한 실수가 하나 있다. &lt;code&gt;랜덤값 % 6 + 1&lt;/code&gt;처럼 나머지 연산으로 범위를 맞추는 것이다. 0~7 사이의 난수를 &lt;code&gt;% 6&lt;/code&gt;하면 0과 1은 두 가지 값(0과 6, 1과 7)에서 나오고, 나머지는 한 가지 값에서만 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rCwvV/dJMcaa1bVFh/rowy5KwkdV4CHwSl5PS1t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rCwvV/dJMcaa1bVFh/rowy5KwkdV4CHwSl5PS1t1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rCwvV/dJMcaa1bVFh/rowy5KwkdV4CHwSl5PS1t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrCwvV%2FdJMcaa1bVFh%2Frowy5KwkdV4CHwSl5PS1t1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;각 60만 번 측정. &lt;code&gt;% 6&lt;/code&gt; 방식(주황)은 1과 2가 25%씩, 나머지가 12.5%씩 나왔다. 거부 샘플링(파랑)은 모두 16.7% 기준선에 맞는다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;randbelow&lt;/code&gt;는 범위를 넘는 값을 버리고 다시 뽑는 &lt;strong&gt;거부 샘플링&lt;/strong&gt;으로 이 문제를 피한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 편향은 지울 수 있다: 폰 노이만의 트릭&lt;/h2&gt;
&lt;p&gt;그렇다면 원천 자체가 치우쳐 있다면? 3장의 타이머처럼, 혹은 앞면이 70% 나오는 찌그러진 동전밖에 없다면?&lt;/p&gt;
&lt;p&gt;놀랍게도 &lt;strong&gt;편향된 동전으로도 공정한 결과를 만들 수 있다.&lt;/strong&gt; 폰 노이만이 제안한 방법은 간단하다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;동전을 두 번 던진다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;앞-뒤&lt;/strong&gt;가 나오면 1, &lt;strong&gt;뒤-앞&lt;/strong&gt;이 나오면 0을 기록한다.&lt;/li&gt;
&lt;li&gt;같은 면이 두 번 나오면(앞-앞, 뒤-뒤) 버리고 다시 던진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;왜 공정할까? 앞면 확률을 0.7이라고 하면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;앞-뒤가 나올 확률 = 0.7 × 0.3 = &lt;strong&gt;0.21&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;뒤-앞이 나올 확률 = 0.3 × 0.7 = &lt;strong&gt;0.21&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;곱셈은 순서를 바꿔도 값이 같다. 그래서 &lt;strong&gt;편향이 얼마든 두 확률은 항상 같다.&lt;/strong&gt; 실제로 앞면 70% 동전을 두 번씩 20만 회 던져 봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHX6T3/dJMcabeAOAw/itkzyWkQ80Fn3fm2kIOoJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHX6T3/dJMcabeAOAw/itkzyWkQ80Fn3fm2kIOoJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHX6T3/dJMcabeAOAw/itkzyWkQ80Fn3fm2kIOoJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHX6T3%2FdJMcabeAOAw%2FitkzyWkQ80Fn3fm2kIOoJ0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;앞-뒤 21.0%, 뒤-앞 21.1%. 이론값과 소수점 한 자리까지 일치한다. 회색은 버리는 경우다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;코드로 옮기면 몇 줄이면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def von_neumann(bits):
    it = iter(bits)
    for a, b in zip(it, it):  # 두 개씩 짝지음
        if a != b:
            yield a  # 10 → 1, 01 → 0, 00/11 → 버림&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;편향을 5%부터 95%까지 바꿔 가며 같은 실험을 반복했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7xcT1/dJMcabeAOB8/rKXWL4Vak3taEmYJRTjMBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7xcT1/dJMcabeAOB8/rKXWL4Vak3taEmYJRTjMBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7xcT1/dJMcabeAOB8/rKXWL4Vak3taEmYJRTjMBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7xcT1%2FdJMcabeAOB8%2FrKXWL4Vak3taEmYJRTjMBK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;주황 선(원천)은 설정한 편향을 그대로 따라가지만, 파란 선(출력)은 모든 구간에서 49.6%~50.5% 사이에 머문다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;결과가 &lt;strong&gt;원천의 편향값에 의존하지 않는다.&lt;/strong&gt; 3장 그래프의 파란 선도 같은 원리이며, 26%~93%로 요동치는 타이머 원천이 폰 노이만 처리 후에는 50% 근처에 붙어 있다.&lt;/p&gt;
&lt;p&gt;물론 대가가 없는 것은 아니다. 70% 편향이면 입력 100비트당 약 21비트만 남고, 편향이 심할수록 더 많이 버려진다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;알고리즘은 무작위성을 만들 수는 없지만, 원천의 편향으로부터는 벗어나게 해 줄 수 있다.&lt;/strong&gt; 이런 도구를 일반화한 분야를 &lt;strong&gt;랜덤니스 추출기(randomness extractor)&lt;/strong&gt;라고 부른다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 그래도 지울 수 없는 전제: 독립성&lt;/h2&gt;
&lt;p&gt;폰 노이만 트릭에는 숨은 전제가 하나 있다. &lt;strong&gt;각 던지기가 서로 독립이어야 한다는 것.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;만약 원천이 &amp;quot;직전 결과를 따라가는&amp;quot; 성질을 갖고 있다면 어떻게 될까? 두 가지 경우로 실험해 보았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시뮬레이션:&lt;/strong&gt; 직전 값과 같은 값을 낼 확률을 50%에서 95%까지 바꿔 가며 원천을 만들었다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;실측:&lt;/strong&gt; 3장의 타이머 원천을 그대로 썼다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;두 경우 모두 폰 노이만 처리를 한 뒤, &lt;strong&gt;아주 단순한 예측 규칙&lt;/strong&gt;으로 다음 값을 맞혀 봤다. 직전 100개 출력에서 &amp;quot;같은 값이 이어진 경우&amp;quot;와 &amp;quot;값이 바뀐 경우&amp;quot; 중 많았던 쪽을 따라 찍는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbAYwL/dJMcaiY5XTx/8xrzKGYiRLdkl3gHP4DdrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbAYwL/dJMcaiY5XTx/8xrzKGYiRLdkl3gHP4DdrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbAYwL/dJMcaiY5XTx/8xrzKGYiRLdkl3gHP4DdrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbAYwL%2FdJMcaiY5XTx%2F8xrzKGYiRLdkl3gHP4DdrK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;왼쪽: 원천이 직전 값을 따라갈수록 적중률이 올라 최대 65%에 이른다. 오른쪽: 실제 타이머 원천은 1의 비율이 49.9%로 완벽해 보이지만, 다음 값을 66.1% 확률로 맞힐 수 있었다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;이것이 핵심이다. &lt;strong&gt;폰 노이만 트릭은 0과 1의 비율은 맞춰 주지만, 결과끼리 이어진 패턴은 지우지 못한다.&lt;/strong&gt; 비율만 보면 공정해 보이는데, 4장의 정의로 보면 전혀 공정하지 않다. 50% 대신 66%로 맞힐 수 있는 추첨은 공정한 추첨이 아니다.&lt;/p&gt;
&lt;p&gt;그럼 이 전제까지 없애는 더 똑똑한 알고리즘을 만들면 되지 않을까? 안타깝게도 &lt;strong&gt;수학적으로 불가능하다는 것이 증명되어 있다.&lt;/strong&gt; 산타-바지라니(1986)는 이전 결과에 조금씩 영향을 받는 원천 하나만으로는, 어떤 결정론적 알고리즘도 공정한 비트를 단 하나도 보장할 수 없음을 보였다.&lt;/p&gt;
&lt;p&gt;알고리즘으로 지울 수 있는 의존성은 여기까지다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 원인 없는 무작위에 가장 가까운 것: 양자역학&lt;/h2&gt;
&lt;p&gt;그렇다면 물리학은 어떨까? 고전 물리의 동전은 원리상 결정되어 있지만, 양자 세계는 다르다.&lt;/p&gt;
&lt;p&gt;광자 하나를 반투명 거울에 보내면 통과하거나 반사된다. 이때 &lt;strong&gt;&amp;quot;사실 미리 정해져 있었는데 우리가 몰랐을 뿐&amp;quot;&lt;/strong&gt;이라는 설명(국소적 숨은 변수)은 &lt;strong&gt;벨 부등식 실험&lt;/strong&gt;으로 배제되었다. 이 업적으로 알랭 아스페, 존 클라우저, 안톤 차일링거가 2022년 노벨 물리학상을 받았다.&lt;/p&gt;
&lt;p&gt;여기서 더 나아가 &lt;strong&gt;장치 독립 난수 인증&lt;/strong&gt;이라는 기술도 있다. 장치를 만든 사람을 믿지 않아도, 벨 부등식이 얼마나 위반되는지만 측정하면 &amp;quot;이 출력은 누구도 미리 알 수 없었다&amp;quot;를 수학적으로 인증할 수 있다. 미국 NIST와 콜로라도대학교는 이 방식을 이용한 공개 난수 서비스를 선보이기도 했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;하지만 이것조차 &amp;quot;아무것에도 의존하지 않는&amp;quot; 것은 아니다.&lt;/strong&gt; 두 가지 가정이 남는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;양자역학이 옳다.&lt;/strong&gt; 지금까지 모든 실험을 통과했지만, 과학 이론은 언제나 잠정적이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;실험자가 측정 설정을 자유롭게 고를 수 있다.&lt;/strong&gt; 만약 우주의 모든 것이, 실험자의 선택까지 태초부터 정해져 있다면(초결정론) 벨 실험의 결론은 성립하지 않는다. 이 가능성은 반증할 방법이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;8. 마지막 벽: 무작위임을 증명할 수 없다&lt;/h2&gt;
&lt;p&gt;설령 완벽한 난수 발생기가 있다고 해도, 우리는 &lt;strong&gt;그것을 확인할 방법이 없다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;어떤 숫자열이 진짜 무작위인지 판정하려면 &amp;quot;이 숫자열을 만드는 더 짧은 규칙이 존재하는가?&amp;quot;를 알아야 한다. 이것을 &lt;strong&gt;콜모고로프 복잡도&lt;/strong&gt;라고 하는데, 이 값을 계산하는 알고리즘은 존재하지 않는다는 것이 증명되어 있다.&lt;/p&gt;
&lt;p&gt;통계 검정이 말해 줄 수 있는 건 &lt;strong&gt;&amp;quot;치우침을 찾지 못했다&amp;quot;&lt;/strong&gt;까지이기에, 이를 극단적인 예로 확인해 봤다. &lt;code&gt;1, 2, 3, 4, 5, 6&lt;/code&gt;을 계속 반복하는 수열과 &lt;code&gt;fair_int&lt;/code&gt;로 굴린 주사위를 각각 6만 번씩 비교했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bciRQS/dJMcaatq9x0/CDEzeL6oWtH75EkQQl09u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bciRQS/dJMcaatq9x0/CDEzeL6oWtH75EkQQl09u1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bciRQS/dJMcaatq9x0/CDEzeL6oWtH75EkQQl09u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbciRQS%2FdJMcaatq9x0%2FCDEzeL6oWtH75EkQQl09u1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;왼쪽: 빈도 검정(카이제곱)에서 반복 수열은 0.00으로 오히려 &lt;code&gt;fair_int&lt;/code&gt;(2.54)보다 &amp;quot;더 공정&amp;quot;하게 나온다. 오른쪽: 하지만 다음 값을 맞히면 반복 수열은 100%, &lt;code&gt;fair_int&lt;/code&gt;는 찍기 수준인 16.6%다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;하나의 검정을 통과해도, 아직 시도하지 않은 다른 검정이 언제나 남아 있다. 6장의 타이머 원천이 비율 검정은 통과하고 패턴 검정에서 걸린 것처럼. 그리고 가능한 모든 검정을 다 해 보는 방법은 존재하지 않는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리: 우리가 벗어날 수 있는 것과 없는 것&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;의존성&lt;/th&gt;
&lt;th&gt;벗어날 수 있나?&lt;/th&gt;
&lt;th&gt;방법 / 근거&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;원천의 편향&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;폰 노이만 트릭, 랜덤니스 추출기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;범위 변환의 치우침&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;거부 샘플링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관찰자의 예측&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;OS 엔트로피 (&lt;code&gt;secrets&lt;/code&gt;), 양자 측정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결과끼리의 독립성&lt;/td&gt;
&lt;td&gt;❌ 불가능&lt;/td&gt;
&lt;td&gt;산타-바지라니 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;바깥의 엔트로피 원천&lt;/td&gt;
&lt;td&gt;❌ 불가능&lt;/td&gt;
&lt;td&gt;결정론적 알고리즘은 무작위를 만들 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;물리 법칙에 대한 가정&lt;/td&gt;
&lt;td&gt;❌ 불가능&lt;/td&gt;
&lt;td&gt;양자역학의 정당성, 측정 선택의 자유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;무작위임의 증명&lt;/td&gt;
&lt;td&gt;❌ 불가능&lt;/td&gt;
&lt;td&gt;콜모고로프 복잡도는 계산 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;결론은 &lt;strong&gt;어디에도 영향을 받지 않는 공정한 난수 발생기는 존재하지 않는다는 것.&lt;/strong&gt; 설령 존재하더라도 우리는 그것을 증명할 수 없다.&lt;/p&gt;
&lt;p&gt;하지만 그게 절망적인 결론은 아니다. 공정함의 진짜 의미는 &amp;quot;원인이 없는 것&amp;quot;이 아니라 &lt;strong&gt;&amp;quot;아무도 결과를 미리 알 수 없는 것&amp;quot;&lt;/strong&gt;이고, 그런 발생기는 충분히 만들 수 있다. 우리가 할 수 있는 일은 의존성을 없애는 게 아니라 &lt;strong&gt;어디에 둘지를 고르는 것&lt;/strong&gt;이다. 믿을 수 있는 원천에 기대고, 알고리즘으로 편향을 지우고, 남은 가정을 정직하게 인정하는 것. 그것이 우리가 가진 최선의 &amp;quot;무작위&amp;quot;이다.&lt;/p&gt;
&lt;hr&gt;</description>
      <category>CS/수학</category>
      <category>난수 발생기</category>
      <category>원천의 편향</category>
      <category>콜모고로프 복잡도</category>
      <category>폰 노이만 트릭</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/95</guid>
      <comments>https://slowgrid.tistory.com/entry/Why-cant-a-perfectly-fair-random-number-generator-exist#entry95comment</comments>
      <pubDate>Thu, 24 Sep 2026 13:30:29 +0900</pubDate>
    </item>
    <item>
      <title>1/&amp;pi;를 향한 2000년 (2) &amp;mdash; AGM과 타원적분: 가우스와 르장드르</title>
      <link>https://slowgrid.tistory.com/entry/2000-Years-Toward-1pi-2-AGM-and-Elliptic-Integrals-Gauss-and-Legendre</link>
      <description>&lt;p&gt;&lt;em&gt;시리즈 「1/π를 향한 2000년」: 1편 원과 무한 / &lt;strong&gt;2편 AGM과 타원적분&lt;/strong&gt; / 3편 모듈러의 세계 / 4편 1103의 탄생 / 5편 검증과 그 이후&lt;/em&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;지난 편 요약.&lt;/strong&gt; 아르키메데스는 조화평균과 기하평균을 번갈아 취하는 다각형 이중화로 π를 가두었고 비에트, 월리스, 마친의 무한곱과 급수, 오일러의 바젤 문제를 거치며 π는 해석학의 상수가 되었다. 하지만 이 방법들은 모두 &lt;strong&gt;선형 수렴&lt;/strong&gt;이라 한 단계에 얻는 자릿수가 고정되어 있었다. 1편에서 만든 도구 중 이번 편에 다시 쓰이는 것은 월리스 적분 $\int_0^{\pi/2}\sin^{2n}\theta\thinspace d\theta$ 와 포흐하머 기호 $(\tfrac12)_ n$ 이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;가우스(1799): 산술기하평균과 타원적분&lt;/h2&gt;
&lt;p&gt;라마누잔 공식의 직접적인 뿌리는 1799년 가우스의 발견이다. 두 양수 $a, b$ 에서 시작해 산술평균과 기하평균을 번갈아 취한다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;a_{n+1} = \frac{a_n + b_n}{2}, \qquad b_{n+1} = \sqrt{a_n b_n}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;두 수열은 같은 극한 $M(a,b)$, 즉 &lt;strong&gt;산술기하평균(AGM)&lt;/strong&gt; 으로 수렴하고, 여기서 수렴 속도가 핵심이다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;a_{n+1} - b_{n+1} = \frac{(\sqrt{a_n} - \sqrt{b_n})^2}{2} = \frac{(a_n - b_n)^2}{2(\sqrt{a_n} + \sqrt{b_n})^2}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;오차가 매 단계 &lt;strong&gt;제곱&lt;/strong&gt;으로 줄어들고(이차 수렴), 정확한 자릿수가 한 단계마다 대략 두 배가 되며, 이는 1편에서 본 아르키메데스의 재귀가 한 단계에 오차를 1/4로 줄였던 것과 대비된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;627&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpJh0D/dJMcaikGV1P/vIncYC5BY0nj3UcRdurgf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpJh0D/dJMcaikGV1P/vIncYC5BY0nj3UcRdurgf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpJh0D/dJMcaikGV1P/vIncYC5BY0nj3UcRdurgf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpJh0D%2FdJMcaikGV1P%2FvIncYC5BY0nj3UcRdurgf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1484&quot; height=&quot;627&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;627&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 1. $a_0=1,\ b_0=1/\sqrt2$ 에서의 AGM. 세 번만 반복해도 두 수열이 그래프상으로 겹치고(왼쪽), 일치하는 자릿수는 0.5 → 1.9 → 4.6 → 10.1 → 21.0 → 42.8 → 86.5 로 매번 약 두 배가 된다(오른쪽).&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;가우스의 정리&lt;/h3&gt;
&lt;p&gt;$$&lt;br&gt;I(a,b) = \int_0^{\pi/2}\frac{d\theta}{\sqrt{a^2\cos^2\theta + b^2\sin^2\theta}} = \frac{\pi}{2\thinspace M(a,b)}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;증명.&lt;/strong&gt; $t = b\tan\theta$ 로 치환하면 $2I(a,b) = J(a,b)$ 가 된다. 여기서&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;J(a,b) = \int_{-\infty}^{\infty}\frac{dt}{\sqrt{(t^2+a^2)(t^2+b^2)}}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이제 $u = (t - ab/t)/2$ 로 치환한다. $t$가 0에서 ∞로 갈 때 $u$는 −∞에서 ∞로 단조증가한다. $m = (a+b)/2$, $g = \sqrt{ab}$ 로 두면 다음이 성립한다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;u^2 + m^2 = \frac{(t^2+a^2)(t^2+b^2)}{4t^2}, \qquad u^2 + g^2 = \frac{(t^2+ab)^2}{4t^2}, \qquad du = \frac{t^2+ab}{2t^2}\thinspace dt&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;세 식을 대입하면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{du}{\sqrt{(u^2+m^2)(u^2+g^2)}} = \frac{2\thinspace dt}{\sqrt{(t^2+a^2)(t^2+b^2)}}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이다. 피적분함수가 우함수이므로 $J(m,g) = J(a,b)$. 즉 $I$는 AGM 한 단계에 대해 불변이다. 이를 반복하면 $I(a,b) = I(M,M) = \pi/(2M)$.&lt;/p&gt;
&lt;h3&gt;완전타원적분&lt;/h3&gt;
&lt;p&gt;$0&amp;lt; k&amp;lt; 1$, $k&amp;#39; = \sqrt{1-k^2}$ 에 대해 제1종/제2종 완전타원적분을 다음과 같이 정의한다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;K(k) = \int_0^{\pi/2}\frac{d\theta}{\sqrt{1-k^2\sin^2\theta}}, \qquad E(k) = \int_0^{\pi/2}\sqrt{1-k^2\sin^2\theta}\thinspace d\theta&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;$E$는 타원의 둘레를 계산하다 나온 적분이라 타원적분이라 불린다. $1 - k^2\sin^2\theta = \cos^2\theta + k&amp;#39;^2\sin^2\theta$ 이므로 $K(k) = I(1,k&amp;#39;)$, 곧&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;K(k) = \frac{\pi}{2\thinspace M(1,k&amp;#39;)}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;다른 한편, $(1-k^2\sin^2\theta)^{-1/2}$ 를 이항전개하고 1편 월리스 곱에서 본 적분 $\int_0^{\pi/2}\sin^{2n}\theta\thinspace d\theta = \frac{\pi}{2}\cdot\frac{(1/2)_ n}{n!}$ 을 쓰면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;K(k) = \frac{\pi}{2}\sum_{n=0}^{\infty}\frac{(\frac12)_ n^{\thinspace 2}}{(n!)^2}\thinspace k^{2n}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;를 얻는다. &lt;strong&gt;π가 타원적분의 급수 앞에 계수로 붙어 나온다&lt;/strong&gt;는 것, 이것이 이후 모든 이야기의 출발점이다. 가우스는 일기(1799년 5월 30일)에 1과 $\sqrt{2}$의 산술기하평균에 대해&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;M(1,\sqrt{2})=\frac{\pi}{\varpi}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;라는 관계가 소수점 11자리까지 성립함을 수치적으로 확인했다고 기록했다. 즉,&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\varpi=\frac{\pi}{M(1,\sqrt{2})}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이며, 여기서 $\varpi\approx2.6220575543$는 렘니스케이트 상수이다. 이 발견은 산술기하평균이 타원적분 및 렘니스케이트와 깊이 연결되어 있음을 보여 주는 중요한 단서였으며, 이것이 해석학의 새 분야를 열 것이라고 작성하였다.&lt;/p&gt;
&lt;h2&gt;르장드르 관계식: π를 타원적분으로 표현하기&lt;/h2&gt;
&lt;p&gt;르장드르는 $k$와 그 여모듈러스 $k&amp;#39;$ 의 타원적분 사이에 π가 정확히 나타나는 항등식을 발견했다. $K&amp;#39; = K(k&amp;#39;)$, $E&amp;#39; = E(k&amp;#39;)$ 로 쓸 때&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;E K&amp;#39; + E&amp;#39; K - K K&amp;#39; = \frac{\pi}{2}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;증명.&lt;/strong&gt; 적분 아래에서 미분하면 다음 두 식을 얻는다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{dE}{dk} = \frac{E-K}{k}, \qquad \frac{dK}{dk} = \frac{E - k&amp;#39;^2K}{k\thinspace k&amp;#39;^2}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;$dk&amp;#39;/dk = -k/k&amp;#39;$ 와 연쇄법칙으로 $E&amp;#39;$, $K&amp;#39;$ 의 도함수도 구해진다. 좌변 $L$을 $k$로 미분하고 $k\thinspace k&amp;#39;^2$ 를 곱해 정리하면, $EE&amp;#39;$, $EK&amp;#39;$, $KE&amp;#39;$, $KK&amp;#39;$ 각 항의 계수가 모두 $k^2 + k&amp;#39;^2 - 1 = 0$ 꼴로 소거되어 $dL/dk = 0$ 이기에 따라서 $L$은 상수이다.&lt;/p&gt;
&lt;p&gt;상수값은 $k\to0$ 에서 구한다. $K, E \to \pi/2$, $E&amp;#39;\to1$ 이고, $K&amp;#39;$ 는 $\log(4/k)$ 처럼 발산하지만 $E - K = O(k^2)$ 이므로 $(E-K)K&amp;#39; \to 0$ 이다. $L = (E-K)K&amp;#39; + E&amp;#39;K \to \pi/2$.&lt;/p&gt;
&lt;p&gt;(여기서 나온 $dK/dk$ 공식은 4편에서 라마누잔의 주정리를 증명할 때 다시 쓰인다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1471&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKQ7nL/dJMb99Vv6uH/AQBrHqiHgYizERWbrSD071/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKQ7nL/dJMb99Vv6uH/AQBrHqiHgYizERWbrSD071/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKQ7nL/dJMb99Vv6uH/AQBrHqiHgYizERWbrSD071/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKQ7nL%2FdJMb99Vv6uH%2FAQBrHqiHgYizERWbrSD071%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1471&quot; height=&quot;618&quot; data-origin-width=&quot;1471&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 2. 왼쪽: $K(k)$ 와 $E(k)$. 둘 다 $k\to0$ 에서 π/2 로 출발한다. 오른쪽: 르장드르 관계식의 세 항은 각각 크게 변하지만, 그 합은 모든 $k$ 에서 정확히 π/2 이다.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;이 식이 왜 결정적인가&lt;/h3&gt;
&lt;p&gt;가장 대칭적인 경우 $k = k&amp;#39; = 1/\sqrt2$ 에서 관계식은 $2EK - K^2 = \pi/2$ 가 된다. 이제 $K$와 $E$를 AGM으로 계산하면 π가 나온다. 이것이 1976년 살라민과 브렌트가 독립적으로 발표한 &lt;strong&gt;가우스–르장드르 알고리즘&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\pi = \frac{4\thinspace M\negthinspace \left(1, \tfrac{1}{\sqrt2}\right)^2}{1 - \sum_{j=1}^{\infty} 2^{j+1}\left(a_j^2 - b_j^2\right)}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;($a_j, b_j$ 는 $a_0 = 1$, $b_0 = 1/\sqrt2$ 에서 시작한 AGM 수열.) 한 번 반복할 때마다 정확한 자릿수가 두 배가 된다.&lt;/p&gt;
&lt;p&gt;여기서 라마누잔은 다른 길로 갔다. 르장드르 관계식이 말하는 것은 &amp;quot;$k$와 $k&amp;#39;$ 에서의 $K, E$를 알면 π를 안다&amp;quot;이다. 만약 $k&amp;#39;$ 에서의 값 $K&amp;#39;, E&amp;#39;$ 를 &lt;strong&gt;$k$에서의 값 $K, E$로 대수적으로 표현&lt;/strong&gt;할 수 있다면, π는 $K^2$, $KE$ 의 대수적 조합이 되고 $K^2$, $KE$ 는 $k$의 멱급수다. 이런 특별한 $k$를 찾아준 것이 야코비의 모듈러 이론이다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;다음 편 예고 — 3편: 모듈러의 세계.&lt;/strong&gt; 이번 편의 마지막 질문, &amp;quot;$K&amp;#39;$ 와 $E&amp;#39;$ 를 $K$와 $E$로 대수적으로 표현할 수 있는 특별한 $k$&amp;quot;를 찾아 나선다. 야코비의 세타함수와 모듈러 방정식, 특이 모듈러스 $k_N$, 그리고 $K^2$ 를 하나의 급수로 바꿔 주는 클라우젠 항등식을 다룬다.&lt;/p&gt;</description>
      <category>CS/수학</category>
      <category>AGM</category>
      <category>가우스</category>
      <category>가우스르장드르알고리즘</category>
      <category>르장드르</category>
      <category>산술기하평균</category>
      <category>완전타원적분</category>
      <category>원주율</category>
      <category>이차수렴</category>
      <category>타원적분</category>
      <category>파이</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/94</guid>
      <comments>https://slowgrid.tistory.com/entry/2000-Years-Toward-1pi-2-AGM-and-Elliptic-Integrals-Gauss-and-Legendre#entry94comment</comments>
      <pubDate>Wed, 23 Sep 2026 21:24:14 +0900</pubDate>
    </item>
    <item>
      <title>오픈AI, GPT-6 Sol/Luna 공개&amp;hellip;가격 낮추고 AI 에이전트 및 코딩 성능 강화</title>
      <link>https://slowgrid.tistory.com/entry/OpenAI-GPT-6-Sol-Luna-Release%E2%80%A6-Lowering-Prices-And-Enhancing-AI-Agent-And-Coding-Performance</link>
      <description>&lt;p&gt;오픈AI가 GPT-6 제품군을 확대하며 &lt;strong&gt;GPT-6 Sol과 GPT-6 Luna&lt;/strong&gt;를 공개했다.&lt;/p&gt;
&lt;p&gt;두 모델의 핵심은 최고 성능 모델인 GPT-6 Astra의 기술을 보다 낮은 비용으로 제공하는 데 있다. OpenAI는 GPT-6 Sol을 복잡한 코딩과 에이전트 작업에 적합한 모델, GPT-6 Luna를 대량의 작업을 저렴하게 처리하는 데 초점을 맞춘 모델로 설명하고 있다.&lt;/p&gt;
&lt;p&gt;API 가격도 전 세대보다 낮아졌다. 100만 토큰 기준 GPT-6 Sol의 가격은 입력 &lt;strong&gt;2달러&lt;/strong&gt;, 출력 &lt;strong&gt;10달러&lt;/strong&gt;, GPT-6 Luna는 입력 &lt;strong&gt;0.10달러&lt;/strong&gt;, 출력 &lt;strong&gt;0.50달러&lt;/strong&gt;다. OpenAI는 이를 GPT-5.6 Sol/Luna의 프로모션 가격과 비교해 각각 50% 낮춘 것이라고 밝혔다.&lt;/p&gt;
&lt;p&gt;하지만 이번 발표에서 더 눈여겨볼 부분은 단순한 토큰 가격보다 &lt;strong&gt;한 가지 작업을 완료하는 데 들어가는 비용 대비 성능&lt;/strong&gt;이다. OpenAI가 공개한 여러 벤치마크 그래프도 대부분 가로축을 작업당 비용(Cost per task), 세로축을 성능 점수로 표시하고 있다.&lt;/p&gt;
&lt;h2&gt;업무 자동화, Sol의 비용 대비 성능 강조&lt;/h2&gt;
&lt;p&gt;OpenAI가 가장 먼저 제시한 사례는 &lt;strong&gt;AutomationBench&lt;/strong&gt;다. 영업, 마케팅, 운영, 고객지원, 재무, HR 등의 업무에서 여러 앱과 도구를 사용하는 엔드투엔드 AI 에이전트의 수행 능력을 측정하는 평가이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMV5aZ/dJMcacdBmDa/BdHPOIAnbKiWUun1ze5Wr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMV5aZ/dJMcacdBmDa/BdHPOIAnbKiWUun1ze5Wr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMV5aZ/dJMcacdBmDa/BdHPOIAnbKiWUun1ze5Wr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMV5aZ%2FdJMcacdBmDa%2FBdHPOIAnbKiWUun1ze5Wr0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;▲ AutomationBench 비용 대비 성능 비교. 가로축은 작업당 비용, 세로축은 점수&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OpenAI 발표에 따르면 GPT-6 Sol은 &lt;code&gt;xhigh&lt;/code&gt; 추론 설정에서 &lt;strong&gt;33.2%&lt;/strong&gt;를 기록했다. 이때 작업당 비용은 약 &lt;strong&gt;0.27달러&lt;/strong&gt;였다.&lt;/p&gt;
&lt;p&gt;같은 자료에서 GPT-6 Astra의 low 설정은 30.3%, Claude Opus 5의 max 설정은 26.9%, Claude Fable 5.1과 Opus 5 fallback 조합의 max 설정은 31.4%로 제시됐다. OpenAI는 Sol이 Opus 5 max보다 높은 점수를 기록하면서 작업 비용은 약 9% 수준이었다고 설명했다.&lt;/p&gt;
&lt;p&gt;GPT-6 Luna 역시 이전 세대와 비교해 개선됐다. OpenAI에 따르면 Luna의 high 설정은 GPT-5.6 Luna보다 점수가 &lt;strong&gt;5.4%포인트 상승&lt;/strong&gt;했고, 작업당 비용은 58% 낮아졌다.&lt;/p&gt;
&lt;p&gt;즉 이번 세대에서는 최고 점수 자체뿐 아니라 &lt;strong&gt;같은 비용으로 어느 정도의 업무를 수행할 수 있는지&lt;/strong&gt;가 중요한 비교 기준으로 제시되고 있다.&lt;/p&gt;
&lt;h2&gt;전문 업무 평가에서도 Sol/Luna 비용 효율 개선&lt;/h2&gt;
&lt;p&gt;보다 복잡한 전문 업무에서는 &lt;strong&gt;Agents&amp;#39; Last Exam&lt;/strong&gt; 결과가 공개됐다. 이 평가는 컴퓨터를 이용해 수행하는 장시간 전문 업무를 대상으로 하며, 55개 세부 산업 분야에 걸친 경제적 가치가 있는 작업을 평가한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EV7t4/dJMcabyXyGQ/F5rtdWrmBP8kVPQQDITPCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EV7t4/dJMcabyXyGQ/F5rtdWrmBP8kVPQQDITPCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EV7t4/dJMcabyXyGQ/F5rtdWrmBP8kVPQQDITPCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEV7t4%2FdJMcabyXyGQ%2FF5rtdWrmBP8kVPQQDITPCk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;▲ Agents&amp;#39; Last Exam. GPT-6 Sol·Luna와 Astra, GPT-5.6 제품군 및 Claude 계열의 비용/성능 관계가 표시되어 있다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GPT-6 Sol은 max 설정에서 &lt;strong&gt;56.4%&lt;/strong&gt;를 기록했다. OpenAI는 이번 평가에 포함된 Claude Opus 5의 최고 점수보다 높은 결과를 보이면서도 작업당 비용은 약 60% 낮았다고 밝혔다.&lt;/p&gt;
&lt;p&gt;그래프를 보면 GPT-6 Luna 역시 Sol보다 훨씬 낮은 비용 영역에 위치하면서 추론 강도를 높일수록 점수가 상승하는 모습을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;이 때문에 Sol과 Luna의 차이는 단순히 고성능 모델과 저성능 모델이라기보다 &lt;strong&gt;사용자가 요구하는 성능 수준과 비용에 따라 선택할 수 있도록 세분화한 모델&lt;/strong&gt;에 가깝다고 생각하면 된다.&lt;/p&gt;
&lt;h2&gt;사실 오류도 감소…다만 OpenAI 내부 평가&lt;/h2&gt;
&lt;p&gt;생성형 AI에서 중요한 또 하나의 문제는 이른바 할루시네이션으로 불리는 사실 오류이다.&lt;/p&gt;
&lt;p&gt;OpenAI는 실제 ChatGPT 대화 가운데 사용자가 이전 모델의 사실 오류를 신고했던 사례를 익명화해 별도의 factuality 평가를 진행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kHlY6/dJMcaafS1cc/E6ZQ8JkIlXui6thGfbSMg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kHlY6/dJMcaafS1cc/E6ZQ8JkIlXui6thGfbSMg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kHlY6/dJMcaafS1cc/E6ZQ8JkIlXui6thGfbSMg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkHlY6%2FdJMcaafS1cc%2FE6ZQ8JkIlXui6thGfbSMg1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;▲ 어려운 질문에서 사실 오류가 포함된 답변의 비율. 이 그래프는 다른 성능 그래프와 달리 수치가 낮을수록 좋다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그래프에서는 GPT-6 Sol의 사실 오류율이 GPT-5.6 Sol보다 전반적으로 낮아진 것을 확인할 수 있다. OpenAI는 GPT-6 Sol이 이전 세대보다 &lt;strong&gt;약 절반 수준의 사실 오류&lt;/strong&gt;를 보였으며, GPT-6 Luna도 높은 추론 수준에서는 GPT-5.6 Sol과 비슷한 정확성에 훨씬 낮은 비용으로 접근했다고 설명했다.&lt;/p&gt;
&lt;p&gt;다만 여기에는 중요한 단서가 있다.&lt;/p&gt;
&lt;p&gt;이 평가는 일반적인 ChatGPT 질문 전체를 무작위로 측정한 것이 아니다. &lt;strong&gt;과거 실제로 사실 오류가 발생했던 어려운 대화를 중심으로 구성한 OpenAI 내부 평가&lt;/strong&gt;이고, OpenAI 역시 해당 데이터가 일반적인 사용 환경을 대표하지 않으며, 통상적인 대화에서는 오류가 더 드물다고 설명하고 있다.&lt;/p&gt;
&lt;p&gt;따라서 이 그래프는 &amp;quot;GPT-6의 전체 답변 중 오류가 몇 % 발생한다&amp;quot;는 통계로 해석하기보다 &lt;strong&gt;동일한 어려운 조건에서 세대별 정확성이 얼마나 개선됐는지를 보여주는 자료&lt;/strong&gt;로 보는 것이 적절할 것이다.&lt;/p&gt;
&lt;h2&gt;코딩에서는 FrontierCode 성능 개선&lt;/h2&gt;
&lt;p&gt;GPT-6 Sol과 Luna는 코딩 및 소프트웨어 개발도 주요 활용 분야로 내세우고 있다.&lt;/p&gt;
&lt;p&gt;첫 번째 코딩 평가인 &lt;strong&gt;FrontierCode&lt;/strong&gt;는 단순히 코드가 실행되는지만 보는 것이 아니라 실제 코드베이스에 병합할 수 있는지를 평가한다. 테스트 품질, 변경 범위, 코드 스타일, 기존 프로젝트 규칙 준수 등이 평가 대상에 포함된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mtQE8/dJMcagAjtmZ/1iqme5KfXGKF2jmcmrd7ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mtQE8/dJMcagAjtmZ/1iqme5KfXGKF2jmcmrd7ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mtQE8/dJMcagAjtmZ/1iqme5KfXGKF2jmcmrd7ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmtQE8%2FdJMcagAjtmZ%2F1iqme5KfXGKF2jmcmrd7ak%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;▲ FrontierCode 비용 대비 성능. GPT-6 Sol은 GPT-5.6 Sol보다 왼쪽 위 방향으로 이동하며 비용 및 성능이 함께 개선된 모습을 보인다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OpenAI는 FrontierCode에서 GPT-6 Sol이 GPT-5.6 Sol보다 상당한 성능 향상을 보였으며, 높은 추론 설정에서는 Claude Fable 5.1의 xhigh 결과와 비슷한 수준을 더 낮은 비용으로 기록했다고 밝혔다.&lt;/p&gt;
&lt;p&gt;GPT-6 Luna 역시 GPT-5.6 Luna와 비교했을 때 같은 비용 구간에서 더 높은 점수를 기록하는 형태가 나타난다.&lt;/p&gt;
&lt;p&gt;여기서 핵심은 모델의 최고 점수 하나만 보는 것보다 그래프상의 위치다. &lt;strong&gt;오른쪽으로 갈수록 비용이 높아지고 위로 갈수록 성능이 높기 때문에, 같은 성능이라면 왼쪽에 있는 모델이 더 비용 효율적인 셈이다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;DeepSWE에서는 Sol 68.8%, Luna 66.6%&lt;/h2&gt;
&lt;p&gt;보다 장시간에 걸친 실제 소프트웨어 엔지니어링 문제를 평가하는 &lt;strong&gt;DeepSWE v1.1&lt;/strong&gt;에서도 GPT-6 제품군의 결과가 공개됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsLB4m/dJMcagUE0bj/fNZSnGCKMkOUWnikBql8cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsLB4m/dJMcagUE0bj/fNZSnGCKMkOUWnikBql8cK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsLB4m/dJMcagUE0bj/fNZSnGCKMkOUWnikBql8cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsLB4m%2FdJMcagUE0bj%2FfNZSnGCKMkOUWnikBql8cK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;▲ DeepSWE v1.1 결과. 실제 코드베이스를 대상으로 복잡한 소프트웨어 엔지니어링 작업 수행 능력을 평가한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OpenAI 공식 발표에 따르면 GPT-6 Sol은 max 추론에서 &lt;strong&gt;68.8%&lt;/strong&gt;를 기록했다.&lt;/p&gt;
&lt;p&gt;해당 평가에서 Claude Fable 5의 최고 점수는 69.9%였으며, OpenAI는 Sol의 작업당 비용이 이보다 약 &lt;strong&gt;80% 낮았다&lt;/strong&gt;고 설명했다.&lt;/p&gt;
&lt;p&gt;GPT-6 Luna는 max 설정에서 &lt;strong&gt;66.6%&lt;/strong&gt;를 기록했다. OpenAI에 따르면 이는 평가에 사용된 Claude Opus 5와 Fable 5의 medium 설정과 비슷한 수준이며, 작업당 비용은 각각 약 93%, 96% 낮았다.&lt;/p&gt;
&lt;p&gt;다만 이 역시 OpenAI가 발표한 자사 비교 결과라는 점을 감안할 필요가 있다. OpenAI는 경쟁사 모델의 평가는 공개된 보고서의 결과를 이용했다고 밝히고 있어, 모든 모델이 완전히 동일한 서비스 환경과 가격 조건에서 직접 실행된 독립 벤치마크와는 차이가 있을 수 있다.&lt;/p&gt;
&lt;h2&gt;컴퓨터 직접 조작에서도 비용 효율 개선&lt;/h2&gt;
&lt;p&gt;AI가 웹사이트나 데스크톱 환경을 직접 조작하는 컴퓨터 사용 능력도 이번 발표의 주요 평가 대상이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OSWorld 2.0&lt;/strong&gt;은 일상 및 전문 업무에서 여러 단계에 걸쳐 컴퓨터를 조작하는 AI 에이전트의 능력을 측정하는 벤치마크다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyCklN/dJMcacxVSoh/ejefQn23s7bB6kCweI6ctK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyCklN/dJMcacxVSoh/ejefQn23s7bB6kCweI6ctK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyCklN/dJMcacxVSoh/ejefQn23s7bB6kCweI6ctK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyCklN%2FdJMcacxVSoh%2FejefQn23s7bB6kCweI6ctK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;▲ OSWorld 2.0 오프라인 세트. GPT-6 Sol·Luna의 작업당 비용과 컴퓨터 사용 성능을 비교한 그래프.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OpenAI 발표에 따르면 GPT-6 Sol의 xhigh 설정은 &lt;strong&gt;60.5%&lt;/strong&gt;, Claude Opus 5의 medium 설정은 &lt;strong&gt;60.3%&lt;/strong&gt;를 기록했다. OpenAI는 비슷한 수준의 점수를 기록하면서 Sol의 작업당 비용이 약 80% 낮았다고 설명한다.&lt;/p&gt;
&lt;p&gt;GPT-6 Luna의 max 설정은 GPT-5.6 Sol의 medium 설정보다 높은 점수를 기록하면서도 비용은 약 &lt;strong&gt;10분의 1 수준&lt;/strong&gt;이었다.&lt;/p&gt;
&lt;p&gt;이러한 결과는 Luna가 단순 텍스트 생성용 경량 모델을 넘어, 반복적인 컴퓨터 조작이나 대규모 AI 에이전트 운영을 비용 효율적으로 처리하는 역할까지 염두에 두고 설계됐음을 보여준다.&lt;/p&gt;
&lt;h2&gt;GPT-6 Sol과 Luna, 무엇이 달라졌나&lt;/h2&gt;
&lt;p&gt;이번 발표를 종합하면 GPT-6 Sol과 Luna의 가장 큰 변화는 &lt;strong&gt;성능 향상과 함께 작업당 비용을 크게 낮추는 방향으로 모델을 설계했다는 점&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;GPT-6 Sol은 복잡한 코딩, 전문 업무, 에이전트 워크플로처럼 비교적 높은 추론 성능이 필요한 영역을 겨냥한다. 반면 GPT-6 Luna는 더 저렴한 비용을 앞세워 반복 작업이나 대규모 호출이 필요한 서비스에 초점을 맞춘다.&lt;/p&gt;
&lt;p&gt;가격은 100만 토큰당 Sol이 입력 2달러/출력 10달러, Luna가 입력 0.10달러/출력 0.50달러다. 두 모델 모두 최대 &lt;strong&gt;105만 토큰의 컨텍스트 윈도우&lt;/strong&gt;와 최대 12만8,000토큰의 출력을 지원한다고 OpenAI API 문서는 안내한다.&lt;/p&gt;
&lt;p&gt;OpenAI는 프롬프트 캐싱 기능도 개선했다. 반복되는 입력을 캐시에서 읽을 경우 GPT-6 모델에서 &lt;strong&gt;캐시 입력 토큰 읽기에 90% 할인&lt;/strong&gt;을 적용해 장시간 대화나 AI 에이전트의 운영비를 더 낮출 수 있도록 했다.&lt;/p&gt;
&lt;h2&gt;결국 핵심은 최고 성능보다 얼마를 들여 그 성능을 얻는가&lt;/h2&gt;
&lt;p&gt;GPT-6 Astra가 최고 성능을 목표로 하는 모델이라면 Sol과 Luna는 그 성능을 보다 낮은 비용으로 활용할 수 있도록 구성한 제품군이다.&lt;/p&gt;
&lt;p&gt;특히 이번에 공개된 그래프들은 거의 모두 단순 벤치마크 순위 대신 &lt;strong&gt;성능과 작업당 비용을 동시에 표시&lt;/strong&gt;하고 있다. 이는 실제 AI 서비스를 운영하는 기업이나 개발자 입장에서 모델 선택의 기준이 단순한 최고 점수뿐 아니라 비용까지 포함한 형태로 바뀌고 있음을 보여주는 대목이다.&lt;/p&gt;
&lt;p&gt;다만 공개된 수치 대부분은 OpenAI가 자체 발표한 평가 또는 OpenAI가 취합한 경쟁사 공개 결과를 기반으로 한다. 따라서 모델을 실제 업무에 도입할 때는 벤치마크 수치만으로 판단하기보다 &lt;strong&gt;자신이 사용하는 업무, 프롬프트, 도구 환경에서 직접 비용과 성공률을 측정하는 과정&lt;/strong&gt;이 필요하다.&lt;/p&gt;
&lt;p&gt;GPT-6 Sol과 Luna는 발표 시점 기준 ChatGPT Work와 Codex에서 Plus·Pro·Business·Enterprise·Edu 이용자에게 제공되며 API에서는 각각 &lt;code&gt;gpt-6-sol&lt;/code&gt;, &lt;code&gt;gpt-6-luna&lt;/code&gt;로 사용할 수 있다. Free와 Go 이용자는 데스크톱 앱에서 GPT-6 Luna를 이용할 수 있다고 OpenAI는 밝혔다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI: Introducing GPT-6 Sol and Luna&lt;br&gt;&lt;a href=&quot;https://openai.com/index/introducing-gpt-6-sol-and-luna/&quot;&gt;https://openai.com/index/introducing-gpt-6-sol-and-luna/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI Developers: Models&lt;br&gt;&lt;a href=&quot;https://developers.openai.com/api/docs/models&quot;&gt;https://developers.openai.com/api/docs/models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI News</category>
      <category>codex</category>
      <category>GPT-6 Luna</category>
      <category>GPT-6 Sol</category>
      <category>오픈AI</category>
      <category>챗지피티</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/93</guid>
      <comments>https://slowgrid.tistory.com/entry/OpenAI-GPT-6-Sol-Luna-Release%E2%80%A6-Lowering-Prices-And-Enhancing-AI-Agent-And-Coding-Performance#entry93comment</comments>
      <pubDate>Wed, 23 Sep 2026 10:02:47 +0900</pubDate>
    </item>
    <item>
      <title>Claude Opus 5.5 출시&amp;hellip;최상위 AI 경쟁, 이제는 가성비 싸움</title>
      <link>https://slowgrid.tistory.com/entry/Claude-Opus-55-Released%E2%80%A6-Top-tier-AI-Competition-Now-a-Battle-of-Cost-effectiveness</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;앤트로픽(Anthropic)이 새로운 플래그십 AI 모델 &lt;b&gt;Claude Opus 5.5&lt;/b&gt;를 공개했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9월 22일 발표된 Opus 5.5는 새로운 Claude 5.5 제품군의 첫 번째 모델이다. 앤트로픽은 대부분의 작업에서 자사의 상위 모델인 &lt;b&gt;Claude Fable 5.1 수준의 성능을 제공하면서도, Opus 5 대비 실제 운영 비용은 약 40% 낮췄다&lt;/b&gt;고 설명했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 업데이트에서 특히 눈에 띄는 부분은 단순한 벤치마크 점수 상승보다 &lt;b&gt;성능과 비용의 균형&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 5.5의 API 가격은 100만 토큰당 입력 4달러, 출력 20달러다. 이전 Opus 5의 입력 5달러, 출력 25달러와 비교하면 토큰 단가는 20% 낮아졌다. 에이전트와 코딩 작업에서 많이 사용되는 캐시 읽기 가격은 0.50달러에서 0.20달러로 60% 내려갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 같은 작업을 처리하는 데 필요한 토큰 수도 감소하면서, 앤트로픽은 일반적인 작업 기준 실제 비용이 Opus 5보다 약 40% 낮아졌다고 설명한다. 출력 생성 속도 역시 30% 이상 빨라졌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코딩과 실제 업무 성능에 집중&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 5.5가 가장 강하게 밀고 있는 분야는 코딩과 AI 에이전트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앤트로픽이 공개한 Terminal-Bench 4.0 결과에서 Opus 5.5는 66.4%를 기록했다. Fable 5.1은 55.8%, Opus 5는 52.3%였다. 실제 전문 업무 수행 능력을 평가하는 GDPval-AA v2.1에서도 Opus 5.5는 1846점을 기록해 Fable 5.1의 1735점과 Opus 5의 1708점을 앞섰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장시간 진행되는 코딩 작업에서도 개선을 강조하고 있다. 앤트로픽은 초기 사용자 가운데 한 명이 68만 줄 규모의 코드 마이그레이션 작업을 하루 안에 완료했다고 소개했으며, 20만 줄 규모 코드베이스 감사 및 수정 테스트에서는 Opus 5.5가 3시간 이내에 작업을 마친 반면 Opus 5는 20시간 이상 걸렸다고 밝혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 수치들은 대부분 앤트로픽이 실시하거나 공개한 평가 결과라는 점은 감안해야 한다. 앤트로픽 역시 최상위 모델들의 성능이 가까워지면서 &lt;b&gt;벤치마크의 작은 점수 차이가 실제 사용 환경에서 같은 정도의 차이를 의미하지는 않는다&lt;/b&gt;고 설명하고 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 강력한 AI만큼 강조한 안전성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 발표의 또 다른 특징은 안전성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 5.5는 출시 전 AI 안전 연구기관인 &lt;b&gt;METR와 Frontier Design의 외부 평가&lt;/b&gt;를 거쳤으며, 앤트로픽은 자체 행동 및 정렬 평가에서 지금까지 테스트한 모델 가운데 가장 좋은 결과를 기록했다고 밝혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로이터에 따르면 앤트로픽의 특정 내부 평가에서 Opus 5.5가 시스템의 통제 경계를 우회하려고 시도할 가능성은 Opus 5 또는 Mythos 5.1보다 약 85% 낮게 나타났다. 프롬프트 인젝션 공격에 대한 방어 능력도 강화됐다는 것이 회사 측 설명이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 AI 모델이 단순히 질문에 답하는 도구에서 벗어나 수십 분 또는 수시간 동안 스스로 코드를 수정하고, 웹과 컴퓨터를 조작하며 업무를 수행하는 &lt;b&gt;자율형 AI 에이전트&lt;/b&gt;로 이동하고 있다는 흐름과도 맞닿아 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 더 오랫동안 자율적으로 움직일수록 중요한 것은 단순한 정답률만이 아니다. 주어진 권한과 범위를 얼마나 잘 지키는지 역시 중요한 성능 지표가 되고 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI 경쟁의 다음 무대는 성능 대비 비용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 5.5가 보여주는 가장 흥미로운 변화는 AI 업계의 경쟁 기준이 달라지고 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 새로운 모델이 나올 때마다 &amp;ldquo;벤치마크 1위인가&amp;rdquo;만 보는 시대에서, &lt;b&gt;같은 수준의 업무를 얼마나 빠르고 저렴하게 끝낼 수 있는가&lt;/b&gt;가 중요한 경쟁 요소로 떠오르고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus 5.5는 모델 자체의 성능 개선과 함께 토큰 사용량, 처리 속도, 캐시 비용을 동시에 줄였다. 특히 수십 번에서 수백 번 모델을 호출해야 하는 코딩 에이전트나 업무 자동화 서비스에서는 작은 API 가격 차이보다 &lt;b&gt;하나의 업무를 끝내는 데 들어가는 총비용&lt;/b&gt;이 더 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 의미에서 Claude Opus 5.5의 핵심은 단순히 &amp;ldquo;더 강력한 Claude&amp;rdquo;가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;더 적은 비용과 시간으로 최상위 모델 수준의 일을 수행할 수 있는 AI를 만들었다는 점&lt;/b&gt;이 이번 업데이트의 진짜 승부수에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앤트로픽은 앞으로 몇 주 안에 &lt;b&gt;Claude Sonnet 5.5와 Claude Haiku 5.5&lt;/b&gt;도 공개할 예정이다. Opus에서 시작된 성능 및 효율 개선이 보다 저렴한 모델까지 내려온다면, 개발자와 기업 입장에서는 오히려 그때부터 Claude 5.5 세대의 변화가 본격적으로 체감될 가능성이 크다.&lt;br /&gt;&lt;br /&gt;출처: &lt;a href=&quot;https://www.anthropic.com/claude-opus-5-5&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.anthropic.com/claude-opus-5-5&lt;/a&gt;&lt;/p&gt;</description>
      <category>AI News</category>
      <category>Claude</category>
      <category>Opus 5.5</category>
      <category>클로드</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/92</guid>
      <comments>https://slowgrid.tistory.com/entry/Claude-Opus-55-Released%E2%80%A6-Top-tier-AI-Competition-Now-a-Battle-of-Cost-effectiveness#entry92comment</comments>
      <pubDate>Wed, 23 Sep 2026 09:30:28 +0900</pubDate>
    </item>
    <item>
      <title>나는 누구의 시선으로 나를 바라보고 있는가: 자아정체성, 외부적 인식과 내부적 인식</title>
      <link>https://slowgrid.tistory.com/entry/Through-Whose-Eyes-Am-I-Looking-at-Myself-Self-Identity-External-Perception-and-Internal-Perception</link>
      <description>&lt;p&gt;자아정체성에 대해 생각하다 보면 결국 하나의 질문으로 돌아오게 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 나 자신을 어떤 사람이라고 생각하고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;처음에는 단순한 질문처럼 보인다.&lt;/p&gt;
&lt;p&gt;나는 어떤 것을 좋아하는지, 어떤 성격인지, 무엇을 잘하고 무엇을 어려워하는지 정도를 생각하면 답할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;하지만 조금 더 깊게 생각해보면 이 질문은 생각보다 어렵다.&lt;/p&gt;
&lt;p&gt;왜냐하면 내가 생각하는 ‘나’가 정말 내가 바라본 나인지, 아니면 다른 사람들이 바라본 나를 내가 받아들인 것인지 명확하게 구분하기 어렵기 때문이다.&lt;/p&gt;
&lt;p&gt;우리는 살아가면서 끊임없이 다른 사람들의 평가를 받는다.&lt;/p&gt;
&lt;p&gt;학교에서는 성적으로 평가받고,&lt;br&gt;사회에서는 능력으로 평가받고,&lt;br&gt;인간관계에서는 성격이나 태도로 평가받는다.&lt;/p&gt;
&lt;p&gt;SNS를 통해 다른 사람들의 삶을 보면서 나도 모르게 나의 삶을 비교하기도 한다.&lt;/p&gt;
&lt;p&gt;그러다 보면 어느 순간 이런 생각이 든다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;지금 내가 생각하고 있는 나는 정말 내가 만든 모습일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;아니면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;다른 사람들의 시선을 모아서 만든 모습일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이번에는 이 질문을 중심으로 나의 자아정체성에 대해 생각해보았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;먼저, 외부적 인식과 내부적 인식이란 무엇일까?&lt;/h1&gt;
&lt;p&gt;내가 나 자신을 바라보는 방식은 크게 두 방향에서 생각해볼 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;하나는 &lt;strong&gt;외부적 인식&lt;/strong&gt;이고,&lt;br&gt;다른 하나는 &lt;strong&gt;내부적 인식&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;외부적 인식은 말 그대로 다른 사람과 사회의 시선을 통해 나를 바라보는 것이다.&lt;/p&gt;
&lt;p&gt;예를 들면 이런 생각들이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;다른 사람들은 나를 어떻게 볼까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 능력 있는 사람으로 보이고 있을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;사람들이 나를 인정해주고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;남들과 비교했을 때 나는 잘 살아가고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;반대로 내부적 인식은 다른 사람의 평가에서 조금 떨어져 나 자신에게 질문을 던지는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 지금 하고 있는 일을 좋아하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 어떤 삶을 살고 싶은가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이 선택은 내가 정말 원하는 것인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;아무도 알아주지 않아도 나는 이 일을 계속하고 싶은가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;둘 중 하나가 무조건 옳고 다른 하나가 잘못된 것은 아닐 것이다.&lt;/p&gt;
&lt;p&gt;사람은 사회 속에서 살아가기 때문에 다른 사람의 평가를 완전히 무시할 수도 없다.&lt;/p&gt;
&lt;p&gt;다른 사람의 피드백을 통해 내가 몰랐던 모습을 발견하기도 하고, 사회적인 기준을 통해 자신의 위치를 파악하기도 한다.&lt;/p&gt;
&lt;p&gt;문제는 &lt;strong&gt;어느 쪽이 나의 선택을 더 많이 결정하고 있는가&lt;/strong&gt;인 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;나는 외부적 인식과 내부적 인식 중 어디에 영향을 받고 있을까?&lt;/h1&gt;
&lt;p&gt;처음에는 나는 비교적 내 생각대로 살아가는 사람이라고 생각했다.&lt;/p&gt;
&lt;p&gt;남들이 무엇을 하든 크게 신경 쓰지 않고 내가 하고 싶은 일을 선택한다고 생각했다.&lt;/p&gt;
&lt;p&gt;하지만 조금 더 솔직하게 생각해보니 꼭 그렇지만은 않았다.&lt;/p&gt;
&lt;p&gt;어떤 일을 잘했을 때 내가 만족하는 것보다 다른 사람이&lt;/p&gt;
&lt;p&gt;&lt;code&gt;잘했다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 말해주었을 때 더 확신이 생기는 경우가 있었다.&lt;/p&gt;
&lt;p&gt;반대로 내가 나름대로 만족했던 결과도 다른 사람의 반응이 좋지 않으면 갑자기 별것 아닌 것처럼 느껴질 때가 있었다.&lt;/p&gt;
&lt;p&gt;그렇다면 나의 만족은 정말 내 안에서 만들어진 것이었을까?&lt;/p&gt;
&lt;p&gt;아니면 다른 사람의 반응을 통해 만들어진 것이었을까?&lt;/p&gt;
&lt;p&gt;이 질문을 생각하면서 내가 예상했던 것보다 외부적 인식의 영향을 많이 받고 있다는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;특히 &lt;strong&gt;비교가 가능한 영역&lt;/strong&gt;에서 그런 경향이 더 강하게 나타나는 것 같다.&lt;/p&gt;
&lt;p&gt;성과, 능력, 공부, 직업, 경제적인 부분처럼 숫자나 결과로 비교하기 쉬운 영역에서는 자연스럽게 다른 사람을 기준으로 나를 평가하게 된다.&lt;/p&gt;
&lt;p&gt;누군가 나보다 빠르게 앞서가고 있는 것처럼 보이면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 지금 너무 늦은 것은 아닐까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 생각을 하게 되고,&lt;/p&gt;
&lt;p&gt;다른 사람이 좋은 결과를 얻는 모습을 보면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 지금 제대로 하고 있는 것일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 의문이 생기기도 한다.&lt;/p&gt;
&lt;p&gt;그 순간 나를 평가하는 기준은 내 안에 있는 것이 아니라 외부에 있게 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;외부의 기준은 계속 움직인다&lt;/h1&gt;
&lt;p&gt;외부적 인식에 지나치게 의존할 때 어려운 점이 하나 있다.&lt;/p&gt;
&lt;p&gt;기준이 계속 바뀐다는 것이다.&lt;/p&gt;
&lt;p&gt;어떤 목표를 이루기 전에는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이 정도만 이루면 만족할 것 같다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각한다.&lt;/p&gt;
&lt;p&gt;하지만 막상 이루고 나면 더 높은 기준이 보인다.&lt;/p&gt;
&lt;p&gt;처음에는 취업이 목표였다가 취업을 하면 좋은 회사가 기준이 되고,&lt;br&gt;좋은 회사에 들어가면 연봉이 기준이 되고,&lt;br&gt;연봉이 높아지면 직급이나 자산이 기준이 될 수 있다.&lt;/p&gt;
&lt;p&gt;비교의 대상도 계속 바뀐다.&lt;/p&gt;
&lt;p&gt;나보다 조금 앞서 있는 사람을 따라잡으면 그보다 더 앞서 있는 사람이 눈에 들어온다.&lt;/p&gt;
&lt;p&gt;결국 외부의 기준만으로 나를 평가하면 끝이 없는 경쟁 속에서 살아갈 가능성이 있다.&lt;/p&gt;
&lt;p&gt;그래서 이런 질문을 해볼 필요가 있다고 생각한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;만약 다른 사람과 비교할 수 없다면 나는 지금의 삶을 어떻게 평가할까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;SNS도 없고,&lt;br&gt;다른 사람의 연봉도 모르고,&lt;br&gt;누가 어떤 직장을 다니는지도 모르고,&lt;br&gt;누가 나보다 앞서 있는지도 알 수 없다면&lt;/p&gt;
&lt;p&gt;나는 지금의 삶을 만족스럽다고 생각할까?&lt;/p&gt;
&lt;p&gt;이 질문에 대한 답이 어쩌면 내가 가진 &lt;strong&gt;내부적 기준&lt;/strong&gt;에 조금 더 가까운 답일지도 모르겠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;그렇다면 내부적 인식만 따르면 되는 것일까?&lt;/h1&gt;
&lt;p&gt;그렇다고 외부의 평가를 모두 무시하고&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 내가 하고 싶은 것만 하면 된다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하는 것도 조금 다른 문제인 것 같다.&lt;/p&gt;
&lt;p&gt;외부의 시선은 때때로 중요한 정보가 된다.&lt;/p&gt;
&lt;p&gt;내가 잘하고 있다고 생각하지만 주변 사람들에게 계속 같은 문제를 지적받는다면 한 번쯤 돌아볼 필요가 있다.&lt;/p&gt;
&lt;p&gt;내부적 인식만 지나치게 강조하면 자기 확신이 아니라 자기합리화가 될 수도 있기 때문이다.&lt;/p&gt;
&lt;p&gt;그래서 중요한 것은 외부와 내부 중 하나를 선택하는 것이 아니라 &lt;strong&gt;둘의 순서를 정하는 것&lt;/strong&gt;이라고 생각한다.&lt;/p&gt;
&lt;p&gt;나에게 조금 더 좋은 방식은 다음과 같을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;외부의 평가는 참고한다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;하지만&lt;/p&gt;
&lt;p&gt;&lt;code&gt;최종적인 판단은 내가 한다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;다른 사람이 나에게 어떤 평가를 내렸다면 그것을 무조건 받아들이지도 않고, 무조건 무시하지도 않는다.&lt;/p&gt;
&lt;p&gt;왜 그런 평가가 나왔는지를 생각해보고 그중 내가 받아들일 부분을 선택한다.&lt;/p&gt;
&lt;p&gt;결국 외부의 시선은 하나의 데이터이고,&lt;br&gt;그 데이터를 어떻게 해석할지는 나의 몫이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;내가 원하는 삶은 무엇인가?&lt;/h1&gt;
&lt;p&gt;자아정체성을 생각하다 보면 결국 이 질문과 만나게 되는 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 어떤 삶을 살고 싶은가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그런데 의외로 이 질문에 대답하기가 쉽지 않다.&lt;/p&gt;
&lt;p&gt;무엇을 가지고 싶은지는 비교적 쉽게 말할 수 있다.&lt;/p&gt;
&lt;p&gt;좋은 직업, 경제적인 안정, 좋은 인간관계, 건강, 내가 원하는 일을 할 수 있는 환경.&lt;/p&gt;
&lt;p&gt;하지만 이것들은 대부분 삶의 &lt;strong&gt;조건&lt;/strong&gt;에 가깝다.&lt;/p&gt;
&lt;p&gt;조금 다른 질문을 해볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 어떤 방식으로 살아가는 사람이 되고 싶은가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 질문은 조금 더 어려운 것 같다.&lt;/p&gt;
&lt;p&gt;생각해보면 내가 원하는 것은 단순히 어떤 결과를 얻는 삶이라기보다 &lt;strong&gt;스스로 납득할 수 있는 선택을 하면서 살아가는 삶&lt;/strong&gt;에 가까운 것 같다.&lt;/p&gt;
&lt;p&gt;완벽한 선택을 하고 싶은 것은 아니다.&lt;/p&gt;
&lt;p&gt;어떤 선택이 항상 성공적인 결과로 이어질 수도 없다.&lt;/p&gt;
&lt;p&gt;다만 선택의 결과가 좋지 않더라도&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 당시의 나는 충분히 고민했고, 내가 중요하다고 생각하는 기준으로 선택했다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 말할 수 있었으면 좋겠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;그렇다면 내 인생의 지침은 무엇일까?&lt;/h1&gt;
&lt;p&gt;이 질문에 대해서는 오랫동안 거창한 문장이 필요하다고 생각했던 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 반드시 무엇을 이루겠다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;혹은&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 이런 사람이 되겠다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;와 같이 명확하고 강한 문장이 있어야 삶의 지침이라고 생각했다.&lt;/p&gt;
&lt;p&gt;하지만 지금은 조금 다르게 생각한다.&lt;/p&gt;
&lt;p&gt;삶의 지침은 반드시 거대한 목표일 필요는 없다.&lt;/p&gt;
&lt;p&gt;오히려 중요한 순간에 선택의 방향을 알려주는 &lt;strong&gt;몇 가지 기준&lt;/strong&gt;에 가까울 수 있다.&lt;/p&gt;
&lt;p&gt;현재 내가 중요하다고 생각하는 기준을 정리하면 다음과 같다.&lt;/p&gt;
&lt;h3&gt;1. 내가 납득할 수 있는 선택을 하자&lt;/h3&gt;
&lt;p&gt;모든 사람을 만족시키는 선택은 어렵다.&lt;/p&gt;
&lt;p&gt;누군가는 좋은 선택이라고 말하고 누군가는 이해하지 못할 수도 있다.&lt;/p&gt;
&lt;p&gt;그래서 다른 사람이 어떻게 평가할지를 생각하기 전에&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 이 선택을 스스로 설명할 수 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 생각하고 싶다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 비교보다는 어제의 나를 기준으로 보자&lt;/h3&gt;
&lt;p&gt;비교 자체를 완전히 하지 않는 것은 어렵다.&lt;/p&gt;
&lt;p&gt;하지만 비교의 결과를 나에 대한 최종 평가로 받아들이지는 않으려고 한다.&lt;/p&gt;
&lt;p&gt;누군가 나보다 앞서 있다고 해서 내가 잘못 살아가고 있다는 뜻은 아니다.&lt;/p&gt;
&lt;p&gt;사람마다 출발점도 다르고 원하는 방향도 다르다.&lt;/p&gt;
&lt;p&gt;그래서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 남들보다 얼마나 앞에 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;보다는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 이전보다 무엇을 알게 되었고 무엇을 할 수 있게 되었는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 보는 것이 더 나에게 맞는 기준이라고 생각한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. 결과와 나 자신을 분리하자&lt;/h3&gt;
&lt;p&gt;실패한 일이 있다고 해서 실패한 사람이 되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;어떤 결과가 좋지 않았다면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이 방법은 잘되지 않았다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각할 수 있다.&lt;/p&gt;
&lt;p&gt;그런데 우리는 종종 그것을&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 능력이 없다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 자기평가로 확장한다.&lt;/p&gt;
&lt;p&gt;나 역시 어떤 결과가 좋지 않을 때 결과와 나 자신을 쉽게 연결하는 경우가 있다.&lt;/p&gt;
&lt;p&gt;그래서 앞으로는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이번 결과는 나에 대해 무엇을 알려주는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하려 한다.&lt;/p&gt;
&lt;p&gt;결과는 나를 판단하는 판결문이라기보다 다음 선택을 위한 정보에 가깝다고 보고 싶다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4. 계속 질문할 수 있는 사람이 되자&lt;/h3&gt;
&lt;p&gt;지금 가지고 있는 생각이 항상 옳을 것이라고 생각하지 않는다.&lt;/p&gt;
&lt;p&gt;지금 중요하다고 생각하는 가치도 시간이 지나면 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 삶의 지침을 한 번 정한 뒤 평생 바꾸지 않는 것보다 중요한 것은 계속 질문하는 것이라고 생각한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 지금 무엇을 중요하게 생각하고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;왜 이것을 중요하게 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이 선택은 내가 원하는 것인가, 인정받고 싶은 것인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;지금 가고 있는 방향이 내가 원하는 방향과 맞는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 질문들을 가끔씩 다시 꺼내볼 수 있다면 적어도 아무 생각 없이 다른 사람의 기준을 따라가지는 않을 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;내 삶의 지침은 명확한가?&lt;/h1&gt;
&lt;p&gt;그렇다면 나는 명확한 삶의 지침을 가지고 있는가?&lt;/p&gt;
&lt;p&gt;지금의 답은&lt;/p&gt;
&lt;p&gt;&lt;code&gt;완전히 명확하지는 않다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;에 가깝다.&lt;/p&gt;
&lt;p&gt;예전에는 이것이 부족함이라고 생각했다.&lt;/p&gt;
&lt;p&gt;자신의 길을 정확하게 알고 있는 사람은 강한 사람이고, 계속 고민하는 사람은 아직 방향을 찾지 못한 사람이라고 생각하기도 했다.&lt;/p&gt;
&lt;p&gt;하지만 지금은 삶의 지침이 반드시 처음부터 완성되어 있어야 하는 것은 아니라고 생각한다.&lt;/p&gt;
&lt;p&gt;삶을 살아가면서 경험하는 것들이 달라지고, 중요하게 생각하는 가치도 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 삶의 지침 역시 조금씩 수정될 수 있다.&lt;/p&gt;
&lt;p&gt;중요한 것은 완벽한 지침을 가지고 있는가가 아니라&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 어떤 기준으로 선택하고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 스스로 알고 있는 것이다.&lt;/p&gt;
&lt;p&gt;생각해보면 나는 이미 몇 가지 기준을 가지고 있었다.&lt;/p&gt;
&lt;p&gt;배우고 성장하는 것을 중요하게 생각하고,&lt;br&gt;내가 선택한 일에는 책임을 지려고 하고,&lt;br&gt;다른 사람의 기준만을 따라가기보다는 스스로 이해하고 납득하려고 한다.&lt;/p&gt;
&lt;p&gt;명문화하지 않았을 뿐 반복해서 선택해온 방향이 있었다.&lt;/p&gt;
&lt;p&gt;어쩌면 삶의 지침은 새롭게 만드는 것이 아니라 지금까지 내가 해온 선택들 속에서 발견하는 것일 수도 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;나에게 적용해보기&lt;/h1&gt;
&lt;p&gt;이번 생각을 정리하면서 앞으로 가끔 다음 질문들을 스스로에게 던져보려고 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;요즘 나는 누구의 기준으로 나를 평가하고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;최근 가장 신경 쓰이는 타인의 평가는 무엇인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;아무도 나를 평가하지 않는다면 나는 어떤 선택을 할 것인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 지금 원하는 것은 정말 내가 원하는 것인가, 아니면 남들에게 인정받고 싶은 것인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;최근의 선택들을 보면 나는 무엇을 중요하게 생각하는 사람인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그리고 마지막으로&lt;/p&gt;
&lt;p&gt;&lt;code&gt;지금 내가 살고 있는 방식은 내가 되고 싶은 사람의 모습과 가까워지고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 질문이 현재의 나에게는 가장 중요한 질문인 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;내가 느낀 점&lt;/h1&gt;
&lt;p&gt;자아정체성에 대해 생각하면서 가장 크게 느낀 것은 &lt;strong&gt;나를 바라보는 시선이라고 해서 모두 나에게서 나온 것은 아니라는 점&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;내가 당연하다고 생각했던 기준 중에는 사회에서 자연스럽게 받아들인 것도 있고, 주변 사람의 기대에서 만들어진 것도 있고, 다른 사람과의 비교를 통해 생긴 것도 있었다.&lt;/p&gt;
&lt;p&gt;그 기준들이 무조건 잘못된 것은 아니다.&lt;/p&gt;
&lt;p&gt;다만 그것을 내 생각이라고 믿기 전에 한 번쯤 질문해볼 필요가 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 왜 이것을 중요하다고 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그 질문을 반복하다 보면 외부에서 만들어진 기준과 내가 정말 중요하게 생각하는 기준을 조금씩 구분할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;그리고 그 과정에서 자아정체성도 조금씩 선명해지는 것이 아닐까 생각한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;오늘의 결론&lt;/h1&gt;
&lt;p&gt;지금의 나는 외부적 인식과 내부적 인식 중 어느 하나에만 영향을 받는 사람은 아니다.&lt;/p&gt;
&lt;p&gt;다른 사람의 평가에 흔들리기도 하고,&lt;br&gt;비교하면서 불안해지기도 하며,&lt;br&gt;인정받았을 때 자신감을 얻기도 한다.&lt;/p&gt;
&lt;p&gt;그런 의미에서 나는 분명 외부적 인식의 영향을 받고 있다.&lt;/p&gt;
&lt;p&gt;하지만 중요한 것은 외부의 영향을 받지 않는 사람이 되는 것이 아니라, &lt;strong&gt;외부의 평가를 그대로 나의 정체성으로 받아들이지 않는 것&lt;/strong&gt;이라고 생각한다.&lt;/p&gt;
&lt;p&gt;다른 사람의 생각은 참고할 수 있다.&lt;/p&gt;
&lt;p&gt;사회의 기준도 참고할 수 있다.&lt;/p&gt;
&lt;p&gt;그러나 마지막에는 내가 질문해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그래서 나는 어떻게 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그리고 현재 내가 생각하는 삶의 지침을 한 문장으로 정리한다면 이렇게 표현할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;다른 사람의 시선을 참고하되, 내가 납득할 수 있는 방향으로 살아가자.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;어쩌면 자아정체성이란&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 이런 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 완성된 답을 찾는 것이 아니라,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 어떤 기준으로 생각하고 선택하며 살아가고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 질문에 계속 답해가는 과정인지도 모르겠다.&lt;/p&gt;</description>
      <category>etc/자아의 바다</category>
      <category>내부적 인식</category>
      <category>외부적 인식</category>
      <category>자아정체성</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/91</guid>
      <comments>https://slowgrid.tistory.com/entry/Through-Whose-Eyes-Am-I-Looking-at-Myself-Self-Identity-External-Perception-and-Internal-Perception#entry91comment</comments>
      <pubDate>Tue, 22 Sep 2026 13:07:41 +0900</pubDate>
    </item>
    <item>
      <title>온프레미스 환경 구축의 전체 과정과 클라우드가 필요해지는 이유</title>
      <link>https://slowgrid.tistory.com/entry/The-entire-process-of-building-an-on-premises-environment-and-why-the-cloud-becomes-necessary</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;온프레미스 구축은 서버를 설치하는 일이 아니라, 서비스를 계속 제공하기 위해 필요한 시설, 장비, 소프트웨어, 보안, 복구, 운영 체계를 직접 갖추는 일이다. 클라우드의 필요성은 이 책임이 사라져서가 아니라, 그중 일부를 서비스로 구매함으로써 변화에 대응하는 속도와 운영의 효율을 높일 수 있다는 데서 출발한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;목차&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#part1&quot;&gt;온프레미스의 의미와 책임 범위&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part2&quot;&gt;전체 구축 흐름&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part3&quot;&gt;온프레미스 구축: 12단계 상세 설명&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part4&quot;&gt;구축 이후의 운영과 구조적 부담&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part5&quot;&gt;이 부담이 클라우드의 필요성으로 이어지는 과정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part6&quot;&gt;클라우드의 한계와 환경 선택 기준&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part7&quot;&gt;현실적인 전환 순서와 최종 점검표&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part8&quot;&gt;결론&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a id=&quot;part1&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1. 온프레미스의 의미와 책임 범위&lt;/h2&gt;
&lt;h3&gt;1.1. 온프레미스는 무엇인가&lt;/h3&gt;
&lt;p&gt;온프레미스(On-premises)는 조직이 자신의 시설에 서버, 저장장치, 네트워크 장비 등을 설치하고 직접 통제하는 방식이다. 예를 들어 회사 전산실에 서버를 두고 사내 업무 시스템이나 고객용 서비스를 운영하는 경우가 여기에 해당한다.&lt;/p&gt;
&lt;p&gt;회사가 소유한 장비를 외부 인터넷 데이터센터(IDC)의 임대 공간에 설치하는 &lt;strong&gt;콜로케이션(Colocation)&lt;/strong&gt;은 엄밀히는 사내 온프레미스와 설치 위치가 다르다. 다만 장비 선정과 운영의 상당 부분을 회사가 책임진다는 공통점이 있다. 이 문서는 두 방식을 함께 검토하되, 전력/냉각/출입 통제를 누가 제공하는지는 계약과 운영 범위에 따라 구분한다.&lt;/p&gt;
&lt;p&gt;온프레미스에서는 물리적 기반부터 운영체제와 애플리케이션까지 조직이 전체 계층에 대한 책임을 진다. 외부 업체에 일부 작업을 맡기더라도, 요구사항을 정하고 제공 수준을 확인하는 책임까지 자동으로 없어지는 것은 아니다. 클라우드에서는 서비스 유형에 따라 이 책임 중 일부가 제공자에게 넘어간다.[^shared]&lt;/p&gt;
&lt;h3&gt;1.2. 서버 외에 무엇을 준비해야 하는가&lt;/h3&gt;
&lt;p&gt;사용자는 웹 화면 하나를 보지만, 그 화면을 지속적으로 제공하려면 여러 계층이 맞물려야 한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;계층&lt;/th&gt;
&lt;th&gt;준비해야 할 대상&lt;/th&gt;
&lt;th&gt;운영 중 확인할 질문&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;시설&lt;/td&gt;
&lt;td&gt;공간, 전력, 냉각, 출입 통제, 환경 감시&lt;/td&gt;
&lt;td&gt;정전이나 냉각 장애가 발생하면 어떻게 되는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;하드웨어&lt;/td&gt;
&lt;td&gt;서버, 디스크, 저장장치, 스위치, 방화벽&lt;/td&gt;
&lt;td&gt;장비가 고장 나면 무엇으로 대체하는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기반 소프트웨어&lt;/td&gt;
&lt;td&gt;운영체제, 가상화, DNS, 시간 동기화, 인증&lt;/td&gt;
&lt;td&gt;업데이트와 장애 복구를 누가 담당하는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서비스&lt;/td&gt;
&lt;td&gt;데이터베이스, 웹 서버, 애플리케이션, 배포&lt;/td&gt;
&lt;td&gt;사용자가 정상적으로 업무를 완료할 수 있는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 및 통제&lt;/td&gt;
&lt;td&gt;모니터링, 백업, 권한 관리, 변경 관리&lt;/td&gt;
&lt;td&gt;누가 문제를 발견하고 어떤 절차로 해결하는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이 표는 구축 범위를 빠뜨리지 않기 위한 분류이며, 모든 회사가 모든 장비를 따로 구매해야 한다는 뜻은 아니다. 규모가 작다면 여러 역할을 통합할 수 있지만, 통합했을 때 함께 멈추는 범위까지 이해해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lpvD2/dJMcacdDhxu/3ams58lnMmMKWpOYKPw3n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lpvD2/dJMcacdDhxu/3ams58lnMmMKWpOYKPw3n0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lpvD2/dJMcacdDhxu/3ams58lnMmMKWpOYKPw3n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlpvD2%2FdJMcacdDhxu%2F3ams58lnMmMKWpOYKPw3n0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 1. 웹 화면 하나를 제공하기 위해 온프레미스 조직이 책임지는 계층&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;1.3. 온프레미스와 클라우드는 완전한 반대말이 아니다&lt;/h3&gt;
&lt;p&gt;온프레미스는 주로 &lt;strong&gt;어디에 두고 누가 통제하는가&lt;/strong&gt;에 관한 개념이고, 클라우드는 &lt;strong&gt;자원을 어떻게 제공하고 소비하는가&lt;/strong&gt;에 관한 운영 모델이다. NIST는 클라우드를 공유 자원에 필요할 때 접근하고, 적은 관리 개입으로 자원을 신속하게 할당 및 반납할 수 있는 모델로 설명한다.[^nist-cloud]&lt;/p&gt;
&lt;p&gt;따라서 회사 안에 장비를 설치해도 셀프서비스, 자원 풀링, 자동화 등을 갖춘 프라이빗 클라우드를 운영할 수 있다. 반대로 가상머신 몇 대를 운영한다고 해서 곧바로 클라우드가 되는 것은 아니다. NIST의 프라이빗 클라우드 정의도 조직 내부 또는 외부에 존재할 수 있음을 명시한다.[^private]&lt;/p&gt;
&lt;p&gt;이 문서에서 뒤에 다루는 ‘클라우드 도입’은 주로 &lt;strong&gt;퍼블릭 클라우드와 관리형 서비스의 활용&lt;/strong&gt;을 의미한다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a id=&quot;part2&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;2. 전체 구축 흐름&lt;/h2&gt;
&lt;p&gt;전체 과정은 다음처럼 연결된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;업무 요구사항 → 구조 설계 → 용량 및 예산 산정 → 시설 준비 → 장비 조달 및 설치 → 네트워크 → 서버/가상화 → 데이터/서비스 → 보안 → 백업 및 복구 → 배포 및 관측 → 검증 및 운영 이관&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;실제 프로젝트에서는 이 작업들이 일부 병행된다. 특히 보안, 백업, 모니터링은 마지막에 덧붙이는 기능이 아니라 초기 설계부터 반영해야 한다. 아래의 순서는 설명을 위한 순서이지, 앞 단계가 끝날 때까지 뒤 단계의 검토를 미루라는 의미가 아니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dUmGsF/dJMcab6UZZT/5iV3zjY1FlCV67wbtEn0x0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dUmGsF/dJMcab6UZZT/5iV3zjY1FlCV67wbtEn0x0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dUmGsF/dJMcab6UZZT/5iV3zjY1FlCV67wbtEn0x0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdUmGsF%2FdJMcab6UZZT%2F5iV3zjY1FlCV67wbtEn0x0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 2. 12단계 구축 흐름과 처음부터 병행되는 보안·백업·모니터링&lt;/em&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;핵심 질문&lt;/th&gt;
&lt;th&gt;대표 산출물&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1. 요구사항 정의&lt;/td&gt;
&lt;td&gt;무엇을 어느 수준으로 제공해야 하는가?&lt;/td&gt;
&lt;td&gt;요구사항 정의서, 서비스 및 복구 목표&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. 아키텍처 설계&lt;/td&gt;
&lt;td&gt;무엇을 연결하고 어디를 분리할 것인가?&lt;/td&gt;
&lt;td&gt;논리/물리 구성도, 의존성 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. 용량 및 비용 산정&lt;/td&gt;
&lt;td&gt;어느 정도 자원과 예산이 필요한가?&lt;/td&gt;
&lt;td&gt;용량 산정서, 총소유비용 비교표&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. 시설 준비&lt;/td&gt;
&lt;td&gt;장비를 안전하게 계속 가동할 수 있는가?&lt;/td&gt;
&lt;td&gt;전력/냉각/랙 배치 계획&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. 조달 및 물리 설치&lt;/td&gt;
&lt;td&gt;설계한 장비를 확보하고 검수했는가?&lt;/td&gt;
&lt;td&gt;장비 명세서, 자산 목록, 검수 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. 네트워크 구축&lt;/td&gt;
&lt;td&gt;필요한 통신만 안정적으로 가능한가?&lt;/td&gt;
&lt;td&gt;주소 계획, 통신 정책, 연결 시험 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. 서버/실행 환경&lt;/td&gt;
&lt;td&gt;일관되고 복구 가능한 실행 기반인가?&lt;/td&gt;
&lt;td&gt;표준 이미지, 설정 기준, 클러스터 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. 데이터/서비스&lt;/td&gt;
&lt;td&gt;데이터가 안전하고 업무가 정상 동작하는가?&lt;/td&gt;
&lt;td&gt;데이터베이스 및 애플리케이션 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9. 보안 체계&lt;/td&gt;
&lt;td&gt;누가 무엇에 접근하고 어떻게 추적하는가?&lt;/td&gt;
&lt;td&gt;권한표, 보안 기준, 감사 체계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10. 백업 및 재해복구&lt;/td&gt;
&lt;td&gt;잃거나 멈췄을 때 목표대로 복구하는가?&lt;/td&gt;
&lt;td&gt;백업 정책, 복구 절차, 복원 시험 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11. 배포 및 관측&lt;/td&gt;
&lt;td&gt;변경을 반복하고 문제를 발견할 수 있는가?&lt;/td&gt;
&lt;td&gt;배포 파이프라인, 대시보드, 경보 규칙&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12. 검증 및 운영 이관&lt;/td&gt;
&lt;td&gt;담당자가 바뀌어도 운영 가능한가?&lt;/td&gt;
&lt;td&gt;인수시험 보고서, 운영 매뉴얼, 책임표&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a id=&quot;part3&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;3. 온프레미스 구축: 12단계 상세 설명&lt;/h2&gt;
&lt;h3&gt;3.1. 1단계 — 업무 요구사항과 서비스 목표를 정의&lt;/h3&gt;
&lt;h4&gt;장비 사양보다 업무의 성격을 먼저 정한다&lt;/h4&gt;
&lt;p&gt;첫 질문은 “서버를 몇 대 살 것인가?”가 아니라 “어떤 업무를 어느 수준으로 지원해야 하는가?”이다. 내부 결재 시스템, 고객 주문 서비스, 공장 제어 시스템은 모두 서버를 사용하지만 허용할 수 있는 지연과 장애의 영향은 다르다.&lt;/p&gt;
&lt;p&gt;담당 부서와 함께 사용자 수, 요청량, 업무 시간, 데이터 증가량, 외부 연동, 장애 시 손실과 복구 우선순위를 정리한다. 기존 서비스라면 실제 사용량과 장애 기록을 확인하고, 신규 서비스라면 가정을 세운 뒤 시제품과 부하 시험으로 보정한다.&lt;/p&gt;
&lt;p&gt;설명을 위해 다음과 같은 고객용 업무 포털을 가정하자.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;예시 가정&lt;/th&gt;
&lt;th&gt;설계에 미치는 영향&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;서비스 시간&lt;/td&gt;
&lt;td&gt;상시 이용&lt;/td&gt;
&lt;td&gt;야간 경보와 대응 체계 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;트래픽&lt;/td&gt;
&lt;td&gt;평상시 초당 100건, 행사 시 초당 600건&lt;/td&gt;
&lt;td&gt;최대 부하와 확장 방식 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;초기 2TB, 매월 0.1TB 증가&lt;/td&gt;
&lt;td&gt;운영 및 백업 저장공간 계획&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;응답 목표&lt;/td&gt;
&lt;td&gt;정해진 핵심 요청의 95%가 300ms 이내&lt;/td&gt;
&lt;td&gt;실제 요청 조합으로 성능 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복구 목표&lt;/td&gt;
&lt;td&gt;RTO 1시간, RPO 15분&lt;/td&gt;
&lt;td&gt;복구 방식과 데이터 보호 주기 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;환경 구분&lt;/td&gt;
&lt;td&gt;개발/검증/운영 분리&lt;/td&gt;
&lt;td&gt;별도 자원과 접근 정책 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;여기서 p95 응답시간은 측정 대상 요청의 95%가 해당 시간 안에 처리된다는 의미이며, 측정 지점이 사용자 단말인지, 서버 입구인지도 명시해야 한다. 또한 동시접속자 수만으로 서버 사양을 정할 수는 없다. 같은 사용자 수라도 조회와 파일 업로드, 복잡한 검색은 자원 소비가 다르기 때문이다.&lt;/p&gt;
&lt;h4&gt;RTO와 RPO를 구분한다&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;RTO(복구시간목표)&lt;/strong&gt;는 장애 후 복구를 완료해야 하는 목표 시간이고, &lt;strong&gt;RPO(복구시점목표)&lt;/strong&gt;는 데이터를 어느 시점까지 되돌릴 수 있어야 하는지를 정하며, 실무에서는 허용 가능한 데이터 손실의 시간 범위로 표현한다.[^rto][^rpo]&lt;/p&gt;
&lt;p&gt;예시의 RTO 1시간은 정해진 장애 상황에서 1시간 안에 업무를 재개해야 한다는 뜻이고, RPO 15분은 장애 직전 데이터 중 최대 15분 범위의 손실을 허용한다는 뜻이다. 다만 단순히 백업을 15분마다 예약했다고 RPO가 보장되지는 않는다. 백업 성공 여부, 완료 시점, 원격지 전송 지연까지 확인해야 한다.&lt;/p&gt;
&lt;p&gt;서비스 가용성 목표와 RTO도 같은 지표가 아니다. 가용성은 정해진 기간의 서비스 제공 수준을, RTO는 특정 사고의 복구 목표를 다룬다. 두 목표가 충돌하지 않는지 함께 검토한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yInRQ/dJMcajqaYhg/ABVDtihn2IVFk0Dnbwzeq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yInRQ/dJMcajqaYhg/ABVDtihn2IVFk0Dnbwzeq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yInRQ/dJMcajqaYhg/ABVDtihn2IVFk0Dnbwzeq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyInRQ%2FdJMcajqaYhg%2FABVDtihn2IVFk0Dnbwzeq1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 3. RPO는 장애 이전, RTO는 장애 이후를 본다 — RTO에 포함되는 복구 과정&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 업무 담당자와 운영 담당자가 성능·가용성·복구 목표, 예산 상한과 책임 범위를 합의한다. 이 합의가 없으면 ‘과도한 구축’인지 ‘부족한 구축’인지 판단할 기준도 없다.&lt;/p&gt;
&lt;h3&gt;3.2. 2단계 — 전체 아키텍처와 장애 범위를 설계&lt;/h3&gt;
&lt;h4&gt;논리 구조와 물리 구조를 나눠 그린다&lt;/h4&gt;
&lt;p&gt;논리 구조에서는 웹 요청을 받는 계층, 업무를 처리하는 애플리케이션 계층, 데이터를 보관하는 계층, 관리 기능을 구분한다. 예시 서비스의 요청 경로는 &lt;strong&gt;사용자 → 접근 통제·부하 분산 → 애플리케이션 → 데이터베이스&lt;/strong&gt;로 설명할 수 있다.&lt;/p&gt;
&lt;p&gt;물리 구조에서는 각 역할을 어느 서버와 랙에 배치하고, 어느 스위치와 전원에 연결할지 정한다. 논리적으로 서버가 두 대여도 같은 물리 호스트 위의 가상머신이라면 호스트 장애에 함께 영향을 받는다.&lt;/p&gt;
&lt;p&gt;그러나 반드시 전용 장비를 계층마다 한 대씩 두어야 하는 것은 아니다. 중요한 것은 역할, 신뢰 경계, 자원 경쟁과 장애 영향을 명확히 구분하는 것이다.&lt;/p&gt;
&lt;h4&gt;‘장비 두 대’가 아니라 ‘공통 장애 지점’을 확인한다&lt;/h4&gt;
&lt;p&gt;고가용성(HA)은 일부 구성요소의 장애가 전체 서비스 중단으로 이어지지 않도록 설계하는 접근이다. 핵심 요청 경로의 의존성과 장애 시 우회 가능성을 확인해야 하며, 이중화 수준은 비용/복잡도/복구 목표와 함께 결정해야 한다.[^redundancy]&lt;/p&gt;
&lt;p&gt;예를 들어 애플리케이션을 두 대로 늘렸어도 데이터베이스가 한 대라면 데이터베이스 장애가 전체 서비스를 멈출 수 있으며, 서버 전원장치가 두 개여도 같은 전원 경로에 연결했다면 그 경로의 장애에는 취약하고, 인터넷 회선이 두 개여도 상위 경로나 건물 인입 구간을 공유하는지 검토해야 한다.&lt;/p&gt;
&lt;p&gt;이처럼 함께 영향을 받는 범위를 &lt;strong&gt;장애 도메인&lt;/strong&gt;이라고 부른다. 설계 검토에서는 서버, 저장장치, 랙, 전원, 네트워크 경로, 건물 순서로 실패 상황을 가정해 본다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9qsNq/dJMcajqaYhj/o3Dr1RXnTsAoP43F1lQyrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9qsNq/dJMcajqaYhj/o3Dr1RXnTsAoP43F1lQyrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9qsNq/dJMcajqaYhj/o3Dr1RXnTsAoP43F1lQyrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9qsNq%2FdJMcajqaYhj%2Fo3Dr1RXnTsAoP43F1lQyrK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 4. 논리적으로는 이중화처럼 보여도 물리적으로 함께 멈추는 장애 도메인&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 주요 구성요소별로 “고장 나면 무엇이 멈추고, 어떻게 전환하며, 남은 자원으로 얼마의 부하를 처리하는가?”에 답할 수 있어야하며, 이때 복구 가능한 범위와 감수하기로 한 단일 장애 지점도 기록한다.&lt;/p&gt;
&lt;h3&gt;3.3. 3단계 — 용량과 총소유비용을 산정&lt;/h3&gt;
&lt;h4&gt;평균 사용량이 아니라 최대 부하와 장애 상황을 함께 본다&lt;/h4&gt;
&lt;p&gt;CPU, 메모리, 저장공간, 디스크 처리 성능, 네트워크 대역폭을 각각 산정한다. CPU 여유가 충분해도 데이터베이스의 디스크 지연이 크면 서비스는 느릴 수 있기에, 정상 상태뿐 아니라 백업, 배치 작업, 장비 교체와 장애 복구 중의 부하도 고려한다.&lt;/p&gt;
&lt;p&gt;다음은 단순한 계산 예시이다. 동일한 요청 조합에서 애플리케이션 인스턴스 하나가 목표 응답시간을 지키며 초당 250건을 처리한다고 실측했고, 최대 부하가 초당 600건이라고 하자. 인스턴스 하나의 장애까지 견디려면 다음 조건이 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;(N - 1) × 250 ≥ 600
N ≥ 4&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 계산은 인스턴스가 동일한 처리 능력을 가지며 요청을 분산할 수 있다는 가정에 따른다. 네 개의 인스턴스가 물리 서버 두 대에 두 개씩 배치되면, 물리 서버 한 대의 장애에는 두 개가 동시에 사라지므로 같은 결과를 적용할 수 없다. 실제 산정에서는 데이터베이스 병목과 공유 저장장치, 물리 호스트 장애까지 따로 검증해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dx2xzV/dJMcahFZyA9/1Yxuda4mS8Yeswzz2xkRKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dx2xzV/dJMcahFZyA9/1Yxuda4mS8Yeswzz2xkRKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dx2xzV/dJMcahFZyA9/1Yxuda4mS8Yeswzz2xkRKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdx2xzV%2FdJMcahFZyA9%2F1Yxuda4mS8Yeswzz2xkRKk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 5. 같은 N=4라도 장애 단위에 따라 최대 부하 충족 여부가 달라진다&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;저장공간은 원본 데이터 크기보다 넓게 본다&lt;/h4&gt;
&lt;p&gt;앞선 예시에서 36개월의 데이터 증가와 예상 데이터 대비 25%의 운영 여유를 단순 반영하면 다음과 같다. 단위는 십진 TB이며 압축 및 중복 제거 효과는 가정하지 않았다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;예상 데이터 = 2TB + (0.1TB × 36개월) = 5.6TB
운영 여유를 포함한 논리 용량 = 5.6TB × 1.25 = 7TB&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 7TB가 곧 구매해야 할 디스크 총용량은 아니다. 복제 및 패리티 등 데이터 보호에 쓰이는 용량, 별도로 산정한 로그와 임시 공간, 스냅샷, 백업 보존공간을 추가해야 한다. 데이터 크기를 셀 때 이미 포함한 항목을 다시 더하지 않도록 산정 기준도 명확히 한다.&lt;/p&gt;
&lt;h4&gt;구매비가 아니라 전체 비용을 비교한다&lt;/h4&gt;
&lt;p&gt;예산 검토에는 &lt;strong&gt;총소유비용(TCO)&lt;/strong&gt; 관점이 필요하다. 다음은 비교를 위한 항목 틀이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;온프레미스 비교 비용
= 장비 및 소프트웨어 도입비
+ 시설/회선/전력/냉각 비용
+ 유지보수/지원/운영 인건비
+ 보안 및 백업/재해복구 비용
+ 증설/교체/폐기 비용&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;초기 구매비와 감가상각비를 동일한 비교표에서 중복 합산해서는 안 되며, 현금 지출 기준인지 기간별 비용 배분 기준인지 먼저 정한다. 장애 위험과 출시 지연에 따른 기회비용은 불확실성이 크므로 기본 비용표와 구분해 시나리오로 검토한다.&lt;/p&gt;
&lt;p&gt;클라우드와 비교할 때도 동일한 성능, 가용성, 백업, 지원 수준을 맞춰야 하며, 이중화된 온프레미스와 단일 클라우드 서버의 비용을 비교하면 의미가 왜곡된다. AWS의 비용 최적화 지침도 온프레미스의 미래 수요 예측과 조달 복잡성을 주요 고려사항으로 다룬다.[^cost]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 사양의 근거가 측정값 또는 명시된 가정으로 남고, 사용량이 적거나 많아지는 경우의 비용과 증설 조건까지 설명할 수 있다.&lt;/p&gt;
&lt;h3&gt;3.4. 4단계 — 전산실 또는 데이터센터 공간을 준비&lt;/h3&gt;
&lt;p&gt;장비를 구매하기 전에 설치 공간의 전력과 냉각 용량을 확인한다. 데이터센터의 기반에는 전력 공급, UPS, 냉각, 환경 제어와 물리적 보안이 포함된다. 서버의 처리 성능과 별개로 이러한 기반이 서비스의 지속 가능성을 좌우한다.[^datacenter]&lt;/p&gt;
&lt;p&gt;다음은 현장 설계에서 확인할 항목의 예시이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;확인할 내용&lt;/th&gt;
&lt;th&gt;빠뜨렸을 때의 문제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;랙/공간&lt;/td&gt;
&lt;td&gt;장비 깊이, 하중, 정비 공간, 향후 증설 공간&lt;/td&gt;
&lt;td&gt;장비를 설치하거나 교체하지 못함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전력&lt;/td&gt;
&lt;td&gt;실제 부하, 공급 용량, 전원 경로, 접지&lt;/td&gt;
&lt;td&gt;차단기 작동이나 전원 장애로 중단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPS&lt;/td&gt;
&lt;td&gt;부하별 유지 시간, 배터리 상태, 종료 및 전환 절차&lt;/td&gt;
&lt;td&gt;순간 정전 또는 전환 중 서비스 중단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;냉각&lt;/td&gt;
&lt;td&gt;발열량, 공기 흐름, 고온 지점, 장애 대응&lt;/td&gt;
&lt;td&gt;과열과 성능 저하, 장비 정지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출입 통제&lt;/td&gt;
&lt;td&gt;접근 권한, 작업 승인, 출입 기록&lt;/td&gt;
&lt;td&gt;무단 접근과 변경 추적 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;환경 감시&lt;/td&gt;
&lt;td&gt;온습도, 누수, 연기 및 화재 감지와 경보&lt;/td&gt;
&lt;td&gt;시설 이상을 늦게 발견&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;UPS는 무제한으로 전력을 공급하는 장치가 아니기에, 실제 부하에서 확보할 수 있는 시간 안에 다른 전원으로 전환하거나 안전하게 종료할 수 있어야 한다. 따라서 장비 이중 전원 구성 역시 상위 전원 경로까지 검토해야 한다.&lt;/p&gt;
&lt;p&gt;콜로케이션을 이용한다면 시설 업무 일부는 IDC 사업자가 제공한다. 그렇더라도 계약 전력, 추가 전력 확보 가능성, 원격 작업 지원, 장애 통보, 장비 반입과 출입 절차를 확인해야 한다. 전기/냉각/소방 설계와 시공은 적격 시설 전문가의 검토를 거친다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 정상 운전뿐 아니라 전력 및 냉각 이상과 유지보수 작업 중의 대응 절차가 확인되어야 한다. 시설 기반이 부족하면 서버만 늘리는 증설은 불가능하기 때문이다.&lt;/p&gt;
&lt;h3&gt;3.5. 5단계 — 장비를 조달하고 물리적으로 설치&lt;/h3&gt;
&lt;h4&gt;사양뿐 아니라 호환성과 지원 조건을 확인한다&lt;/h4&gt;
&lt;p&gt;서버의 CPU와 메모리만으로 구매 명세를 완성할 수 없으며 저장장치, 네트워크 카드, 전원장치, 랙 레일, 케이블, 광모듈, 관리 기능과 필요한 소프트웨어까지 함께 확인해야 한다.&lt;/p&gt;
&lt;p&gt;특히 운영체제 및 가상화 소프트웨어와 하드웨어의 지원 조합, 펌웨어 호환성, 드라이버 지원을 검토한다. 라이선스는 제품에 따라 코어 수, 사용자 수, 인스턴스 수 등 기준이 다를 수 있으므로 구매 시점의 계약 조건을 확인해야 하며, 납기와 예비 부품 확보, 지원 종료 시점도 계획에 넣는다.&lt;/p&gt;
&lt;p&gt;유지보수 계약의 ‘응답 시간’과 ‘복구 시간’은 구분해서 읽어야 하며, 지원 접수가 빠르다는 것이 서비스가 같은 시간 안에 복구된다는 뜻은 아니다.&lt;/p&gt;
&lt;h4&gt;설치와 자산 등록을 함께 수행한다&lt;/h4&gt;
&lt;p&gt;장비가 도착하면 발주 명세와 수량을 대조하고 초기 불량을 확인한다. 이후 랙 장착, 전원 및 네트워크 연결, 케이블 라벨 부착, 자산번호 등록, 관리 접속 설정을 진행한다. 일련번호, 설치 위치, 보증 및 지원 기간과 담당자를 기록한다.&lt;/p&gt;
&lt;p&gt;운영망과 분리된 &lt;strong&gt;대역외 관리 경로&lt;/strong&gt;도 검토해야 하며, 이는 운영체제가 응답하지 않아도 서버 상태를 확인하거나 콘솔에 접근하기 위한 경로이기에, 강한 권한을 가지므로 일반 사용자망이나 인터넷에 무분별하게 노출해서는 안 된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 자산 목록만으로 장비의 위치, 연결 관계, 담당자, 지원 조건을 추적할 수 있고, 기본 진단과 재부팅 검사가 통과한다.&lt;/p&gt;
&lt;h3&gt;3.6. 6단계 — 네트워크와 공통 기반 서비스를 구축&lt;/h3&gt;
&lt;h4&gt;주소와 통신 경로를 먼저 정리한다&lt;/h4&gt;
&lt;p&gt;IP 주소, 서브넷, VLAN, 라우팅, DNS, 시간 동기화 정책을 정하며, 향후 지사나 클라우드와 연결할 가능성이 있다면 주소 대역의 중복도 미리 검토한다. 주소 체계를 잘못 정하면 이후 연결 작업에서 재주소화나 복잡한 우회 구성이 필요해질 수 있다.&lt;/p&gt;
&lt;p&gt;네트워크는 역할과 위험에 따라 분리한다. 다음은 예시이며, 실제 분리 수준은 규모와 보안 요구에 맞춘다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;배치 대상&lt;/th&gt;
&lt;th&gt;통신 정책의 방향&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;외부 접점&lt;/td&gt;
&lt;td&gt;공개 웹 진입점, 역방향 프록시, 부하 분산&lt;/td&gt;
&lt;td&gt;필요한 외부 요청만 허용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;애플리케이션&lt;/td&gt;
&lt;td&gt;업무 로직을 처리하는 서버&lt;/td&gt;
&lt;td&gt;지정된 진입점 및 데이터 계층과 통신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;데이터베이스와 핵심 저장소&lt;/td&gt;
&lt;td&gt;승인된 서비스 및 관리 경로로 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관리&lt;/td&gt;
&lt;td&gt;관리 콘솔, 배포, 장비 제어&lt;/td&gt;
&lt;td&gt;승인된 관리자와 관리 단말로 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;백업&lt;/td&gt;
&lt;td&gt;백업 서버와 저장소&lt;/td&gt;
&lt;td&gt;백업 및 복구에 필요한 경로로 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;VLAN을 나누는 것만으로 접근 통제가 완성되지는 않는다. 영역 사이에서 허용할 출발지/목적지/서비스를 정의하고 방화벽이나 적절한 접근 통제 정책으로 적용해야 한다. 내부망이라는 이유만으로 모든 접근을 신뢰해서는 안 된다는 원칙은 NIST의 제로 트러스트 설명과도 맞닿아 있다.[^zero]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGdFYs/dJMcab0fxYm/SDqcl4Nm7WDtSDJMTSLko0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGdFYs/dJMcab0fxYm/SDqcl4Nm7WDtSDJMTSLko0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGdFYs/dJMcab0fxYm/SDqcl4Nm7WDtSDJMTSLko0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGdFYs%2FdJMcab0fxYm%2FSDqcl4Nm7WDtSDJMTSLko0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 6. 역할별 네트워크 영역과 허용·관리·차단 통신 경로 예시&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;이름 해석과 시간 동기화도 운영 기반이다&lt;/h4&gt;
&lt;p&gt;DNS는 서비스 이름을 실제 접속 대상으로 연결하고, 시간 동기화는 인증과 로그 분석의 기준을 맞춘다. 애플리케이션 서버만 이중화하고 DNS나 인증 체계를 단일 장애 지점으로 남기지 않았는지 확인한다.&lt;/p&gt;
&lt;p&gt;관리자는 승인된 VPN이나 관리용 접속 게이트웨이 등을 통해 접근하도록 설계한다. 다만 VPN에 접속했다는 사실만으로 모든 시스템의 관리자 권한을 주지 않는 것이 중요하다. 연결 이후에도 사용자와 작업 대상에 맞는 권한을 적용한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 허용해야 할 통신은 성공하고 차단해야 할 통신은 실패하는지 모두 시험한다. 회선 및 스위치 장애 시 전환, 이름 해석, 시간 동기화, 관리 접속까지 검증해야 한다.&lt;/p&gt;
&lt;h3&gt;3.7. 7단계 — 서버 운영체제와 가상화/컨테이너 환경을 구성&lt;/h3&gt;
&lt;h4&gt;서버마다 수작업으로 다른 설정을 만들지 않는다&lt;/h4&gt;
&lt;p&gt;운영체제 종류와 지원 기간, 디스크 구성, 계정 정책, 보안 설정, 업데이트 방식과 모니터링 설정을 표준화한다. 서버의 이름, 역할, 담당자, 설치 버전과 네트워크 정보를 자산 목록에 연결한다.&lt;/p&gt;
&lt;p&gt;같은 역할의 서버를 같은 절차로 다시 만들 수 있도록 표준 이미지와 자동 설정을 준비하며, 이때 불필요한 서비스와 기본 계정은 정리하고, 비밀번호/개인 키/환경별 비밀정보를 공통 이미지에 넣지 않는다. 설치 후에는 재부팅해 서비스와 저장공간 연결이 정상적으로 복원되는지도 확인한다.&lt;/p&gt;
&lt;h4&gt;가상화는 자원 활용을 개선하지만 물리 용량을 늘리지는 않는다&lt;/h4&gt;
&lt;p&gt;가상화는 하이퍼바이저를 이용해 물리 자원을 나누고 여러 가상머신(VM)을 운영하는 방식이다. VM 생성/이동/복제와 환경 분리에 유용하며, 컨테이너는 일반적으로 호스트 운영체제의 커널을 공유하면서 애플리케이션을 격리한다는 차이가 있다.[^virtualization]&lt;/p&gt;
&lt;p&gt;가상화 환경에서는 CPU와 메모리를 얼마나 배분할지, 중요 VM을 서로 다른 물리 호스트에 둘지, 호스트 장애 시 VM을 어디에서 재기동할지 정한다. 호스트 한 대가 빠져도 중요 업무를 수용할 자원을 남기는 N+1 방식은 검토 가능한 설계 중 하나다. 다만 호스트별 용량이 다르거나 공유 저장장치가 병목이라면 서버 대수만으로 충족 여부를 판단할 수 없다.&lt;/p&gt;
&lt;p&gt;VM이 빠르게 만들어져도 물리 CPU, 메모리, 디스크와 전력은 이미 확보한 범위 안에 있다. &lt;strong&gt;가상화는 할당을 유연하게 만들지만 자원 자체를 무한하게 만들지는 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;컨테이너와 Kubernetes는 필요에 따라 선택한다&lt;/h4&gt;
&lt;p&gt;모든 온프레미스 환경에 Kubernetes가 필요한 것은 아니다. 서비스 수와 배포 빈도가 낮고 운영 인력이 적다면 단순한 VM 구성이 더 관리하기 쉬울 수 있다.&lt;/p&gt;
&lt;p&gt;Kubernetes를 선택한다면 워커 노드만이 아니라 제어 영역, API 접근, 인증서, 네트워크, 저장장치 연결과 클러스터 상태 저장소의 복구까지 계획해야 한다. 공식 문서도 운영 환경에서 가용성, 확장, 접근 관리와 제어 영역의 복구를 별도로 검토하도록 안내한다.[^k8s]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 동일한 서버를 재현할 수 있고, 호스트 / 노드 장애 및 유지보수 중에도 합의한 서비스 수준을 유지하는지 시험한다. 기술의 개수보다 운영팀이 감당할 수 있는 복잡도로 서버가 구성되는 것이 중요하다.&lt;/p&gt;
&lt;h3&gt;3.8. 8단계 — 저장장치, 데이터베이스와 애플리케이션을 배치&lt;/h3&gt;
&lt;h4&gt;데이터의 사용 방식에 맞게 저장소를 선택한다&lt;/h4&gt;
&lt;p&gt;데이터베이스, 공유 문서, 이미지/동영상, 로그는 접근 방식과 성능 요구가 다르다. 블록/파일/객체 저장 방식 중 업무와 소프트웨어가 지원하는 방식을 선택하고, 저장공간뿐 아니라 지연시간과 읽기 및 쓰기 처리량을 검증한다.&lt;/p&gt;
&lt;p&gt;디스크 장애에 대비하는 RAID 구성이나 스토리지 복제는 특정 장애에 대한 보호 수단이지만, 사용자의 실수로 지운 데이터나 잘못된 변경까지 이전 상태로 되돌려 주는 것은 아니다. 같은 저장장치에만 의존하는 스냅샷도 그 저장장치 전체의 손실에 대비한 독립 백업으로 간주해서는 안 된다.&lt;/p&gt;
&lt;p&gt;따라서 저장장치의 장애 보호, 과거 시점 복원, 별도 위치의 복구본 보관을 서로 다른 요구로 나눠 설계한다. 연속 복제 역시 잘못된 데이터 변경을 전달할 수 있으므로 시점별 백업을 대체한다고 가정하지 않는다.[^dr]&lt;/p&gt;
&lt;h4&gt;데이터베이스는 설치보다 지속 운영이 어렵다&lt;/h4&gt;
&lt;p&gt;데이터베이스를 설치한 뒤 문자 인코딩, 시간대, 접속 권한, 연결 수, 로그 보존과 암호화 구성을 정한다. 복제를 사용한다면 복제 지연, 장애 감지, 대기 서버 승격, 애플리케이션 접속 전환과 원래 구성으로의 복귀를 검토한다.&lt;/p&gt;
&lt;p&gt;주 서버와 대기 서버를 두었다고 자동 전환이 완성되는 것은 아니다. 네트워크가 끊겼을 때 양쪽이 모두 자신을 주 서버로 판단하는 상황을 방지해야 하며, 선택한 제품의 정족수/중재/격리 방식에 맞게 구성한다. 읽기 전용 복제본도 쓰기 부하를 자동으로 분산해 주는 장치는 아니다.&lt;/p&gt;
&lt;p&gt;여러 시스템이 연결된 업무라면 개별 데이터베이스의 복원뿐 아니라 주문/결제/재고 같은 업무 데이터의 일관성도 확인한다. 이것은 단일 서버가 켜졌는지와 다른 검증이다.&lt;/p&gt;
&lt;h4&gt;애플리케이션이 특정 서버에 과도하게 의존하지 않게 한다&lt;/h4&gt;
&lt;p&gt;한 서버의 로컬 디스크에만 업로드 파일이나 세션을 저장하면 요청을 다른 서버로 분산할 때 문제가 생길 수 있다. 서비스 특성에 따라 공유 저장소나 별도 상태 저장소를 활용하되, 그 저장소 역시 새로운 의존성이 된다는 점을 반영한다.&lt;/p&gt;
&lt;p&gt;데이터베이스 접속정보, 외부 서비스 주소, 인증정보와 같은 환경별 설정은 코드와 분리해 관리한다. 개발 및 검증 환경에 운영 데이터를 사용할 때는 필요한 비식별화와 접근 제한을 검토한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 정상 기능뿐 아니라 데이터 증가, 복제 지연, 서버 교체와 장애 전환 상황에서도 데이터 정합성과 업무 완료 여부를 확인한다.&lt;/p&gt;
&lt;h3&gt;3.9. 9단계 — 보안과 접근 통제를 운영 절차로 만들기&lt;/h3&gt;
&lt;h4&gt;방화벽 구매만으로 보안이 완성되지는 않는다&lt;/h4&gt;
&lt;p&gt;보안은 네트워크, 사용자, 장비, 애플리케이션과 데이터 전반에 걸친 운영 체계이다. 누구에게 어떤 자원에 대한 권한을 주고, 그 권한을 언제 회수하며, 변경을 어떻게 추적할지 정해야 한다. 내부에 있다는 이유 하나만으로 신뢰하지 않는다는 원칙이 출발점이다.[^zero]&lt;/p&gt;
&lt;p&gt;개인별 계정을 사용하고 관리자 권한은 필요한 업무 범위로 제한한다. 사람의 계정과 애플리케이션의 서비스 계정을 구분하고, 관리자 접속에는 다중요소 인증과 승인된 관리 경로를 적용한다. 퇴사/직무 변경/외주 계약 종료 시 권한을 회수하는 절차도 운영에 포함한다.&lt;/p&gt;
&lt;h4&gt;보호 대상과 증거를 함께 설계한다&lt;/h4&gt;
&lt;p&gt;전송 중인 데이터와 저장된 데이터의 암호화 범위를 정하고, 암호화 키를 어디에 보관하며 누가 복구할 수 있는지 결정한다. 데이터를 백업했어도 필요한 키를 잃으면 복원할 수 없으므로 키의 보관/교체/복구 절차를 별도로 다룬다.&lt;/p&gt;
&lt;p&gt;운영체제, 가상화, 데이터베이스, 네트워크 장비와 애플리케이션의 취약점 및 패치 책임자를 지정하며, 업데이트는 검증 환경에서 확인하고, 적용 순서와 실패 시 복구 절차를 마련한다. 단순히 모든 장비를 동시에 최신 버전으로 올리는 방식은 피한다.&lt;/p&gt;
&lt;p&gt;중요한 로그인, 관리자 작업, 권한 변경과 설정 변경 기록은 중앙에서 수집해야 하며 보존 기간과 조회 권한을 정하고, 로그에 비밀번호나 불필요한 개인정보가 남지 않도록 한다. 보안 경보의 수신자와 사고 발생 시 격리 및 조사 절차도 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 권한 부여 및 회수, 관리자 작업 추적, 패치, 비밀정보 관리와 사고 대응이 담당자 개인의 기억에 의존하지 않는다.&lt;/p&gt;
&lt;h3&gt;3.10. 10단계 — 백업과 재해복구를 실제 복원까지 설계&lt;/h3&gt;
&lt;h4&gt;고가용성, 백업, 재해복구는 서로 대체하지 않는다&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;주로 답하려는 질문&lt;/th&gt;
&lt;th&gt;대표 접근&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;고가용성(HA)&lt;/td&gt;
&lt;td&gt;일부 장비가 고장 나도 계속 서비스할 수 있는가?&lt;/td&gt;
&lt;td&gt;중복 구성, 상태 감지, 장애 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;백업&lt;/td&gt;
&lt;td&gt;손실 및 오류 이전의 데이터로 돌아갈 수 있는가?&lt;/td&gt;
&lt;td&gt;시점별 사본, 보존 정책, 복원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재해복구(DR)&lt;/td&gt;
&lt;td&gt;시설이나 시스템 전체를 잃어도 업무를 재개할 수 있는가?&lt;/td&gt;
&lt;td&gt;별도 위치, 복구 자원, 전환 및 복귀 절차&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이 표는 보호 목적을 구분하기 위한 것이다. 실제 솔루션의 기능은 겹칠 수 있지만, 한 기능을 갖췄다는 이유로 나머지 검토를 생략해서는 안 된다. 비상 및 복구 계획은 시스템의 업무 영향과 우선순위에 따라 수립해야 한다.[^contingency]&lt;/p&gt;
&lt;h4&gt;백업 정책을 데이터별로 정한다&lt;/h4&gt;
&lt;p&gt;백업 대상에는 데이터베이스와 사용자 파일뿐 아니라 애플리케이션 배포물, 설정, 네트워크 구성과 필요한 복구 정보도 포함한다. 데이터별 변경 속도, 허용 손실과 보존 필요에 맞춰 전체/증분/로그 백업 등의 방식을 선택한다.&lt;/p&gt;
&lt;p&gt;운영 환경과 동일한 사고에 함께 노출되지 않도록 별도 위치와 권한 경계를 마련하고, 필요한 경우 오프라인 사본이나 변경 및 삭제 제한 기능을 검토한다. NCSC는 백업 장치를 사용하지 않을 때 연결을 분리하고, 백업을 어떻게 복원하는지와 필요한 데이터가 들어 있는지를 확인하도록 안내한다.[^backup]&lt;/p&gt;
&lt;p&gt;데이터베이스의 백업은 해당 제품이 지원하는 일관된 방식으로 수행하며, 실행 중인 파일을 임의로 복사한 결과가 유효한 복구본이라고 단정하지 않는다. 백업 작업의 성공 알림과 실제 복원 가능성은 별개로 검증한다.&lt;/p&gt;
&lt;h4&gt;복구 시간에는 업무 재개까지의 전체 과정을 넣는다&lt;/h4&gt;
&lt;p&gt;다음은 복구 절차를 설계할 때 사용할 수 있는 시간 분해 예시이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;실제 업무 재개까지의 시간
= 장애 탐지 및 상황 판단
+ 복구 승인 및 착수
+ 네트워크/인증/실행 환경 준비
+ 데이터 복원과 필요한 변경분 반영
+ 애플리케이션 기동
+ 업무 검증과 사용자 접속 전환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;조직이 RTO를 측정하는 시작점과 종료점도 명시해야 한다. 백업을 복사하는 데 20분이 걸렸더라도 전체 업무를 정상화하는 데 2시간이 걸리면, 예시의 1시간 목표를 충족했다고 할 수 없다.&lt;/p&gt;
&lt;p&gt;재해복구 시험에서는 원래 전산실을 사용할 수 없고 담당자 일부가 부재한 상황도 가정해 보아야 하며, 대체 위치에서 DNS, 인증, 암호화 키, 라이선스, 외부 연동과 운영 매뉴얼에 접근할 수 있어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 실제 복원과 업무 검증을 수행하고, 달성한 복구시간/복구시점과 목표 간 차이를 기록한다. 별도 저장소가 존재한다는 사실만으로 완료 처리하지 않는다.&lt;/p&gt;
&lt;h3&gt;3.11. 11단계 — 배포 자동화와 모니터링을 구성&lt;/h3&gt;
&lt;h4&gt;변경 과정을 반복 가능하게 만든다&lt;/h4&gt;
&lt;p&gt;수동으로 서버에 접속해 파일을 복사하는 방식은 변경 이력과 재현성을 확보하기 어렵기에, 소스 변경, 빌드, 테스트, 승인, 배포와 결과 확인을 연결하는 지속적 통합 및 배포(CI/CD) 절차를 준비한다.&lt;/p&gt;
&lt;p&gt;특히, 배포 실패 시 이전 버전으로 되돌리는 방법도 정한다. 데이터베이스 구조 변경은 애플리케이션 버전만 되돌려 해결되지 않을 수 있으므로, 이전 및 새 버전이 공존할 수 있는 변경 순서를 검토한다.&lt;/p&gt;
&lt;p&gt;인프라 설정 역시 가능한 범위에서 코드로 관리한다. IaC(Infrastructure as Code)는 자원 정의를 버전 관리하고 반복적으로 적용하는 접근이며, 클라우드뿐 아니라 온프레미스 자원에도 적용할 수 있으며, Terraform 공식 문서도 두 환경의 자원을 코드로 관리하는 용도를 설명한다.[^iac]&lt;/p&gt;
&lt;p&gt;자동화가 잘못된 설정을 빠르게 확산시키지 않도록 변경 검토, 사전 검사, 단계적 적용을 함께 둔다. 자동화용 계정과 상태 파일에도 필요한 접근 통제를 적용한다.&lt;/p&gt;
&lt;h4&gt;서버 상태가 아니라 사용자 경험까지 관측한다&lt;/h4&gt;
&lt;p&gt;CPU와 메모리 사용률은 원인을 찾는 데 유용하지만 서비스가 정상이라는 증거는 아니다. Google SRE는 사용자 대상 시스템의 핵심 신호로 지연시간, 트래픽, 오류, 포화도를 제시한다.[^sre]&lt;/p&gt;
&lt;p&gt;이를 예시 서비스에 적용하면 요청량, 핵심 API 응답시간, 업무 실패율, 처리 대기열과 저장공간 증가 등을 함께 본다. 필요에 따라 메트릭, 로그와 요청 추적 정보를 연결하고 로그인/조회/처리 완료 같은 실제 사용자 경로를 점검한다.&lt;/p&gt;
&lt;p&gt;경보에는 담당자, 심각도, 예상 영향, 확인 방법과 대응 절차를 붙임과 동시에, 모든 일시적 변화에 경보를 발생시키기보다 사용자가 영향을 받거나 곧 영향을 받을 상황을 우선하도록 설계한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 장애나 배포 실패를 의도적으로 발생시켰을 때 경보가 전달되고, 담당자가 정해진 절차로 확인 및 대응할 수 있다.&lt;/p&gt;
&lt;h3&gt;3.12. 12단계 — 통합 시험을 수행하고 운영으로 이관&lt;/h3&gt;
&lt;h4&gt;정상 동작만 확인하고 바로 오픈하지 않는다&lt;/h4&gt;
&lt;p&gt;구축 완료 시험에는 기능, 성능, 장애 전환, 보안 통제와 복구 검증을 포함한다. 다음은 예시 인수시험이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시험&lt;/th&gt;
&lt;th&gt;수행할 상황&lt;/th&gt;
&lt;th&gt;합격 기준의 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;기능&lt;/td&gt;
&lt;td&gt;로그인부터 핵심 업무 완료까지 수행&lt;/td&gt;
&lt;td&gt;업무 결과와 데이터가 일치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능&lt;/td&gt;
&lt;td&gt;실제 요청 조합으로 정상 및 최대 부하 발생&lt;/td&gt;
&lt;td&gt;합의한 응답시간·오류율 충족&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장기 부하&lt;/td&gt;
&lt;td&gt;배치 및 백업과 서비스가 겹치는 상황 재현&lt;/td&gt;
&lt;td&gt;누수/포화/누적 지연이 허용 범위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장애 전환&lt;/td&gt;
&lt;td&gt;서버/노드/네트워크 경로 일부 중단&lt;/td&gt;
&lt;td&gt;영향과 복구가 목표 범위 안에 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 보호&lt;/td&gt;
&lt;td&gt;격리된 환경에 백업 복원&lt;/td&gt;
&lt;td&gt;데이터 일관성과 목표 복구시점 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안&lt;/td&gt;
&lt;td&gt;비인가 접근과 불필요한 통신 시도&lt;/td&gt;
&lt;td&gt;차단과 기록이 정상 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포 복구&lt;/td&gt;
&lt;td&gt;의도적으로 배포를 실패시킴&lt;/td&gt;
&lt;td&gt;사전에 정한 복구 절차로 정상화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;장애 시험은 사전에 승인된 범위에서 안전하게 수행하며, 운영 중인 시설의 전원이나 핵심 장비를 임의로 차단하는 식의 검증은 하지 않는다.&lt;/p&gt;
&lt;h4&gt;오픈은 변경 작업이며, 운영 이관은 별도 산출물이다&lt;/h4&gt;
&lt;p&gt;신규 시스템으로 전환할 때는 데이터 이관, 최종 동기화, 사용자 공지, 전환 시점과 복귀 조건을 정하며, 실패했을 때 원래 환경으로 되돌리는 동안 발생한 데이터를 어떻게 처리할지도 검토한다.&lt;/p&gt;
&lt;p&gt;운영팀에는 구성도, 자산 목록, 계정 및 권한 관리 절차, 백업 정책, 경보 설명, 장애 대응 절차와 업체 연락체계를 전달한다. 초기 안정화 기간의 담당자와 관측 항목도 지정한다. 관리 비밀번호를 일반 문서에 모아 전달하는 방식은 피한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;완료 기준:&lt;/strong&gt; 설계자가 자리를 비워도 운영팀이 상태를 파악하고 승인된 변경/장애 대응/복구를 수행할 수 있다. 서버가 켜졌다는 것은 설치 완료일 뿐, 운영 준비 완료와 같지 않다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a id=&quot;part4&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;4. 구축 이후의 운영과 구조적 부담&lt;/h2&gt;
&lt;h3&gt;4.1. 구축은 일회성이지만 운영 책임은 지속적이다&lt;/h3&gt;
&lt;p&gt;서비스를 시작하면 새로운 작업이 계속 발생한다. 계정이 추가 및 삭제되고, 인증서가 만료되며, 데이터가 늘고, 소프트웨어 지원 기간이 끝난다. 보안 업데이트, 장비 고장, 외부 연동 변경과 장애 대응도 이어진다.&lt;/p&gt;
&lt;p&gt;다음은 운영 계획을 만들 때 사용할 수 있는 주기별 분류이며, 실제 주기는 위험도와 서비스 목표에 맞춰 정한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시점&lt;/th&gt;
&lt;th&gt;주요 활동&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;상시 및 일상&lt;/td&gt;
&lt;td&gt;서비스 상태, 경보, 백업 실패, 보안 이상과 자원 부족 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정기&lt;/td&gt;
&lt;td&gt;용량 추세, 권한, 패치, 인증서, 비용/지원 계약 점검&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;변경 시&lt;/td&gt;
&lt;td&gt;영향 분석, 승인, 사전 시험, 적용, 결과 확인과 문서 갱신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주기적 훈련&lt;/td&gt;
&lt;td&gt;복원, 장애 전환, 재해복구와 운영 인수인계 점검&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;수명주기 종료 시&lt;/td&gt;
&lt;td&gt;장비 및 소프트웨어 교체, 데이터 이전 및 삭제, 자산 및 계약 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;따라서 운영 인력도 단순히 ‘서버를 관리할 사람 한 명’으로 계산해서는 안 되며 휴가, 야간, 동시 장애를 포함해 대응 가능한 체계를 설계해야 한다. 이는 온프레미스뿐 아니라 클라우드에서도 필요한 조직적 책임이다.&lt;/p&gt;
&lt;h3&gt;4.2. 수요가 변하면 구축 과정의 일부를 다시 수행한다&lt;/h3&gt;
&lt;p&gt;예시 서비스의 행사 트래픽이 예상보다 커졌다고 해보자. 애플리케이션 인스턴스만 늘리면 끝날 수도 있지만, 물리 자원이 부족하다면 다시 장비 구매로 되돌아가야 한다. 여기에 스위치 포트, 저장장치 처리량, 전력과 냉각 용량이 부족하면 기반 시설까지 다시 검토해야 한다.&lt;/p&gt;
&lt;p&gt;반대로 서비스 사용량이 줄어도 이미 구매한 장비의 지출을 사용량에 비례해 즉시 되돌릴 수는 없다. 다른 업무에 재배치할 수 있는지, 유지보수와 시설 비용을 얼마나 줄일 수 있는지를 따로 판단해야 한다.&lt;/p&gt;
&lt;p&gt;이 구조 때문에 온프레미스에서는 미래 수요를 예측해 여유 용량을 확보하는 선택과, 과잉 투자하지 않고 수요 증가 시 부족 위험을 감수하는 선택 사이의 균형이 중요해진다.[^cost]&lt;/p&gt;
&lt;h3&gt;4.3. 이중화와 복구 수준이 높아질수록 운영 범위도 넓어진다&lt;/h3&gt;
&lt;p&gt;중복 서버를 추가하면 장애 대응 능력을 높일 수 있지만 배포 대상과 점검 항목도 늘어나며, 별도 재해복구 거점을 만들면 저장공간뿐 아니라 네트워크, 보안, 복제, 전환과 복귀 절차까지 관리해야 한다.&lt;/p&gt;
&lt;p&gt;이것은 온프레미스가 기술적으로 부족하다는 뜻이 아니고, 직접 통제할 수 있는 범위가 넓은 만큼, 그 범위를 유지하기 위한 자원과 전문성도 필요하다는 뜻이다. 그렇기에 복잡도를 늘릴 때마다 실제로 줄어드는 위험과 추가 운영 부담을 함께 평가해야 한다.[^redundancy]&lt;/p&gt;
&lt;p&gt;결국 여기서 질문은 “이 장비를 더 살 수 있는가?”에서 &lt;strong&gt;“새로운 요구가 생길 때마다 이 전체 과정을 직접 반복하는 것이 조직에 유리한가?”&lt;/strong&gt;로 바뀐다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a id=&quot;part5&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;5. 이 부담이 클라우드의 필요성으로 이어지는 과정&lt;/h2&gt;
&lt;h3&gt;5.1. 조달과 설치의 문제를 자원 할당의 문제로 바꾸기&lt;/h3&gt;
&lt;p&gt;온프레미스에서 새 서비스의 기반을 마련하려면 여유 장비를 확인하고, 없으면 구매, 입고, 설치, 설정, 검증을 거쳐야 한다. 가상화와 내부 자동화로 빠르게 대응할 수 있더라도, 확보한 물리 용량을 모두 사용하면 다시 조달 과정으로 돌아간다.&lt;/p&gt;
&lt;p&gt;클라우드는 제공자가 운영하는 자원 풀을 네트워크를 통해 할당받는 모델이다. 필요한 자원을 신속하게 만들고 반납할 수 있다는 점은 NIST의 클라우드 정의에서도 핵심으로 다뤄진다.[^nist-cloud]&lt;/p&gt;
&lt;p&gt;이는 개발 및 검증 환경을 만들거나 새로운 서비스를 시험할 때 특히 의미가 있다. 조직이 작은 규모로 시작해 결과를 보고 확장하는 선택을 할 수 있기 때문이다. 다만 클라우드에서도 계정 승인, 보안 검토, 네트워크 설계와 애플리케이션 검증은 필요하다. 그래서 &lt;strong&gt;자원 생성이 빨라지는 것과 서비스 오픈의 모든 절차가 없어지는 것은 다르다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;5.2. 수요 예측의 불확실성을 탄력적 자원 사용으로 완화하기&lt;/h3&gt;
&lt;p&gt;예시의 서비스는 평상시 초당 100건, 행사 때 초당 600건의 요청을 처리한다. 최대 부하를 위해 충분한 자체 장비를 미리 확보하면 평상시에 남는 자원이 생길 수 있다. 반대로 평상시에 맞춰 구매하면 행사 시점에 부족해질 수 있다.&lt;/p&gt;
&lt;p&gt;클라우드에서는 요구에 따라 자원을 늘리고 줄이는 방식을 활용할 수 있다. 예를 들어 EC2 Auto Scaling은 설정한 최소/최대 범위와 정책에 따라 인스턴스를 증감한다. 이는 요청량 변동에 대응하는 한 가지 구현 사례이다.[^autoscale]&lt;/p&gt;
&lt;p&gt;그러나 서버 개수가 늘어난다고 애플리케이션의 전체 처리량이 항상 비례해서 늘어나는 것은 아니며 세션, 저장소, 데이터베이스, 외부 API 같은 공통 의존성이 확장을 제한할 수 있다. 확장 상한, 자원 할당량, 시작 및 예열 시간과 예산도 계획해야 한다.&lt;/p&gt;
&lt;p&gt;따라서 클라우드의 탄력성을 제대로 활용하려면 부하 분산, 상태 관리, 비동기 처리와 병목 제거를 함께 검토해야 한다. 온프레미스도 여유 자원 안에서는 자동 확장이 가능하지만, 클라우드는 자체 물리 증설을 직접 수행하지 않고 할당을 늘릴 수 있다는 차이가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOaqUb/dJMcabeBmu0/V7MPhJQQvHw2AdJ7FOR3C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOaqUb/dJMcabeBmu0/V7MPhJQQvHw2AdJ7FOR3C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOaqUb/dJMcabeBmu0/V7MPhJQQvHw2AdJ7FOR3C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOaqUb%2FdJMcabeBmu0%2FV7MPhJQQvHw2AdJ7FOR3C1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 7. 고정 용량의 유휴·부족 위험과 수요에 따른 탄력적 할당&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;5.3. 관리형 서비스로 직접 운영할 계층을 줄이기&lt;/h3&gt;
&lt;p&gt;온프레미스 데이터베이스를 운영하려면 하드웨어, 운영체제, 데이터베이스 엔진, 패치, 백업과 장애 대응을 함께 관리해야 하지만 관리형 데이터베이스를 사용하면 이 가운데 제공자가 수행하는 업무의 범위를 넓힐 수 있다.&lt;/p&gt;
&lt;p&gt;이 차이를 이해하려면 IaaS, PaaS, SaaS를 구분해야 한다. IaaS는 가상머신 등 기반 자원을 제공하고, PaaS는 애플리케이션 실행 및 개발 플랫폼을, SaaS는 완성된 소프트웨어를 제공한다. 서비스 모델에 따라 제공자에게 이전되는 운영 책임이 달라진다.[^shared]&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;온프레미스&lt;/th&gt;
&lt;th&gt;IaaS&lt;/th&gt;
&lt;th&gt;관리형 플랫폼/PaaS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;시설/물리 장비&lt;/td&gt;
&lt;td&gt;조직이 직접 관리하거나 별도 위탁&lt;/td&gt;
&lt;td&gt;제공자 관리&lt;/td&gt;
&lt;td&gt;제공자 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM 운영체제&lt;/td&gt;
&lt;td&gt;조직 관리&lt;/td&gt;
&lt;td&gt;일반적으로 조직 관리&lt;/td&gt;
&lt;td&gt;서비스에 따라 제공자 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;플랫폼 및 엔진&lt;/td&gt;
&lt;td&gt;조직 관리&lt;/td&gt;
&lt;td&gt;일반적으로 조직 관리&lt;/td&gt;
&lt;td&gt;제공자가 상당 부분 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;애플리케이션 코드 및 업무 로직&lt;/td&gt;
&lt;td&gt;조직 관리&lt;/td&gt;
&lt;td&gt;조직 관리&lt;/td&gt;
&lt;td&gt;조직 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 사용/접근 권한/고객 설정&lt;/td&gt;
&lt;td&gt;조직 책임&lt;/td&gt;
&lt;td&gt;조직 책임&lt;/td&gt;
&lt;td&gt;조직 책임&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이 표는 일반적인 책임 구분을 단순화한 것이기에 구체적인 패치, 백업, 복원, 암호화와 장애 전환의 책임은 선택한 서비스와 계약에 따라 확인해야 한다.[^shared]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3c17c/dJMcaikGbuv/ZmoiilKYnHE0sSwlqDIyK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3c17c/dJMcaikGbuv/ZmoiilKYnHE0sSwlqDIyK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3c17c/dJMcaikGbuv/ZmoiilKYnHE0sSwlqDIyK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3c17c%2FdJMcaikGbuv%2FZmoiilKYnHE0sSwlqDIyK0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 8. 서비스 모델별 일반적인 운영 책임 구분&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;따라서 &lt;strong&gt;기존 서버를 클라우드 가상머신으로만 옮기면 물리 장비의 부담은 줄어도 운영체제와 애플리케이션 운영 업무는 상당 부분 남는다.&lt;/strong&gt; 관리형 서비스를 도입하고 운영 절차까지 바꾸어야 기대한 업무 절감이 실제로 나타나는지 평가할 수 있다.&lt;/p&gt;
&lt;h3&gt;5.4. 재해복구와 지역 확장의 초기 부담을 낮추기&lt;/h3&gt;
&lt;p&gt;자체 재해복구 거점을 만들려면 다른 장소의 공간, 회선, 장비, 보안과 운영 절차를 확보해야 하지만 클라우드를 복구 거점으로 활용하면 별도의 물리 시설을 직접 조달하는 범위를 줄일 수 있다.&lt;/p&gt;
&lt;p&gt;대신 어느 수준까지 미리 준비할지는 선택해야 한다. 백업만 보관하고 사고 때 환경을 재구축할 수도 있고, 최소 핵심 자원을 유지하거나 축소된 대기 환경을 계속 실행할 수도 있다. AWS의 재해복구 지침도 백업 및 복원, 파일럿 라이트, 웜 스탠바이, 다중 사이트 활성 구성처럼 비용과 복구 목표가 다른 접근을 구분한다.[^dr]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfOfG0/dJMcadwHKDQ/bJDtPTVxXZqD1fY7GM1AtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfOfG0/dJMcadwHKDQ/bJDtPTVxXZqD1fY7GM1AtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfOfG0/dJMcadwHKDQ/bJDtPTVxXZqD1fY7GM1AtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfOfG0%2FdJMcadwHKDQ%2FbJDtPTVxXZqD1fY7GM1AtK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 9. 재해복구 방식별로 평상시 켜 두는 자원과 비용·복구 속도의 관계&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;다른 지역에서 서비스를 제공하는 경우에도 가까운 실행 거점을 활용하는 선택지가 생긴다. 다만 지역을 늘리는 것만으로 지연시간과 복구 문제가 해결되지는 않는다. 데이터 위치, 복제 지연, 인증, 외부 연동과 지역 간 통신을 함께 설계해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;클라우드는 재해복구를 설계할 재료를 제공할 뿐 실제 복구 목표를 충족하는 구조와 실제 전환 및 복귀 시험은 여전히 조직이 확인해야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;5.5. 투자 방식과 실험의 위험을 바꾸기&lt;/h3&gt;
&lt;p&gt;온프레미스에서는 미래 수요를 예상해 장비와 공간을 먼저 확보하는 비중이 커질 수 있지만, 클라우드의 사용량 기반 과금은 필요한 범위에서 시작하고 사용량에 따라 비용을 조정하는 선택지를 제공한다. 다만 실제 과금에는 정액, 약정, 사용량 등 다양한 방식이 존재한다.[^pricing]&lt;/p&gt;
&lt;p&gt;이 차이는 성공 여부가 불확실한 신규 서비스나 짧은 실험에서 특히 더 중요하다. 소규모로 검증한 뒤 확장하면, 처음부터 큰 장비 투자를 확정하는 위험을 줄일 수 있기 때문이다. 반면 사용량이 항상 크고 안정적이라면 자체 인프라의 높은 활용률이 유리할 수도 있다.&lt;/p&gt;
&lt;p&gt;핵심은 ‘클라우드가 무조건 저렴하다’가 아닌 &lt;strong&gt;수요가 불확실할 때 자원 확보의 규모와 시점을 조절할 수 있다&lt;/strong&gt;는 것이다. 약정을 맺거나 자원을 계속 켜 두면 그 유연성의 일부는 줄어든다.&lt;/p&gt;
&lt;h3&gt;5.6. 조직의 역량을 어디에 쓸 것인지 선택하게 하기&lt;/h3&gt;
&lt;p&gt;서비스가 늘수록 운영팀은 서버뿐 아니라 데이터베이스, 메시지 처리, 검색, 분석, 보안과 배포 체계를 관리하게 된다. &lt;/p&gt;
&lt;p&gt;예를 들어 고객의 업무를 더 잘 처리하는 기능이 핵심 경쟁력인 조직이라면, 반복적인 기반 관리에 쓸 인력을 서비스 개선과 데이터 품질에 더 배분하는 편이 유리할 수 있다. 반대로 특수 하드웨어 운영이나 엄격한 현장 통제가 핵심이라면 그 영역을 직접 유지하는 것이 합리적일 수 있다.&lt;/p&gt;
&lt;p&gt;이 관점에서 클라우드는 단순한 서버 임대가 아니라 &lt;strong&gt;직접 책임질 범위와 서비스로 구매할 범위를 다시 정하는 수단&lt;/strong&gt;이다. 다음 표는 앞선 구축 과정과 클라우드의 필요성을 연결한 요약이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;온프레미스에서 발생하는 부담&lt;/th&gt;
&lt;th&gt;조직에 미치는 영향&lt;/th&gt;
&lt;th&gt;클라우드로 바뀔 수 있는 부분&lt;/th&gt;
&lt;th&gt;남는 과제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;장비 구매 및 설치의 반복&lt;/td&gt;
&lt;td&gt;신규 서비스 준비와 증설 지연&lt;/td&gt;
&lt;td&gt;API 및 템플릿 기반 자원 할당&lt;/td&gt;
&lt;td&gt;보안/설계/승인 절차&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대 부하 대비 선구매&lt;/td&gt;
&lt;td&gt;유휴 자원 또는 용량 부족&lt;/td&gt;
&lt;td&gt;수요에 따른 증감&lt;/td&gt;
&lt;td&gt;확장 가능한 애플리케이션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여러 기반 계층의 직접 운영&lt;/td&gt;
&lt;td&gt;패치 및 유지보수 업무 증가&lt;/td&gt;
&lt;td&gt;관리형 서비스 활용&lt;/td&gt;
&lt;td&gt;고객 설정/권한/데이터 책임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;별도 복구 거점 구축&lt;/td&gt;
&lt;td&gt;중복 투자와 운영 복잡도&lt;/td&gt;
&lt;td&gt;원격 백업 및 복구 환경 활용&lt;/td&gt;
&lt;td&gt;RTO 및 RPO 검증과 전환 훈련&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;신규 기술마다 기반 확보&lt;/td&gt;
&lt;td&gt;실험의 초기 비용과 위험&lt;/td&gt;
&lt;td&gt;필요한 기간 및 범위의 서비스 사용&lt;/td&gt;
&lt;td&gt;비용 통제와 종속성 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시설 및 하드웨어 수명주기&lt;/td&gt;
&lt;td&gt;정기적인 교체 프로젝트&lt;/td&gt;
&lt;td&gt;물리 기반 관리의 외부화&lt;/td&gt;
&lt;td&gt;서비스 수명주기와 이전 전략&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a id=&quot;part6&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;6. 클라우드의 한계와 환경 선택 기준&lt;/h2&gt;
&lt;h3&gt;6.1. 클라우드는 운영을 없애지 않고 운영의 대상을 바꾼다&lt;/h3&gt;
&lt;p&gt;클라우드 제공자가 시설과 장비를 관리해도 데이터 분류, 사용자 권한, 애플리케이션 보안과 고객이 제어하는 설정은 조직이 책임져야 한다. IaaS에서의 운영체제 관리도 대표적인 잔존 책임이다. 이를 공동 책임 모델이라고 한다.[^shared]&lt;/p&gt;
&lt;p&gt;그러므로 저장소의 외부 공개, 과도한 관리자 권한, 잘못된 네트워크 설정과 취약한 애플리케이션을 클라우드라는 이유로 안전하다고 볼 수 없다. 보안이 ‘필요 없어진다’가 아니라 보호해야 할 계층과 사용할 수 있는 도구가 달라지는 것이다.&lt;/p&gt;
&lt;p&gt;클라우드에서도 단일 인스턴스, 단일 장애 영역, 공통 인증 의존성이나 잘못된 배포는 장애 지점이 될 수 있으며, 제공자의 서비스 수준 약정이 개별 애플리케이션의 실제 복구 성능을 자동 보장하는 것도 아니다. 그렇기에 목표에 맞게 구성하고 시험해야 하는 것이 중요하다.[^redundancy]&lt;/p&gt;
&lt;h3&gt;6.2. 비용은 사용량과 설계에 따라 달라진다&lt;/h3&gt;
&lt;p&gt;클라우드 비용을 가상머신 요금만으로 계산해서는 안 되며 컴퓨팅, 저장공간, 데이터 전송과 관리형 서비스 사용량을 함께 산정해야 한다. AWS의 과금 설명도 컴퓨팅, 저장공간, 외부로 나가는 데이터 전송을 주요 비용 요인으로 구분한다.[^pricing]&lt;/p&gt;
&lt;p&gt;실무 비교표에는 선택한 서비스의 과금 단위를 확인해 다음 항목을 반영한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;클라우드 비교 비용
= 컴퓨팅 및 관리형 서비스 사용료
+ 저장공간/백업/로그 비용
+ 데이터 전송 및 연결/네트워크 처리 비용
+ 보안/지원/운영 인건비
+ 이전/검증/병행 운영 비용
+ 향후 반출/전환 비용&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중요한 것은 월 청구서를 작게 보이게 만드는 것이 아니라, 같은 서비스 목표를 달성하는 총비용과 위험을 비교하는 것이다. 사용량이 낮은 시점과 높은 시점, 장기간 안정적으로 사용하는 경우를 나눠 계산하며 할인이나 약정은 절감 효과와 함께 의무 사용량/기간의 제약도 평가한다.&lt;/p&gt;
&lt;p&gt;그리고 서비스별 비용 책임자를 정하고 자원에 소유자/업무/환경 정보를 붙여서 예산 경보, 불필요한 자원 정리와 적정 용량 조정을 운영 활동으로 만든다. 비용 최적화는 최초 설계 한 번으로 끝나는 작업이 아니라 지속적인 사용량 및 구성 관리이기 때문이다.[^cost]&lt;/p&gt;
&lt;h3&gt;6.3. 온프레미스를 유지하는 편이 나은 조건도 있다&lt;/h3&gt;
&lt;p&gt;다음은 절대적인 분류가 아니라 검토의 출발점이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;업무 조건&lt;/th&gt;
&lt;th&gt;검토 방향&lt;/th&gt;
&lt;th&gt;확인해야 할 핵심 변수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;수요가 불규칙하고 빠른 실험이 중요함&lt;/td&gt;
&lt;td&gt;퍼블릭 클라우드 우선 검토&lt;/td&gt;
&lt;td&gt;확장 가능성, 중단 및 종료 가능성, 비용 상한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장기간 부하가 크고 예측 가능함&lt;/td&gt;
&lt;td&gt;온프레미스와 클라우드 TCO 비교&lt;/td&gt;
&lt;td&gt;장비 활용률, 인력, 전력, 할인 및 약정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;매우 낮은 지연과 현장 독립 동작이 필요함&lt;/td&gt;
&lt;td&gt;온프레미스 및 엣지 우선 검토&lt;/td&gt;
&lt;td&gt;통신 단절 시 동작, 현장 장비 연동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특정 하드웨어/운영체제/라이선스 의존성이 큼&lt;/td&gt;
&lt;td&gt;유지 또는 선택적 이전 검토&lt;/td&gt;
&lt;td&gt;호환성, 지원 및 라이선스 조건, 변경 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 위치와 접근 통제 요구가 엄격함&lt;/td&gt;
&lt;td&gt;요구를 만족하는 배치 및 서비스 검토&lt;/td&gt;
&lt;td&gt;데이터 흐름, 운영자 접근, 계약 및 감사 요구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일부 업무만 변동성이 크거나 외부 확장이 필요함&lt;/td&gt;
&lt;td&gt;하이브리드 검토&lt;/td&gt;
&lt;td&gt;연결 비용, 지연, 양쪽 운영 복잡도&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;데이터 위치나 규제 요구는 “클라우드라서 불가능” 또는 “인증된 서비스라서 자동 충족”이라고 단정하지 않고, 실제 처리/보관 위치, 접근 경로와 계약을 보안/법무/업무 담당자가 함께 검토하는 것이 권장된다.&lt;/p&gt;
&lt;h3&gt;6.4. 하이브리드는 타협안일 수 있지만 자동으로 단순하지는 않다&lt;/h3&gt;
&lt;p&gt;핵심 현장 시스템은 온프레미스에 두고 개발 및 검증, 외부 웹 서비스, 분석과 백업 일부를 클라우드에서 운영하는 방식이 가능하다. 이는 기존 자산을 활용하면서 새로운 요구를 수용하는 접근이다.&lt;/p&gt;
&lt;p&gt;다만 양쪽을 연결하면 인증, 주소 체계, 네트워크, 보안 정책, 모니터링과 비용 관리도 연결해야 하기에 회선 장애가 전체 업무에 미치는 영향을 시험하고, 데이터가 빈번하게 오가는 경계를 어디에 둘지 신중하게 정한다.&lt;/p&gt;
&lt;p&gt;그리고 온프레미스 애플리케이션이 클라우드 데이터베이스를 매 요청마다 여러 번 호출하는 식으로 경계를 나누면 통신 지연과 장애 의존성이 커질 수 있기에 업무와 데이터의 결합 관계를 기준으로 이전 단위를 정하는 편이 합리적이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SucpR/dJMcahzhAYc/vFWkz4CbLACjvywisKNhMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SucpR/dJMcahzhAYc/vFWkz4CbLACjvywisKNhMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SucpR/dJMcahzhAYc/vFWkz4CbLACjvywisKNhMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSucpR%2FdJMcahzhAYc%2FvFWkz4CbLACjvywisKNhMk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 10. 하이브리드 경계를 요청 경로 중간이 아니라 업무·데이터 단위로 나누기&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;따라서 하이브리드는 ‘좋은 것만 결합한 구조’가 아니라, &lt;strong&gt;유지할 제약과 추가로 관리할 복잡도를 함께 선택하는 구조&lt;/strong&gt;로 이해해야만 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a id=&quot;part7&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;7. 현실적인 전환 순서와 최종 점검표&lt;/h2&gt;
&lt;h3&gt;7.1. 모든 서버를 한 번에 이전하는 것부터 시작하지 않기&lt;/h3&gt;
&lt;p&gt;전환의 출발점은 기존 환경의 자산과 의존성을 파악하는 것이다. 서버별로 역할, 소유자, 성능, 데이터량, 통신 상대, 라이선스와 복구 목표를 정리하고 실제로 사용하지 않는 시스템은 이전보다 폐기 대상인지 먼저 확인한다.&lt;/p&gt;
&lt;p&gt;그다음 서비스별로 유지, 단순 이전, 관리형 서비스 활용, 구조 변경, 상용 서비스 대체 또는 폐기를 판단하는데 여기서 빠르게 옮기는 것이 목표인지, 운영 부담을 줄이는 것이 목표인지에 따라 선택이 달라진다.&lt;/p&gt;
&lt;p&gt;아래는 단계적 전환을 위한 예시 순서다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;수행할 일&lt;/th&gt;
&lt;th&gt;다음 단계로 넘어갈 기준&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;현황 파악&lt;/td&gt;
&lt;td&gt;자산, 의존성, 비용, 서비스 목표 수집&lt;/td&gt;
&lt;td&gt;이전 단위와 제약이 설명됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배치 결정&lt;/td&gt;
&lt;td&gt;유지, 이전, 개선, 대체, 폐기 분류&lt;/td&gt;
&lt;td&gt;서비스별 목적과 책임자 확정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공통 기반 구축&lt;/td&gt;
&lt;td&gt;계정, 권한, 네트워크, 로깅, 비용 정책 마련&lt;/td&gt;
&lt;td&gt;기본 보안 및 운영 통제가 검증됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시범 적용&lt;/td&gt;
&lt;td&gt;영향 범위가 작은 업무로 성능/복구/비용 실측&lt;/td&gt;
&lt;td&gt;목표 충족과 운영 가능성 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단계적 이전&lt;/td&gt;
&lt;td&gt;의존성을 고려한 데이터/서비스 전환&lt;/td&gt;
&lt;td&gt;정합성 검증과 복귀 조건 충족&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최적화·정리&lt;/td&gt;
&lt;td&gt;자원 조정, 관리형 전환, 기존 자산 정리&lt;/td&gt;
&lt;td&gt;중복 비용 해소와 목표 효과 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;7.2. 클라우드에서도 공통 운영 기반을 먼저 마련한다&lt;/h3&gt;
&lt;p&gt;클라우드의 계정/프로젝트 구조, 관리자 인증, 운영 및 개발 환경 분리, 네트워크, 중앙 로그, 보안 정책과 비용 관리 기준을 준비한다. 이러한 공통 기반을 랜딩 존(Landing Zone)이라고 부르며, Microsoft의 랜딩 존 지침은 공통 거버넌스, 보안, 연결 기반과 개별 워크로드 환경을 구분한다.[^landingzone]&lt;/p&gt;
&lt;p&gt;그러나 소규모 조직이 반드시 대기업과 동일한 복잡도를 도입할 필요는 없다. 그러나 누가 자원을 만들고 비용을 부담하는지, 관리자 접근을 어떻게 제한하는지, 중요한 기록을 어디에 남기는지는 규모와 관계없이 정해야만 한다.&lt;/p&gt;
&lt;h3&gt;7.3. 데이터 이전과 복귀 계획을 함께 검증한다&lt;/h3&gt;
&lt;p&gt;데이터를 옮길 때는 초기 복사, 이후 변경분 동기화, 최종 전환과 검증 순서를 정한다. 허용 중단 시간이 짧다면 그에 맞는 동기화 수단과 검증 방법이 필요하다. 그리고 단순히 파일 크기가 같다는 이유로 데이터 이전이 완료됐다고 보지 않는다.&lt;/p&gt;
&lt;p&gt;전환 중 어느 시스템이 쓰기를 받아들이는지 명확히 해야 하는데 두 환경에서 동시에 변경이 발생하면 충돌과 정합성 문제가 생길 수 있다. 실패 시 원래 환경으로 되돌릴 기준과 그 사이 발생한 데이터의 처리 방법까지 사전에 정한다.&lt;/p&gt;
&lt;p&gt;이전 성공 후에도 성능, 오류율, 복구 결과와 실제 비용을 관측하며, 검증이 끝나면 기존 장비와 라이선스를 유지할 이유를 재평가하고, 안전한 데이터 정리와 계약 종료를 수행한다. 자원만 옮기고 기존 환경을 계속 유지하면 예상한 비용 효과가 나타나지 않을 수 있다.&lt;/p&gt;
&lt;h3&gt;7.4. 구축과 전환의 최종 점검표&lt;/h3&gt;
&lt;p&gt;다음 표는 온프레미스 구축 또는 클라우드 전환의 완료 여부를 검토할 때 사용할 수 있는 공통 질문이다. 모든 항목을 획일적으로 같은 수준으로 구현하기보다는, 미충족 항목의 영향과 수용 여부를 명시한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;확인 질문&lt;/th&gt;
&lt;th&gt;남겨야 할 증거&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;요구사항&lt;/td&gt;
&lt;td&gt;성능, 가용성, 복구 목표가 합의됐는가?&lt;/td&gt;
&lt;td&gt;승인된 목표와 측정 기준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;아키텍처&lt;/td&gt;
&lt;td&gt;의존성과 공통 장애 지점을 알고 있는가?&lt;/td&gt;
&lt;td&gt;구성도와 장애 영향 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;용량&lt;/td&gt;
&lt;td&gt;최대 부하와 장애 및 정비 상황을 견디는가?&lt;/td&gt;
&lt;td&gt;용량 산정과 부하 시험 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시설·연결&lt;/td&gt;
&lt;td&gt;필요한 전력, 냉각, 통신을 지속 제공할 수 있는가?&lt;/td&gt;
&lt;td&gt;현장 검증 또는 제공 범위 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;접근 통제&lt;/td&gt;
&lt;td&gt;사람·서비스별 최소 권한을 적용했는가?&lt;/td&gt;
&lt;td&gt;권한표와 접근 시험 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;변경, 복제, 복원 후 정합성을 보장하는가?&lt;/td&gt;
&lt;td&gt;업무 및 데이터 검증 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복구&lt;/td&gt;
&lt;td&gt;실제 복원으로 RTO 및 RPO를 확인했는가?&lt;/td&gt;
&lt;td&gt;복구 훈련 보고서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;변경&lt;/td&gt;
&lt;td&gt;배포와 실패 복구를 반복할 수 있는가?&lt;/td&gt;
&lt;td&gt;자동화 정의와 변경 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관측&lt;/td&gt;
&lt;td&gt;사용자 영향을 발견하고 담당자에게 전달하는가?&lt;/td&gt;
&lt;td&gt;대시보드와 경보 전달 시험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영&lt;/td&gt;
&lt;td&gt;담당자 부재에도 대응 가능한가?&lt;/td&gt;
&lt;td&gt;운영 매뉴얼과 책임 및 연락체계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;같은 조건으로 비교하고 지속 관리하는가?&lt;/td&gt;
&lt;td&gt;TCO 가정, 사용량 및 비용 보고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;수명주기&lt;/td&gt;
&lt;td&gt;교체, 이전, 폐기와 데이터 반출을 계획했는가?&lt;/td&gt;
&lt;td&gt;지원 종료, 전환, 정리 계획&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a id=&quot;part8&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;8. 결론 — 클라우드는 서버의 위치보다 책임과 대응 속도를 바꾸는 선택&lt;/h2&gt;
&lt;p&gt;온프레미스 환경을 구축하려면 업무 요구사항을 정하고, 논리 및 물리 구조를 설계하고, 용량과 비용을 산정해야 하며 그 위에 시설과 장비, 네트워크, 실행 환경, 데이터, 보안, 백업, 배포와 모니터링을 갖춘 뒤 실제 장애 및 복구 시험을 거쳐야 한다. 구축 후에는 변경과 증설, 보안 업데이트와 수명주기를 계속 관리해줘야 한다.&lt;/p&gt;
&lt;p&gt;이 과정을 직접 수행하면 높은 통제권을 얻는 대신 수요 증가, 신규 서비스, 더 높은 복구 수준과 새로운 기술이 요구될 때마다 직접 관리하는 범위를 다시 확장해야 할 수 있다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;strong&gt;클라우드의 필요성은 사업이 요구하는 변화의 속도와 범위를 자체 인프라의 조달 및 운영 방식만으로 감당하기 어려워지는 지점에서 커진다.&lt;/strong&gt; 자원 할당을 빠르게 하고, 변동하는 수요에 맞춰 사용 범위를 조절하며, 관리형 서비스를 통해 직접 운영하는 계층을 줄일 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;다만 모든 시스템을 결국 클라우드로 옮겨야 한다는 결론은 아니다. 안정적인 대규모 부하, 특수 장비, 현장 독립성처럼 자체 운영의 가치가 큰 영역은 남길 수 있으며 클라우드에서도 보안, 비용, 데이터와 복구 책임은 계속 관리해야 한다.&lt;/p&gt;
&lt;p&gt;그렇기에 최종 판단 기준은 다음 한 문장으로 정리할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;온프레미스를 구축할 수 있는가보다 중요한 것은, 앞으로도 어느 부분을 직접 구축 및 운영하는 것이 조직의 서비스 목표와 경쟁력에 도움이 되는가.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;p&gt;[^shared]: Microsoft Learn, &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/security/fundamentals/shared-responsibility&quot;&gt;Shared responsibility in the cloud&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^nist-cloud]: NIST SP 800-145, &lt;a href=&quot;https://csrc.nist.gov/pubs/sp/800/145/final&quot;&gt;The NIST Definition of Cloud Computing&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^private]: NIST CSRC Glossary, &lt;a href=&quot;https://csrc.nist.gov/glossary/term/private_cloud&quot;&gt;Private cloud&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^rto]: NIST CSRC Glossary, &lt;a href=&quot;https://csrc.nist.gov/glossary/term/recovery_time_objective&quot;&gt;Recovery Time Objective&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^rpo]: NIST CSRC Glossary, &lt;a href=&quot;https://csrc.nist.gov/glossary/term/recovery_point_objective&quot;&gt;Recovery Point Objective&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^redundancy]: Microsoft Azure Architecture Center, &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/architecture/guide/design-principles/redundancy&quot;&gt;Make all things redundant&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^cost]: AWS Well-Architected Framework, &lt;a href=&quot;https://docs.aws.amazon.com/wellarchitected/latest/cost-optimization-pillar/welcome.html&quot;&gt;Cost Optimization Pillar&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^datacenter]: IBM, &lt;a href=&quot;https://www.ibm.com/think/topics/data-centers&quot;&gt;What is a data center?&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^zero]: NIST SP 800-207, &lt;a href=&quot;https://csrc.nist.gov/pubs/sp/800/207/final&quot;&gt;Zero Trust Architecture&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^virtualization]: Red Hat, &lt;a href=&quot;https://www.redhat.com/en/topics/virtualization/what-is-virtualization&quot;&gt;What is virtualization?&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^k8s]: Kubernetes Documentation, &lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/&quot;&gt;Production environment&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;[^dr]: AWS, &lt;a href=&quot;https://docs.aws.amazon.com/whitepapers/latest/disaster-recovery-workloads-on-aws/disaster-recovery-options-in-the-cloud.html&quot;&gt;Disaster recovery options in the cloud&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^contingency]: NIST SP 800-34 Rev. 1, &lt;a href=&quot;https://csrc.nist.gov/pubs/sp/800/34/r1/upd1/final&quot;&gt;Contingency Planning Guide for Federal Information Systems&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^backup]: UK National Cyber Security Centre, &lt;a href=&quot;https://www.ncsc.gov.uk/collection/small-organisations-guide-to-cyber-security/backing-up-your-data&quot;&gt;Backing up your data&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^iac]: HashiCorp Developer, &lt;a href=&quot;https://developer.hashicorp.com/terraform/intro&quot;&gt;What is Terraform?&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^sre]: Google SRE Book, &lt;a href=&quot;https://sre.google/sre-book/monitoring-distributed-systems/&quot;&gt;Monitoring Distributed Systems&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^autoscale]: AWS Documentation, &lt;a href=&quot;https://docs.aws.amazon.com/autoscaling/ec2/userguide/what-is-amazon-ec2-auto-scaling.html&quot;&gt;What is Amazon EC2 Auto Scaling?&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;[^pricing]: AWS, &lt;a href=&quot;https://docs.aws.amazon.com/whitepapers/latest/how-aws-pricing-works/key-principles.html&quot;&gt;Key principles — How AWS Pricing Works&lt;/a&gt;, &lt;a href=&quot;https://aws.amazon.com/pricing/&quot;&gt;AWS Pricing&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;[^landingzone]: Microsoft Cloud Adoption Framework, &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/&quot;&gt;What is an Azure landing zone?&lt;/a&gt;. &lt;/p&gt;</description>
      <category>Guides</category>
      <category>가이드</category>
      <category>구축</category>
      <category>데이터</category>
      <category>백업</category>
      <category>온프레미스</category>
      <category>클라우드</category>
      <category>환경</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/90</guid>
      <comments>https://slowgrid.tistory.com/entry/The-entire-process-of-building-an-on-premises-environment-and-why-the-cloud-becomes-necessary#entry90comment</comments>
      <pubDate>Mon, 21 Sep 2026 19:06:25 +0900</pubDate>
    </item>
    <item>
      <title>1/&amp;pi;를 향한 2000년 (1) &amp;mdash; 원과 무한: 아르키메데스에서 오일러까지</title>
      <link>https://slowgrid.tistory.com/entry/2000-Years-Toward-1%CF%80-1-%E2%80%94-Circles-and-Infinity-From-Archimedes-to-Euler</link>
      <description>&lt;h1&gt;1/π를 향한 2000년 (1) — 원과 무한: 아르키메데스에서 오일러까지&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;시리즈 「1/π를 향한 2000년」: &lt;strong&gt;1편 원과 무한&lt;/strong&gt; / 2편 AGM과 타원적분 / 3편 모듈러의 세계 / 4편 1103의 탄생 / 5편 검증과 그 이후&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;들어가며: 한 항에 여덟 자리&lt;/h2&gt;
&lt;p&gt;1914년, 스리니바사 라마누잔은 다음 공식을 논문 &amp;quot;Modular equations and approximations to π&amp;quot;(&lt;em&gt;Quarterly Journal of Mathematics&lt;/em&gt; 45권)에 실었다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{1}{\pi} = \frac{2\sqrt{2}}{99^2}\sum_{k=0}^{\infty}\frac{(4k)!}{(k!)^4}\cdot\frac{26390k+1103}{396^{4k}}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이 급수는 항을 하나 더할 때마다 π의 정확한 자릿수가 약 8개씩 늘어난다. 첫 항 하나만으로 $\pi \approx 9801/(2\sqrt2 \cdot 1103) = 3.14159273\ldots$ 을 얻고, 두 항이면 이미 소수점 아래 15자리를 넘는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAsTr4/dJMcafuF0bu/IkSR2mwKSCdGPbH4CiKvd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAsTr4/dJMcafuF0bu/IkSR2mwKSCdGPbH4CiKvd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAsTr4/dJMcafuF0bu/IkSR2mwKSCdGPbH4CiKvd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAsTr4%2FdJMcafuF0bu%2FIkSR2mwKSCdGPbH4CiKvd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1041&quot; height=&quot;583&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;583&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 1. 공식 속 각 숫자가 어디서 오는지 한눈에 본 그림.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;이 시리즈의 목표는 &amp;quot;이 공식이 어디서 왔는가&amp;quot;를 수학적으로 따라가는 것이다. 그 길은 아래 연표와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dp81s8/dJMcajqbHqm/uklS35ryi0f6km8mmRgDK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dp81s8/dJMcajqbHqm/uklS35ryi0f6km8mmRgDK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dp81s8/dJMcajqbHqm/uklS35ryi0f6km8mmRgDK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdp81s8%2FdJMcajqbHqm%2FuklS35ryi0f6km8mmRgDK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1308&quot; height=&quot;514&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 2. 아르키메데스에서 추드노프스키까지.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;시리즈는 다섯 편으로 이루어진다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;원과 무한&lt;/strong&gt; (이 글) — 아르키메데스의 다각형, 무한곱과 급수, 오일러&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AGM과 타원적분&lt;/strong&gt; — 가우스의 산술기하평균, 르장드르 관계식&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모듈러의 세계&lt;/strong&gt; — 야코비의 세타함수와 특이 모듈러스, 초기하급수&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1103의 탄생&lt;/strong&gt; — 라마누잔의 주정리와 58의 비밀&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검증과 그 이후&lt;/strong&gt; — 수렴 속도, 고스퍼, 보르와인, 추드노프스키&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;1편은 대부분 고등학교 수학과 미적분으로 따라갈 수 있으며, 여기서 만드는 도구(평균의 반복, 월리스 적분, 포흐하머 기호)가 뒤 편에서 계속 다시 쓰인다.&lt;/p&gt;
&lt;h2&gt;아르키메데스(기원전 3세기): 다각형으로 가두기&lt;/h2&gt;
&lt;p&gt;아르키메데스는 π를 $3 + \tfrac{10}{71} &amp;lt; \pi &amp;lt; 3 + \tfrac{1}{7}$ 사이에 가두었는데, 방법은 원에 내접/외접하는 정다각형의 둘레를 비교하는 것이다.&lt;/p&gt;
&lt;p&gt;지름이 1인 원에서 외접 정 $n$각형의 둘레를 $a_n$, 내접 정 $n$각형의 둘레를 $b_n$이라 하면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;a_n = n\tan\frac{\pi}{n}, \qquad b_n = n\sin\frac{\pi}{n}, \qquad b_n &amp;lt; \pi &amp;lt; a_n&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이다. 삼각함수를 몰랐던 아르키메데스가 실제로 쓴 것은 변의 수를 두 배로 늘리는 다음 재귀식과 같은 내용이다.&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;a_{2n} = \frac{2a_n b_n}{a_n + b_n}, \qquad b_{2n} = \sqrt{a_{2n}b_n}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;증명.&lt;/strong&gt; $x = \pi/n$, $t = \tan x$, $s = \sin x$ 라 하면 $t\cos x = s$ 이므로&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\tan\frac{x}{2} = \frac{s}{1+\cos x} = \frac{st}{t + t\cos x} = \frac{st}{t+s}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;따라서 $2n\tan(x/2) = \dfrac{2(nt)(ns)}{nt + ns}$, 즉 $a_{2n}$은 $a_n$과 $b_n$의 조화평균이다. 또&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;a_{2n}b_n = 2n\tan\frac{x}{2}\cdot 2n\sin\frac{x}{2}\cos\frac{x}{2} = \Bigl(2n\sin\frac{x}{2}\Bigr)^2 = b_{2n}^2&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이므로 $b_{2n}$은 기하평균이다.&lt;/p&gt;
&lt;p&gt;정육각형($a_6 = 2\sqrt3$, $b_6 = 3$)에서 시작해 6 → 12 → 24 → 48 → 96각형까지 가면 $223/71 &amp;lt; \pi &amp;lt; 22/7$ 를 얻는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBvnhB/dJMcairmVeP/ar2stAZRB34NIopxVNKLb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBvnhB/dJMcairmVeP/ar2stAZRB34NIopxVNKLb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBvnhB/dJMcairmVeP/ar2stAZRB34NIopxVNKLb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBvnhB%2FdJMcairmVeP%2Far2stAZRB34NIopxVNKLb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1544&quot; height=&quot;615&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;그림 3. 왼쪽 두 그림은 내접 정다각형과 외접 정다각형, 오른쪽은 변을 두 배로 늘릴 때 상한과 하한이 π로 좁혀지는 모습.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;여기서 주목할 점이 두 가지 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;평균의 반복&lt;/strong&gt;이라는 구조가 이미 여기 있다. 2000년 뒤 가우스의 산술기하평균(AGM)이 같은 뼈대를 훨씬 강력하게 되살린다(2편).&lt;/li&gt;
&lt;li&gt;수렴은 느리다. $a_n - b_n \approx \pi^3/(2n^2)$ 이라, 변을 두 배로 늘릴 때마다 오차가 1/4로만 줄어든다. 두 배 늘리기 약 1.7번에 한 자리씩 얻는 셈이며, 라마누잔 급수의 &amp;quot;한 항에 8자리&amp;quot;와 비교하면 차이가 뚜렷하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;무한의 등장(1400–1706): 곱과 급수&lt;/h2&gt;
&lt;p&gt;16–17세기에 π는 처음으로 &amp;quot;유한한 계산의 극한&amp;quot;이 아니라 &lt;strong&gt;무한한 식의 값&lt;/strong&gt;으로 쓰이기 시작한다.&lt;/p&gt;
&lt;h3&gt;비에트의 무한곱 (1593)&lt;/h3&gt;
&lt;p&gt;배각공식 $\sin x = 2\sin(x/2)\cos(x/2)$ 를 $n$번 반복하면 $\sin x = 2^n \sin(x/2^n)\prod_{j=1}^{n}\cos(x/2^j)$ 이다. $n\to\infty$ 에서 $2^n\sin(x/2^n)\to x$ 이므로 $\dfrac{\sin x}{x} = \prod_{j\ge1}\cos(x/2^j)$. 여기에 $x = \pi/2$ 를 넣고 $\cos(\theta/2) = \sqrt{(1+\cos\theta)/2}$ 를 반복 적용하면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{2}{\pi} = \frac{\sqrt{2}}{2}\cdot\frac{\sqrt{2+\sqrt{2}}}{2}\cdot\frac{\sqrt{2+\sqrt{2+\sqrt{2}}}}{2}\cdots&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이것은 아르키메데스의 다각형 이중화를 무한히 수행한 것을 하나의 식으로 쓴 것이다.&lt;/p&gt;
&lt;h3&gt;월리스의 곱 (1655)&lt;/h3&gt;
&lt;p&gt;$I_n = \int_0^{\pi/2}\sin^n x,dx$ 로 두면 부분적분으로 $I_n = \frac{n-1}{n}I_{n-2}$ 를 얻는다. $I_0 = \pi/2$, $I_1 = 1$ 에서 출발하면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;I_{2m} = \frac{\pi}{2}\prod_{j=1}^{m}\frac{2j-1}{2j}, \qquad I_{2m+1} = \prod_{j=1}^{m}\frac{2j}{2j+1}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;$I_n$ 은 $n$에 대해 감소하므로 $I_{2m+1} \le I_{2m} \le I_{2m-1}$, 그리고 $I_{2m+1}/I_{2m-1} = 2m/(2m+1) \to 1$ 이다. 따라서 $I_{2m}/I_{2m+1}\to1$ 이 되어&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{\pi}{2} = \prod_{n=1}^{\infty}\frac{(2n)^2}{(2n-1)(2n+1)} = \frac{2}{1}\cdot\frac{2}{3}\cdot\frac{4}{3}\cdot\frac{4}{5}\cdot\frac{6}{5}\cdot\frac{6}{7}\cdots&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이 곱에 등장하는 $\frac12\cdot\frac32\cdot\frac52\cdots$ 형태의 비율은 뒤에서 라마누잔 공식의 계수 $(\tfrac12)&lt;em&gt;k$ 로 다시 나타난다(3편). 적분 $I&lt;/em&gt;{2m}$ 도 2편에서 타원적분을 급수로 바꿀 때 그대로 쓰인다.&lt;/p&gt;
&lt;h3&gt;마다바–그레고리–라이프니츠 급수 (c.1400 / 1671 / 1674)&lt;/h3&gt;
&lt;p&gt;$1/(1+t^2) = 1 - t^2 + t^4 - \cdots$ 를 0부터 $x$까지 적분하면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\arctan x = x - \frac{x^3}{3} + \frac{x^5}{5} - \cdots \quad (|x|\le 1), \qquad \frac{\pi}{4} = 1 - \frac13 + \frac15 - \frac17 + \cdots&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;인도 케랄라 학파의 마다바가 유럽보다 약 250년 먼저 발견했다. 하지만 $x = 1$ 에서는 오차가 대략 $1/(2N)$ 이라, 10자리를 얻으려면 수십억 항이 필요하다.&lt;/p&gt;
&lt;h3&gt;마친의 공식 (1706)&lt;/h3&gt;
&lt;p&gt;해결책은 $x$를 작게 만드는 것이다. 복소수 $(5+i)^4$ 를 계산하면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;(5+i)^2 = 24 + 10i,\quad (5+i)^4 = 476 + 480i,\quad (476+480i)(239-i) = 114244\cdot(1+i)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;복소수의 곱에서 편각은 더해지므로 $4\arctan\frac15 - \arctan\frac{1}{239} = \arg(1+i)$ 이고, 곧&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{\pi}{4} = 4\arctan\frac15 - \arctan\frac{1}{239}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이다. $\arctan(1/5)$ 급수는 항마다 약 $\log_{10}25 \approx 1.4$ 자리씩 얻는다. 마친은 이것으로 π를 100자리까지 계산했다. 이후 200년 가까이 π 계산은 이런 마친형 arctan 공식의 시대였다. 1873년 윌리엄 섕크스는 707자리를 손으로 계산했는데, 528번째 자리부터 틀려 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBKgMq/dJMcadQ6md6/fCiNohukPE22L5PBhs8Ej1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBKgMq/dJMcadQ6md6/fCiNohukPE22L5PBhs8Ej1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBKgMq/dJMcadQ6md6/fCiNohukPE22L5PBhs8Ej1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBKgMq%2FdJMcadQ6md6%2FfCiNohukPE22L5PBhs8Ej1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;em&gt;그림 4. 왼쪽은 라이프니츠 급수, 월리스 곱, 비에트 곱의 부분값, 오른쪽은 오차를 로그 축으로 비교한 것. 라이프니츠와 월리스는 오차가 1/n 정도로만 줄고, 비에트와 마친은 일정 비율로 줄어든다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;여기까지의 방법은 모두 &lt;strong&gt;선형 수렴&lt;/strong&gt;(또는 그보다 느림)이며, 한 단계마다 일정한 자릿수를 더 얻는다. 한 단계에 얻는 자릿수를 1.4에서 8로, 14로 끌어올리려면 그제까지와는 다른 전혀 새로운 수학이 필요했다.&lt;/p&gt;
&lt;h2&gt;오일러(18세기): π는 원 밖에서도 나타난다&lt;/h2&gt;
&lt;p&gt;오일러는 π가 기하학의 상수이기 이전에 &lt;strong&gt;해석학의 상수&lt;/strong&gt;라는 것을 보여주었으며 대표적인 예가 바젤 문제(1735)다.&lt;/p&gt;
&lt;p&gt;$\sin x$ 는 $x = \pm n\pi$ 에서 0이 되므로, 오일러는 다항식처럼 인수분해할 수 있다고 보았다(엄밀한 정당화는 훗날 바이어슈트라스의 곱정리가 제공한다).&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\frac{\sin x}{x} = \prod_{n=1}^{\infty}\left(1 - \frac{x^2}{n^2\pi^2}\right)&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;왼쪽의 테일러 전개 $1 - x^2/6 + \cdots$ 와 오른쪽의 $x^2$ 계수 $-\sum 1/(n^2\pi^2)$ 를 비교하면&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;\sum_{n=1}^{\infty}\frac{1}{n^2} = \frac{\pi^2}{6}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이 곱에 $x = \pi/2$ 를 넣으면 앞에서 본 월리스 곱이 그대로 나온다. 서로 무관해 보였던 결과들이 하나의 함수 이론으로 묶이기 시작한 것이다.&lt;/p&gt;
&lt;p&gt;라마누잔 공식을 이해하는 데 특히 중요한 오일러의 유산은 &lt;strong&gt;감마함수&lt;/strong&gt;이다. $\Gamma(1/2) = \sqrt\pi$ 이고, 이로부터 상승계승(포흐하머 기호)&lt;/p&gt;
&lt;p&gt;$$&lt;br&gt;(a)_k = a(a+1)\cdots(a+k-1) = \frac{\Gamma(a+k)}{\Gamma(a)}, \qquad \left(\tfrac12\right)_k = \frac{(2k)!}{4^k k!}&lt;br&gt;$$&lt;/p&gt;
&lt;p&gt;이 정의된다. 3편에서 보겠지만 라마누잔 공식의 $(4k)!/(k!)^4$ 는 바로 이 기호들의 곱이다.&lt;/p&gt;
&lt;p&gt;같은 세기와 다음 세기에 π의 정체도 밝혀졌다. 람베르트는 1761년 π가 무리수임을, 린데만은 1882년 π가 초월수임을 증명했다. 따라서 π는 정수 계수 다항식의 근이 아니고, 자와 컴퍼스로 원을 정사각형으로 만드는 작도는 불가능하다. 그런데 라마누잔 공식은 π를 &lt;strong&gt;대수적 수와 유리수만으로&lt;/strong&gt; 된 급수로 표현한다. 이것이 어떻게 가능한지는 이 시리즈의 나머지 네 편을 거쳐가며 이해하게 될 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;다음 편 예고 — 2편: AGM과 타원적분.&lt;/strong&gt; 아르키메데스의 평균의 반복을 가우스가 되살린다. 한 단계마다 정확한 자릿수가 두 배가 되는 산술기하평균, 그리고 π가 타원적분 사이의 항등식으로 정확히 나타나는 르장드르 관계식을 다룬다.&lt;/p&gt;</description>
      <category>CS/수학</category>
      <category>감마함수</category>
      <category>라마누잔공식</category>
      <category>라이프니츠급수</category>
      <category>마친공식</category>
      <category>무한</category>
      <category>바젤문제</category>
      <category>비에트공식</category>
      <category>수렴</category>
      <category>월리스곱</category>
      <category>포흐하머기호</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/89</guid>
      <comments>https://slowgrid.tistory.com/entry/2000-Years-Toward-1%CF%80-1-%E2%80%94-Circles-and-Infinity-From-Archimedes-to-Euler#entry89comment</comments>
      <pubDate>Sun, 20 Sep 2026 22:29:39 +0900</pubDate>
    </item>
    <item>
      <title>나는 나를 얼마나 정확하게 보고 있을까: 주관적 자아, 객관적 자아 그리고 자기고양편향</title>
      <link>https://slowgrid.tistory.com/entry/How-Accurately-Do-I-See-Myself-Subjective-Self-Objective-Self-and-Self-Enhancement-Bias</link>
      <description>&lt;p&gt;나 자신에 대해 생각하다 보면 가끔 이런 질문이 생긴다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 생각하는 나는 정말 다른 사람들이 보는 나와 비슷할까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;평소에는 내가 어떤 사람인지 어느 정도 알고 있다고 생각하며 살아간다.&lt;/p&gt;
&lt;p&gt;나는 어떤 성격인지, 무엇을 잘하는지, 무엇을 어려워하는지 스스로 나름대로 판단하고 있는 것 같다.&lt;/p&gt;
&lt;p&gt;그런데 다른 사람과 이야기를 하다 보면 가끔 예상하지 못한 평가를 들을 때가 있다.&lt;/p&gt;
&lt;p&gt;나는 조용한 편이라고 생각했는데 누군가는 나를 활발한 사람이라고 말하기도 하고, 나는 별로 꼼꼼하지 않다고 생각했는데 다른 사람은 오히려 나를 세심한 사람이라고 평가하기도 한다.&lt;/p&gt;
&lt;p&gt;그럴 때면 조금 이상한 느낌이 든다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 알고 있는 나와 다른 사람이 알고 있는 나는 같은 사람일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이번에는 내가 바라보는 나, 다른 사람이 바라보는 나를 각각 생각해보고 그 차이가 왜 생기는지 살펴보려고 한다.&lt;/p&gt;
&lt;p&gt;그리고 그 과정에서 우리가 스스로를 조금 더 긍정적으로 바라보려는 &lt;strong&gt;자기고양편향&lt;/strong&gt;에 대해서도 생각해보았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;1. 내가 생각하는 나: 주관적 자아&lt;/h1&gt;
&lt;p&gt;먼저 다른 사람의 평가를 생각하지 않고 내가 바라보는 나의 모습을 적어보았다.&lt;/p&gt;
&lt;p&gt;내가 생각하는 나는 비교적 &lt;strong&gt;책임감이 있는 사람&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;한번 해야 한다고 생각한 일은 가능하면 끝까지 해내려고 하고, 다른 사람과 약속한 일은 지키려고 노력하는 편이다.&lt;/p&gt;
&lt;p&gt;물론 모든 일을 완벽하게 해내는 것은 아니다.&lt;/p&gt;
&lt;p&gt;귀찮아서 미루기도 하고, 해야 할 일이 많아지면 부담을 느낄 때도 있다.&lt;/p&gt;
&lt;p&gt;그래도 결국 내가 해야 하는 일이라면 어떻게든 마무리하려고 한다.&lt;/p&gt;
&lt;p&gt;나는 또 &lt;strong&gt;생각이 많은 편&lt;/strong&gt;이라고 생각한다.&lt;/p&gt;
&lt;p&gt;어떤 선택을 하기 전에 여러 가능성을 생각하고, 내가 한 행동이 다른 사람에게 어떻게 보였을지를 나중에 다시 생각하는 경우도 많다.&lt;/p&gt;
&lt;p&gt;좋게 말하면 신중한 성격이지만, 반대로 보면 필요 이상으로 고민하는 성격이라고 할 수도 있을 것 같다.&lt;/p&gt;
&lt;p&gt;그리고 처음 만나는 사람에게는 조금 조용하지만 친해지고 나면 말이 많아지는 편이라고 생각한다.&lt;/p&gt;
&lt;p&gt;그래서 스스로를 완전히 내향적인 사람이라고 생각하지도 않고, 그렇다고 매우 외향적인 사람이라고 생각하지도 않는다.&lt;/p&gt;
&lt;p&gt;상황과 상대방에 따라 조금씩 다른 모습을 보이는 것 같다.&lt;/p&gt;
&lt;p&gt;또 하나는 &lt;strong&gt;나름대로 다른 사람을 배려하려고 하는 사람&lt;/strong&gt;이라고 생각한다는 점이다.&lt;/p&gt;
&lt;p&gt;상대방이 기분 나빠하지 않을지 생각하고 말을 고르기도 하고, 누군가 어려운 상황에 있으면 가능하면 도와주려고 한다.&lt;/p&gt;
&lt;p&gt;하지만 여기에서 한 가지 의문이 생긴다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 정말 배려심이 많은 사람일까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이것은 어디까지나 내가 나 자신에게 내린 평가다.&lt;/p&gt;
&lt;p&gt;나는 내가 배려하려고 했던 순간들을 잘 기억하고 있기 때문에 스스로를 배려하는 사람이라고 생각할 가능성이 있다.&lt;/p&gt;
&lt;p&gt;하지만 다른 사람들은 전혀 다르게 느끼고 있을 수도 있다.&lt;/p&gt;
&lt;p&gt;이렇게 내가 나의 생각과 경험을 바탕으로 형성한 모습이 내가 생각하는 &lt;strong&gt;주관적 자아&lt;/strong&gt;라고 볼 수 있을 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;2. 다른 사람이 바라보는 나: 객관적 자아&lt;/h1&gt;
&lt;p&gt;이번에는 조금 시선을 바꾸어 생각해보았다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;다른 사람들은 나를 어떤 사람이라고 생각할까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;물론 모든 사람에게 직접 물어본 것은 아니기 때문에 완벽하게 객관적인 평가라고 할 수는 없다.&lt;/p&gt;
&lt;p&gt;그래도 가족이나 친구들이 평소에 나에게 했던 말을 떠올려보면 내가 생각하지 못했던 모습들이 있다.&lt;/p&gt;
&lt;p&gt;주변 사람들은 나를 비교적 &lt;strong&gt;차분하고 신중한 사람&lt;/strong&gt;으로 보는 경우가 많다.&lt;/p&gt;
&lt;p&gt;나는 속으로 여러 가지 생각을 많이 하고 있기 때문에 스스로는 항상 차분하다고 느끼지 않는다.&lt;/p&gt;
&lt;p&gt;하지만 다른 사람에게는 내가 머릿속에서 어떤 고민을 하는지 모두 보이지 않는다.&lt;/p&gt;
&lt;p&gt;겉으로 표현되는 행동만 보면 말을 하기 전에 생각하고 행동하는 모습이 더 많이 보일 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 다른 사람에게는 내가 생각하는 것보다 훨씬 차분한 사람처럼 보일 수도 있다.&lt;/p&gt;
&lt;p&gt;반대로 나는 스스로 책임감이 강하다고 생각하지만 다른 사람에게는 때때로 &lt;strong&gt;일을 지나치게 오래 고민하는 사람&lt;/strong&gt;처럼 보일 수도 있을 것 같다.&lt;/p&gt;
&lt;p&gt;나는 실수하지 않기 위해 충분히 생각한다고 여기지만, 다른 사람에게는 결정이 느린 모습으로 보일 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;또 나는 내가 친한 사람에게는 장난도 많이 치고 말도 많이 한다고 생각한다.&lt;/p&gt;
&lt;p&gt;하지만 나와 아직 친하지 않은 사람은 그런 모습을 볼 기회가 많지 않다.&lt;/p&gt;
&lt;p&gt;그 사람의 입장에서는 나를&lt;/p&gt;
&lt;p&gt;&lt;code&gt;조용한 사람&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;혹은&lt;/p&gt;
&lt;p&gt;&lt;code&gt;조금 무뚝뚝한 사람&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이라고 생각할 수도 있다.&lt;/p&gt;
&lt;p&gt;이 부분을 생각하면서 한 가지 재미있는 점을 발견했다.&lt;/p&gt;
&lt;p&gt;나는 나 자신을 평가할 때 &lt;strong&gt;생각과 의도까지 포함해서 평가한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;반면 다른 사람은 주로 &lt;strong&gt;내가 실제로 보여준 행동을 바탕으로 나를 평가한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;나는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;상대방을 배려하려고 했다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 의도를 알고 있다.&lt;/p&gt;
&lt;p&gt;하지만 상대방이 그 배려를 실제 행동에서 느끼지 못했다면 상대방에게 나는 배려심 있는 사람이 아닐 수도 있다.&lt;/p&gt;
&lt;p&gt;그래서 다른 사람이 보는 나의 모습은 내가 생각하는 나와 다를 수밖에 없는 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;3. 주관적 자아와 객관적 자아는 왜 다를까?&lt;/h1&gt;
&lt;p&gt;두 가지 모습을 정리해보니 완전히 다른 사람이 등장하는 것은 아니었다.&lt;/p&gt;
&lt;p&gt;공통되는 부분도 있지만 강조되는 부분이 달랐다.&lt;/p&gt;
&lt;p&gt;내가 생각하는 나는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;책임감이 있다.&lt;/li&gt;
&lt;li&gt;생각이 많고 신중하다.&lt;/li&gt;
&lt;li&gt;다른 사람을 배려하려고 한다.&lt;/li&gt;
&lt;li&gt;친해지면 말이 많아진다.&lt;/li&gt;
&lt;li&gt;중요한 일은 끝까지 하려고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정도로 정리할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;반면 다른 사람이 바라보는 나는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;차분하다.&lt;/li&gt;
&lt;li&gt;신중하지만 결정을 오래 고민할 때가 있다.&lt;/li&gt;
&lt;li&gt;처음에는 조용해 보인다.&lt;/li&gt;
&lt;li&gt;맡은 일은 비교적 성실하게 한다.&lt;/li&gt;
&lt;li&gt;친해진 뒤의 모습과 처음의 모습에 차이가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;정도로 정리할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;두 평가에는 비슷한 부분이 있다.&lt;/p&gt;
&lt;p&gt;하지만 그 의미는 조금씩 다르다.&lt;/p&gt;
&lt;p&gt;예를 들어 나는 나 자신을 &lt;strong&gt;신중하다&lt;/strong&gt;고 평가하지만, 다른 사람에게는 같은 모습이 &lt;strong&gt;결정을 오래 고민한다&lt;/strong&gt;는 특징으로 보일 수 있다.&lt;/p&gt;
&lt;p&gt;또 나는 내가 사람을 배려한다고 생각하지만 다른 사람은 그것을 단순히 조용하거나 상대방의 의견을 잘 따르는 성격이라고 생각할 수도 있다.&lt;/p&gt;
&lt;p&gt;결국 같은 행동이라도 어떤 시선에서 바라보느냐에 따라 해석이 달라진다.&lt;/p&gt;
&lt;p&gt;여기에서 주관적 자아와 객관적 자아의 가장 큰 차이를 발견할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 나의 의도와 생각까지 알고 있지만, 다른 사람은 주로 나의 행동을 본다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;반대로 나 역시 나에게는 너무 익숙하기 때문에 발견하지 못하는 모습을 다른 사람은 쉽게 발견할 수도 있다.&lt;/p&gt;
&lt;p&gt;그래서 어느 한쪽만이 진짜 나라고 할 수는 없을 것 같다.&lt;/p&gt;
&lt;p&gt;내가 생각하는 나도 나의 한 부분이고, 다른 사람에게 반복해서 보이는 모습도 분명 나의 한 부분이다.&lt;/p&gt;
&lt;p&gt;오히려 두 가지를 함께 볼 때 내가 어떤 사람인지 조금 더 입체적으로 이해할 수 있는 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;그렇다면 어느 쪽이 더 정확한 나일까?&lt;/h1&gt;
&lt;p&gt;처음에는 주관적 자아보다 객관적 자아가 더 정확한 모습이 아닐까 생각했다.&lt;/p&gt;
&lt;p&gt;&amp;#39;객관적&amp;#39;이라는 단어 자체가 더 정확하게 들리기 때문이다.&lt;/p&gt;
&lt;p&gt;하지만 생각해보면 타인의 평가도 완전히 객관적일 수는 없다.&lt;/p&gt;
&lt;p&gt;사람마다 나를 만나는 상황이 다르고, 나와의 관계도 다르기 때문이다.&lt;/p&gt;
&lt;p&gt;가족이 보는 나와 친구가 보는 나는 다를 수 있고, 친구가 보는 나와 학교나 직장에서 만난 사람이 보는 나 역시 다를 수 있다.&lt;/p&gt;
&lt;p&gt;어떤 사람은 내가 긴장하고 있을 때의 모습만 봤을 수도 있고, 어떤 사람은 가장 편안하게 행동하고 있을 때의 나를 봤을 수도 있다.&lt;/p&gt;
&lt;p&gt;그렇다면 타인의 평가 역시 하나의 절대적인 정답이라기보다는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 사람의 시선에서 관찰된 나&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 보는 것이 더 적절한 것 같다.&lt;/p&gt;
&lt;p&gt;결국 중요한 것은 어느 한쪽의 평가를 무조건 믿는 것이 아니라 두 가지 사이의 차이를 살펴보는 것이다.&lt;/p&gt;
&lt;p&gt;내가 생각하는 나와 다른 사람들이 반복해서 말하는 나의 모습이 크게 다르다면 그 이유를 한번 생각해볼 필요가 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;4. 나는 나를 실제보다 좋게 생각하고 있을까?&lt;/h1&gt;
&lt;p&gt;여기에서 자연스럽게 &lt;strong&gt;자기고양편향&lt;/strong&gt;이라는 개념을 생각해볼 수 있다.&lt;/p&gt;
&lt;p&gt;자기고양편향은 자신의 능력이나 성격, 행동 등을 실제보다 조금 더 긍정적으로 평가하려는 경향을 의미한다.&lt;/p&gt;
&lt;p&gt;생각해보면 일상에서도 이런 모습을 쉽게 발견할 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 시험에서 좋은 점수를 받았을 때는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 열심히 공부했기 때문에 잘 본 것이다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하면서도,&lt;/p&gt;
&lt;p&gt;시험을 못 봤을 때는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;문제가 너무 어려웠다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;시험 범위가 너무 넓었다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;컨디션이 좋지 않았다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각할 때가 있다.&lt;/p&gt;
&lt;p&gt;좋은 결과가 나왔을 때는 나의 능력을 원인으로 생각하면서 좋지 않은 결과가 나왔을 때는 외부 환경을 원인으로 생각하는 것이다.&lt;/p&gt;
&lt;p&gt;인간관계에서도 비슷한 모습을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;친구와 의견 충돌이 생겼을 때 나는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 최대한 좋게 이야기하려고 했는데 상대방이 너무 예민하게 반응했다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각할 수 있다.&lt;/p&gt;
&lt;p&gt;그런데 상대방 역시 똑같이&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 잘 이야기하려고 했는데 쟤가 먼저 기분 나쁘게 말했다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각할 수도 있다.&lt;/p&gt;
&lt;p&gt;두 사람이 모두 자신을 조금 더 긍정적인 방향으로 해석하고 있는 셈이다.&lt;/p&gt;
&lt;p&gt;조별과제에서도 비슷하다.&lt;/p&gt;
&lt;p&gt;결과가 좋으면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;우리 조가 잘한 데에는 내가 맡은 부분도 꽤 중요했다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각하기 쉽다.&lt;/p&gt;
&lt;p&gt;반면 결과가 좋지 않다면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내 부분은 괜찮았는데 전체적인 방향이 좋지 않았다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각할 수도 있다.&lt;/p&gt;
&lt;p&gt;가만히 생각해보면 나 역시 이런 생각을 한 적이 있다.&lt;/p&gt;
&lt;p&gt;내가 성공했던 일은 나의 노력이나 능력과 연결하면서, 실패했던 일은 상황이나 운과 연결했던 경험이 있기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;자기고양편향은 나쁜 것일까?&lt;/h1&gt;
&lt;p&gt;처음에는 &amp;#39;편향&amp;#39;이라는 말을 들으면 무조건 고쳐야 할 문제처럼 느껴졌다.&lt;/p&gt;
&lt;p&gt;하지만 자기고양편향이 항상 부정적인 것만은 아닌 것 같다.&lt;/p&gt;
&lt;p&gt;어느 정도의 긍정적인 자기평가는 자신감을 유지하는 데 도움을 줄 수 있다.&lt;/p&gt;
&lt;p&gt;한 번 실패했다고 해서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;역시 나는 아무것도 못하는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각한다면 새로운 일을 다시 시도하기 어려울 것이다.&lt;/p&gt;
&lt;p&gt;반대로&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이번에는 잘되지 않았지만 다음에는 조금 더 잘할 수 있다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각한다면 다시 도전할 가능성이 높아진다.&lt;/p&gt;
&lt;p&gt;이런 의미에서 자기 자신을 어느 정도 긍정적으로 바라보는 것은 실패를 견디고 다시 시작하는 힘이 될 수 있다.&lt;/p&gt;
&lt;p&gt;또 모든 실수를 자신의 능력 부족 때문이라고 생각하지 않는 것도 필요하다.&lt;/p&gt;
&lt;p&gt;실제로 어떤 실패에는 환경이나 운, 예상하지 못한 상황이 영향을 줄 수도 있기 때문이다.&lt;/p&gt;
&lt;p&gt;그래서 적당한 자기고양은 자신감을 지켜주는 하나의 심리적인 장치가 될 수 있다고 생각한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;하지만 지나치면 문제가 될 수도 있다&lt;/h1&gt;
&lt;p&gt;반대로 자기고양편향이 지나치게 강해지면 문제가 생길 수 있다.&lt;/p&gt;
&lt;p&gt;무엇이든 잘된 것은 내 능력 덕분이고 잘못된 것은 다른 사람이나 환경 때문이라고 생각한다면 자신의 부족한 부분을 발견하기 어렵다.&lt;/p&gt;
&lt;p&gt;예를 들어 여러 사람이 반복해서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;너는 다른 사람의 말을 잘 듣지 않는 것 같아.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 말하는데도&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 사람들이 나를 제대로 이해하지 못하는 것이다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고만 생각한다면 변화할 기회를 놓칠 수 있다.&lt;/p&gt;
&lt;p&gt;팀 프로젝트에서도 자신의 기여는 크게 생각하고 다른 사람의 기여는 작게 생각하면 갈등이 생길 수 있다.&lt;/p&gt;
&lt;p&gt;무엇보다 자신의 모습을 실제보다 지나치게 높게 평가하게 되면 객관적인 피드백 자체를 받아들이기 어려워진다.&lt;/p&gt;
&lt;p&gt;그래서 자기고양편향은 &lt;strong&gt;있다/없다&lt;/strong&gt;의 문제보다 &lt;strong&gt;어느 정도인가&lt;/strong&gt;가 더 중요한 것 같다.&lt;/p&gt;
&lt;p&gt;적당한 자기고양은 자신감을 만들어주지만, 지나친 자기고양은 자기합리화가 될 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;그렇다면 나는 나 자신을 어떻게 바라봐야 할까?&lt;/h1&gt;
&lt;p&gt;이번에 주관적 자아와 객관적 자아를 비교하면서 한 가지 생각이 들었다.&lt;/p&gt;
&lt;p&gt;자기 자신을 정확하게 이해한다는 것은 스스로에 대해 완벽한 답을 가지고 있는 것이 아닐지도 모른다.&lt;/p&gt;
&lt;p&gt;오히려&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 생각하는 나&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;와&lt;/p&gt;
&lt;p&gt;&lt;code&gt;다른 사람이 경험하는 나&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;사이의 차이를 계속해서 확인하는 과정에 가까운 것 같다.&lt;/p&gt;
&lt;p&gt;내가 나 자신을 긍정적으로 생각하는 것은 필요하다.&lt;/p&gt;
&lt;p&gt;하지만 그 생각 때문에 다른 사람의 피드백을 모두 무시해서도 안 된다.&lt;/p&gt;
&lt;p&gt;반대로 다른 사람이 나를 어떻게 평가하는지만 신경 쓰다 보면 나 자신의 생각을 잃어버릴 수도 있다.&lt;/p&gt;
&lt;p&gt;그래서 나에게 필요한 태도는 두 가지 시선을 모두 사용하는 것이라고 생각한다.&lt;/p&gt;
&lt;p&gt;다른 사람의 평가는 나를 이해하기 위한 하나의 정보로 받아들이고, 그 평가가 정말 나에게 해당하는지는 스스로 다시 생각해보는 것이다.&lt;/p&gt;
&lt;p&gt;특히 여러 사람이 비슷한 이야기를 반복해서 한다면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;왜 사람들이 나를 그렇게 보고 있을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 질문을 해볼 필요가 있다.&lt;/p&gt;
&lt;p&gt;그리고 동시에&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 왜 나 자신을 이렇게 생각하고 있을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 질문도 해볼 수 있다.&lt;/p&gt;
&lt;p&gt;두 질문의 답을 함께 살펴볼 때 내가 미처 보지 못했던 나의 모습이 나타날 수도 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;나에게 적용해보기&lt;/h1&gt;
&lt;p&gt;이번 내용을 정리하면서 앞으로 가끔 이런 질문들을 나 자신에게 해보려고 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 나를 어떤 사람이라고 생각하고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;다른 사람들은 나를 어떤 사람이라고 말하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;두 평가에서 가장 크게 차이가 나는 부분은 무엇인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 나의 장점은 크게 보고 단점은 작게 보고 있지는 않은가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;다른 사람의 지적을 들었을 때 바로 변명부터 하고 있지는 않은가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;반대로 다른 사람의 평가를 지나치게 중요하게 받아들이고 있지는 않은가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 질문에 항상 정확한 답을 내릴 수는 없을 것이다.&lt;/p&gt;
&lt;p&gt;그래도 가끔 이런 질문을 해보는 것 자체가 나를 조금 더 객관적으로 바라보는 데 도움이 될 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;내가 느낀 점&lt;/h1&gt;
&lt;p&gt;이번에 나에 대한 자기평가와 타인의 평가를 나누어 생각해보면서 평소 내가 알고 있다고 생각했던 &amp;#39;나&amp;#39;도 사실 하나의 관점에서 만들어진 모습이라는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;나는 나의 생각과 의도를 알고 있기 때문에 자연스럽게 그것을 중심으로 나를 평가한다.&lt;/p&gt;
&lt;p&gt;반면 다른 사람은 내가 실제로 보여준 말과 행동을 중심으로 나를 평가한다.&lt;/p&gt;
&lt;p&gt;그래서 두 모습 사이에 차이가 생기는 것은 어쩌면 당연한 일일지도 모른다.&lt;/p&gt;
&lt;p&gt;중요한 것은 그 차이가 있다는 사실 자체가 아니라&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그 차이를 발견했을 때 어떻게 받아들이는가&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;인 것 같다.&lt;/p&gt;
&lt;p&gt;타인의 평가가 항상 맞는 것도 아니고 내가 생각하는 내가 항상 맞는 것도 아니다.&lt;/p&gt;
&lt;p&gt;내가 보지 못하는 모습을 다른 사람이 볼 수도 있고, 다른 사람이 알지 못하는 나의 모습을 나만 알고 있을 수도 있다.&lt;/p&gt;
&lt;p&gt;또 자기고양편향 역시 완전히 없애야 하는 것이라기보다 나에게 어느 정도 나타나고 있는지를 의식하는 것이 중요하다는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;자신을 긍정적으로 바라보는 것은 필요하지만, 그 긍정이 현실을 보지 못하게 만드는 순간에는 문제가 될 수 있기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;오늘의 결론&lt;/h1&gt;
&lt;p&gt;이번 생각을 한 문장으로 정리한다면 이렇게 말할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;내가 보는 나와 다른 사람이 보는 나 사이에서 진짜 나를 조금씩 찾아가자.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;나는 앞으로도 나 자신을 완벽하게 객관적으로 바라보기는 어려울 것이다.&lt;/p&gt;
&lt;p&gt;사람은 누구나 자기 자신을 자신의 시선으로 바라볼 수밖에 없기 때문이다.&lt;/p&gt;
&lt;p&gt;그리고 때로는 나에게 유리한 방향으로 나를 해석하기도 할 것이다.&lt;/p&gt;
&lt;p&gt;하지만 그것이 잘못이라고 생각하지는 않는다.&lt;/p&gt;
&lt;p&gt;다만 가끔은 다른 사람의 시선에서 나를 바라보고,&lt;/p&gt;
&lt;p&gt;내가 믿고 있는 나의 모습이 실제 행동에서도 나타나고 있는지 확인해볼 필요가 있다.&lt;/p&gt;
&lt;p&gt;결국 자신을 안다는 것은&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 이런 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 단정하는 것이 아니라,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;나는 나를 이렇게 생각하고 있는데, 다른 사람에게는 어떻게 보이고 있을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 질문을 계속해보는 과정인 것 같다.&lt;/p&gt;
&lt;p&gt;그리고 그 두 가지 시선 사이에서 내가 미처 알지 못했던 나를 발견해가는 것이 자기이해의 시작이라고 생각한다.&lt;/p&gt;</description>
      <category>etc/자아의 바다</category>
      <category>객관적 자아</category>
      <category>자기고양편향</category>
      <category>주관적 자아</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/88</guid>
      <comments>https://slowgrid.tistory.com/entry/How-Accurately-Do-I-See-Myself-Subjective-Self-Objective-Self-and-Self-Enhancement-Bias#entry88comment</comments>
      <pubDate>Sat, 19 Sep 2026 14:42:16 +0900</pubDate>
    </item>
    <item>
      <title>[YOLO 기초 8] 학습이 끝나면 무엇이 남을까?</title>
      <link>https://slowgrid.tistory.com/entry/YOLO-Basics-8-What-Remains-After-Learning-Is-Over</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;best.pt&lt;/code&gt;, &lt;code&gt;last.pt&lt;/code&gt;, 체크포인트, &lt;code&gt;resume=True&lt;/code&gt;, &lt;code&gt;results.csv&lt;/code&gt;, &lt;code&gt;args.yaml&lt;/code&gt;&lt;br&gt;&lt;strong&gt;버전 메모&lt;/strong&gt;&lt;br&gt;이 글은 &lt;strong&gt;2026-09-09 최종 검증 기준 Ultralytics 공식 문서와 공개 구현&lt;/strong&gt;을 직접 확인하면서 정리했다. &lt;code&gt;optimizer=auto&lt;/code&gt;, &lt;code&gt;cls_remap&lt;/code&gt;, 체크포인트 저장 형식, YOLO26의 &lt;code&gt;nms&lt;/code&gt;/손실 이름처럼 구현에 가까운 부분은 버전에 따라 달라질 수 있다. 그래서 실제 실험에서는 &lt;code&gt;ultralytics&lt;/code&gt; 버전, &lt;code&gt;args.yaml&lt;/code&gt;, 학습 로그를 함께 확인해 두는 편이 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;학습 루프가 끝나면 단순히 &amp;quot;모델 하나&amp;quot;만 남는 것이 아니다.&lt;/p&gt;
&lt;p&gt;Ultralytics 학습 실행에는 보통 이런 산출물이 남는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;weights/best.pt
weights/last.pt
results.csv
args.yaml
각종 평가 곡선과 혼동 행렬
실행 로그&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 파일들을 이해해야 &lt;strong&gt;어느 체크포인트를 추론에 써야 하는지&lt;/strong&gt;, &lt;strong&gt;중단된 학습을 어떻게 이어야 하는지&lt;/strong&gt;, &lt;strong&gt;실험이 실제 어떤 설정으로 실행됐는지&lt;/strong&gt;를 확인할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;먼저 &amp;quot;가중치&amp;quot;와 &amp;quot;학습 상태&amp;quot;를 구분해 보면&lt;/h2&gt;
&lt;p&gt;체크포인트를 이해할 때 가장 헷갈렸던 건 &lt;code&gt;.pt&lt;/code&gt;를 그냥 &amp;quot;가중치 파일&amp;quot;이라고 부르는 것과, 실제로 학습을 재개하는 데 필요한 상태가 더 많다는 점이었다.&lt;/p&gt;
&lt;p&gt;학습 중에는 대략 다음 두 종류의 정보가 함께 필요할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;모델 자체와 관련된 정보
→ 모델 구조를 다시 구성하는 데 필요한 정보
→ 모델 / EMA 가중치

학습 진행 상태와 관련된 정보
→ 현재 epoch
→ optimizer 상태
→ GradScaler 상태
→ best_fitness
→ train_args / metrics 등&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;추론만 한다면 주로 최종 모델 가중치가 중요하지만, &lt;strong&gt;중간에 끊긴 같은 학습을 그대로 이어가려면 optimizer나 epoch 같은 진행 상태도 중요하다.&lt;/strong&gt; 그래서 같은 &lt;code&gt;.pt&lt;/code&gt; 확장자라도 언제 저장됐고 어떤 목적으로 정리된 체크포인트인지에 따라 의미가 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;이 구분을 먼저 잡아두면 &lt;code&gt;last.pt&lt;/code&gt;, &lt;code&gt;best.pt&lt;/code&gt;, &lt;code&gt;resume=True&lt;/code&gt;, 새로운 fine-tuning의 차이가 훨씬 자연스럽게 연결된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. &lt;code&gt;last.pt&lt;/code&gt;는 무엇인가?&lt;/h2&gt;
&lt;p&gt;YOLO 학습 결과에서는 보통 &lt;strong&gt;체크포인트(checkpoint)&lt;/strong&gt;가 저장된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;last.pt&lt;/code&gt;는 이름 그대로 &lt;strong&gt;실제로 마지막까지 수행된 에포크의 상태&lt;/strong&gt;를 저장한 체크포인트다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;에포크 1
...
에포크 49
에포크 50
   ↓
last.pt&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다만 조기 종료(Early Stopping)가 발생했다면 사용자가 처음 지정한 최대 에포크가 아니라 &lt;strong&gt;조기 종료되기 직전의 마지막 수행 에포크&lt;/strong&gt;가 &lt;code&gt;last.pt&lt;/code&gt;가 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;last.pt&lt;/code&gt;는 학습을 이어서 재개(resume)해야 할 때 특히 유용하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. &lt;code&gt;best.pt&lt;/code&gt;는 무엇인가?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;best.pt&lt;/code&gt;라는 이름만 보면 마지막에 가장 잘 학습된 모델처럼 느껴지지만, 실제로는 검증 결과에서 가장 높은 &lt;strong&gt;적합도(fitness)&lt;/strong&gt;를 기록한 시점의 체크포인트다.&lt;/p&gt;
&lt;p&gt;현재 Ultralytics 객체 탐지 기본 설정에서는 &lt;strong&gt;적합도(fitness)&lt;/strong&gt;가 사실상 &lt;strong&gt;&lt;code&gt;mAP50-95&lt;/code&gt;&lt;/strong&gt;를 기준으로 한다. 즉 기본 가중치가 &lt;code&gt;[정밀도, 재현율, mAP50, mAP50-95]&lt;/code&gt; 중 &lt;code&gt;mAP50-95&lt;/code&gt;에만 적용된다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;best.pt
=
무조건 마지막 에포크&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 아니다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;에포크 35
→ 검증 mAP50-95 최고

에포크 36 이후
→ 학습 손실은 더 감소할 수 있음
→ 검증 성능은 조금씩 하락

에포크 50
→ 학습 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;best.pt
→ 에포크 35의 체크포인트

last.pt
→ 에포크 50의 체크포인트&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;이 차이가 바로 &lt;code&gt;학습 손실이 계속 내려가도 마지막 모델이 가장 좋은 모델은 아닐 수 있다&lt;/code&gt;는 것을 보여준다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 왜 최종 추론에는 &lt;code&gt;best.pt&lt;/code&gt;를 자주 사용할까?&lt;/h2&gt;
&lt;p&gt;현재 프로젝트에서는 최종 추론에&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;results/clahe/best.pt&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 사용한다.&lt;/p&gt;
&lt;p&gt;이 체크포인트에는 선택된 모델의 가중치와 모델을 다시 불러오는 데 필요한 정보가 포함되어 있어 추론이나 검증에 사용할 수 있다. 학습을 그대로 이어서 재개하는 목적이라면 일반적으로 &lt;code&gt;last.pt&lt;/code&gt;를 사용하는 편이 더 자연스럽다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from ultralytics import YOLO

model = YOLO(&amp;quot;results/clahe/best.pt&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;새로운 열화상 이미지에서 예측을 수행하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습
 ↓
best.pt
 ↓
추론(Inference)
 ↓
보행자 탐지&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 이어진다.&lt;/p&gt;
&lt;p&gt;다만 &lt;code&gt;best.pt&lt;/code&gt;의 &amp;quot;best&amp;quot;는 &lt;strong&gt;프레임워크가 사용한 검증 기준에서 가장 좋았다&lt;/strong&gt;는 뜻이지, 내 서비스 요구사항에서 반드시 최고라는 뜻은 아니다. 이 기준과 실제 운영 요구사항의 차이는 Precision/Recall/mAP를 다루는 다음 글에서 다시 살펴본다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 체크포인트에는 가중치만 있는 것이 아니다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.pt&lt;/code&gt;를 단순히 가중치 파일이라고 부르면 이해하기 쉽지만, 실제 Ultralytics 체크포인트에는 더 많은 정보가 들어갈 수 있다.&lt;/p&gt;
&lt;p&gt;공식 YOLO26 체크포인트에서는 학습에 사용된 설정을 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from ultralytics import YOLO

model = YOLO(&amp;quot;yolo26n.pt&amp;quot;)
print(model.ckpt[&amp;quot;train_args&amp;quot;])&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기에는 이런 사전 학습 레시피가 들어갈 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;옵티마이저
학습률
배치 크기
입력 크기(`imgsz`)
데이터 증강
손실 가중치
워밍업 설정
학습 결과 기록&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 Ultralytics 학습기(Trainer)가 &lt;strong&gt;학습 도중 저장하는 체크포인트&lt;/strong&gt;에는 더 많은 상태가 포함될 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;epoch
best_fitness
EMA 모델
옵티마이저 상태
GradScaler 상태
train_args
train_metrics
train_results
Ultralytics 버전
Git 정보&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;등이다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 여기에는 꽤 중요한 차이가 있다.&lt;/p&gt;
&lt;h3&gt;학습 도중 체크포인트와 학습 완료 후 체크포인트는 상태가 다를 수 있다&lt;/h3&gt;
&lt;p&gt;현재 학습기(Trainer)는 정상적으로 학습이 끝난 뒤 &lt;code&gt;last.pt&lt;/code&gt;와 &lt;code&gt;best.pt&lt;/code&gt;에 대해 옵티마이저 정보를 제거하는 최종 정리 과정을 수행한다.&lt;/p&gt;
&lt;p&gt;이 과정에서는 EMA 모델을 최종 모델로 사용하고, 옵티마이저 / 스케일러 같은 재개용 상태를 제거한다.&lt;/p&gt;
&lt;p&gt;즉&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 도중 last.pt
→ 학습 재개에 필요한 상태를 포함할 수 있음

정상 종료 후 최종 last.pt / best.pt
→ 추론 / 검증 / 새로운 미세 조정에 적합하도록 정리된 체크포인트&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 구분해서 이해하는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. &lt;code&gt;resume=True&lt;/code&gt;와 미세 조정은 같은 말이 아니다&lt;/h2&gt;
&lt;p&gt;둘은 자주 혼동된다.&lt;/p&gt;
&lt;h3&gt;학습 재개(Resume)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;중간에 끊긴 같은 학습 실행
 ↓
재개 가능한 last.pt
 ↓
저장된 에포크 / 옵티마이저 / 스케일러 / EMA 상태 등을 복원
 ↓
기존 설정과 진행 상태를 이어서 학습&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 학습기(Trainer)는 학습률 스케줄러 객체 자체를 그대로 직렬화해 저장하기보다, 저장된 설정과 시작 에포크를 바탕으로 스케줄러를 다시 구성하고 위치를 맞춘다.&lt;/p&gt;
&lt;p&gt;정리하면 단순히&lt;/p&gt;
&lt;p&gt;&lt;code&gt;scheduler state까지 그대로 저장했다가 통째로 복원한다&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;가 아닌 &lt;strong&gt;학습 상태를 재구성해 이어가는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;h3&gt;미세 조정(Fine-Tuning)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;사전 학습 모델 또는 이전 best.pt
 ↓
새로운 학습 실행 시작
 ↓
새 데이터셋 / 새 하이퍼파라미터로 추가 학습&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model.train(resume=True)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;는 보통&lt;/p&gt;
&lt;p&gt;&lt;code&gt;중간에 끊긴 같은 실험을 이어서 실행&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;하는 의미에 가깝다.&lt;/p&gt;
&lt;p&gt;반면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model = YOLO(&amp;quot;previous_best.pt&amp;quot;)
model.train(data=&amp;quot;new.yaml&amp;quot;, ...)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;은&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이전 가중치를 초기값으로 사용하는 새로운 미세 조정 실험&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;에 가깝다.&lt;/p&gt;
&lt;p&gt;그리고 같은 글에서 확인했듯 학습이 정상적으로 완전히 끝난 뒤 최종 정리된 체크포인트는 옵티마이저 상태가 제거될 수 있으므로, 이런 파일은 일반적으로 &lt;strong&gt;새로운 미세 조정 시작점&lt;/strong&gt;으로 생각해야 한다.&lt;/p&gt;
&lt;p&gt;실험 관리에서는 이 둘을 구분해야 결과 디렉터리와 학습 이력을 올바르게 해석할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 학습 결과 디렉터리에서 무엇을 확인할까?&lt;/h2&gt;
&lt;p&gt;Ultralytics 학습 실행에는 일반적으로 이런 산출물이 생성된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;weights/
  best.pt
  last.pt

results.csv
args.yaml
results.png
confusion_matrix*.png
PR_curve.png
F1_curve.png&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정확한 파일은 Ultralytics 버전과 설정에 따라 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;각 파일은 다음 용도로 볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;args.yaml
→ 이번 실행에 사용된 설정 확인

results.csv
→ 에포크별 학습 손실 / 검증 지표 / 학습률 분석

best.pt
→ 검증 기준으로 가장 좋았던 체크포인트

last.pt
→ 마지막 학습 시점의 체크포인트

PR_curve.png
→ Precision-Recall 관계 분석

F1_curve.png
→ confidence threshold에 따른 F1 변화 분석

confusion_matrix*.png
→ 어떤 클래스에서 어떤 오류가 발생했는지 분석&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 프로젝트는 &lt;code&gt;person&lt;/code&gt; 한 클래스만 사용하므로 혼동 행렬의 클래스 간 혼동보다 &lt;strong&gt;배경을 객체로 잘못 검출하는 False Positive와 객체를 놓치는 False Negative&lt;/strong&gt; 쪽이 더 중요하며,&lt;/p&gt;
&lt;p&gt;또 전처리 비교 실험에서는 &lt;code&gt;best.pt&lt;/code&gt; 하나만 남기는 것보다 &lt;strong&gt;실행 디렉터리 전체&lt;/strong&gt;를 보관하는 편이 좋다.&lt;/p&gt;
&lt;p&gt;최소한 다음은 함께 보관하는 것을 권장한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;weights/
results.csv
args.yaml
실행 로그
사용한 dataset YAML
전처리 설정
ultralytics / torch / CUDA 버전
Git commit 또는 코드 버전&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;7. 왜 학습 스크립트와 설정 파일을 따로 관리할까?&lt;/h2&gt;
&lt;p&gt;간단한 실험이라면 노트북(Notebook) 환경에서 직접 &lt;code&gt;model.train()&lt;/code&gt;을 호출해도 된다.&lt;/p&gt;
&lt;p&gt;하지만 여러 실험을 반복해야 한다면 스크립트와 설정 파일을 분리하는 편이 관리하기 좋다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;train.py
 ↓
실험 이름
 ↓
설정 파일
 ↓
데이터셋 YAML
 ↓
모델
 ↓
학습
 ↓
결과 디렉터리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 파이프라인을 만들 수 있다.&lt;/p&gt;
&lt;p&gt;이렇게 하면 다음 내용을 추적하기 쉬워진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;어떤 설정으로 실행했는가?
어떤 데이터셋을 사용했는가?
어떤 전처리를 사용했는가?
어떤 seed를 사용했는가?
실제 옵티마이저 / 학습률은 무엇이었는가?
결과가 어디에 저장됐는가?&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;8. 실제 실행 로그에서 확인하면 좋은 것&lt;/h2&gt;
&lt;p&gt;학습을 실행한 뒤 콘솔 로그와 &lt;code&gt;results.csv&lt;/code&gt;에서 다음 내용을 확인하면 좋다.&lt;/p&gt;
&lt;h3&gt;1) 데이터셋 수&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 이미지 개수
검증 이미지 개수
클래스 개수&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;전처리 실험별로 이 수가 같은지 확인한다.&lt;/p&gt;
&lt;h3&gt;2) 실제 옵티마이저&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;optimizer=auto&lt;/code&gt;를 사용했다면 AdamW인지 MuSGD인지 확인한다.&lt;/p&gt;
&lt;h3&gt;3) 실제 학습률&lt;/h3&gt;
&lt;p&gt;코드의 &lt;code&gt;lr0&lt;/code&gt;가 아니라 자동 설정 후 실제 적용된 학습률을 확인한다.&lt;/p&gt;
&lt;h3&gt;4) 실제 배치 크기&lt;/h3&gt;
&lt;p&gt;첫 에포크 OOM 자동 재시도가 발생했다면 원래 &lt;code&gt;batch=16&lt;/code&gt;보다 작아졌을 수 있으므로 로그를 확인한다.&lt;/p&gt;
&lt;h3&gt;5) 손실 항목&lt;/h3&gt;
&lt;p&gt;현재 공개 &lt;code&gt;main&lt;/code&gt;의 YOLO26(&lt;code&gt;reg_max=1&lt;/code&gt;)에서는 세 번째 검출 loss가 &lt;code&gt;l1_loss&lt;/code&gt;라는 이름으로 기록된다. 설정에는 여전히 &lt;code&gt;dfl&lt;/code&gt; gain 키가 남아 있지만 실제 계산은 DFL 분포 연산이 아니라 box-distance L1 계열 손실이다. 과거 버전의 로그나 checkpoint에서 &lt;code&gt;dfl_loss&lt;/code&gt;라는 이름을 보더라도 기존 Distribution Focal Loss와 바로 같은 것으로 해석하지 않는다.&lt;/p&gt;
&lt;h3&gt;6) 학습률 변화&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;lr/pg...&lt;/code&gt; 열을 함께 보면서 손실과 성능 변화가 워밍업 / 스케줄 구간과 연결되는지 본다.&lt;/p&gt;
&lt;h3&gt;7) 최고 성능 에포크&lt;/h3&gt;
&lt;p&gt;최고 성능이 마지막 에포크에 나왔는지, 중간 에포크에서 나왔는지 확인한다.&lt;/p&gt;
&lt;p&gt;이런 기록이 있어야 나중에&lt;/p&gt;
&lt;p&gt;&lt;code&gt;왜 베이스라인보다 CLAHE가 좋아졌는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;또는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;왜 같은 코드를 다시 실행했는데 결과가 달라졌는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;같은 질문에 답하기 쉬워진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;last.pt
→ 마지막으로 수행된 학습 시점의 체크포인트

best.pt
→ 검증 fitness가 가장 좋았던 시점의 체크포인트

resume=True
→ 중단된 같은 학습 실행의 상태를 이어가는 것

YOLO(&amp;quot;previous_best.pt&amp;quot;).train(...)
→ 이전 가중치를 초기값으로 새로운 미세 조정 실행을 시작하는 것&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또 &lt;code&gt;.pt&lt;/code&gt;는 단순한 파라미터 배열만이 아니라 모델과 학습을 재구성하거나 분석하는 데 필요한 메타데이터를 포함할 수 있다. 학습 중 체크포인트와 정상 종료 후 정리된 체크포인트는 옵티마이저 및 GradScaler 같은 재개용 상태의 포함 여부가 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;재현 가능한 실험을 위해서는 &lt;code&gt;best.pt&lt;/code&gt; 하나만 보관하기보다 최소한 다음을 함께 보관해 두는 편이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;weights/
results.csv
args.yaml
실행 로그
사용한 dataset YAML
전처리 설정
ultralytics / torch / CUDA 버전
코드 버전 또는 Git commit&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 학습 결과 파일이 무엇을 의미하는지 알았으니, 다음 질문은 &lt;strong&gt;그 체크포인트가 정말 좋은 모델인지 어떻게 판단하느냐&lt;/strong&gt;다. 다음 글에서는 학습 손실, 검증 성능, 과적합과 일반화를 구분해서 살펴볼 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고한 공식 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ultralytics Train Mode: &lt;a href=&quot;https://docs.ultralytics.com/modes/train/&quot;&gt;https://docs.ultralytics.com/modes/train/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics Configuration: &lt;a href=&quot;https://docs.ultralytics.com/usage/cfg/&quot;&gt;https://docs.ultralytics.com/usage/cfg/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics &lt;code&gt;BaseTrainer&lt;/code&gt;: &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/trainer.py&quot;&gt;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/trainer.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics Loss 구현: &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/loss.py&quot;&gt;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/loss.py&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>args.yaml</category>
      <category>best.pt</category>
      <category>last.pt</category>
      <category>results.csv</category>
      <category>resume=True</category>
      <category>YoLO</category>
      <category>체크포인트</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/87</guid>
      <comments>https://slowgrid.tistory.com/entry/YOLO-Basics-8-What-Remains-After-Learning-Is-Over#entry87comment</comments>
      <pubDate>Fri, 18 Sep 2026 20:33:06 +0900</pubDate>
    </item>
    <item>
      <title>[YOLO 기초 7] model.train() 내부에서는 실제로 무슨 일이 일어날까?</title>
      <link>https://slowgrid.tistory.com/entry/YOLO-Basics-7-What-Actually-Happens-Inside-modeltrain</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;순전파, 손실, 역전파, 기울기 누적, 워밍업, AMP, EMA&lt;br&gt;&lt;strong&gt;버전 메모&lt;/strong&gt;&lt;br&gt;이 글은 &lt;strong&gt;2026-09-09 최종 검증 기준 Ultralytics 공식 문서와 공개 구현&lt;/strong&gt;을 직접 확인하면서 정리했다. &lt;code&gt;optimizer=auto&lt;/code&gt;, &lt;code&gt;cls_remap&lt;/code&gt;, 체크포인트 저장 형식, YOLO26의 &lt;code&gt;nms&lt;/code&gt;/손실 이름처럼 구현에 가까운 부분은 버전에 따라 달라질 수 있다. 그래서 실제 실험에서는 &lt;code&gt;ultralytics&lt;/code&gt; 버전, &lt;code&gt;args.yaml&lt;/code&gt;, 학습 로그를 함께 확인해 두는 편이 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;앞에서는 &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;epochs&lt;/code&gt;, &lt;code&gt;batch&lt;/code&gt;, &lt;code&gt;imgsz&lt;/code&gt;, &lt;code&gt;optimizer&lt;/code&gt; 같은 학습 인자가 무엇을 의미하는지 살펴봤다.&lt;/p&gt;
&lt;p&gt;이번에는 &lt;code&gt;model.train()&lt;/code&gt; 한 줄 뒤에서 돌아가는 &lt;strong&gt;내부 학습 루프&lt;/strong&gt;를 순서대로 따라가 볼 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;배치 준비
↓
순전파
↓
예측과 정답 매칭
↓
손실 계산
↓
역전파
↓
기울기 누적 / 스케일 복원 / 클리핑
↓
옵티마이저 실행
↓
가중치 갱신
↓
EMA 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YOLO26의 Dual Head, STAL, DFL-Free 구조 자체는 앞선 글에서 자세히 다뤘으므로 여기서는 &lt;strong&gt;Trainer가 이 손실을 실제 최적화 루프 안에서 어떻게 처리하는지&lt;/strong&gt;에 초점을 둔다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. &lt;code&gt;model.train()&lt;/code&gt; 내부의 큰 흐름&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;model.train()&lt;/code&gt;은 한 줄이지만 내부에서는 모델, 데이터로더, 옵티마이저, 학습률 스케줄러, AMP, EMA, 검증기 등을 준비한 뒤 많은 반복 연산을 수행한다.&lt;/p&gt;
&lt;p&gt;먼저 학습 시작 전에는 대략 이런 준비가 일어난다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;설정 읽기
 ↓
seed / deterministic 설정
 ↓
데이터셋 확인
 ↓
모델 생성 및 가중치 불러오기
 ↓
데이터로더 생성
 ↓
옵티마이저 생성
 ↓
학습률 스케줄러 생성
 ↓
AMP / GradScaler 준비
 ↓
EMA 모델 준비&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그다음 각 배치에서는 대략 다음 과정이 반복된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지 / 라벨 불러오기
 ↓
데이터 증강
 ↓
바운딩 박스 좌표 함께 변환
 ↓
텐서 전처리
 ↓
GPU로 이동
 ↓
워밍업에 따른 LR / momentum / accumulate 조정
 ↓
순전파
 ↓
예측값 생성
 ↓
정답 라벨과 매칭
 ↓
손실 계산
 ↓
역전파
 ↓
기울기 계산 및 누적
 ↓
업데이트 시점이면 기울기 스케일 복원
 ↓
기울기 클리핑
 ↓
옵티마이저 실행
 ↓
가중치 갱신
 ↓
EMA 갱신
 ↓
다음 배치&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 공개 Trainer는 옵티마이저 실행 직전에 기울기를 원래 크기로 되돌린 뒤 &lt;code&gt;max_norm=10.0&lt;/code&gt; 기준으로 &lt;strong&gt;기울기 클리핑(gradient clipping)&lt;/strong&gt;을 수행한다.&lt;/p&gt;
&lt;h3&gt;순전파와 가중치 업데이트 사이에는 무엇이 있을까?&lt;/h3&gt;
&lt;p&gt;처음 학습 코드를 보면 &lt;code&gt;forward → backward → update&lt;/code&gt; 정도로만 생각하기 쉽다. 그런데 객체 탐지에서는 순전파 결과를 바로 역전파할 수 있는 것이 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;순전파
↓
많은 prediction candidate 생성
↓
GT와 candidate를 label assignment로 매칭
↓
어떤 prediction이 positive인지 결정
↓
box / class / distance 손실 계산
↓
전체 손실을 기준으로 backward
↓
각 파라미터에 gradient 계산
↓
누적 조건을 만족하면 optimizer step&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 &lt;strong&gt;label assignment는 단순한 평가용 후처리가 아니라 학습 손실을 만들기 전에 어떤 prediction을 정답과 연결할지 결정하는 과정&lt;/strong&gt;이다. 여기서 선택된 positive와 target을 바탕으로 손실이 계산되고, 그 손실의 미분값이 역전파되어 각 파라미터의 gradient가 만들어진다.&lt;/p&gt;
&lt;p&gt;또 &lt;code&gt;backward()&lt;/code&gt;가 호출됐다고 해서 그 순간 바로 가중치가 바뀌는 것도 아니다. 기울기는 먼저 파라미터에 계산 및 누적되고, 실제 가중치 변화는 누적 조건을 만족해 &lt;strong&gt;옵티마이저가 실행되는 시점&lt;/strong&gt;에 일어난다. 그래서 앞 글에서 본 것처럼 배치 반복 횟수와 weight update 횟수가 항상 같지는 않다.&lt;/p&gt;
&lt;h3&gt;YOLO26의 손실 계산&lt;/h3&gt;
&lt;p&gt;YOLO26은 one-to-many와 one-to-one 두 검출 경로를 함께 학습한다. one-to-many 쪽은 더 많은 positive를 통해 풍부한 supervision을 주고, one-to-one 쪽은 더 엄격한 대응을 학습한다. 공식 학습 설명에서는 Progressive Loss를 통해 학습 진행에 따라 두 경로의 비중을 조절하고, 작은 객체의 candidate assignment에는 STAL이 관여한다.&lt;/p&gt;
&lt;p&gt;여기서 현재 공개 &lt;code&gt;Detect.forward()&lt;/code&gt; 구현을 보면 한 가지 중요한 차이가 있다. &lt;strong&gt;학습 중 one-to-one head에는 Backbone/Neck feature의 &lt;code&gt;detach()&lt;/code&gt;된 tensor가 들어간다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Backbone + Neck feature x
├─ one-to-many head
│    → x를 그대로 사용
│    → one-to-many loss의 gradient가 Backbone/Neck까지 연결
│
└─ one-to-one head
     → x.detach() 사용
     → one-to-one head 자체의 파라미터는 학습
     → 하지만 이 branch를 통한 gradient는 Backbone/Neck으로 역전파되지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 두 head가 같은 Backbone/Neck의 &lt;strong&gt;feature 값&lt;/strong&gt;을 사용한다는 말과, 두 loss가 모두 같은 방식으로 Backbone/Neck을 업데이트한다는 말은 다르다. 현재 구현에서는 one-to-one branch가 shared feature에서 gradient graph를 끊어 두고, one-to-one head 자체를 별도로 최적화한다.&lt;/p&gt;
&lt;p&gt;Progressive Loss도 실제 backward에 들어가는 loss를 단순 합으로 두지 않는다. 현재 공개 &lt;code&gt;main&lt;/code&gt;의 &lt;code&gt;E2ELoss&lt;/code&gt;는 대략 다음처럼 동작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;L_total
= w_o2m(t) × L_one-to-many
+ w_o2o(t) × L_one-to-one

초기
w_o2m = 0.8
w_o2o = 0.2

학습이 진행되면
w_o2m → 0.1
w_o2o → 0.9&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trainer는 에포크가 끝날 때 criterion의 &lt;code&gt;update()&lt;/code&gt;를 호출해 이 비중을 갱신한다. 현재 &lt;code&gt;main&lt;/code&gt;에서는 one-to-many assignment에 &lt;code&gt;tal_topk=10&lt;/code&gt;, one-to-one 쪽에는 &lt;code&gt;tal_topk=7&lt;/code&gt;, &lt;code&gt;tal_topk2=1&lt;/code&gt;을 사용한다. &lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;reg_max=1&lt;/code&gt;인 YOLO26은 기존의 분포 기반 DFL 연산을 제거한 DFL-free 구조다. &lt;strong&gt;현재 공개 &lt;code&gt;main&lt;/code&gt;에서는 이 세 번째 loss 항의 이름을 &lt;code&gt;l1_loss&lt;/code&gt;로 표시한다.&lt;/strong&gt; 설정의 gain 키는 여전히 &lt;code&gt;dfl&lt;/code&gt;을 재사용하지만, &lt;code&gt;reg_max=1&lt;/code&gt;이면 실제 계산은 box-distance L1 계열 손실이다. 과거 버전 로그나 checkpoint metadata에서는 &lt;code&gt;dfl_loss&lt;/code&gt;라는 이름을 볼 수 있으므로 이름만 보고 기존 Distribution Focal Loss라고 판단하면 안 된다.&lt;/p&gt;
&lt;p&gt;Trainer 관점에서 정리하면 &lt;strong&gt;prediction → assignment → branch별 loss 계산 → Progressive Loss 가중 결합 → backward&lt;/strong&gt; 순서로 학습 신호가 만들어진다. 다만 one-to-one branch는 feature를 detach해서 사용한다는 점까지 같이 기억해야 실제 gradient 흐름을 정확히 이해할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 기울기 누적(Gradient Accumulation)&lt;/h2&gt;
&lt;p&gt;앞에서 &lt;code&gt;batch=16&lt;/code&gt;을 한 번에 16장의 이미지를 처리하는 배치 크기로 보았다.&lt;/p&gt;
&lt;p&gt;현재 Ultralytics Trainer에는 &lt;code&gt;nbs&lt;/code&gt;라는 &lt;strong&gt;명목상 배치 크기&lt;/strong&gt;가 있고 기본값은 64다.&lt;/p&gt;
&lt;p&gt;Trainer는 초기값으로 대략&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;accumulate = round(nbs / batch)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 사용한다.&lt;/p&gt;
&lt;p&gt;현재 설정에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;nbs = 64
batch = 16

64 / 16 = 4&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이므로 워밍업 이후 약 4개의 배치 기울기를 모아 한 번 옵티마이저를 실행할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Batch 1
순전파 → 역전파
→ 기울기 저장

Batch 2
순전파 → 역전파
→ 기울기 누적

Batch 3
순전파 → 역전파
→ 기울기 누적

Batch 4
순전파 → 역전파
→ 기울기 누적
→ 기울기 클리핑
→ 옵티마이저 실행
→ EMA 갱신
→ 기울기 초기화&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 워밍업 동안에는 &lt;code&gt;accumulate&lt;/code&gt; 값도 1에서 목표 값 쪽으로 점진적으로 바뀐다.&lt;/p&gt;
&lt;p&gt;이 때문에 정확한 가중치 갱신 횟수를 단순히&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;2110 / 16&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 식으로 계산하면 안 된다.&lt;/p&gt;
&lt;p&gt;또 Ultralytics는 실제 배치 크기와 &lt;code&gt;accumulate&lt;/code&gt;, &lt;code&gt;nbs&lt;/code&gt;를 이용해 &lt;code&gt;weight_decay&lt;/code&gt;도 스케일링한다.&lt;/p&gt;
&lt;p&gt;다시 말해 배치 크기는 단순히 GPU 메모리 사용량만 바꾸는 값이 아니라 &lt;strong&gt;최적화 동작에도 영향을 주는 값&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 워밍업과 학습률 스케줄러&lt;/h2&gt;
&lt;p&gt;학습 초반부터 목표 학습률을 바로 사용하면 큰 가중치 변화 때문에 학습이 불안정해질 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 Ultralytics는 기본적으로 워밍업 구간을 둔다.&lt;/p&gt;
&lt;p&gt;현재 기본값은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;warmup_epochs = 3.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;워밍업 동안에는 학습률, momentum, 기울기 누적 횟수 등이 목표 값으로 점진적으로 이동한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 초반
→ 워밍업
→ LR / momentum / accumulate 점진적 변화

중반
→ 본 학습 구간

후반
→ 학습률 스케줄에 따라 변화&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 에포크가 진행될수록 학습률 스케줄러가 파라미터 그룹별 학습률을 조정한다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;results.csv&lt;/code&gt;를 볼 때 손실과 mAP만 보지 말고&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;lr/pg0
lr/pg1
lr/pg2
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 열도 함께 보면 좋다.&lt;/p&gt;
&lt;p&gt;그러면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;성능 변화가 학습률 스케줄의 어느 구간에서 발생했는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 해석하는 데 도움이 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. AMP와 기울기 스케일링&lt;/h2&gt;
&lt;p&gt;현재 기본값은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;amp=True&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;Ultralytics에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;amp=True 또는 amp=&amp;quot;fp16&amp;quot;
→ FP16 자동 혼합 정밀도

amp=&amp;quot;bf16&amp;quot;
→ BF16 혼합 정밀도

amp=False 또는 amp=&amp;quot;fp32&amp;quot;
→ FP32&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 구분한다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;amp=True&lt;/code&gt;를&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FP16 또는 BF16 중 알아서 하나를 사용한다&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 이해하면 정확하지 않으며, 현재 기준으로 &lt;code&gt;True&lt;/code&gt;는 FP16 경로이다.&lt;/p&gt;
&lt;p&gt;자동 혼합 정밀도를 사용하면 일부 연산을 낮은 정밀도로 수행해 지원 GPU에서 다음 효과를 기대할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GPU 메모리 사용량 감소
+
처리량 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 FP16은 표현 범위가 좁아 아주 작은 기울기가 0에 가까워지는 언더플로(underflow) 문제가 생길 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 FP16 경로에서는 &lt;code&gt;GradScaler&lt;/code&gt;를 사용해 손실을 일시적으로 크게 만든 뒤 역전파하고, 옵티마이저를 실행하기 전에 기울기를 다시 원래 크기로 되돌린다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;손실
 ↓ × 스케일
스케일된 손실
 ↓
역전파
 ↓
스케일된 기울기
 ↓ ÷ 스케일
원래 크기의 기울기
 ↓
기울기 클리핑
 ↓
옵티마이저 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trainer에서는 BF16을 사용할 때는 일반적인 FP16용 &lt;code&gt;GradScaler&lt;/code&gt;를 활성화하지 않는다.&lt;/p&gt;
&lt;p&gt;사용자는 &lt;code&gt;model.train()&lt;/code&gt; 한 줄만 호출하지만 내부에서는 이런 수치 안정성 처리까지 함께 이루어진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 기울기 클리핑(Gradient Clipping)&lt;/h2&gt;
&lt;p&gt;현재 공개 Trainer는 옵티마이저 실행 직전에 기울기를 원래 크기로 되돌린 뒤 &lt;code&gt;max_norm=10.0&lt;/code&gt; 기준으로 기울기 클리핑을 수행한다.&lt;/p&gt;
&lt;p&gt;개념만 놓고 보면 다음 위치에 들어간다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;역전파
↓
기울기 누적
↓
AMP 사용 시 기울기 스케일 복원
↓
기울기 클리핑
↓
옵티마이저 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기울기 클리핑은 특정 스텝에서 기울기가 지나치게 커지는 것을 제한해 학습 안정성에 도움을 주는 수치적 안전장치로 볼 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. EMA: 부드러운 가중치를 하나 더 추적한다&lt;/h2&gt;
&lt;p&gt;Ultralytics Trainer는 학습 중 &lt;strong&gt;EMA(Exponential Moving Average, 지수 이동 평균)&lt;/strong&gt; 모델을 유지한다.&lt;/p&gt;
&lt;p&gt;옵티마이저가 가중치를 갱신할 때마다 EMA도 함께 갱신된다.&lt;/p&gt;
&lt;p&gt;이것도 개념을 놓고 보면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;현재 학습 가중치
      +
이전 EMA 가중치
      ↓
새 EMA 가중치&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 최근 가중치를 반영하되 한 번의 업데이트에 너무 크게 흔들리지 않도록 부드럽게 추적하는 방식이다.&lt;/p&gt;
&lt;p&gt;현재 Trainer의 옵티마이저 단계는 대략 다음 순서다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;기울기 스케일 복원
 ↓
기울기 클리핑
 ↓
옵티마이저 실행
 ↓
GradScaler 갱신
 ↓
기울기 초기화
 ↓
EMA 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 체크포인트를 저장할 때도 EMA 모델이 중요한 역할을 한다.&lt;/p&gt;
&lt;p&gt;이 때문에 학습 중 콘솔에서 보고 있는 순간적인 모델 가중치와 최종 &lt;code&gt;.pt&lt;/code&gt;에 남는 모델이 완전히 같은 개념이라고 할 수 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 한 에포크가 끝나면 무엇을 할까?&lt;/h2&gt;
&lt;p&gt;배치 반복이 끝나면 한 에포크의 학습이 완료된다.&lt;/p&gt;
&lt;p&gt;현재 기본값 &lt;code&gt;val=True&lt;/code&gt;에서는 검증도 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 Epoch 학습 완료
 ↓
EMA 속성 동기화
 ↓
검증 데이터셋으로 예측
 ↓
Precision / Recall / mAP 등 계산
 ↓
학습 손실 + 검증 지표 + 학습률 기록
 ↓
best fitness 갱신 여부 확인
 ↓
체크포인트 저장
 ↓
조기 종료 조건 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;검출 학습에서는 &lt;code&gt;best.pt&lt;/code&gt;를 단순히 &lt;strong&gt;마지막 에포크의 모델&lt;/strong&gt;로 저장하지 않는다.&lt;/p&gt;
&lt;p&gt;검증에서 사용되는 fitness가 가장 좋았던 시점의 체크포인트를 &lt;code&gt;best.pt&lt;/code&gt;로 저장한다. 현재 Ultralytics의 일반 검출 Trainer 문서에서는 기본 fitness를 &lt;code&gt;mAP@0.5:0.95&lt;/code&gt;로 설명한다.&lt;/p&gt;
&lt;p&gt;즉&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;last.pt
→ 가장 최근 에포크의 상태

best.pt
→ 검증 기준으로 가장 좋았던 에포크의 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 이해해 두면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;model.train()&lt;/code&gt;은 단순히 예측하고 바로 가중치를 바꾸는 함수가 아니다. 실제로는 다음 과정이 서로 맞물린다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지 / 라벨 준비
↓
데이터 증강과 텐서 전처리
↓
순전파
↓
예측과 GT 매칭
↓
손실 계산
↓
역전파
↓
기울기 누적
↓
AMP 사용 시 unscale
↓
기울기 클리핑
↓
optimizer.step()
↓
가중치 갱신
↓
EMA 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 학습 초반에는 워밍업이 학습률·momentum·누적 횟수를 점진적으로 조정하며, 에포크가 진행되면 학습률 스케줄러가 파라미터 그룹의 학습률을 바꾼다.&lt;/p&gt;
&lt;p&gt;결국 &lt;code&gt;batch=16&lt;/code&gt;, &lt;code&gt;lr0=...&lt;/code&gt; 같은 한두 값만으로 실제 최적화 동작을 모두 설명할 수 없다. &lt;strong&gt;적용된 옵티마이저, nbs, accumulate, AMP, warmup, scheduler까지 함께 봐야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;다음 글에서는 이렇게 학습한 상태가 &lt;code&gt;best.pt&lt;/code&gt;, &lt;code&gt;last.pt&lt;/code&gt;, &lt;code&gt;results.csv&lt;/code&gt;, &lt;code&gt;args.yaml&lt;/code&gt;에 어떻게 남고, &lt;code&gt;resume=True&lt;/code&gt;와 새로운 미세 조정이 어떻게 다른지 살펴볼 예정이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고한 공식 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ultralytics Train Mode: &lt;a href=&quot;https://docs.ultralytics.com/modes/train/&quot;&gt;https://docs.ultralytics.com/modes/train/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics Configuration: &lt;a href=&quot;https://docs.ultralytics.com/usage/cfg/&quot;&gt;https://docs.ultralytics.com/usage/cfg/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;YOLO26 Training Recipe: &lt;a href=&quot;https://docs.ultralytics.com/guides/yolo26-training-recipe/&quot;&gt;https://docs.ultralytics.com/guides/yolo26-training-recipe/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics &lt;code&gt;Detect&lt;/code&gt; Head: &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/nn/modules/head.py&quot;&gt;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/nn/modules/head.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics Loss 구현: &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/loss.py&quot;&gt;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/loss.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics &lt;code&gt;BaseTrainer&lt;/code&gt;: &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/trainer.py&quot;&gt;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/trainer.py&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>AMP</category>
      <category>ema</category>
      <category>train</category>
      <category>Ultralytics</category>
      <category>YOLO26</category>
      <category>기울기 누적</category>
      <category>손실</category>
      <category>순전파</category>
      <category>역전파</category>
      <category>워밍업</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/86</guid>
      <comments>https://slowgrid.tistory.com/entry/YOLO-Basics-7-What-Actually-Happens-Inside-modeltrain#entry86comment</comments>
      <pubDate>Thu, 17 Sep 2026 20:45:16 +0900</pubDate>
    </item>
    <item>
      <title>나는 나 자신을 어떻게 인식하고 있는가?: 브루스 브레켄의 자기개념 6가지 차원</title>
      <link>https://slowgrid.tistory.com/entry/Bruce-Breckens-6-Dimensions-of-Self-Concept-How-Do-I-Perceive-Myself</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 공부한 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 브루스 브레켄(Bruce A. Bracken)의 &lt;b&gt;다차원적 자기개념(Multidimensional Self-Concept)&lt;/b&gt;에 대해 공부해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 흔히 자기 자신에 대해 이야기할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 자신감이 있는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 자존감이 낮은 편이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 하나의 표현으로 자신을 설명하곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 인간이 자신을 바라보는 방식은 생각보다 훨씬 복잡하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 사람은 친구들과 관계를 맺는 데에는 자신감을 가지고 있으면서도 공부에 대해서는 자신이 없을 수 있다. 또 다른 사람은 자신의 업무 능력에는 확신을 가지고 있지만 외모나 신체적인 능력에 대해서는 부정적인 생각을 가지고 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;자신에 대한 평가는 하나의 차원으로만 이루어지는 것이 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄은 이러한 관점에서 자기개념을 하나의 단일한 특성이 아니라 &lt;b&gt;여러 생활 영역에서 형성되는 다차원적인 심리적 구성개념&lt;/b&gt;으로 설명하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그가 개발한 &lt;b&gt;다차원적 자기개념 척도(Multidimensional Self-Concept Scale, MSCS)&lt;/b&gt;에서는 자기개념을 다음의 여섯 영역으로 구분한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사회적 자기개념(Social Self-Concept)&lt;/li&gt;
&lt;li&gt;유능감 자기개념(Competence Self-Concept)&lt;/li&gt;
&lt;li&gt;정서적 자기개념(Affect Self-Concept)&lt;/li&gt;
&lt;li&gt;학업적 자기개념(Academic Self-Concept)&lt;/li&gt;
&lt;li&gt;가족 자기개념(Family Self-Concept)&lt;/li&gt;
&lt;li&gt;신체적 자기개념(Physical Self-Concept)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이러한 영역들과 함께 개인이 자신을 전체적으로 어떻게 평가하는지를 나타내는 &lt;b&gt;전반적 자기개념(Global Self-Concept)&lt;/b&gt;을 측정한다. 브레켄의 MSCS는 1992년에 출판되었으며 여섯 개의 영역별 자기개념과 전반적 자기개념을 측정하도록 구성되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;먼저, 자기개념이란 무엇일까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기개념(Self-Concept)이란 쉽게 말하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lsquo;나는 어떤 사람인가?&amp;rsquo;에 대해 스스로 가지고 있는 인식과 평가의 체계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 자신의 능력, 인간관계, 정서, 학업, 가족관계, 신체적인 특성 등에 대해 자신이 어떻게 생각하고 평가하는지가 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 자기개념이 단순히 객관적인 능력이나 실제 모습을 의미하는 것이 아니라는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 실제로 수학 성적이 높은 학생이라도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 수학을 잘하지 못한다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 현재 성적이 아주 높지 않더라도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 노력하면 수학 문제를 충분히 해결할 수 있다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 자신을 평가할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 수행(actual performance)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자신이 지각하는 능력(perceived competence)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은 반드시 일치하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기개념을 연구할 때 중요한 것은 바로 이러한 &lt;b&gt;자기 자신에 대한 주관적인 지각과 평가&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄의 MSCS에 대한 심리측정 문헌에서도 자기개념은 개인이 과거의 경험과 행동을 평가하면서 형성되고, 현재 행동과 이후 행동에도 관련되는 &lt;b&gt;다차원적이고 맥락 의존적인 구성개념&lt;/b&gt;으로 설명된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;자기개념과 자아존중감은 같은 것일까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기개념을 공부하다 보면 자연스럽게 &lt;b&gt;자아존중감(Self-Esteem)&lt;/b&gt;이라는 용어를 만나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개념은 서로 밀접하게 관련되어 있기 때문에 일상에서는 비슷한 의미로 사용되는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개념적으로 보면 조금 다르게 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기개념은 주로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 어떤 사람인가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 무엇을 잘하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 사람들과 어떻게 관계를 맺는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 자신에 대한 &lt;b&gt;구체적인 인식과 평가&lt;/b&gt;를 포함한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 자아존중감은 일반적으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 가치 있는 사람이라고 느끼는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 자신에 대한 &lt;b&gt;전반적인 가치 판단이나 존중의 정도&lt;/b&gt;와 더욱 관련된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄과 Howell의 연구에서도 자기개념과 자아존중감의 관계를 살펴보면서 자기개념이 단순한 하나의 점수가 아니라 여러 하위영역을 가진 다차원적 구성개념이라는 점을 검토하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 자기개념을 이해할 때는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;자존감이 높다 / 낮다&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 하나의 평가보다는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lsquo;나는 어떤 영역에서 나 자신을 긍정적으로 또는 부정적으로 인식하고 있는가?&amp;rsquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 살펴보는 것이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;브레켄이 자기개념을 &amp;lsquo;다차원적&amp;rsquo;이라고 본 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기개념 연구에서 중요한 변화 중 하나는 자기개념을 단일한 성격 특성처럼 보는 것에서 벗어나, &lt;b&gt;여러 영역으로 구성된 체계로 이해하기 시작했다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄 역시 이러한 다차원적 관점에서 자기개념을 설명하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람은 생활하는 과정에서 다양한 환경을 경험한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학교에서는 학생이라는 역할을 경험하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가정에서는 가족 구성원으로 생활하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친구들과는 사회적인 관계를 형성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 자신의 감정을 경험하고, 새로운 문제를 해결하고, 자신의 외모와 신체적 능력을 평가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 한 사람이 가지고 있는 자기개념도 이러한 다양한 생활 영역에 따라 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄의 자기개념 구조를 간단하게 나타내면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사회적 자기개념
       │
유능감 자기개념
       │
정서적 자기개념
       │
학업적 자기개념
       │
가족 자기개념
       │
신체적 자기개념
       &amp;darr;
전반적 자기개념&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 중요한 점은 이 여섯 가지 영역이 서로 아무 관계 없이 독립적으로 존재하는 것은 아니라는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영역별 자기개념은 구별될 수 있지만 동시에 서로 관련되어 있으며, 자기 자신에 대한 보다 전반적인 인식과 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄 등의 후속 연구에서도 여러 자기개념 검사 자료를 분석했을 때 사회적, 정서적, 유능감, 학업적, 가족, 신체적 자기개념에 해당하는 주요 요인과 함께 일반적인 자기개념 요인이 확인되어 &lt;b&gt;자기개념의 다차원적 구조를 지지하는 결과&lt;/b&gt;가 보고되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. 사회적 자기개념&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Social Self-Concept&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사회적 자기개념은 &lt;b&gt;다른 사람과의 관계 속에서 자기 자신을 어떻게 인식하고 평가하는가&lt;/b&gt;와 관련된 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 생각하면 다음과 같은 질문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 사람들과 잘 어울리는 사람인가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;다른 사람들이 나를 좋아한다고 느끼는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 다른 사람과 관계를 형성하는 데 자신이 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 집단 안에서 자연스럽게 행동할 수 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람은 다른 사람과 관계를 맺으면서 다양한 피드백을 경험한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친구들이 나를 찾아주거나 내 이야기를 잘 들어주는 경험이 반복된다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 사람들이 함께 있고 싶어 하는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 자기개념이 형성될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 거절이나 갈등의 경험이 반복될 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;사람들은 나를 좋아하지 않는 것 같다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 사회적 자기개념이 형성될 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 것은 실제 사회적 기술 자체와 사회적 자기개념이 반드시 같은 것은 아니라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객관적으로는 인간관계를 잘 유지하고 있음에도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 사람들과 잘 어울리지 못한다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 자신을 인식할 수도 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 사회적 자기개념은 단순히 친구가 몇 명인지와 같은 객관적인 사실만으로 설명할 수 있는 것이 아니라 &lt;b&gt;사회적 상황 속에서 스스로를 어떻게 바라보고 있는가&lt;/b&gt;에 초점을 둔다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. 유능감 자기개념&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Competence Self-Concept&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유능감 자기개념은 자신의 능력과 문제 해결 능력에 대한 인식과 관련된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 필요한 일을 제대로 해낼 수 있는 사람인가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 질문과 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 목표를 달성하는 능력, 새로운 상황에 적응하는 능력, 문제를 해결하는 능력, 주어진 일을 성공적으로 수행할 수 있다는 인식 등이 포함될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 처음 접하는 프로젝트를 맡았을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;처음이라 어렵겠지만 하나씩 알아가면 해결할 수 있을 것 같다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각하는 사람이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 이런 일을 해낼 능력이 없다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각하는 사람도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 사람에게 실제로 주어진 능력 수준이 비슷하더라도 &lt;b&gt;자신의 능력에 대한 주관적인 평가&lt;/b&gt;는 크게 다를 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 실패를 경험했을 때 이러한 차이가 잘 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 실패를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;이번 방법이 잘못되었다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 해석할 수도 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 원래 능력이 부족한 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 해석할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후자의 해석이 반복될 경우 단순한 한 번의 실패 경험이 자신의 능력 전체에 대한 부정적인 자기개념으로 확대될 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 유능감 자기개념을 이해할 때는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lsquo;실제로 얼마나 잘하는가?&amp;rsquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐만 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lsquo;자신이 스스로의 능력을 얼마나 신뢰하는가?&amp;rsquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 함께 살펴봐야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. 정서적 자기개념&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Affect Self-Concept&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정서적 자기개념은 자신의 감정과 정서적인 반응에 대해 가지고 있는 자기인식을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 질문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 감정을 잘 조절하는 사람인가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 쉽게 불안해지는 사람이라고 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;스트레스를 받았을 때 비교적 안정적으로 대응하는 편인가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;감정적인 상황에서 나 자신을 어떻게 평가하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람은 슬픔, 기쁨, 분노, 불안, 긴장과 같은 다양한 감정을 경험한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 중요한 것은 어떤 감정을 느꼈다는 사실 자체만이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 경험을 통해 자기 자신을 어떻게 해석하는지가 자기개념 형성에 영향을 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 중요한 발표 전에 긴장한 경험을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;중요한 발표니까 긴장할 수도 있지.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 해석할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 긴장하면 아무것도 할 수 없는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 자신을 평가할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 감정적 경험을 하더라도 자신에 대한 해석은 달라질 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 정서적 자기개념은 감정을 느끼는 빈도만을 의미하는 것이 아니라 &lt;b&gt;자신의 정서적 특성과 반응 방식에 대해 스스로 가지고 있는 인식&lt;/b&gt;과 관련된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. 학업적 자기개념&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Academic Self-Concept&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학업적 자기개념은 학교생활이나 학습 상황에서 자신의 능력과 수행을 어떻게 평가하고 있는지를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 공부를 잘하는 편이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 새로운 내용을 이해하는 능력이 있다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 시험에 약한 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;수학은 나와 맞지 않는다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 공부를 열심히 해도 잘하지 못할 것이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학업적 자기개념을 이해할 때 특히 중요한 것은 &lt;b&gt;실제 성적과 자기개념을 구분하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 동일하게 70점을 받은 두 학생이 있다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 학생은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;이번 단원은 충분히 공부하지 못했으니까 다음에는 더 잘할 수 있을 것 같다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 다른 학생은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 역시 공부에 소질이 없다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 학생의 객관적인 점수는 같지만 자신에 대한 해석은 크게 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 학업적 자기개념은 단순히 성적이나 지능 수준을 의미하는 것이 아니라 &lt;b&gt;학습자로서 자기 자신을 어떻게 인식하고 있는가&lt;/b&gt;를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점 때문에 교육현장에서는 결과만큼이나 학생이 자신의 성공과 실패를 어떤 방식으로 해석하는지를 살펴볼 필요가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. 가족 자기개념&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Family Self-Concept&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가족 자기개념은 가족이라는 환경 안에서 자신을 어떻게 지각하고 평가하고 있는지를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 가족에게 사랑받고 있다고 느끼는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;가족은 나를 중요한 사람이라고 생각한다고 느끼는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 가족에게 내 생각을 편하게 이야기할 수 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 가족 구성원으로서 만족감을 느끼는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 질문과 관련될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가족은 성장 과정에서 비교적 지속적으로 경험하게 되는 중요한 사회적 환경 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가족 안에서 반복적으로 인정받고 존중받는 경험을 한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 가족에게 중요한 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 긍정적인 자기개념이 형성될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 지속적인 비교나 갈등, 거절 등을 경험한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 가족의 기대를 충족시키지 못하는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 인식이 형성될 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 역시 중요한 것은 실제 가족관계에 대한 객관적인 평가뿐 아니라 &lt;b&gt;본인이 그 관계를 어떻게 지각하고 있는가&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. 신체적 자기개념&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Physical Self-Concept&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신체적 자기개념은 자신의 외모와 신체적인 특성 및 능력에 대해 가지고 있는 자기인식을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 신체적 자기개념을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;외모에 만족하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 생각하기 쉽지만 그 범위는 조금 더 넓다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 내 외모를 어떻게 평가하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 운동 능력이 있다고 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 내 신체에 만족하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;다른 사람 앞에서 내 모습에 자신감을 느끼는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 요소를 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 사람은 자신의 외모에는 긍정적이지만 운동 능력에는 자신이 없을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 운동이나 신체 활동에는 자신이 있지만 외모에 대해서는 부정적으로 생각할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 신체적 자기개념은 단순한 외모 만족을 넘어서 &lt;b&gt;자신의 신체적 모습과 신체적 능력을 어떻게 지각하고 평가하는가&lt;/b&gt;와 관련된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;전반적 자기개념&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Global Self-Concept&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄의 이론에서 여섯 가지 하위영역과 함께 중요한 것이 &lt;b&gt;전반적 자기개념(Global Self-Concept)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전반적 자기개념은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;전체적으로 볼 때 나는 나 자신을 어떻게 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 관련된 보다 포괄적인 자기평가라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 한 사람이 다음과 같은 자기개념을 가지고 있다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사회적 자기개념      높음
유능감 자기개념      높음
정서적 자기개념      보통
학업적 자기개념      낮음
가족 자기개념        높음
신체적 자기개념      낮음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사람을 단순히&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;자기개념이 높다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;자기개념이 낮다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 평가한다면 중요한 정보를 놓칠 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 이 사람은 인간관계나 자신의 능력에 대해서는 상당히 긍정적으로 평가하면서도 학업이나 신체 영역에서는 자신을 부정적으로 평가하고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 브레켄의 다차원적 관점이 가지는 중요한 의미는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전반적인 평가만 보는 것이 아니라 구체적으로 어느 영역에서 긍정적 또는 부정적인 자기인식이 형성되어 있는지를 살펴볼 수 있다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;브레켄의 MSCS는 어떻게 구성되어 있을까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄은 이러한 이론적 구조를 실제로 측정하기 위해 &lt;b&gt;Multidimensional Self-Concept Scale(MSCS)&lt;/b&gt;을 개발하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSCS는 원래 &lt;b&gt;9세에서 19세 아동/청소년을 대상으로 하는 자기보고식 척도&lt;/b&gt;로, 총 150개 문항으로 구성되며 사회적, 유능감, 정서적, 학업적, 가족, 신체적 영역을 측정한다. 각 하위영역은 25개 문항으로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;주요 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;사회적 자기개념&lt;/td&gt;
&lt;td&gt;다른 사람과의 관계 및 사회적 수용에 대한 자기인식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유능감 자기개념&lt;/td&gt;
&lt;td&gt;자신의 능력, 문제 해결 및 수행 능력에 대한 인식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정서적 자기개념&lt;/td&gt;
&lt;td&gt;자신의 정서와 감정적 반응에 대한 인식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학업적 자기개념&lt;/td&gt;
&lt;td&gt;학습 및 학업 수행 능력에 대한 자기평가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가족 자기개념&lt;/td&gt;
&lt;td&gt;가족관계와 가족 내 자신의 위치에 대한 인식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;신체적 자기개념&lt;/td&gt;
&lt;td&gt;외모와 신체적 능력에 대한 자기평가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전반적 자기개념&lt;/td&gt;
&lt;td&gt;여러 생활영역을 포함한 자신에 대한 포괄적인 인식&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 여섯 영역을 활용해 스스로 자신의 모습을 돌아보는 것과 &lt;b&gt;표준화된 MSCS 검사를 실시하고 해석하는 것은 구분할 필요가 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 자기성찰을 위해 여섯 영역을 활용할 수는 있지만, 이를 실제 심리검사 결과처럼 해석해서는 안 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;이 이론이 중요한 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄의 이론을 공부하면서 가장 중요하다고 느낀 부분은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 영역에서의 실패를 자기 자신 전체에 대한 실패로 일반화할 필요가 없다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 시험에서 좋지 않은 결과를 받았다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;이번 시험 결과가 좋지 않았다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 학업 영역의 경험이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리는 때때로 이것을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 공부를 못한다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 더 나아가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 능력이 없는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 확대해석한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친구와 갈등이 생긴 경우에도 마찬가지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;이번 관계에서 갈등이 발생했다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 하나의 사회적 경험인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 인간관계를 못한다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 문제가 있는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 자기평가로 확장될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄의 다차원적 관점에서 보면 이런 일반화를 조금 다르게 생각해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 경험은 &lt;b&gt;자기개념을 구성하는 특정 영역과 관련된 경험&lt;/b&gt;일 수 있으며, 그것만으로 자신의 모든 특성을 설명할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 관점은 상담이나 교육 장면에서도 의미가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;이 학생은 자신감이 없다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 판단하는 것보다는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;어떤 영역에서 자신감이 낮은가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 살펴보는 것이 더 구체적인 정보를 제공할 수 있기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;나에게 적용해보기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄의 여섯 가지 자기개념 영역을 나 자신에게 적용해보면 다음과 같은 질문을 던져볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사회적 자기개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 인간관계 속에서 나를 어떤 사람이라고 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;사람들이 나를 어떻게 받아들이고 있다고 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유능감 자기개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;새로운 문제를 만났을 때 나는 해결할 수 있다고 믿는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 내가 맡은 일을 해낼 능력이 있다고 생각하는가?&lt;/code&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정서적 자기개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 내 감정을 어떻게 바라보고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;감정이 크게 변할 때 나는 나 자신을 어떻게 평가하는가?&lt;/code&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;학업적 자기개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 배우고 학습하는 능력에 대해 나 자신을 어떻게 평가하는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;한 번의 실패를 나의 학습 능력 전체에 대한 평가로 확대하고 있지는 않은가?&lt;/code&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가족 자기개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 가족 안에서 어떤 사람이라고 느끼는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 가족으로부터 존중받고 있다고 느끼는가?&lt;/code&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;신체적 자기개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 나의 외모와 신체적 능력을 어떻게 바라보고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;다른 사람과 비교하지 않고 내 신체를 바라볼 수 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 마지막으로 다음 질문을 해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;이 여섯 영역 가운데 나는 어떤 영역의 평가를 나라는 사람 전체의 평가처럼 받아들이고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문이 오히려 브레켄의 이론을 이해하는 데 가장 중요한 질문일지도 모르겠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;자기개념은 고정된 것일까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기개념을 공부할 때 한 가지 주의해야 할 점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기개념은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 원래 이런 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 영원히 고정되어 있는 성질로만 이해하기 어렵다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기개념은 다양한 경험과 타인의 반응, 성공과 실패, 사회적 비교 등을 경험하면서 형성되고 변화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 처음에는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 발표를 못하는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각했던 사람이라도 여러 차례 발표 경험을 쌓고 긍정적인 피드백을 받게 되면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;긴장하기는 하지만 발표를 해낼 수 있는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 방향으로 자기개념이 변화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 자기개념을 이해한다는 것은 현재의 나에게 고정된 이름표를 붙이는 과정이라기보다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지금 나는 나 자신을 어떤 방식으로 인식하고 있는지를 발견하는 과정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 이해하는 편이 더 적절하다고 생각한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;내가 느낀 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄의 자기개념 이론을 공부하면서 가장 인상적이었던 점은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나라는 사람을 하나의 기준으로 판단할 필요가 없다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 가끔 특정 영역에서 경험한 실패를 자기 자신 전체의 문제처럼 받아들인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험에서 실패하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 능력이 없다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인간관계에서 갈등이 발생하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 사람들과 잘 지내지 못한다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외모에 자신이 없으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 매력이 없는 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 브레켄의 관점에서 자신을 바라본다면 질문이 조금 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 어떤 영역에서 나 자신을 부정적으로 평가하고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;그 평가가 정말 나라는 사람 전체를 설명할 수 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 물을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누군가는 학업적인 자기개념은 낮지만 매우 좋은 사회적 관계를 형성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누군가는 신체적인 자기개념은 낮지만 문제를 해결하는 자신의 능력에는 높은 자신감을 가지고 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 누군가는 밖에서는 자신감이 부족하지만 가족 안에서는 자신이 충분히 사랑받고 가치 있는 사람이라고 느낄 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 하나의 영역에서 느끼는 부족함이 나라는 사람 전체의 부족함을 의미하지는 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;오늘의 결론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브루스 브레켄은 자기개념을 단순히&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 나를 좋아한다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 자신감이 없다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 하나의 차원으로 바라보지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그는 자기개념을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사회적 자기개념&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;유능감 자기개념&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정서적 자기개념&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;학업적 자기개념&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가족 자기개념&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;신체적 자기개념&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 여섯 가지 생활 영역에서 살펴보았으며, 이와 함께 자신에 대한 보다 포괄적인 평가인 &lt;b&gt;전반적 자기개념&lt;/b&gt;을 다루었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이론에서 중요한 것은 단순히 여섯 가지 용어를 외우는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;자기 자신에 대한 평가는 영역에 따라 달라질 수 있다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 사실을 이해하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 부족하다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 느낄 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 구체적으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 어떤 영역에서 나를 부족하다고 생각하고 있는가?&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 질문해볼 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 막연하게 나 전체의 문제라고 생각했던 것이 실제로는 학업, 인간관계, 정서, 가족, 신체 또는 특정 능력과 관련된 &lt;b&gt;한 영역의 자기평가&lt;/b&gt;였다는 것을 발견할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 자기 자신을 이해한다는 것은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 이런 사람이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 하나의 문장으로 자신을 규정하는 것이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;삶의 여러 영역 속에서 내가 나를 어떻게 바라보고 있는지를 구체적으로 발견해가는 과정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 볼 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브레켄의 다차원적 자기개념 이론은 바로 그 과정을 보다 체계적으로 바라볼 수 있도록 해주는 하나의 틀이라고 생각한다.&lt;/p&gt;</description>
      <category>etc/자아의 바다</category>
      <category>브루스 브레켄</category>
      <category>성장</category>
      <category>성찰</category>
      <category>자기개념</category>
      <category>자기개념 6가지 차원</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/85</guid>
      <comments>https://slowgrid.tistory.com/entry/Bruce-Breckens-6-Dimensions-of-Self-Concept-How-Do-I-Perceive-Myself#entry85comment</comments>
      <pubDate>Wed, 16 Sep 2026 19:04:41 +0900</pubDate>
    </item>
    <item>
      <title>나는 나를 얼마나 알고 있을까?: 조하리의 창을 공부하며</title>
      <link>https://slowgrid.tistory.com/entry/Studying-the-Johari-Window-How-Well-Do-I-Know-Myself</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 배운 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &lt;b&gt;조하리의 창(Johari's Window)&lt;/b&gt;에 대해 공부해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 사람의 성격을 네 가지 영역으로 분류하는 이론이라고 생각했는데, 공부해보니 핵심은 분류 자체보다는 &lt;b&gt;자기 인식(Self-awareness), 피드백(Feedback), 자기개방(Self-disclosure)을 통해 사람과 사람 사이의 이해가 어떻게 변화하는지를 설명하는 모델&lt;/b&gt;에 가까웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조하리의 창은 심리학자 &lt;b&gt;Joseph Luft와 Harrington Ingham이 1955년 그룹 역학과 인간관계를 연구하는 과정에서 개발한 모델&lt;/b&gt;이며,&lt;br /&gt;&lt;code&gt;Johari&lt;/code&gt;라는 이름도 두 사람의 이름인 &lt;b&gt;Joseph + Harrington&lt;/b&gt;에서 따온 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조하리의 창 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oAXL6/dJMcablAh9M/T42BidRsUZF4LBwRhVGFR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oAXL6/dJMcablAh9M/T42BidRsUZF4LBwRhVGFR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oAXL6/dJMcablAh9M/T42BidRsUZF4LBwRhVGFR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoAXL6%2FdJMcablAh9M%2FT42BidRsUZF4LBwRhVGFR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 열린 영역 &amp;mdash; Open Area / Arena&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열린 영역은 &lt;b&gt;나도 알고 있고 다른 사람도 알고 있는 나의 모습&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 내가 스스로 생각하기에도 개발을 좋아하고, 주변 사람들도 내가 개발을 좋아한다는 것을 알고 있다면 이것은 열린 영역에 속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성격뿐만 아니라 행동, 기술, 지식, 태도, 경험처럼 서로 공유되고 있는 정보들도 열린 영역에 포함될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 팀 프로젝트를 진행하면서 다음과 같은 사실을 서로 알고 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나는 백엔드 개발에 관심이 많다.&lt;/code&gt;&lt;br /&gt;&lt;code&gt;나는 발표하는 것보다는 코드를 작성하는 것을 좋아한다.&lt;/code&gt;&lt;br /&gt;&lt;code&gt;나는 문제가 발생하면 원인을 오래 분석하는 편이다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 역시 이것을 알고 있고 팀원들도 알고 있다면 모두 열린 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조하리의 창에서는 일반적으로 &lt;b&gt;열린 영역이 커질수록 서로를 이해하기 쉬워지고 협력과 의사소통도 원활해질 가능성이 높다&lt;/b&gt;고 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 중요한 점이 하나 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;열린 영역을 넓힌다는 것이 자신의 모든 정보를 공개해야 한다는 뜻은 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사생활이나 민감한 정보까지 공유할 필요는 없다. 관계와 상황에 맞는 적절한 자기개방이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 맹목 영역 &amp;mdash; Blind Area / Blind Spot&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맹목 영역은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다른 사람은 알고 있지만 나는 모르는 나의 모습&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 나는 평범하게 설명하고 있다고 생각하는데 주변 사람들은 내가 설명할 때 말을 너무 빠르게 한다고 느낄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 나는 스스로 리더십이 부족하다고 생각하지만 다른 팀원들은 나를 보고 다음과 같이 평가할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;회의가 산으로 갈 때 항상 핵심을 정리해준다.&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 모습도 맹목 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 맹목 영역에는 단점만 들어가는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내가 발견하지 못했던 장점이나 능력 역시 다른 사람이 먼저 발견할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 영역을 줄이는 가장 중요한 방법이 바로 &lt;b&gt;피드백을 받는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람의 관찰을 통해 스스로 인식하지 못했던 행동이나 특성을 발견하면 해당 정보가 맹목 영역에서 열린 영역으로 이동할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 피드백 질문의 예시는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;내가 회의할 때 고쳤으면 하는 습관이 있어?&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;같이 프로젝트하면서 내가 잘한다고 느낀 부분이 뭐였어?&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조하리의 창을 공부하면서 &lt;b&gt;타인의 시선도 자기 자신을 이해할 수 있는 하나의 데이터가 될 수 있다&lt;/b&gt;는 생각이 들었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 숨겨진 영역 &amp;mdash; Hidden Area / Fa&amp;ccedil;ade&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숨겨진 영역은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나는 알고 있지만 다른 사람에게는 알려주지 않은 나의 모습&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적인 고민이나 감정뿐만 아니라 업무 방식이나 선호도 역시 여기에 포함될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 팀 프로젝트를 하면서 사실 어떤 기술을 잘 모르고 있는데도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;이걸 모른다고 하면 실력이 부족해 보일 것 같다.&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 생각 때문에 아무 말도 하지 않는다면, 이것 역시 숨겨진 영역을 만드는 행동이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 발표가 부담스러운데 팀원들에게 이야기하지 않는 경우도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 알고 있지만 팀원들은 모르기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숨겨진 영역의 일부를 열린 영역으로 이동시키는 방법이 바로 &lt;b&gt;자기개방(Self-disclosure)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신의 생각이나 감정, 상황을 적절하게 다른 사람과 공유하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같이 말할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;이 기술은 아직 사용해본 경험이 많지 않아서 조금 시간이 필요할 것 같아요.&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정보를 공유하면 팀원들도 나의 현재 상황을 이해할 수 있기 때문에 역할이나 일정을 조정하기 쉬워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 자기개방도 무조건 많다고 좋은 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조하리의 창을 실제 팀 활동에 적용할 때는 &lt;b&gt;상대와의 신뢰 관계와 심리적 안전을 고려해야 한다.&lt;/b&gt;&lt;br /&gt;특히 개인적인 내용을 공개하도록 강요하는 것은 적절한 활용이라고 보기 어렵다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 미지의 영역 &amp;mdash; Unknown Area&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미지의 영역은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나도 모르고 다른 사람도 아직 모르는 나의 모습&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 조금 이상하게 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;나도 모르고 다른 사람도 모른다면 그런 것이 존재한다는 것을 어떻게 알 수 있을까?&lt;/code&gt;라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 새로운 경험을 생각해보니 이해가 쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 한 번도 발표해보지 않은 사람은 자신이 발표를 잘하는지 알 수 없다. 주변 사람들도 당연히 알 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 우연히 발표를 맡았는데 굉장히 잘한다면 이전까지 그 능력은 미지의 영역에 존재했다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발에서도 마찬가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백엔드만 공부하던 사람이 UI를 만들어보면서 디자인 감각을 발견할 수 있다.&lt;/li&gt;
&lt;li&gt;처음으로 팀장을 맡으면서 프로젝트 조율 능력을 발견할 수 있다.&lt;/li&gt;
&lt;li&gt;새로운 기술을 사용하면서 자신에게 잘 맞는 분야를 발견할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 미지의 영역은 &lt;b&gt;새로운 경험, 도전, 관찰과 성찰을 통해 발견될 수 있는 잠재적 영역&lt;/b&gt;이라고 이해할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;핵심은 '창의 크기가 움직인다'는 것&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조하리의 창에서 가장 중요하다고 느낀 부분은 &lt;b&gt;네 영역이 고정되어 있지 않다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람과 만나고, 경험하고, 피드백을 받고, 자신의 생각을 이야기하면서 각 영역의 크기는 계속 변한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 팀원이 나에게 다음과 같이 이야기했다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;코드 리뷰할 때 설명을 굉장히 자세하게 해주는 것 같아.&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 그것을 의식하지 못했다면 원래 이 특징은 &lt;b&gt;맹목 영역&lt;/b&gt;에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 피드백을 받은 순간 다음과 같이 이동한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;맹목 영역 &amp;rarr; 열린 영역&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 내가 팀원에게&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;사실 발표하는 것을 많이 긴장하는 편이야.&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 이야기하면 다음과 같이 변화한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;숨겨진 영역 &amp;rarr; 열린 영역&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 처음으로 프로젝트 리더를 맡았는데 예상외로 잘한다는 사실을 나와 팀원들이 함께 발견했다면&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;미지의 영역 &amp;rarr; 열린 영역&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 이동할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 조하리의 창은 &lt;b&gt;관계와 경험을 통해 자기 인식의 범위가 변화하는 과정을 설명하는 모델&lt;/b&gt;이라고 생각할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;개발자에게 적용해보기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 개발자는 혼자 코드를 작성하는 시간도 많지만 실제 프로젝트에서는 다른 개발자와 끊임없이 상호작용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 리뷰&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 작성한 코드에서 반복적으로 변수 이름을 모호하게 사용하지만 정작 나는 그것을 인식하지 못하고 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 동료 개발자가 코드 리뷰에서 다음과 같이 이야기할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;변수 이름을 조금 더 구체적으로 작성하면 코드를 읽기가 쉬울 것 같아요.&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 단순히 코드 리뷰를 받은 것이기도 하지만 조하리의 창의 관점에서는 &lt;b&gt;맹목 영역에 있던 정보를 피드백을 통해 열린 영역으로 이동시키는 과정&lt;/b&gt;이라고 볼 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도움 요청&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발하다가 막혔을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;이 부분을 이해하지 못해서 구현이 늦어지고 있어요.&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 공유하는 것은 &lt;b&gt;숨겨진 영역에 있던 정보를 열린 영역으로 이동시키는 자기개방&lt;/b&gt;이라고 생각할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새로운 역할과 기술&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 해본 적 없던 기술을 사용하거나 새로운 역할을 맡는 것은 &lt;b&gt;미지의 영역을 탐색하는 행동&lt;/b&gt;이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생각하면 다음과 같은 개발 문화들도 단순한 업무 프로세스 이상의 의미를 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 리뷰&lt;/li&gt;
&lt;li&gt;회고(Retrospective)&lt;/li&gt;
&lt;li&gt;페어 프로그래밍&lt;/li&gt;
&lt;li&gt;1:1 미팅&lt;/li&gt;
&lt;li&gt;기술 발표&lt;/li&gt;
&lt;li&gt;새로운 기술 도입&lt;/li&gt;
&lt;li&gt;팀 리딩 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 &lt;b&gt;나와 다른 사람에 대한 이해를 넓히는 과정&lt;/b&gt;이 될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;내가 느낀 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조하리의 창을 공부하면서 가장 크게 느낀 것은 &lt;b&gt;자기 자신을 잘 안다고 생각하는 것과 실제로 자신을 잘 이해하는 것은 다를 수 있다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 나를 가장 오래 봐온 사람이기 때문에 내가 나 자신을 가장 잘 안다고 생각하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다른 사람은 내가 볼 수 없는 부분을 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말을 할 때의 습관, 다른 사람의 의견에 반응하는 방식, 문제 상황에서 보이는 태도처럼 나는 너무 익숙해서 의식하지 못하는 행동을 다른 사람은 더 쉽게 발견할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 다른 사람의 평가가 항상 정답이라는 뜻은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피드백도 결국 한 사람의 관찰과 해석이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 중요한 것은 모든 피드백을 그대로 받아들이는 것이 아니라 &lt;b&gt;여러 피드백과 자신의 경험을 비교하면서 스스로를 더 정확하게 이해하는 것&lt;/b&gt;이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 한 가지 배운 것은 &lt;b&gt;자기개방에는 용기가 필요하지만 동시에 경계도 필요하다&lt;/b&gt;는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열린 영역이 넓다고 해서 무조건 좋은 사람이고, 숨겨진 영역이 크다고 해서 문제가 있는 사람이라는 식으로 받아들이면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람에게는 공개하지 않을 권리도 있고 개인적인 영역도 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 중요한 것은 모든 것을 공개하는 것이 아니라 &lt;b&gt;관계와 상황에 필요한 정보를 안전하게 공유할 수 있는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;오늘의 결론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 조하리의 창을 공부하면서 인간관계에서 중요한 것은 단순히 말을 많이 하는 것이 아니라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다른 사람에게 나를 적절하게 보여주는 것, 다른 사람이 바라보는 나에 대해서도 열린 태도로 듣는 것, 그리고 새로운 경험을 통해 아직 알지 못하는 나를 발견하는 것.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지가 반복되면서 열린 영역이 조금씩 넓어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 열린 영역이 넓어진다는 것은 단순히 다른 사람들이 나를 많이 알게 된다는 의미만 있는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나 역시 이전보다 나 자신을 조금 더 정확하게 이해하게 된다는 의미가 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 조하리의 창을 다음과 같이 기억하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;피드백을 통해 내가 보지 못하는 나를 발견하고, 자기개방을 통해 상대가 보지 못하는 나를 보여주며, 새로운 경험을 통해 아무도 몰랐던 나를 발견한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 친구나 팀원 2~3명에게 실제로 피드백을 받아 &lt;b&gt;나만의 조하리의 창&lt;/b&gt;을 직접 만들어보면 이론을 더 확실하게 이해할 수 있을 것 같다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>etc/자아의 바다</category>
      <category>성장</category>
      <category>조하리의 창</category>
      <category>커뮤니케이션</category>
      <category>코드리뷰</category>
      <category>피드백</category>
      <category>협업</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/84</guid>
      <comments>https://slowgrid.tistory.com/entry/Studying-the-Johari-Window-How-Well-Do-I-Know-Myself#entry84comment</comments>
      <pubDate>Tue, 15 Sep 2026 16:31:05 +0900</pubDate>
    </item>
    <item>
      <title>[YOLO 기초 6] model.train()의 설정값은 무엇을 바꿀까?</title>
      <link>https://slowgrid.tistory.com/entry/YOLO-Basics-6-What-settings-should-be-changed-in-modeltrain</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;data&lt;/code&gt;, &lt;code&gt;epochs&lt;/code&gt;, &lt;code&gt;batch&lt;/code&gt;, &lt;code&gt;imgsz&lt;/code&gt;, &lt;code&gt;workers&lt;/code&gt;, &lt;code&gt;seed&lt;/code&gt;, &lt;code&gt;deterministic&lt;/code&gt;, &lt;code&gt;optimizer&lt;/code&gt; 이해하기&lt;br&gt;&lt;strong&gt;버전 메모&lt;/strong&gt;&lt;br&gt;이 글은 &lt;strong&gt;2026-09-09 최종 검증 기준 Ultralytics 공식 문서와 공개 구현&lt;/strong&gt;을 직접 확인하면서 정리했다. &lt;code&gt;optimizer=auto&lt;/code&gt;, &lt;code&gt;cls_remap&lt;/code&gt;, 체크포인트 저장 형식, YOLO26의 &lt;code&gt;nms&lt;/code&gt;/손실 이름처럼 구현에 가까운 부분은 버전에 따라 달라질 수 있다. 그래서 실제 실험에서는 &lt;code&gt;ultralytics&lt;/code&gt; 버전, &lt;code&gt;args.yaml&lt;/code&gt;, 학습 로그를 함께 확인해 두는 편이 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;사전 학습된 모델을 불러왔다면 실제 학습은 보통 다음 한 줄에서 시작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;results = model.train(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 이 함수에 넣는 값과 &lt;strong&gt;코드에 직접 쓰지 않아도 적용되는 Ultralytics 기본값&lt;/strong&gt;에 따라 학습 조건이 달라진다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 &lt;code&gt;model.train()&lt;/code&gt; 내부의 역전파 구현으로 바로 들어가기보다, 먼저 &lt;strong&gt;사용자가 지정하는 학습 인자들이 실제로 무엇을 의미하는지&lt;/strong&gt; 하나씩 정리해 볼 것 이다. 기울기 누적, 워밍업, AMP, EMA처럼 함수 내부에서 실제 가중치가 갱신되는 과정은 다음 글에서 따로 볼 예정이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;학습 인자를 보기 전에 모델 상태부터 확인해 보면&lt;/h2&gt;
&lt;p&gt;현재 프로젝트의 일반적인 미세 조정은 다음처럼 시작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from ultralytics import YOLO

model = YOLO(&amp;quot;yolo26n.pt&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서부터 이미 &lt;strong&gt;사전 학습된 모델을 초기값으로 사용하는 학습 실행&lt;/strong&gt;이라는 조건이 정해진다. 반대로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model = YOLO(&amp;quot;yolo26n.yaml&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 시작하면 모델 구조를 만든 뒤 새로운 가중치에서 시작하는 흐름이 된다. 필요하다면 구조를 YAML로 만든 뒤 가중치만 따로 불러오는 방식도 충분히 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model = YOLO(&amp;quot;yolo26n.yaml&amp;quot;).load(&amp;quot;yolo26n.pt&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;학습을 시작하기 전에&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model.info()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 확인하면 모델의 계층, 파라미터 수, 연산량(GFLOPs) 같은 정보를 볼 수 있다. 모든 계층을 처음부터 외우기보다 우선 다음 큰 흐름이 맞는지 확인해 두면 이후 로그를 읽기가 편하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;입력 이미지
↓
Backbone
↓
P3 / P4 / P5 특징
↓
Neck
↓
Detection Head
↓
객체 위치 + 클래스 예측&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 &lt;code&gt;model.train()&lt;/code&gt;의 인자를 보기 전에도 &lt;strong&gt;어떤 모델 구조와 어떤 초기 가중치에서 학습을 시작하는가&lt;/strong&gt;는 이미 중요한 실험 조건이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 데이터셋 YAML&lt;/h2&gt;
&lt;p&gt;YOLO 학습에서는 데이터셋 정보를 YAML 파일로 전달할 수 있다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트에는 전처리 방식별 데이터셋 YAML이 존재한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;configs/
└─ datasets/
   ├─ baseline.yaml
   ├─ clahe.yaml
   └─ bilateral_clahe.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;데이터셋 YAML에는 보통 다음 정보가 들어간다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 이미지 위치
검증 이미지 위치
테스트 이미지 위치(선택)
클래스 이름&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 프로젝트에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;class 0
→ person&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하나의 클래스를 사용한다.&lt;/p&gt;
&lt;p&gt;전처리 방법을 비교하는 실험이라면 YAML 파일의 경로만 다르게 만드는 것에서 끝나지 않고, &lt;strong&gt;학습/검증에 들어가는 이미지 ID와 라벨이 서로 동일한지&lt;/strong&gt;도 확인할 필요가 있다.&lt;/p&gt;
&lt;p&gt;베이스라인(baseline)의 검증 이미지와 CLAHE의 검증 이미지 구성이 다르면, 성능 차이가 전처리 때문인지 데이터 분할 때문인지 구분하기 어려워지기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 실제 학습 API&lt;/h2&gt;
&lt;p&gt;현재 CLAHE 실험 설정을 단순화하면 대략 이런 형태다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from ultralytics import YOLO

model = YOLO(&amp;quot;yolo26n.pt&amp;quot;)

results = model.train(
    data=&amp;quot;configs/datasets/clahe.yaml&amp;quot;,
    epochs=50,
    imgsz=640,
    batch=16,
    seed=42,
    deterministic=True,
    workers=4,
    device=0,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 한 코드 안에 지금까지 공부한 개념들이 거의 모두 들어 있다.&lt;/p&gt;
&lt;p&gt;그리고 코드에 직접 적지 않았더라도 Ultralytics의 여러 &lt;strong&gt;기본값&lt;/strong&gt;이 함께 적용된다.&lt;/p&gt;
&lt;p&gt;예를 들어 2026-09-08 기준 기본 설정에는 대략 이런 값들이 잡혀 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;optimizer=auto
amp=True
val=True
patience=100
nbs=64
warmup_epochs=3.0
close_mosaic=10
cos_lr=False&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 재현 가능한 실험을 만들려면 함수 호출에 적힌 값만 보는 것이 아니라 &lt;strong&gt;최종적으로 적용된 전체 설정&lt;/strong&gt;을 확인할 필요가 있다.&lt;/p&gt;
&lt;p&gt;여기서 사전 학습 체크포인트의 &lt;code&gt;train_args&lt;/code&gt;와 새 학습의 설정은 구분해야 한다.&lt;br&gt;&lt;code&gt;model.ckpt[&amp;quot;train_args&amp;quot;]&lt;/code&gt;에는 사전 학습 당시의 옵티마이저와 학습률 등이 기록되어 있을 수 있지만, 현재 Ultralytics는 &lt;code&gt;.pt&lt;/code&gt;를 &lt;code&gt;YOLO(...)&lt;/code&gt;로 불러올 때 체크포인트 인자 중 &lt;code&gt;imgsz&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;single_cls&lt;/code&gt;만 모델의 override로 유지한다. 따라서 사전 학습 때 MuSGD를 사용했다고 해서 새 미세 조정에서도 MuSGD가 자동으로 이어지는 것은 아니다.&lt;/p&gt;
&lt;p&gt;따라서 위 코드처럼 &lt;code&gt;optimizer&lt;/code&gt;를 직접 지정하지 않았다면 새 학습에서는 현재 기본값인 &lt;code&gt;optimizer=auto&lt;/code&gt;가 적용된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. &lt;code&gt;data&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;data=&amp;quot;configs/datasets/clahe.yaml&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YOLO에게&lt;/p&gt;
&lt;p&gt;&lt;code&gt;어떤 데이터셋으로 학습할 것인가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 알려준다.&lt;/p&gt;
&lt;p&gt;학습 이미지뿐 아니라 검증 이미지, 클래스 이름 등의 정보도 여기서 읽는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 데이터셋 YAML과 라벨 형식&lt;/h2&gt;
&lt;p&gt;객체 검출 데이터셋 YAML은 보통 다음과 비슷한 정보를 가진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;path: /data/thermal_yolo
train: images/train
val: images/val
test: images/test

names:
  0: person&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;names&lt;/code&gt;의 클래스 ID는 0부터 시작한다.&lt;/p&gt;
&lt;p&gt;그리고 YOLO 객체 검출 라벨은 일반적으로 이미지마다 같은 이름의 &lt;code&gt;.txt&lt;/code&gt; 파일에 다음 형식으로 저장한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;class_id x_center y_center width height&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;좌표는 이미지 너비와 높이를 기준으로 &lt;strong&gt;0~1 범위로 정규화&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 0.52 0.48 0.08 0.21&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;class    = person
중심 x    = 이미지 너비의 52%
중심 y    = 이미지 높이의 48%
box 너비  = 이미지 너비의 8%
box 높이  = 이미지 높이의 21%&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도로 해석할 수 있다.&lt;/p&gt;
&lt;p&gt;학습 파이프라인은 이 라벨을 읽은 뒤 크기 조정, 뒤집기, 스케일 변화, Mosaic 같은 증강을 적용할 때 &lt;strong&gt;바운딩 박스 좌표도 같은 기하 변환에 맞춰 함께 변경&lt;/strong&gt;한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지만 변환
라벨은 그대로&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 되어버리면 정답 박스가 실제 객체 위치와 어긋나므로 학습이 정상적으로 되지 않는다.&lt;/p&gt;
&lt;p&gt;이 때문에 직접 전처리 파이프라인을 만들 때는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;전처리가 픽셀 값만 바꾸는가, 아니면 이미지의 기하 구조까지 바꾸는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 반드시 구분해야 한다.&lt;/p&gt;
&lt;p&gt;CLAHE처럼 픽셀 밝기 분포만 바꾸는 전처리는 바운딩 박스 좌표를 바꿀 필요가 없지만, 크롭(crop) / 크기 조정(resize) / 회전(rotate)처럼 좌표계를 바꾸는 전처리는 라벨도 함께 변환해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. &lt;code&gt;epochs&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;epochs=50&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;전체 학습 데이터셋을 최대 50에포크 동안 반복해서 학습한다는 뜻이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 데이터셋 전체
 ↓
Epoch 1
 ↓
Epoch 2
 ↓
...
 ↓
Epoch 50&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;에포크는 단순히 같은 이미지를 똑같이 50번 보는 것과는 조금 다르다.&lt;/p&gt;
&lt;p&gt;학습 시 데이터 순서가 섞이고 데이터 증강이 적용되기 때문에 같은 원본 이미지라도 에포크마다 모델에 들어가는 모습이 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;또 Ultralytics에는 조기 종료(Early Stopping)를 위한 &lt;code&gt;patience&lt;/code&gt; 설정이 있다. 현재 기본값은 &lt;code&gt;100&lt;/code&gt;이므로 &lt;code&gt;epochs=50&lt;/code&gt;인 이 예시에서는 &lt;strong&gt;검증 성능 정체만으로 50에포크 이전에 조기 종료될 가능성은 사실상 없는 것과 마찬가지이다&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. &lt;code&gt;batch&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;batch=16&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한 번의 데이터로더 반복에서 16장의 이미지를 묶어서 처리한다는 뜻이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;16장 이미지
 ↓
순전파(Forward)
 ↓
손실 계산
 ↓
역전파(Backward)
 ↓
기울기 누적&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 처음 헷갈리기 쉬운 부분이 있다. &lt;strong&gt;&lt;code&gt;batch=16&lt;/code&gt;이라고 해서 반드시 16장을 처리할 때마다 가중치가 한 번 갱신되는 것은 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ultralytics는 &lt;code&gt;nbs&lt;/code&gt;라는 명목상 배치 크기(Nominal Batch Size)를 사용하며 현재 기본값은 &lt;code&gt;64&lt;/code&gt;다. 실제 배치가 더 작으면 여러 배치의 기울기를 모은 뒤 한 번에 옵티마이저를 실행할 수 있다.&lt;/p&gt;
&lt;p&gt;다시 말해 현재 설정에서는 워밍업 이후 대략&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Batch 1: 16장 → 순전파 / 역전파 → 기울기 저장
Batch 2: 16장 → 순전파 / 역전파 → 기울기 누적
Batch 3: 16장 → 순전파 / 역전파 → 기울기 누적
Batch 4: 16장 → 순전파 / 역전파 → 기울기 누적
                                     ↓
                              옵티마이저 실행
                                     ↓
                               가중치 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 동작할 수 있다.&lt;/p&gt;
&lt;p&gt;이때 실제 가중치 갱신 주기는 &lt;code&gt;nbs&lt;/code&gt;와 기울기 누적에 의해 달라질 수 있다. 자세한 내부 동작은 다음 글에서 살펴본다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;참고로 현재 Ultralytics는 단일 GPU 학습 첫 에포크에서 CUDA 메모리 부족(OOM)이 발생하면 배치 크기를 절반으로 줄여 재시도하는 동작도 지원한다. 따라서 실험 로그에서 실제 사용된 배치 크기를 확인하는 습관이 좋다.&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. &lt;code&gt;imgsz&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;imgsz=640&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이것은 YOLO가 학습과 검증에 사용할 목표 입력 크기를 지정하게 된다.&lt;/p&gt;
&lt;p&gt;그러나 직관적으로 원본 이미지가 항상 640×640이라는 뜻은 아니다.&lt;/p&gt;
&lt;p&gt;학습 파이프라인에서 다양한 전처리가 적용될 수 있고, 최종적으로 모델이 처리할 수 있는 stride 호환 크기의 텐서가 만들어진다.&lt;/p&gt;
&lt;p&gt;이 부분은 다음처럼 이해하는 편이 정확하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원본 이미지
 ↓
데이터 증강 / 크기 조정 / 패딩
 ↓
라벨 좌표도 같은 변환 적용
 ↓
모델 입력 텐서&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rect&lt;/code&gt;, &lt;code&gt;multi_scale&lt;/code&gt;, Mosaic 등의 설정에 따라 실제 변환 과정은 달라질 수 있으므로 &lt;code&gt;imgsz=640&lt;/code&gt;을 단순히&lt;/p&gt;
&lt;p&gt;&lt;code&gt;모든 이미지를 무조건 640×640으로 찌그러뜨린다&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 단순하게 받아들이면 맞지 않게 된다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;imgsz=640&lt;/code&gt;을 더 정확히 이해하면&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;imgsz=640&lt;/code&gt;은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원본 이미지
 ↓
무조건 640×640로 강제 변형&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 아니고,&lt;/p&gt;
&lt;p&gt;일반적인 YOLO 데이터 경로에서는 원본 비율을 고려한 크기 조정과 패딩, 랜덤 스케일, Mosaic, 랜덤 원근 변환(Random Perspective) 같은 데이터 증강이 조합될 수 있다.&lt;/p&gt;
&lt;p&gt;개념만 놓고 보면 다음에 가깝다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원본 이미지
 ↓
학습용 기하 증강
 ↓
크기 조정 / 패딩
 ↓
stride에 맞는 입력 텐서&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 과정에서 바운딩 박스도 동일한 스케일과 이동량을 반영해 변환된다.&lt;/p&gt;
&lt;p&gt;그래서 원본 이미지에서 20px 크기였던 보행자가 모델 입력에서도 항상 20px인 것은 아니다.&lt;/p&gt;
&lt;p&gt;원본 해상도와 스케일 변화에 따라 학습 텐서에서의 객체 크기는 달라진다.&lt;/p&gt;
&lt;p&gt;작은 보행자 문제를 분석할 때는 원본 박스 크기뿐 아니라 &lt;strong&gt;&lt;code&gt;imgsz=640&lt;/code&gt;과 학습 증강을 거친 뒤 객체가 실제 입력에서 어느 정도 크기로 보이는지&lt;/strong&gt;도 함께 생각해야 한다.&lt;/p&gt;
&lt;p&gt;특히 열화상처럼 원래 해상도가 낮거나 원거리 작은 객체 비중이 높은 데이터에서는 이 차이가 성능에 큰 영향을 줄 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. &lt;code&gt;device&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;device=0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;일반적으로 첫 번째로 보이는 CUDA GPU를 사용한다.&lt;/p&gt;
&lt;p&gt;예를 들어 시스템에 GPU가 여러 개 있고 &lt;code&gt;CUDA_VISIBLE_DEVICES&lt;/code&gt;를 별도로 설정했다면 &lt;code&gt;0&lt;/code&gt;은 &lt;strong&gt;물리적인 GPU 0번이 아니라 현재 프로세스에서 보이는 첫 번째 GPU&lt;/strong&gt;를 의미한다.&lt;/p&gt;
&lt;p&gt;Ultralytics에서는 CPU, Apple MPS, 여러 GPU, 자동 GPU 선택 등 다른 장치 설정도 가능하지만 현재 프로젝트에서는 단일 GPU를 사용한다.&lt;/p&gt;
&lt;p&gt;실험 재현성을 높이려면 &lt;code&gt;device=0&lt;/code&gt;만 기록하는 것보다 실제 GPU 모델명과 CUDA/PyTorch 버전도 함께 기록해 두는 편이 좋을 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. &lt;code&gt;workers&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;workers=4&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;데이터로더가 이미지를 읽고 전처리할 때 사용할 &lt;strong&gt;작업 프로세스 수&lt;/strong&gt;를 지정한다.&lt;/p&gt;
&lt;p&gt;이것도 개념만 놓고 보면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;디스크에서 이미지 읽기
 +
라벨 읽기
 +
일부 데이터 전처리
 ↓
여러 worker가 병렬 처리
 ↓
GPU가 사용할 배치 준비&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;workers&lt;/code&gt;가 너무 작으면 GPU가 데이터를 기다릴 수 있고, 너무 크면 CPU와 메모리를 과하게 사용하거나 저장장치 I/O가 병목이 될 수 있다.&lt;/p&gt;
&lt;p&gt;이 때문에 큰 값이 항상 더 빠른 것은 아니다.&lt;/p&gt;
&lt;p&gt;현재 코드에서는 &lt;code&gt;workers=4&lt;/code&gt;로 명시했기 때문에 Ultralytics 기본값보다 이 값이 우선 적용된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. &lt;code&gt;seed&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;seed=42&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;학습에는 여러 무작위 요소가 있다.&lt;/p&gt;
&lt;p&gt;예를 들면 이런 것들이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;데이터 순서 섞기
데이터 증강
일부 연산의 무작위성
가중치 초기화(새 모델을 만들 때)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;seed&lt;/code&gt;를 고정하면 Python, NumPy, PyTorch 등 학습 파이프라인의 난수 흐름을 가능한 범위에서 동일하게 맞추는 데 도움이 된다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;strong&gt;seed 하나를 고정했다는 것은 실험 결과가 통계적으로 신뢰할 만하다는 뜻이 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;전처리 방법 A와 B의 차이가 작다면 가능하면 여러 seed로 반복해 평균과 분산을 확인하는 것이 더 좋은 실험 설계다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11. &lt;code&gt;deterministic&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;deterministic=True&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가능한 범위에서 결정론적(deterministic) 알고리즘을 사용하도록 설정한다.&lt;/p&gt;
&lt;p&gt;이 설정은 실험 재현성을 높이는 데 도움이 되지만 일부 연산에서는 속도가 느려질 수 있다.&lt;/p&gt;
&lt;p&gt;또한 seed와 &lt;code&gt;deterministic=True&lt;/code&gt;를 사용하더라도 다음이 달라지면 완전히 같은 결과가 보장되지 않을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GPU 종류
CUDA 버전
PyTorch 버전
Ultralytics 버전
사용되는 연산 커널
분산 학습 구성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;같은 환경에서 실험 재현성을 최대한 높이기 위한 설정&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;정도로 이해하는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;12. &lt;code&gt;optimizer=auto&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;현재 프로젝트 코드에는 &lt;code&gt;optimizer&lt;/code&gt;가 직접 적혀 있지 않으므로 기본값인&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;optimizer=&amp;quot;auto&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 사용된다.&lt;/p&gt;
&lt;p&gt;현재 Ultralytics 기준으로 &lt;code&gt;optimizer=auto&lt;/code&gt;는 학습 규모를 내부적으로 계산해 옵티마이저를 자동 선택한다.&lt;/p&gt;
&lt;p&gt;대략적으로는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;계산된 iterations &amp;gt; 10,000
→ MuSGD

그 외
→ AdamW&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;흐름이다.&lt;/p&gt;
&lt;p&gt;여기서 중요한 것은 이 &lt;code&gt;iterations&lt;/code&gt;가 단순한&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;전체 이미지 수 / batch&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 아니라는 점이다.&lt;/p&gt;
&lt;p&gt;현재 Trainer는 대략 이런 계산을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;iterations
= ceil(학습 이미지 수 / max(batch_size, nbs)) × epochs&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 기본 &lt;code&gt;nbs=64&lt;/code&gt;이므로, 예를 들어 &lt;strong&gt;학습 이미지가 2,110장이고 &lt;code&gt;batch=16&lt;/code&gt;, &lt;code&gt;epochs=50&lt;/code&gt;이라면&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ceil(2110 / max(16, 64)) × 50
= ceil(2110 / 64) × 50
= 33 × 50
= 1,650&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 되어 10,000보다 작다.&lt;/p&gt;
&lt;p&gt;이 조건이라면 현재 구현에서는 &lt;code&gt;optimizer=auto&lt;/code&gt;가 &lt;strong&gt;AdamW&lt;/strong&gt;를 선택하는 쪽에 해당한다.&lt;/p&gt;
&lt;p&gt;또 &lt;code&gt;optimizer=auto&lt;/code&gt;를 사용하면 사용자가 &lt;code&gt;lr0&lt;/code&gt;와 &lt;code&gt;momentum&lt;/code&gt;을 따로 적어도 자동 선택 과정에서 내부 값으로 덮어쓸 수 있다.&lt;/p&gt;
&lt;p&gt;정리하면 실험에서 학습률까지 엄밀한 통제 변수로 고정하고 싶다면 다음처럼 옵티마이저를 명시하는 편이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model.train(
    ...,
    optimizer=&amp;quot;AdamW&amp;quot;,
    lr0=0.001,
    momentum=0.9,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;재현 가능한 실험에서는 결국&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;코드에 무엇을 적었는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;뿐 아니라&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;실제로 어떤 옵티마이저 / 학습률이 적용되었는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 학습 로그와 체크포인트에서 확인할 필요가 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;13. 미세 조정에서는 학습률도 중요하다&lt;/h2&gt;
&lt;p&gt;사전 학습 가중치는 이미 유용한 표현을 가지고 있으므로, 무작위 초기화 모델과 완전히 같은 학습률 전략이 항상 최선인 것은 아니다.&lt;/p&gt;
&lt;p&gt;학습률이 지나치게 크면 이미 학습된 특징이 빠르게 크게 바뀌면서 기존 표현을 잃을 수 있다.&lt;/p&gt;
&lt;p&gt;새 데이터만으로 학습하면서 이전 지식을 잃는 이러한 현상을 &lt;strong&gt;파국적 망각(Catastrophic Forgetting)&lt;/strong&gt;이라고 한다.&lt;/p&gt;
&lt;p&gt;2단계 미세 조정에서는 흔히 이런 전략을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1단계
→ 백본 동결
→ 넥 / 검출 헤드 적응

2단계
→ 백본 동결 해제
→ 더 낮은 학습률로 전체 모델 미세 조정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 &lt;code&gt;optimizer=auto&lt;/code&gt;를 사용할 경우 학습률을 해석할 때 한 가지 주의할 점이 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;optimizer=auto&lt;/code&gt;에서는 사용자가 입력한 &lt;code&gt;lr0&lt;/code&gt;와 일부 최적화 관련 값이 자동 선택 로직에 의해 무시될 수 있다.&lt;/p&gt;
&lt;p&gt;2026-09-08 기준 공식 미세 조정 가이드에서는 전체 학습 반복 횟수를 기준으로 자동 최적화 방식을 선택한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;총 학습 반복 횟수 10,000 이하
→ AdamW + 자동 계산 학습률

총 학습 반복 횟수 10,000 초과
→ MuSGD + lr=0.01&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 기준 역시 Ultralytics 버전에 따라 바뀔 수 있으므로 실제 실행 로그를 기준으로 확인할 필요가 있다.&lt;/p&gt;
&lt;p&gt;그래서 다음처럼 적었다고 해서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model.train(
    optimizer=&amp;quot;auto&amp;quot;,
    lr0=0.001,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반드시 &lt;code&gt;0.001&lt;/code&gt;의 초기 학습률이 사용된다고 생각하면 안 된다.&lt;/p&gt;
&lt;p&gt;학습률을 직접 통제하고 싶다면 최적화 알고리즘도 명시하는 것이 안전하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model.train(
    optimizer=&amp;quot;AdamW&amp;quot;,
    lr0=0.001,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 실험 보고서에는 단순히 &amp;quot;학습률 0.001&amp;quot;만 적는 것이 아니라 실제 &lt;code&gt;args.yaml&lt;/code&gt;과 학습 로그를 확인해 &lt;strong&gt;실제로 적용된 최적화 알고리즘과 학습률 설정&lt;/strong&gt;을 기록해 놓아야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;model.train()&lt;/code&gt;의 인자는 단순한 옵션 목록이 아니다. 서로 연결되어 실제 학습 동작을 바꾼다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;data
→ 어떤 이미지 / 라벨 / 클래스로 학습하는가

epochs
→ 데이터셋을 몇 번 반복할 것인가

batch
→ 한 데이터로더 반복에서 몇 장을 처리하는가
→ nbs / 기울기 누적과 함께 실제 업데이트 주기에 영향

imgsz
→ 학습 입력의 목표 크기와 작은 객체의 실제 픽셀 크기에 영향

seed + deterministic
→ 같은 환경에서 재현성을 높임

optimizer=auto
→ 학습 규모에 따라 실제 옵티마이저와 학습률이 자동 결정될 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;특히 재현 가능한 실험에서는 &lt;strong&gt;코드에 무엇을 적었는가&lt;/strong&gt;뿐 아니라 &lt;code&gt;args.yaml&lt;/code&gt;과 학습 로그에서 &lt;strong&gt;실제로 어떤 값이 적용됐는가&lt;/strong&gt;를 확인할 필요가 있다.&lt;/p&gt;
&lt;p&gt;다음 글에서는 같은 &lt;code&gt;model.train()&lt;/code&gt;을 내부 관점에서 보고, 한 배치가 들어온 뒤 &lt;strong&gt;순전파 → 손실 → 역전파 → 기울기 누적 → 옵티마이저 → EMA&lt;/strong&gt;까지 실제 가중치가 어떻게 갱신되는지 살펴볼 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고한 공식 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ultralytics Train Mode: &lt;a href=&quot;https://docs.ultralytics.com/modes/train/&quot;&gt;https://docs.ultralytics.com/modes/train/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics Configuration: &lt;a href=&quot;https://docs.ultralytics.com/usage/cfg/&quot;&gt;https://docs.ultralytics.com/usage/cfg/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics YOLO26: &lt;a href=&quot;https://docs.ultralytics.com/models/yolo26/&quot;&gt;https://docs.ultralytics.com/models/yolo26/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ultralytics &lt;code&gt;BaseTrainer&lt;/code&gt;: &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/trainer.py&quot;&gt;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/trainer.py&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>batch</category>
      <category>data</category>
      <category>Deterministic</category>
      <category>epochs</category>
      <category>imgsz</category>
      <category>SEED</category>
      <category>Workers</category>
      <category>YoLO</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/83</guid>
      <comments>https://slowgrid.tistory.com/entry/YOLO-Basics-6-What-settings-should-be-changed-in-modeltrain#entry83comment</comments>
      <pubDate>Mon, 14 Sep 2026 17:53:48 +0900</pubDate>
    </item>
    <item>
      <title>0.5!은 얼마일까? | 감마 함수</title>
      <link>https://slowgrid.tistory.com/entry/What-is-05-Gamma-Function</link>
      <description>&lt;!-- 티스토리 HTML 모드용 본문 --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- 수식: MathJax 4 / LaTeX --&gt;
&lt;script&gt;
window.MathJax = {
  tex: {
    inlineMath: [['\\(', '\\)']],
    displayMath: [['\\[', '\\]']],
    processEscapes: true
  },
  chtml: { scale: 1.0 }
};
&lt;/script&gt;
&lt;script src=&quot;https://cdn.jsdelivr.net/npm/mathjax@4.0.0/tex-mml-chtml.js&quot;&gt;&lt;/script&gt;
&lt;div&gt;
&lt;style&gt;
.gamma-post{line-height:1.9;word-break:keep-all;color:#222;font-size:16px}
.gamma-post p{margin:16px 0}
.gamma-post h2{font-size:1.55em;line-height:1.5;margin:14px 0 20px}
.gamma-post section{margin-top:46px;padding-top:20px;border-top:1px solid #e3e7e8}
.gamma-post .section-no{margin:0 0 10px;color:#8a6542;font-size:12px;font-weight:700;letter-spacing:.08em}
.gamma-post .math-display{margin:20px 0;padding:16px 18px;overflow-x:auto;line-height:1.75;border:1px solid #e1e7e9;border-left:3px solid #8faeb5;border-radius:0 7px 7px 0;background:#f7f9fa;-webkit-overflow-scrolling:touch}
.gamma-post .math-display.key{background:#edf5f4;border-left-color:#205a64}
.gamma-post .note{margin:24px 0;padding:18px 20px;border-left:3px solid #bea17e;background:#fbf7f0;border-radius:0 7px 7px 0}
.gamma-post .note strong{color:#205a64}
.gamma-post .image-slot{margin:28px 0;padding:16px 18px;border:1px dashed #9eb7ba;border-radius:8px;background:#f7fafb;color:#51646c;font-size:14px;line-height:1.7}
.gamma-post figure{margin:30px 0 34px}
.gamma-post figcaption{margin-top:10px;color:#566773;font-size:14px;line-height:1.8}
.gamma-post .fig-no{display:block;margin-bottom:4px;color:#205a64;font-size:12px;font-weight:700;letter-spacing:.06em}
.gamma-post .small{color:#64717d;font-size:13px}
.gamma-post mjx-container[display=&quot;true&quot;]{margin:0!important;min-width:max-content}
@media (max-width:640px){.gamma-post .math-display{padding:14px 10px;font-size:.94em}}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;gamma-post&quot;&gt;
&lt;div style=&quot;margin: 0 0 36px 0;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리에게 팩토리얼은 익숙하다. \(4!\)처럼 자연수를 차례로 곱하면 끝이다. 하지만 입력값을 0.5로 바꾸면 얘기가 조금 이상해진다. \(0.5!\)는 대체 뭘 곱해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 팩토리얼이 정수 범위에서만 한정되게 쓰이는 것 아닌가 싶은 느낌이 든다. 그런데 질문을 살짝 바꿔보면 실수 범위의 팩토리얼이 눈에 들어오기 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;&amp;ldquo;정수가 아닌 수를 어떻게 곱할까?&amp;rdquo;&lt;/b&gt;가 아니다. 대신 &lt;b&gt;&amp;ldquo;팩토리얼이 가지고 있던 규칙을 정수 밖에서도 유지할 수 있을까?&amp;rdquo;&lt;/b&gt;라고 물어보는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 적으면 다음과 같다.&lt;/p&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[\boxed{\displaystyle \Gamma\!\left(\frac12\right)=\sqrt{\pi}}\]&lt;/div&gt;
&lt;/div&gt;
&lt;section id=&quot;factorial&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;01 / 출발점&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팩토리얼을 곱셈이 아니라 규칙으로 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팩토리얼의 정의 자체는 단순하다.&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \begin{aligned} 1!&amp;amp;=1,\\ 2!&amp;amp;=1\cdot2,\\ 3!&amp;amp;=1\cdot2\cdot3,\\ 4!&amp;amp;=1\cdot2\cdot3\cdot4. \end{aligned} \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 값을 한 단계씩 이어서 보면 더 중요한 규칙이 하나 보인다.&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ 0!=1,\qquad n!=n(n-1)!. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 건 &amp;ldquo;1부터 \(n\)까지 곱한다&amp;rdquo;는 설명보다 &lt;b&gt;한 단계 올라갈 때 이전 값에 현재 숫자를 곱한다&lt;/b&gt;는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 \(5!\)은 \(4!\)에 5를 곱한 값이고, \(4!\)은 다시 \(3!\)에 4를 곱한 값이다. 이 규칙만 정수 밖으로 자연스럽게 이어갈 수 있다면, 굳이 &amp;ldquo;0.5개를 곱한다&amp;rdquo; 같은 이상한 해석을 만들 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이제 곱셈 대신 다른 표현을 찾는다. 정수가 아닌 입력도 받을 수 있으면서, 팩토리얼과 같은 점화식을 만들어내는 표현이 필요하다.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;integral&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;02 / 곱셈을 적분으로 바꾸기&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팩토리얼을 적분으로 옮겨보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출발은 아래 미분식이다.&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[\frac{d}{dt}t^n=nt^{n-1}\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지수가 하나 내려가면서 앞에 \(n\)이 붙는다. \(n!=n(n-1)!\)과 생김새가 꽤 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 \(e^{-t}\)를 붙이고 적분을 생각해본다.&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[I_n(R)=\int_0^R t^n e^{-t}\,dt\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 무한대까지 적분하지 않고 일단 \(R\)까지만 본다. 이렇게 해두면 부분적분은 평범한 유한 구간에서 처리할 수 있고, 마지막에 \(R\to\infty\)만 보내면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 \(n=0\)이면&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ I_0(R)=\int_0^R e^{-t}\,dt=1-e^{-R} \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이므로 \(R\to\infty\)에서 \(I_0(R)\to1\)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 \(n\ge1\)에서 부분적분을 한다. \(t^n\)을 미분하고 \(e^{-t}\)를 적분하면&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \begin{aligned} I_n(R) &amp;amp;=\left[-t^n e^{-t}\right]_0^R+n\int_0^R t^{n-1}e^{-t}\,dt\\ &amp;amp;=-R^n e^{-R}+nI_{n-1}(R). \end{aligned} \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(R^n e^{-R}\)만 0으로 사라지면 우리가 원하던 모양이 바로 나온다. 실제로 지수함수는 다항식보다 빠르게 커지므로&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ e^R\ge \frac{R^{n+1}}{(n+1)!} \quad\Longrightarrow\quad 0\le R^n e^{-R}\le \frac{(n+1)!}{R}\to0. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 극한을 보내면&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \int_0^\infty t^n e^{-t}\,dt =n\int_0^\infty t^{n-1}e^{-t}\,dt. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작값이 1이고, 한 단계 올라갈 때마다 1, 2, 3, &amp;hellip;이 차례대로 곱해진다. 결국&lt;/p&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[ \boxed{\displaystyle \int_0^\infty t^n e^{-t}\,dt=n!} \qquad(n=0,1,2,\ldots) \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 된다. 원래는 정수를 차례대로 곱해서 만들던 팩토리얼이 이제 곡선 아래의 넓이로 바뀌었다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;image-slot&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWOd9s/dJMcah0bGg7/SeVklApj4wCJWogev2VpNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWOd9s/dJMcah0bGg7/SeVklApj4wCJWogev2VpNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWOd9s/dJMcah0bGg7/SeVklApj4wCJWogev2VpNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWOd9s%2FdJMcah0bGg7%2FSeVklApj4wCJWogev2VpNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1980&quot; height=&quot;1134&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터 중요한 변화가 생긴다. 적분식에서는 지수가 꼭 정수일 필요가 없다. \(t^{0.5}\), \(t^{2.7}\) 같은 값도 \(t&amp;gt;0\)에서는 아무 문제 없이 정의된다.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;gamma&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;03 / 정수 밖으로&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정수 사이를 메워주는 감마 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 지수를 실수까지 열어둔다. 보통 다음 적분을 감마 함수라고 정의한다.&lt;/p&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[ \boxed{\displaystyle \Gamma(s)=\int_0^\infty t^{s-1}e^{-t}\,dt } \qquad(s&amp;gt;0) \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 지수를 \(s\)가 아니라 \(s-1\)로 적는 이유는 팩토리얼과의 관계를 한 줄로 쓰기 좋기 때문이다.&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \Gamma(n+1)=\int_0^\infty t^n e^{-t}\,dt=n!. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 정확한 관계는 \(\Gamma(n)=n!\)가 아니라&lt;/p&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[\boxed{\Gamma(n+1)=n!}\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다. 이 한 칸 차이는 뒤에서 \(0.5!\)을 계산할 때 다시 나온다.&lt;/p&gt;
&lt;div class=&quot;note&quot;&gt;&lt;b&gt;이 적분은 정말 수렴할까?&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(s&amp;gt;0\)일 때 0 근처와 무한대 근처를 따로 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0에서 1 사이에서는 \(e^{-t}\le1\)이므로&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[0\le\int_0^1 t^{s-1}e^{-t}\,dt\le\int_0^1 t^{s-1}\,dt=\frac1s&amp;lt;\infty.\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한대 쪽에서는 \(s-1\le m\)이 되는 음이 아닌 정수 \(m\)을 하나 잡으면 \(t\ge1\)에서 \(t^{s-1}\le t^m\)이다. 따라서&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[0\le\int_1^\infty t^{s-1}e^{-t}\,dt\le\int_1^\infty t^m e^{-t}\,dt&amp;lt;\infty.\]&lt;/div&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 \(s&amp;gt;0\)에서는 감마 적분이 제대로 정의된다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팩토리얼의 점화식도 그대로 살아 있다. \(\Gamma(s+1)\)에 부분적분을 적용하면&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \begin{aligned} \Gamma(s+1) &amp;amp;=\int_0^\infty t^s e^{-t}\,dt\\ &amp;amp;=s\int_0^\infty t^{s-1}e^{-t}\,dt\\ &amp;amp;=s\Gamma(s). \end{aligned} \]&lt;/div&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[\boxed{\Gamma(s+1)=s\Gamma(s)}\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 감마 함수는 정수에서 팩토리얼 값만 맞추는 함수가 아니라, 팩토리얼이 움직이던 규칙까지 그대로 이어받는다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;image-slot&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnZ65J/dJMcahFU57v/Z2qYzhqnXpg7BZlLO1VkPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnZ65J/dJMcahFU57v/Z2qYzhqnXpg7BZlLO1VkPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnZ65J/dJMcahFU57v/Z2qYzhqnXpg7BZlLO1VkPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnZ65J%2FdJMcahFU57v%2FZ2qYzhqnXpg7BZlLO1VkPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1980&quot; height=&quot;1134&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;b&gt;&lt;/b&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 여기서 한 가지는 구분해둘 필요가 있다. 정수에서 값이 맞고 \(F(s+1)=sF(s)\)를 만족한다고 해서 그런 함수가 무조건 감마 함수 하나뿐인 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주기 1인 양수 함수&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ p(s)=\exp\!\bigl(1-\cos(2\pi s)\bigr) \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 잡고 \(F(s)=p(s)\Gamma(s)\)라고 두면, 정수에서는 \(p(n)=1\)이고 \(p(s+1)=p(s)\)이므로 \(F\)도 정수값과 점화식은 똑같이 만족한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;이런 조건을 만족하는 확장 중 왜 감마 함수를 쓰는가?&amp;rdquo;라는 질문에는 조건이 하나 더 필요하다. 대표적으로 &lt;b&gt;로그 볼록성&lt;/b&gt;까지 요구하면 보어&amp;ndash;몰레루프 정리에 의해 감마 함수가 유일하게 정해진다. 여기서는 \(\Gamma(1/2)\)을 계산하는 것이 목적이므로 이 정리의 증명까지 깊게 들어가지는 않는다.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;half&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;04 / 1/2을 넣어보기&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;\(\Gamma(1/2)\)을 넣으면 가우스 적분이 나온다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 감마 함수에 \(\frac12\)을 넣는다.&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \Gamma\!\left(\frac12\right) =\int_0^\infty t^{-1/2}e^{-t}\,dt =\int_0^\infty \frac{e^{-t}}{\sqrt t}\,dt. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분모에 \(\sqrt t\)가 있으니 \(t=u^2\)로 치환하는 것이 자연스럽다. \(dt=2u\,du\)이고 \(u&amp;gt;0\)에서 \(\sqrt t=u\)이므로&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \Gamma\!\left(\frac12\right) =2\int_0^\infty e^{-u^2}\,du. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 \(e^{-u^2}\)는 짝함수다. 오른쪽 절반의 넓이를 두 배 한 값은 실수 전체에서의 적분과 같다.&lt;/p&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[ \boxed{\displaystyle \Gamma\!\left(\frac12\right) =\int_{-\infty}^{\infty}e^{-x^2}\,dx } \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 문제는 감마 함수가 아니라 가우스 적분을 계산하는 문제로 바뀌었다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;image-slot&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qm7AZ/dJMcagtwluS/Gjdw4o8mfWOgnk12DhFkyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qm7AZ/dJMcagtwluS/Gjdw4o8mfWOgnk12DhFkyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qm7AZ/dJMcagtwluS/Gjdw4o8mfWOgnk12DhFkyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqm7AZ%2FdJMcagtwluS%2FGjdw4o8mfWOgnk12DhFkyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1980&quot; height=&quot;1134&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;b&gt;&lt;/b&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[I=\int_{-\infty}^{\infty}e^{-x^2}\,dx\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 두자. 이 적분은 생긴 모습은 단순하지만 한 변수 적분으로는 원시함수를 초등함수 형태로 쓸 수 없다. 그래서 정면으로 적분하려고 하면 계산이 잘 풀리지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 유명한 발상이 하나 나온다. &lt;b&gt;\(I\)를 바로 구하지 말고 \(I^2\)을 구한다.&lt;/b&gt;&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;square&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;05 / 한 차원 늘리기&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적분을 제곱하면 원이 보이기 시작한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄밀하게 보기 위해 먼저 유한 구간에서&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ I_R=\int_{-R}^{R}e^{-x^2}\,dx \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 둔다. 같은 적분을 변수 이름만 \(y\)로 바꿔 곱하면&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \begin{aligned} I_R^2 &amp;amp;=\left(\int_{-R}^{R}e^{-x^2}\,dx\right) \left(\int_{-R}^{R}e^{-y^2}\,dy\right)\\ &amp;amp;=\int_{-R}^{R}\int_{-R}^{R}e^{-(x^2+y^2)}\,dx\,dy. \end{aligned} \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 갑자기 \(x^2+y^2\)가 나타난다. 이 식은 원점에서 점 \((x,y)\)까지의 거리의 제곱이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 \(e^{-(x^2+y^2)}\)의 값은 방향이 아니라 원점에서 얼마나 떨어져 있는지에만 따라 달라진다. 같은 원 위에 있는 점은 전부 같은 함수값을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 변수 적분에서는 전혀 보이지 않던 원의 대칭성이, 적분을 제곱해서 2차원으로 올리는 순간 드러난다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;image-slot&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CEm7E/dJMcagHbDI9/OmyX0ylEUTo81bEvWuSnBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CEm7E/dJMcagHbDI9/OmyX0ylEUTo81bEvWuSnBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CEm7E/dJMcagHbDI9/OmyX0ylEUTo81bEvWuSnBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCEm7E%2FdJMcagHbDI9%2FOmyX0ylEUTo81bEvWuSnBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1980&quot; height=&quot;1260&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;b&gt;&lt;/b&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/section&gt;
&lt;section id=&quot;polar&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;06 / 극좌표로 계산하기&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;극좌표로 바꾸면 빠져 있던 \(r\)이 하나 생긴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원점 중심, 반지름 \(R\)인 원판을 \(D_R\)이라고 두고 원판 위의 적분을 먼저 계산한다.&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ J_R=\iint_{D_R}e^{-(x^2+y^2)}\,dx\,dy. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;극좌표에서는&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ x=r\cos\theta,\qquad y=r\sin\theta,\qquad x^2+y^2=r^2 \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 되고, 넓이 요소는&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[dx\,dy=r\,dr\,d\theta\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 바뀐다. 앞의 \(r\)은 그냥 외워야 하는 장식이 아니다. 반지름이 커질수록 같은 각도 \(d\theta\)가 만드는 호의 길이가 \(r\,d\theta\)만큼 길어지기 때문에 면적에도 \(r\)이 들어간다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;image-slot&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vLcRG/dJMcacdySBy/b1myK8KFjsKTif4x79GNW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vLcRG/dJMcacdySBy/b1myK8KFjsKTif4x79GNW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vLcRG/dJMcacdySBy/b1myK8KFjsKTif4x79GNW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvLcRG%2FdJMcacdySBy%2Fb1myK8KFjsKTif4x79GNW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1980&quot; height=&quot;1134&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;b&gt;&lt;/b&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 계산은 상대적으로 간단해진다.&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \begin{aligned} J_R &amp;amp;=\int_0^{2\pi}\int_0^R e^{-r^2}r\,dr\,d\theta\\ &amp;amp;=2\pi\left[-\frac12e^{-r^2}\right]_0^R\\ &amp;amp;=\pi\left(1-e^{-R^2}\right). \end{aligned} \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 \(R\to\infty\)를 보내면&lt;/p&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[\lim_{R\to\infty}J_R=\pi.\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(\pi\)가 등장하는 이유도 보인다. 각도 \(\theta\)가 원을 한 바퀴 돌면서 \(0\)부터 \(2\pi\)까지 움직이고, 반지름 적분에서 \(1/2\)이 나오면서 최종적으로 \(\pi\)가 남는다.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;rigor&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;07 / 마지막 연결&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정사각형 적분과 원판 적분을 다시 연결한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 그냥 &amp;ldquo;그러니까 \(I^2=\pi\)&amp;rdquo;라고 넘어가면 직관은 맞지만 한 단계가 비게 된다. \(I_R^2\)은 정사각형 위의 적분이고, 방금 계산한 \(J_R\)은 원판 위의 적분이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반지름 \(R\)인 원판 \(D_R\)과 정사각형&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[S_R=[-R,R]\times[-R,R]\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 비교하면&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[D_R\subset S_R\subset D_{\sqrt2R}\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 된다. 적분하는 함수 \(e^{-(x^2+y^2)}\)는 항상 0 이상이므로 영역이 넓어질수록 적분값도 작아지지 않는다. 따라서&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ J_R\le I_R^2\le J_{\sqrt2R}. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양쪽의 극한은 둘 다 \(\pi\)다.&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \lim_{R\to\infty}J_R =\lim_{R\to\infty}J_{\sqrt2R} =\pi. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샌드위치 정리에 의해&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[\lim_{R\to\infty}I_R^2=\pi.\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 \(I_R\to I\)이므로 결국&lt;/p&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[I^2=\pi.\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(I\)는 양수인 함수의 적분이라 \(I&amp;gt;0\)이다. 따라서&lt;/p&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[\boxed{I=\sqrt\pi}\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 된다. 앞에서 \(\Gamma(1/2)=I\)라고 했으므로 드디어&lt;/p&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[\boxed{\displaystyle \Gamma\!\left(\frac12\right)=\sqrt\pi}\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 얻는다.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;half-factorial&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;08 / 처음 질문으로 돌아가기&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 \(0.5!\)은 얼마일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 \(0.5!\)에서 출발했다. 이제 답을 낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감마 함수와 팩토리얼의 관계가 \(\Gamma(n+1)=n!\)이므로 실수까지 확장해서 쓰면&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[x!=\Gamma(x+1)\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 본다. 따라서&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \begin{aligned} \left(\frac12\right)! &amp;amp;=\Gamma\!\left(\frac32\right)\\ &amp;amp;=\frac12\Gamma\!\left(\frac12\right)\\ &amp;amp;=\frac{\sqrt\pi}{2}. \end{aligned} \]&lt;/div&gt;
&lt;div class=&quot;math-display key&quot;&gt;\[\boxed{\displaystyle \left(\frac12\right)!=\frac{\sqrt\pi}{2}}\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반정수 값들도 같은 방식으로 계속 이어진다. 예를 들어 음이 아닌 정수 \(n\)에 대해&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[ \Gamma\!\left(n+\frac12\right) =\frac{(2n)!}{4^n n!}\sqrt\pi. \]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 \(\Gamma(1/2)=\sqrt\pi\) 하나를 알면 \(\Gamma(3/2),\Gamma(5/2),\Gamma(7/2),\ldots\)도 점화식으로 줄줄이 계산할 수 있다.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;ending&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;09 / 정리&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팩토리얼에서 시작했는데 왜 원주율이 나왔을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 질문은 꽤 단순했다. 정수가 아닌 수에도 팩토리얼 값을 줄 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팩토리얼을 곱셈 자체가 아니라 \(n!=n(n-1)!\)이라는 규칙으로 바라보니 적분 표현이 눈에 들어왔다. 그 적분의 지수를 실수까지 열어둔 것이 감마 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 \(s=1/2\)을 넣었더니 \(e^{-x^2}\)이 나타났다. 이 적분을 한 변수에서 바로 계산하기는 어려웠지만, 제곱해서 2차원으로 올리자 \(x^2+y^2\)이 나타나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(x^2+y^2\)은 원점으로부터의 거리 제곱이다. 그 순간 문제는 원의 대칭성을 가진 문제가 되고, 극좌표로 한 바퀴를 돌면서 \(2\pi\)가 등장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &amp;ldquo;팩토리얼 속에 원주율이 숨어 있었다&amp;rdquo;라고 말하면 조금 과장된 표현이다. 더 정확히는 &lt;b&gt;팩토리얼을 연속적으로 확장하는 과정에서 가우스 적분을 만나고, 그 적분의 원형 대칭성 때문에 \(\pi\)가 나타나게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 감마 함수가 재미있는 이유도 여기 있다. 정수의 곱에서 시작한 문제가 적분으로 넘어가고, 다시 2차원 기하로 이어진다. 서로 별개로 보이던 개념들이 한 줄로 연결되는 순간이라 더 재밌는 것 같다.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;extra&quot;&gt;
&lt;p class=&quot;section-no&quot; data-ke-size=&quot;size16&quot;&gt;보충 / 조금 더 궁금하다면&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;감마 함수는 음수에서도 쓸 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 쓴 적분 정의는 \(s&amp;gt;0\)에서만 그대로 수렴한다. 그렇다고 감마 함수가 양수에서 끝나는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점화식&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[\Gamma(s+1)=s\Gamma(s)\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 거꾸로 쓰면&lt;/p&gt;
&lt;div class=&quot;math-display&quot;&gt;\[\Gamma(s)=\frac{\Gamma(s+1)}{s}\]&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 된다. 이 관계를 반복하면 \(0,-1,-2,\ldots\)를 제외한 음의 실수 쪽까지 감마 함수를 이어갈 수 있다. 복소수까지 확장하면 감마 함수는 이 점들에서 단순극을 가지는 유리형 함수가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 부분은 &amp;ldquo;원래 적분에 음수를 그냥 넣는다&amp;rdquo;는 뜻은 아니다. 적분식이 수렴하지 않는 구간에서는 점화식과 해석적 연속을 이용해서 함수를 이어가는 것이기 때문이다.&lt;/p&gt;
&lt;/section&gt;
&lt;/div&gt;</description>
      <category>CS/수학</category>
      <category>가우스</category>
      <category>감마</category>
      <category>적분</category>
      <category>파이</category>
      <category>팩토리얼</category>
      <category>함수</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/82</guid>
      <comments>https://slowgrid.tistory.com/entry/What-is-05-Gamma-Function#entry82comment</comments>
      <pubDate>Sun, 13 Sep 2026 17:20:01 +0900</pubDate>
    </item>
    <item>
      <title>[YOLO 기초 5] 사전 학습 가중치는 새 모델에 어떻게 전달될까?</title>
      <link>https://slowgrid.tistory.com/entry/YOLO-Basics-5-How-are-pre-trained-weights-passed-to-the-new-model</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;.pt&lt;/code&gt;와 &lt;code&gt;.yaml&lt;/code&gt;, Weight Transfer, 클래스 수 변경, 1채널 열화상 입력&lt;br&gt;&lt;strong&gt;버전 메모&lt;/strong&gt;&lt;br&gt;이 글은 &lt;strong&gt;2026-09-09 최종 검증 기준 Ultralytics 공식 문서와 공개 구현&lt;/strong&gt;을 직접 확인하면서 정리하였다. &lt;code&gt;optimizer=auto&lt;/code&gt;, &lt;code&gt;cls_remap&lt;/code&gt;, 체크포인트 저장 형식, YOLO26의 &lt;code&gt;nms&lt;/code&gt;/손실 이름처럼 구현에 가까운 부분은 버전에 따라 달라질 수 있기에, 실제 실험에서는 &lt;code&gt;ultralytics&lt;/code&gt; 버전, &lt;code&gt;args.yaml&lt;/code&gt;, 학습 로그를 함께 확인해 두는 편이 좋을 것이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;앞에서는 사전 학습과 미세 조정을 &lt;strong&gt;왜 사용하는지&lt;/strong&gt; 살펴봤다.&lt;/p&gt;
&lt;p&gt;이번에는 실제 Ultralytics 미세 조정 과정을 따라가면서 다음 질문들을 확인해 볼 것이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yolo26n.pt를 불러오면 80-class COCO 모델의 가중치가 1-class thermal person 모델에 어디까지, 어떤 조건으로 전달될까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 부분을 무작정 코드로 따라가려하면 헷갈리기 쉬운데, 중요한 건 &lt;strong&gt;사전 학습 체크포인트의 모든 텐서가 무조건 그대로 복사되는 것은 아니라는 것이다.&lt;/strong&gt; 목표 데이터셋의 클래스 수나 입력 채널 수가 달라지면 새 모델 구조와 텐서 형상을 확인하고, 호환되는 부분을 중심으로 가중치가 전달된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. &lt;code&gt;.pt&lt;/code&gt;와 &lt;code&gt;.yaml&lt;/code&gt;은 무엇이 다를까?&lt;/h2&gt;
&lt;p&gt;현재 프로젝트는 여기서 출발하고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from ultralytics import YOLO

model = YOLO(&amp;quot;yolo26n.pt&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.pt&lt;/code&gt; 체크포인트는 사전 학습된 가중치와 모델을 다시 구성하는 데 필요한 정보를 담고 있다. 반면 YAML은 모델 구조를 정의하는 설정이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;yolo26n.pt
→ 구조 + 학습된 가중치가 들어 있는 체크포인트
→ 미세 조정의 출발점

yolo26n.yaml
→ 모델 구조 정의
→ 가중치 없이 시작하면 처음부터 학습&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;필요하다면 구조는 YAML에서 만들고 가중치만 따로 옮길 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model = YOLO(&amp;quot;yolo26n.yaml&amp;quot;).load(&amp;quot;yolo26n.pt&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 우리가 알 수 있는건 &lt;code&gt;.pt&lt;/code&gt;에서 시작한다는 건 단순히 파일 확장자가 다르다는 뜻이 아니라, &lt;strong&gt;이미 학습된 파라미터를 초기값으로 사용한다는 뜻&lt;/strong&gt;이라는 것. 다만 목표 데이터셋의 클래스 수와 입력 채널 수 등이 달라지면 모든 파라미터를 똑같이 재사용할 수 있는 것은 아니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 가중치 전달은 어떤 흐름으로 이해하면 될까?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;YOLO(&amp;quot;yolo26n.pt&amp;quot;)&lt;/code&gt;에서 시작해 새 데이터셋으로 &lt;code&gt;train()&lt;/code&gt;을 호출한다고 해서 COCO 모델 전체를 그대로 고정된 구조로 사용하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;앞 글에서 본 내용을 실제 미세 조정 흐름으로 연결하면 다음처럼 생각할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;사전 학습 체크포인트 로드

↓

목표 데이터셋 정보 확인
- 클래스 수
- 클래스 이름
- 입력 채널 수 등

↓

목표 과제에 맞는 모델 구성

↓

사전 학습 체크포인트의 가중치와
새 모델 파라미터의 형상 호환 여부 확인

↓

호환되는 가중치 전달

↓

클래스 출력이나 입력 채널처럼
구조가 달라진 부분은 별도 처리

↓

새 데이터셋에서 미세 조정 시작&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 핵심은 &lt;strong&gt;단순히 &amp;quot;체크포인트 전체를 복사한다&amp;quot;가 이게 아니라 &amp;quot;새 모델에서 재사용할 수 있는 가중치를 전달한다&amp;quot;&lt;/strong&gt;는 점이다.&lt;/p&gt;
&lt;p&gt;그래서 같은 &lt;code&gt;yolo26n.pt&lt;/code&gt;를 시작점으로 사용하더라도 목표 데이터셋이 80개 클래스인지 1개 클래스인지, 입력이 3채널인지 1채널인지에 따라 실제로 그대로 재사용되는 텐서는 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;이제 이 흐름을 클래스 수와 입력 채널이라는 두 가지 구체적인 경우로 나눠서 보면 이해가 쉬워진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 클래스 수가 바뀌면 사전 학습된 검출 헤드는 어떻게 될까?&lt;/h2&gt;
&lt;p&gt;COCO 기반의 사전 학습 객체 검출 모델은 80개 클래스를 예측하도록 학습되어 있다.&lt;/p&gt;
&lt;p&gt;반면 현재 데이터셋은 예를 들어 다음처럼 한 개의 클래스만 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;클래스 0
→ person&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러면 당연하게도 80개 클래스를 출력하던 검출 헤드를 그대로 사용할 수는 없게 된다.&lt;/p&gt;
&lt;p&gt;그래서 Ultralytics는 미세 조정을 시작할 때 목표 데이터셋의 클래스 수에 맞게 새 모델 구조를 만들고, &lt;strong&gt;텐서 형상이 맞는 가중치만 자동으로 전달&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;YOLO26 기준으로 나눠 보면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;백본 / 넥
→ 클래스 수와 직접 관계없는 가중치가 대부분이므로 재사용 가능

검출 헤드의 박스 회귀 부분
→ 클래스 수와 무관한 형상은 재사용 가능

검출 헤드의 클래스 출력 부분
→ 목표 클래스 수에 맞게 조정 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 Ultralytics에는 기본적으로 &lt;code&gt;cls_remap=True&lt;/code&gt;가 설정되어 있어, 원천 모델과 목표 데이터셋의 &lt;strong&gt;클래스 이름이 일치하면 해당 클래스의 분류 헤드 가중치를 가능한 범위에서 재사용&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;예를 들어 목표 데이터셋의 클래스가 &lt;code&gt;person&lt;/code&gt;이라면 COCO의 &lt;code&gt;person&lt;/code&gt;과 이름이 대응되므로 해당 분류 정보가 초기값으로 전달될 수 있다.&lt;/p&gt;
&lt;p&gt;현재 구현은 클래스 이름을 비교할 때 대소문자와 앞뒤 공백을 무시한다.&lt;/p&gt;
&lt;p&gt;다시 말해 다음 정도는 같은 이름으로 취급될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;person
Person
&amp;quot; person &amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pedestrian
human
walker&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 의미는 비슷해도 문자열이 다른 이름은 자동으로 같은 클래스로 판단되지 않는다.&lt;/p&gt;
&lt;p&gt;그래서 데이터셋 YAML의 &lt;code&gt;names&lt;/code&gt;는 단순히 화면에 표시하기 위한 문자열뿐만 아니라 &lt;strong&gt;사전 학습 분류 헤드의 클래스 대응에도 영향을 줄 수 있는 설정&lt;/strong&gt;이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;YOLO26의 헤드는 단순한 하나의 출력층이 아니다.&lt;/strong&gt;&lt;br&gt;YOLO26의 엔드투엔드(end-to-end) 구조에서는 학습 시 일대다(one-to-many)와 일대일(one-to-one) 검출 가지가 함께 존재할 수 있다. 따라서 &amp;quot;클래스 수가 바뀌면 헤드 전체를 전부 버린다&amp;quot;고 단순화하는 것보다는, &lt;strong&gt;호환되는 부분은 재사용하고 클래스 수에 직접 묶인 출력 부분은 재구성 또는 클래스 이름 기반으로 부분 전달한다&lt;/strong&gt;고 이해하는 편이 좋을 것이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;4. 열화상 입력은 3채널로 복제해야 할까, 1채널로 넣어야 할까?&lt;/h2&gt;
&lt;p&gt;이 부분은 현재 Ultralytics 버전에서는 조금 더 정확하게 구분해야 한다.&lt;/p&gt;
&lt;h3&gt;방법 A. 열화상 값을 3채널에 동일하게 복제&lt;/h3&gt;
&lt;p&gt;1채널 열화상 영상을 다음처럼 3채널로 만들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;열화상 1채널
 ↓
같은 회색조 값을 R / G / B 위치에 복제
 ↓
3채널 입력&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;R = G = B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;장점은 기존 YOLO26의 3채널 입력 구조를 바꾸지 않으므로 첫 합성곱을 포함한 사전 학습 가중치의 형상이 그대로 맞는다는 것이다.&lt;/p&gt;
&lt;p&gt;하지만 이 영상은 의미적으로 실제 RGB 영상이 아니다.&lt;/p&gt;
&lt;p&gt;이 때문에 사전 학습에서 학습한 &lt;strong&gt;채널 간 색상 대비 특징&lt;/strong&gt;은 그대로 의미가 없거나 약해질 수 있다.&lt;/p&gt;
&lt;p&gt;반면 다음 특징은 여전히 유용한 초기값이 될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;경계선
형태
윤곽
국소 대비
공간적 패턴&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;방법 B. Ultralytics의 실제 1채널 입력 사용&lt;/h3&gt;
&lt;p&gt;현재 Ultralytics는 객체 검출 데이터셋 YAML에서 다음처럼 입력 채널 수를 지정할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;channels: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경우 데이터 로더는 이미지를 회색조 1채널로 읽고, 학습 모델도 목표 데이터셋의 채널 수에 맞춰 1채널 입력 구조로 만들어진다.&lt;/p&gt;
&lt;p&gt;정리하자면, 최신 버전에서는 반드시 3채널로 복제해야만 YOLO를 학습할 수 있는 것은 아니다.&lt;/p&gt;
&lt;p&gt;다만 &lt;strong&gt;3채널 사전 학습 모델을 1채널 모델로 바꾸면 첫 합성곱 가중치의 형상이 달라진다는 것&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;사전 학습 첫 합성곱
→ 입력 채널 3개

1채널 목표 모델의 첫 합성곱
→ 입력 채널 1개&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 Ultralytics 공개 코드에는 첫 합성곱에 대해 형상이 완전히 일치하지 않아도 겹치는 채널 범위의 가중치를 부분적으로 복사하는 예외 처리가 있다.&lt;/p&gt;
&lt;p&gt;결국 3채널에서 1채널로 바꿨다고 해서 첫 합성곱이 항상 전부 무작위 초기화된다고 단정하면 정확하지 않다.&lt;/p&gt;
&lt;p&gt;하지만 이 부분은 중요한 차이가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;3채널 회색조 복제
→ 기존 3채널 합성곱을 그대로 사용

1채널 모델
→ 첫 합성곱 자체가 1채널 구조로 변경됨&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 코드의 부분 복사 방식은 세 RGB 채널의 가중치를 평균내거나 합쳐 1채널 가중치를 만드는 것과 동일한 방식이 아니다.&lt;/p&gt;
&lt;p&gt;그래서 아래 두 방법은 &lt;strong&gt;서로 다른 실험 조건&lt;/strong&gt;으로 취급하는 것이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A. 3채널 복제 + 기존 사전 학습 구조 유지
B. channels: 1 + 1채널 모델 구조 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 프로젝트는 전처리 방법에 따른 성능만 비교하려는 실험이기에 입력 채널 처리 방식까지 중간에 바꾸지 않는 것이 좋다.&lt;/p&gt;
&lt;h3&gt;열화상 원본 비트 깊이도 확인해야 한다&lt;/h3&gt;
&lt;p&gt;열화상 센서 데이터는 경우에 따라 8비트 영상이 아니라 14비트 또는 16비트 원시 값으로 저장될 수 있다.&lt;/p&gt;
&lt;p&gt;이런 데이터를 학습용 8비트 이미지로 변환한다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원시 열 신호
 ↓
정규화 / 클리핑 / 구간 매핑
 ↓
8비트 영상&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;과정 자체가 중요한 전처리가 된다.&lt;/p&gt;
&lt;p&gt;이 때문에 CLAHE나 양방향 필터(Bilateral Filter)의 효과를 비교하기 전에 &lt;strong&gt;모든 실험이 동일한 원시 값 → 학습 영상 변환 규칙을 사용했는지&lt;/strong&gt; 확인할 필요가 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 가중치 전달을 이해할 때 기억할 원칙&lt;/h2&gt;
&lt;p&gt;앞의 내용을 정리하면 사전 학습 가중치 전달은 다음처럼 이해할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;사전 학습 체크포인트
↓
목표 데이터셋의 클래스 수 / 입력 채널 수 확인
↓
새 목표 모델 구성
↓
형상이 호환되는 가중치 전달
↓
클래스 수나 입력 채널에 직접 묶인 부분은 조정
↓
미세 조정 시작&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 다음 두 단순화는 모두 조심해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;quot;사전 학습 모델을 쓰면 모든 가중치가 그대로 복사된다&amp;quot;
→ 항상 그렇지 않음

&amp;quot;클래스 수나 채널 수가 바뀌면 헤드나 첫 합성곱을 전부 버린다&amp;quot;
→ 현재 구현에서는 호환되는 부분이나 일부 겹치는 범위를 재사용할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;특히 현재 Ultralytics의 클래스 이름 기반 재사용(&lt;code&gt;cls_remap&lt;/code&gt;)과 3채널 → 1채널 첫 합성곱의 부분 복사는 &lt;strong&gt;버전에 민감한 구현 세부사항&lt;/strong&gt;이다. 재현이 중요하다면 설치된 버전과 실제 학습 로그를 함께 확인하는 것이 중요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;.pt&lt;/code&gt;는 사전 학습 가중치를 포함한 체크포인트이고, &lt;code&gt;.yaml&lt;/code&gt;은 주로 모델 구조를 정의한다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;목표 데이터셋의 클래스 수가 바뀌면 모든 가중치가 동일하게 재사용되는 것이 아니라, &lt;strong&gt;호환되는 텐서는 전달하고 클래스 수에 직접 묶인 출력 부분은 조정한다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;person&lt;/code&gt;처럼 원천 모델과 목표 데이터셋의 클래스 이름이 대응되면 현재 Ultralytics의 &lt;code&gt;cls_remap&lt;/code&gt; 동작에 의해 분류 정보를 일부 재사용할 수 있다.&lt;/li&gt;
&lt;li&gt;열화상은 &lt;code&gt;3채널 복제&lt;/code&gt;와 &lt;code&gt;channels: 1&lt;/code&gt;을 모두 사용할 수 있지만, 두 방식은 첫 합성곱 구조와 사전 학습 가중치 활용 방식이 다른 &lt;strong&gt;서로 다른 실험 조건&lt;/strong&gt;이다.&lt;/li&gt;
&lt;li&gt;열화상 원본이 14/16비트라면 8비트 학습 영상으로 바꾸는 과정 자체도 중요한 전처리 조건이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이제 사전 학습 가중치가 어떤 식으로 출발점이 되는지는 알았으니 다음 글에서는 실제로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;model.train(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;에 넣는 &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;epochs&lt;/code&gt;, &lt;code&gt;batch&lt;/code&gt;, &lt;code&gt;imgsz&lt;/code&gt;, &lt;code&gt;seed&lt;/code&gt;, &lt;code&gt;optimizer&lt;/code&gt; 같은 값들이 &lt;strong&gt;학습 조건을 어떻게 바꾸는지&lt;/strong&gt; 살펴볼 것이다.&lt;/p&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>PT</category>
      <category>YOLO26</category>
      <category>가중치</category>
      <category>사전 학습</category>
      <category>열영상</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/81</guid>
      <comments>https://slowgrid.tistory.com/entry/YOLO-Basics-5-How-are-pre-trained-weights-passed-to-the-new-model#entry81comment</comments>
      <pubDate>Sat, 12 Sep 2026 17:51:01 +0900</pubDate>
    </item>
    <item>
      <title>몬테카를로 방법 | 모든 경우를 계산할 수 없다면?</title>
      <link>https://slowgrid.tistory.com/entry/Monte-Carlo-Method-What-if-you-cant-calculate-all-cases</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;카드게임에서 시작해 핵물리학, 금융, 그래픽, AI까지 이어진 무작위 계산 방법&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;오늘 정리한 내용&lt;/h2&gt;
&lt;p&gt;처음 &lt;strong&gt;몬테카를로(Monte Carlo)&lt;/strong&gt;라는 이름을 봤을 때는 알고리즘보다 카지노가 먼저 떠올랐다.&lt;/p&gt;
&lt;p&gt;룰렛, 카드, 주사위 같은 것들이 있는 모나코의 몬테카를로.&lt;/p&gt;
&lt;p&gt;그런데 찾아보니 이 이름은 실제로 카지노와 관련이 있었고, 더 재미있는 건 현대적인 몬테카를로 방법의 출발점 중 하나도 거창한 수식에서 시작된 것이 아니라 &lt;strong&gt;병에서 회복하며 하던 카드게임에서 나온 아이디어&lt;/strong&gt;였다는 것이다.&lt;/p&gt;
&lt;p&gt;처음에는 무작위성을 이용하는 계산이라고 해서 정확한 답과는 조금 거리가 있는 방법이라고 생각했다.&lt;/p&gt;
&lt;p&gt;보통 계산에서는 우연을 줄여야 정확해진다고 생각하기 때문이다.&lt;/p&gt;
&lt;p&gt;그런데 몬테카를로 방법은 방향이 조금 다르다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무작위성을 없애는 대신, 무작위 표본을 아주 많이 만들어 답에 가까워진다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;정리하고 나니 핵심은 생각보다 단순했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;모든 경우를 직접 계산하기 어려우니

가능한 상황을 무작위로 여러 번 만들어보고

그 결과를 모아 전체의 경향을 추정한다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 흐름이 몬테카를로 방법의 기본 구조다.&lt;/p&gt;
&lt;p&gt;정확하게 말하면 몬테카를로는 하나의 특정한 &lt;strong&gt;알고리즘&lt;/strong&gt;이라기보다,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무작위 표본 추출을 반복해 값이나 확률, 분포를 추정하는 여러 계산 기법을 묶어 부르는 방법론&lt;/strong&gt;에 가깝다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;시작은 솔리테어였다&lt;/h3&gt;
&lt;p&gt;1940년대 수학자 &lt;strong&gt;스타니스와프 울람(Stanisław Ulam)&lt;/strong&gt;은 병에서 회복하던 중 시간을 보내며 솔리테어를 즐겼다고 한다.&lt;/p&gt;
&lt;p&gt;그러다 한 가지 궁금증이 생겼다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이 솔리테어 게임이 성공할 확률은 얼마나 될까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;수학자라면 자연스럽게 가능한 카드 배열을 분석해서 확률을 계산하려고 생각할 수 있다.&lt;/p&gt;
&lt;p&gt;울람 역시 처음에는 그런 방향으로 문제를 생각했다.&lt;/p&gt;
&lt;p&gt;문제는 경우의 수가 너무 많다는 것이었다.&lt;/p&gt;
&lt;p&gt;카드가 놓일 수 있는 상황을 하나씩 전부 따지고 들어가면 확률을 구하기도 전에 계산 자체가 너무 복잡해진다.&lt;/p&gt;
&lt;p&gt;여기서 접근을 바꾼다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;모든 경우를 계산하지 말고, 게임을 아주 많이 해보면 어떨까?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;예를 들어 솔리테어를 1,000번 해봤는데 230번 성공했다면,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;230 / 1000 = 0.23&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이므로 성공 확률을 대략 23%라고 추정할 수 있다.&lt;/p&gt;
&lt;p&gt;정확한 식을 먼저 구한 다음 답을 계산하는 것이 아니라,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;무작위 실험

→ 반복

→ 결과 집계

→ 확률 추정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;순서로 답에 가까워지는 것이다.&lt;/p&gt;
&lt;p&gt;몇 번만 실험하면 결과가 크게 흔들릴 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 같은 실험을 충분히 많이 반복하면 관측된 비율은 점점 실제 확률 주변으로 안정되기 시작한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;한 장으로 보는 현대적 몬테카를로 방법의 시작&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMakCm/dJMcaircNOw/wlmvlFzu9KsoZfEFxKJ0W0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMakCm/dJMcaircNOw/wlmvlFzu9KsoZfEFxKJ0W0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMakCm/dJMcaircNOw/wlmvlFzu9KsoZfEFxKJ0W0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMakCm%2FdJMcaircNOw%2FwlmvlFzu9KsoZfEFxKJ0W0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;카드게임에서 떠올린 아이디어가 초기 전자식 컴퓨터와 만나 현대적인 몬테카를로 방법으로 발전했다.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;그런데 카드게임이 어떻게 핵물리학으로 이어졌을까&lt;/h3&gt;
&lt;p&gt;울람은 이 아이디어를 &lt;strong&gt;존 폰 노이만(John von Neumann)&lt;/strong&gt;에게 이야기한다.&lt;/p&gt;
&lt;p&gt;폰 노이만은 이 방식이 단순히 카드게임의 성공 확률을 구하는 데서 끝나지 않을 수 있다는 점을 알아봤다.&lt;/p&gt;
&lt;p&gt;당시 로스앨러모스에서는 핵반응을 연구하면서 &lt;strong&gt;중성자가 어떻게 움직이고, 충돌하고, 흡수되는지&lt;/strong&gt; 계산해야 했다.&lt;/p&gt;
&lt;p&gt;여기에는 불확실한 요소가 많았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;중성자는 어느 방향으로 움직일까?&lt;/li&gt;
&lt;li&gt;언제 다른 원자핵과 충돌할까?&lt;/li&gt;
&lt;li&gt;충돌한 뒤 어느 방향으로 움직일까?&lt;/li&gt;
&lt;li&gt;흡수될까?&lt;/li&gt;
&lt;li&gt;아니면 또 다른 반응을 일으킬까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 모든 가능한 경로를 하나씩 정확하게 추적하려면 계산량이 엄청나게 커진다.&lt;/p&gt;
&lt;p&gt;그런데 울람이 생각한 방식을 적용하면 관점이 달라진다.&lt;/p&gt;
&lt;p&gt;가상의 중성자 하나를 만들고,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이동 거리를 확률적으로 정한다

→ 충돌이 일어나면 결과를 표본 추출한다

→ 새로운 상태에서 다시 이동한다

→ 같은 과정을 반복한다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 가상의 중성자 경로를 수천 번, 수만 번, 수십만 번 만들어 결과를 모으는 것이다.&lt;/p&gt;
&lt;p&gt;그러면 개별 중성자 하나가 정확히 어디로 갈지 예측하지 못하더라도,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;수많은 중성자가 전체적으로 어떻게 행동하는지는 통계적으로 추정할 수 있다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;확률적으로 가능한 사건을 컴퓨터 안에서 반복해서 표본 추출하고, 그 결과를 통계적으로 분석한다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이것이 몬테카를로 방법의 핵심 아이디어다.&lt;/p&gt;
&lt;p&gt;여기서 중요한 점이 하나 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;울람이 무작위 실험이나 통계적 표본 추출 자체를 처음 발명한 것은 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;비슷한 생각은 훨씬 이전부터 존재했고, 이후 살펴볼 뷔퐁의 바늘 문제처럼 무작위 실험을 이용한 수학적 접근도 이미 있었다.&lt;/p&gt;
&lt;p&gt;1940년대 몬테카를로 방법이 특별했던 이유는 다른 곳에 있었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;확률적인 표본 추출과 새롭게 등장한 전자식 컴퓨터가 만났다는 것이다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQke3h/dJMcaaz3nvV/S4vYgQ2jjXWnZUujuhPsH0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQke3h/dJMcaaz3nvV/S4vYgQ2jjXWnZUujuhPsH0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQke3h/dJMcaaz3nvV/S4vYgQ2jjXWnZUujuhPsH0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQke3h%2FdJMcaaz3nvV%2FS4vYgQ2jjXWnZUujuhPsH0%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;몬테카를로 방법은 한 사람의 아이디어에 머무르지 않고 당시 연구자들의 논의와 초기 컴퓨터 기술을 만나 실제 계산 방법으로 발전했다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;당시 등장하기 시작한 전자식 컴퓨터는 이런 방식과 특히 잘 맞았다.&lt;/p&gt;
&lt;p&gt;사람에게는 같은 계산을 수만 번 반복하는 일이 너무 지루하고 느리다.&lt;/p&gt;
&lt;p&gt;하지만 컴퓨터는 같은 계산을 계속 수행하는 데 강하다.&lt;/p&gt;
&lt;p&gt;그래서 이전에는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이론적으로 가능하지만 현실적으로 반복하기 어려웠던 방법&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이&lt;/p&gt;
&lt;p&gt;&lt;code&gt;실제로 사용할 수 있는 계산 전략&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;으로 바뀌기 시작했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo0arJ/dJMcacdtLwC/pJkzqm7ghTfs6M1Txulcuk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo0arJ/dJMcacdtLwC/pJkzqm7ghTfs6M1Txulcuk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo0arJ/dJMcacdtLwC/pJkzqm7ghTfs6M1Txulcuk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo0arJ%2FdJMcacdtLwC%2FpJkzqm7ghTfs6M1Txulcuk%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;전자식 컴퓨터의 등장으로 수천 번, 수만 번의 무작위 실험을 반복하는 전략이 현실적인 계산 방법이 되었다.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;이름은 왜 하필 몬테카를로였을까&lt;/h3&gt;
&lt;p&gt;여기까지 보면 계산 방법의 구조와 이름이 전혀 연결되지 않는 것처럼 보인다.&lt;/p&gt;
&lt;p&gt;그런데 이름의 유래도 방법의 특징과 꽤 잘 어울린다.&lt;/p&gt;
&lt;p&gt;이 계산법에 &lt;strong&gt;Monte Carlo&lt;/strong&gt;라는 이름을 제안한 인물로 알려진 사람은 로스앨러모스의 수학자이자 물리학자인 &lt;strong&gt;니컬러스 메트로폴리스(Nicholas Metropolis)&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;몬테카를로는 모나코의 유명한 카지노 지역이다.&lt;/p&gt;
&lt;p&gt;카지노에서 빠질 수 없는 것이 바로 &lt;strong&gt;확률과 우연&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;룰렛, 카드게임, 주사위처럼 매번 결과가 확률적으로 달라지는 게임이 가득한 곳이다.&lt;/p&gt;
&lt;p&gt;울람의 친척이 몬테카를로에서 도박을 즐겼다는 일화도 이 이름의 배경으로 전해진다.&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;무작위성을 이용해 답을 찾는 계산법에 확률과 도박으로 유명한 장소의 이름을 붙인 것이다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bdfkp/dJMcabZ5kVD/EHnTlHZZLlPXnT34MonLKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bdfkp/dJMcabZ5kVD/EHnTlHZZLlPXnT34MonLKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bdfkp/dJMcabZ5kVD/EHnTlHZZLlPXnT34MonLKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBdfkp%2FdJMcabZ5kVD%2FEHnTlHZZLlPXnT34MonLKk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;확률과 우연을 떠올리게 하는 ‘Monte Carlo’라는 이름은 무작위 표본을 반복하는 계산 방법의 성격과도 잘 맞았다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;그리고 1949년 메트로폴리스와 울람이 &lt;strong&gt;「The Monte Carlo Method」&lt;/strong&gt;라는 논문을 발표하면서 이 이름은 학계에 본격적으로 알려졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;실제로는 어떻게 계산할까&lt;/h3&gt;
&lt;p&gt;몬테카를로 방법을 설명할 때 자주 나오는 예제가 &lt;strong&gt;원주율 π 추정&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;반지름이 1인 원을 &lt;strong&gt;한 변의 길이가 2인 정사각형 안에 꼭 맞게 넣었다고 생각해보자.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그러면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원의 넓이 = &lt;strong&gt;π&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;정사각형의 넓이 = &lt;strong&gt;2 × 2 = 4&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;이 정사각형 안에 점을 완전히 무작위로 계속 찍는다.&lt;/p&gt;
&lt;p&gt;점이 어느 위치에 찍힐 확률이 모두 같다고 하면,&lt;/p&gt;
&lt;p&gt;전체 점 가운데 원 안에 들어가는 점의 비율은 넓이의 비율과 비슷해진다.&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;원의 넓이 / 정사각형의 넓이 = π / 4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이므로&lt;/p&gt;
&lt;p&gt;&lt;code&gt;π ≈ 4 × (원 안에 들어간 점의 수 / 전체 점의 수)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;로 π를 추정할 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 점을 몇 개만 찍으면 값이 크게 흔들린다.&lt;/p&gt;
&lt;p&gt;100개,&lt;/p&gt;
&lt;p&gt;1,000개,&lt;/p&gt;
&lt;p&gt;10,000개,&lt;/p&gt;
&lt;p&gt;1,000,000개처럼&lt;/p&gt;
&lt;p&gt;점의 수를 계속 늘리면 결과는 점점 우리가 알고 있는 &lt;strong&gt;3.14159…&lt;/strong&gt; 근처로 모이기 시작한다.&lt;/p&gt;
&lt;p&gt;여기에는 표본의 수가 많아질수록 관측된 평균이나 비율이 실제 값에 가까워지는 &lt;strong&gt;큰 수의 법칙&lt;/strong&gt;이 배경에 있다.&lt;/p&gt;
&lt;p&gt;그리고 여기서 몬테카를로 방법의 성격이 가장 잘 드러난다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;각각의 점은 무작위인데, 그 점을 충분히 많이 모으면 안정적인 값이 나타난다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;사실 무작위 실험 자체는 더 오래된 아이디어였다&lt;/h3&gt;
&lt;p&gt;울람이 무작위 실험을 수학에 처음 사용한 것은 아니다.&lt;/p&gt;
&lt;p&gt;비슷한 발상은 훨씬 이전에도 존재했다.&lt;/p&gt;
&lt;p&gt;대표적인 예가 18세기 프랑스 수학자 &lt;strong&gt;조르주루이 르클레르 드 뷔퐁(Georges-Louis Leclerc, Comte de Buffon)&lt;/strong&gt;의 &lt;code&gt;뷔퐁의 바늘 문제&lt;/code&gt;다.&lt;/p&gt;
&lt;p&gt;일정한 간격으로 평행선을 그리고 바늘을 무작위로 떨어뜨린 뒤,&lt;/p&gt;
&lt;p&gt;바늘이 선을 가로지르는 비율을 이용해 π와 관계된 값을 추정한다.&lt;/p&gt;
&lt;p&gt;방식만 보면 상당히 몬테카를로스럽다.&lt;/p&gt;
&lt;p&gt;그렇다면 1940년대의 몬테카를로 방법은 무엇이 달랐을까?&lt;/p&gt;
&lt;p&gt;결정적인 변화는 &lt;strong&gt;컴퓨터&lt;/strong&gt;였다.&lt;/p&gt;
&lt;p&gt;사람이 무작위 실험을 100번 하는 것은 가능하다.&lt;/p&gt;
&lt;p&gt;1,000번도 노력하면 가능하다.&lt;/p&gt;
&lt;p&gt;하지만 100만 번, 1억 번이 되면 이야기가 달라진다.&lt;/p&gt;
&lt;p&gt;전자식 컴퓨터가 등장하면서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;같은 계산을 엄청나게 많이 반복한다&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 전략 자체가 현실적인 계산 방법이 되었다.&lt;/p&gt;
&lt;p&gt;그래서 현대적인 몬테카를로 방법의 발전은 대략 다음 세 가지가 만난 결과라고 볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;확률론과 통계적 표본 추출

+

무작위 실험

+

컴퓨터의 반복 계산 능력&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;지금은 어디에 사용할까&lt;/h2&gt;
&lt;p&gt;몬테카를로 방법은 &lt;strong&gt;불확실성이 존재하고 가능한 경우의 수가 너무 많아 직접 계산하기 어려운 문제&lt;/strong&gt;에서 매우 넓게 사용된다.&lt;/p&gt;
&lt;h3&gt;금융&lt;/h3&gt;
&lt;p&gt;미래의 주가나 투자 결과를 하나의 값으로 단정하는 대신 수많은 시나리오를 만들어본다.&lt;/p&gt;
&lt;p&gt;각 시나리오의 결과를 모으면 예상 수익뿐 아니라&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;손실 가능성이나 결과가 어느 범위에 분포하는지&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;함께 볼 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;과학과 공학&lt;/h3&gt;
&lt;p&gt;입자의 이동처럼 확률적으로 일어나는 현상이나, 변수가 너무 많아 직접 계산하기 어려운 시스템을 반복해서 시뮬레이션하는 데 활용한다.&lt;/p&gt;
&lt;p&gt;핵물리학과 입자 수송 문제는 몬테카를로 방법이 발전한 대표적인 분야이기도 하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;기상과 복잡계&lt;/h3&gt;
&lt;p&gt;초기 조건이나 여러 변수에 따라 결과가 달라지는 시스템에서는 하나의 미래만 계산하기보다 조건을 조금씩 바꾼 여러 결과를 비교하는 것이 중요하다.&lt;/p&gt;
&lt;p&gt;여러 시나리오를 만들고 결과의 분포를 살펴본다는 점에서 몬테카를로적 사고와 연결된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;컴퓨터 그래픽&lt;/h3&gt;
&lt;p&gt;우리가 보는 현실의 빛은 광원에서 출발해 물체에 부딪히고, 반사되고, 굴절되면서 눈에 들어온다.&lt;/p&gt;
&lt;p&gt;사실적인 그래픽을 만들려면 수많은 빛의 경로를 계산해야 한다.&lt;/p&gt;
&lt;p&gt;하지만 가능한 빛의 경로를 전부 계산하는 것은 현실적으로 불가능하다.&lt;/p&gt;
&lt;p&gt;그래서 렌더링에서는 일부 빛의 경로를 확률적으로 선택해 계산하고,&lt;/p&gt;
&lt;p&gt;그 결과를 많이 모아 실제와 가까운 조명을 만들어내는 방식이 사용된다.&lt;/p&gt;
&lt;p&gt;대표적으로 &lt;strong&gt;패스 트레이싱(Path Tracing)&lt;/strong&gt;이 이런 몬테카를로 적분과 깊게 연결되어 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;게임과 인공지능&lt;/h3&gt;
&lt;p&gt;여기서 특히 연결되는 개념이 &lt;strong&gt;몬테카를로 트리 탐색(Monte Carlo Tree Search, MCTS)&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;바둑처럼 가능한 수가 너무 많은 게임에서는 앞으로 나올 모든 경우를 끝까지 계산하기 어렵다.&lt;/p&gt;
&lt;p&gt;그래서 방향을 바꾼다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;모든 미래를 계산하는 대신 여러 미래를 시험해본다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;예를 들어 어떤 수를 둔 다음 게임을 계속 진행해보고,&lt;/p&gt;
&lt;p&gt;그 결과가 승리로 이어지는지 반복해서 확인한다.&lt;/p&gt;
&lt;p&gt;그리고 여러 결과를 모아&lt;/p&gt;
&lt;p&gt;&lt;code&gt;어떤 선택에서 좋은 결과가 더 많이 나왔는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 판단한다.&lt;/p&gt;
&lt;p&gt;다만 MCTS는 단순히 미래를 완전히 무작위로만 플레이하는 방법은 아니다.&lt;/p&gt;
&lt;p&gt;일반적인 MCTS는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;선택 Selection

→ 확장 Expansion

→ 시뮬레이션 Simulation

→ 역전파 Backpropagation&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;과정을 반복한다.&lt;/p&gt;
&lt;p&gt;탐색 과정에서 결과가 좋았던 선택은 더 유망한 선택으로 평가되고,&lt;/p&gt;
&lt;p&gt;이후에는 그런 가지를 좀 더 집중적으로 탐색한다.&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이미 많이 확인해서 좋아 보이는 선택을 활용하면서도, 아직 충분히 살펴보지 않은 선택도 탐색하는 균형&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 중요하다.&lt;/p&gt;
&lt;p&gt;현대적인 시스템에서는 여기에 신경망의 정책이나 가치 평가가 결합되기도 한다.&lt;/p&gt;
&lt;p&gt;이 구조를 보고 나니 다시 솔리테어 이야기가 떠올랐다.&lt;/p&gt;
&lt;p&gt;1940년대 울람이&lt;/p&gt;
&lt;p&gt;&lt;code&gt;모든 카드 배열을 계산하기 어렵다면 게임을 여러 번 해보면 어떨까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 생각했던 것과 기본적인 방향에는 닮은 점이 있다.&lt;/p&gt;
&lt;p&gt;다루는 문제는 카드게임에서 핵물리학, 그래픽, 금융, 바둑과 AI까지 크게 달라졌지만,&lt;/p&gt;
&lt;p&gt;그 안에는 공통된 생각이 숨어 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;모든 경우를 계산할 수 없다면?&lt;/h2&gt;
&lt;p&gt;결국 몬테카를로 방법이 던지는 질문은 단순하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;모든 경우를 계산할 수 없다면 어떻게 할 것인가?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;모든 경우를 아는 대신,&lt;/p&gt;
&lt;p&gt;그중 일부를 무작위로 경험해본다.&lt;/p&gt;
&lt;p&gt;그리고 그 경험을 충분히 많이 모아 전체를 추정한다.&lt;/p&gt;
&lt;p&gt;개별 결과에는 우연이 들어 있지만,&lt;/p&gt;
&lt;p&gt;수많은 결과를 모으면 오히려 꽤 안정적인 패턴을 발견할 수 있다.&lt;/p&gt;
&lt;p&gt;카드 한 벌의 성공 확률을 궁금해하던 아이디어가&lt;/p&gt;
&lt;p&gt;중성자의 움직임을 계산하고,&lt;/p&gt;
&lt;p&gt;빛을 렌더링하고,&lt;/p&gt;
&lt;p&gt;금융 시장의 위험을 추정하고,&lt;/p&gt;
&lt;p&gt;AI가 게임의 다음 수를 탐색하는 방법으로까지 이어진 이유도 여기에 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;전부 계산할 수 없을 때, 일부를 무작위로 계산해 전체를 알아내는 것.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이것이 몬테카를로 방법이다.&lt;/p&gt;</description>
      <category>CS/수학</category>
      <category>Montecarlo</category>
      <category>몬테카를로</category>
      <category>인공지능</category>
      <category>존폰노이만</category>
      <category>컴퓨터과학</category>
      <category>확률</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/80</guid>
      <comments>https://slowgrid.tistory.com/entry/Monte-Carlo-Method-What-if-you-cant-calculate-all-cases#entry80comment</comments>
      <pubDate>Fri, 11 Sep 2026 17:50:18 +0900</pubDate>
    </item>
    <item>
      <title>[YOLO 기초 4] 왜 YOLO를 처음부터 학습하지 않을까?</title>
      <link>https://slowgrid.tistory.com/entry/YOLO-Basics-4-Why-not-learn-YOLO-from-the-beginning</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사전 학습(Pretraining), 전이 학습(Transfer Learning), 미세 조정(Fine-Tuning), 도메인 이동(Domain Shift) 이해하기  &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;버전 메모&lt;/strong&gt;&lt;br&gt;이 글은 &lt;strong&gt;2026-09-09 최종 검증 기준 Ultralytics 공식 문서와 공개 구현&lt;/strong&gt;을 직접 확인하면서 정리했다. &lt;code&gt;optimizer=auto&lt;/code&gt;, &lt;code&gt;cls_remap&lt;/code&gt;, 체크포인트 저장 형식, YOLO26의 &lt;code&gt;nms&lt;/code&gt;/손실 이름처럼 구현에 가까운 부분은 버전에 따라 달라질 수 있다. 실제 실험에서는 &lt;code&gt;ultralytics&lt;/code&gt; 버전, &lt;code&gt;args.yaml&lt;/code&gt;, 학습 로그도 함께 확인해 두는 편이 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;현재 열화상 보행자 프로젝트에서는 YOLO26n을 처음부터 학습하지 않는다. 무작위 가중치에서 시작하는 대신, &lt;code&gt;yolo26n.pt&lt;/code&gt;에 들어 있는 사전 학습 가중치를 불러와 열화상 데이터에 맞게 다시 학습한다.&lt;/p&gt;
&lt;p&gt;흐름만 보면 간단하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;사전 학습된 YOLO26n
↓
열화상 보행자 데이터로 추가 학습
↓
열화상 보행자 검출 모델&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 여기서 한 가지 궁금한 점이 생긴다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;일반 RGB 이미지에서 먼저 학습된 모델을 왜 열화상 보행자 검출의 출발점으로 사용할 수 있을까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;열화상은 RGB 이미지와 생김새도 다르고, 이미지가 만들어지는 방식도 다르다. 그런데도 굳이 RGB 중심의 데이터로 학습한 모델을 가져와 사용하는 이유가 있다.&lt;/p&gt;
&lt;p&gt;그 이유를 이해하려면 먼저 &lt;strong&gt;처음부터 학습하기&lt;/strong&gt;, &lt;strong&gt;사전 학습&lt;/strong&gt;, &lt;strong&gt;전이 학습&lt;/strong&gt;, &lt;strong&gt;미세 조정&lt;/strong&gt;이 각각 무엇인지 구분해 둘 필요가 있다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 구현 세부사항으로 바로 들어가기보다, 이 개념들이 서로 어떻게 연결되는지부터 살펴보고, &lt;code&gt;.pt&lt;/code&gt;와 &lt;code&gt;.yaml&lt;/code&gt;에서 실제로 어떤 가중치가 전달되는지, 클래스 수와 입력 채널 수가 달라지면 무엇이 달라지는지는 다음 글에서 따로 정리할 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 처음부터 학습하기(Training from Scratch)&lt;/h2&gt;
&lt;p&gt;먼저 모델을 정말 아무것도 모르는 상태에서 시작한다고 생각해보자.&lt;/p&gt;
&lt;p&gt;처음부터 학습한다는 것은 &lt;strong&gt;모델 구조만 준비한 뒤 가중치를 무작위로 초기화하고, 현재 데이터셋만으로 필요한 모든 특징을 학습하는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;모델 구조
↓
무작위 가중치
↓
현재 데이터셋
↓
학습&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 초기 가중치에는 이미지에서 무엇을 봐야 하는지에 대한 정보가 없다.&lt;/p&gt;
&lt;p&gt;그래서 모델은 현재 데이터셋을 보면서 경계선, 형태, 질감, 객체의 부분 패턴, 객체와 배경의 차이 같은 기초적인 시각 특징부터 최종 객체 검출 규칙까지 하나씩 배워야 한다.&lt;/p&gt;
&lt;p&gt;데이터가 충분히 크고 학습 자원도 넉넉하다면 가능한 방법이다. 하지만 데이터가 작을수록 학습이 느리게 수렴하거나, 현재 데이터에 지나치게 맞춰지는 과적합 위험이 커질 수 있다.&lt;/p&gt;
&lt;p&gt;그렇다면 이미 다른 이미지에서 이런 기본적인 특징을 어느 정도 배운 모델이 있다면 어떨까?&lt;/p&gt;
&lt;p&gt;바로 그 지점에서 &lt;strong&gt;사전 학습 모델&lt;/strong&gt;을 사용하게 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 사전 학습 모델(Pretrained Model)&lt;/h2&gt;
&lt;p&gt;사전 학습 모델은 말 그대로 &lt;strong&gt;다른 데이터셋에서 먼저 학습된 가중치를 가진 모델&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;즉, 새 데이터셋을 보기 전에 이미 여러 이미지를 학습하면서 어느 정도의 시각적 표현을 만들어 둔 상태이다.&lt;/p&gt;
&lt;p&gt;Ultralytics의 공식 &lt;code&gt;yolo26n.pt&lt;/code&gt;도 이런 모델이다. 최종적으로는 &lt;strong&gt;COCO 80-class 객체 검출 모델&lt;/strong&gt;이지만, 학습 이력은 COCO 한 단계로 끝나지 않는다.&lt;/p&gt;
&lt;p&gt;공식 YOLO26 Training Recipe를 보면 모든 base model은 먼저 &lt;strong&gt;Objects365v1에서 150에포크 사전 학습&lt;/strong&gt;한 뒤, 같은 크기의 checkpoint를 시작점으로 &lt;strong&gt;COCO에서 다시 미세 조정&lt;/strong&gt;했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Objects365v1
↓
150 epochs pretraining
↓
yolo26n-objv1-150.pt
↓
COCO 80-class fine-tuning
↓
yolo26n.pt&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 문서나 예제에서 &lt;code&gt;yolo26n.pt&lt;/code&gt;를 흔히 &lt;strong&gt;COCO-pretrained&lt;/strong&gt;라고 부르더라도, 이것이 COCO를 무작위 가중치에서 바로 학습했다는 뜻은 아니다.&lt;/p&gt;
&lt;p&gt;즉, 여러 이미지에서 객체를 검출하면서 대략 이런 특징을 학습했다고 이해하면 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;낮은 수준 특징&lt;/strong&gt;: 경계선, 방향, 국소 대비, 단순한 형태&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;중간 수준 특징&lt;/strong&gt;: 객체의 부분 구조와 반복 패턴&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;높은 수준 특징&lt;/strong&gt;: 특정 객체 범주를 구분하는 데 유용한 표현&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이처럼 모델이 이미지에서 학습해 둔 표현을 &lt;strong&gt;특징 표현(Feature Representation)&lt;/strong&gt;이라고 부른다.&lt;/p&gt;
&lt;p&gt;그리고 이 이미 학습된 표현을 새로운 문제에 다시 활용하는 것이 &lt;strong&gt;전이 학습&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 전이 학습(Transfer Learning)&lt;/h2&gt;
&lt;p&gt;전이 학습은 이름이 조금 거창하지만 개념은 단순하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이미 한 문제에서 학습한 지식을 다른 문제의 출발점으로 사용하는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;일반적인 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원천 데이터셋(Source Dataset)
↓
사전 학습 모델
↓
목표 데이터셋(Target Dataset)
↓
새로운 과제에 활용&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 열화상 프로젝트에 연결하면 이렇게 볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;일반 RGB 이미지 중심의 데이터
↓
사전 학습된 YOLO26n
↓
야간 열화상 보행자 데이터
↓
열화상 보행자 검출&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 중요한 점은 &lt;strong&gt;전이 학습이 하나의 특정 학습 방법을 뜻하는 것은 아니라는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;사전 학습된 모델을 가져온 뒤에는 여러 선택이 가능하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;앞쪽 특징 추출 계층은 고정하고 뒤쪽 검출 부분만 학습할 수도 있다.&lt;/li&gt;
&lt;li&gt;일부 계층만 동결할 수도 있다.&lt;/li&gt;
&lt;li&gt;전체 모델을 다시 학습할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이처럼 이미 학습된 지식을 새로운 과제에 재사용하는 큰 개념이 전이 학습이고, 그중 &lt;strong&gt;사전 학습 가중치를 시작점으로 목표 데이터셋에 맞게 가중치를 다시 조정하는 과정&lt;/strong&gt;을 보통 &lt;strong&gt;미세 조정(Fine-Tuning)&lt;/strong&gt;이라고 부른다.&lt;/p&gt;
&lt;p&gt;정리하면 관계는 이렇게 이해하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;전이 학습은 큰 개념이고, 미세 조정은 그 안에서 사용할 수 있는 대표적인 방법 중 하나라는 것&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 미세 조정(Fine-Tuning)&lt;/h2&gt;
&lt;p&gt;미세 조정은 이미 학습된 가중치에서 출발하되, &lt;strong&gt;새로운 데이터셋에 맞게 다시 학습해서 가중치를 조정하는 과정&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트에서는 다음과 같은 흐름이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;사전 학습된 YOLO26n
↓
열화상 보행자 데이터
↓
미세 조정
↓
열화상 보행자 검출 모델&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드로 보면 훨씬 단순하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from ultralytics import YOLO

model = YOLO(&amp;quot;yolo26n.pt&amp;quot;)
model.train(data=&amp;quot;thermal.yaml&amp;quot;, ...)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 &lt;code&gt;YOLO(&amp;quot;yolo26n.pt&amp;quot;)&lt;/code&gt;에서 시작한다는 점이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.pt&lt;/code&gt; 체크포인트를 불러온 뒤 &lt;code&gt;train()&lt;/code&gt;을 실행하면 Ultralytics가 목표 데이터셋의 클래스 수와 입력 채널 수 등을 확인하고, &lt;strong&gt;형상이 호환되는 사전 학습 가중치를 새 모델에 전달한 뒤 학습을 시작&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;그래서 사용자가 백본 가중치를 하나씩 직접 복사하는 코드를 작성할 필요는 없다.&lt;/p&gt;
&lt;p&gt;여기까지 보면 사전 학습을 사용하는 이유가 어느 정도 이해된다. 이미 배운 시각 특징을 출발점으로 쓰면 처음부터 모든 것을 다시 배울 필요가 없기 때문이다.&lt;/p&gt;
&lt;p&gt;하지만 현재 프로젝트에는 한 가지 큰 차이가 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;사전 학습에 사용된 일반 RGB 이미지와 우리가 사용하는 열화상 이미지는 꽤 다르다는 것&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그런데도 이게 정말 도움이 될까?&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 그런데 RGB와 열화상은 너무 다르지 않을까?&lt;/h2&gt;
&lt;p&gt;이 부분이 전이 학습을 생각할 때 가장 자연스럽게 생기는 의문이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RGB 이미지에서 학습한 YOLO26n의 가중치가 열화상 이미지에서도 정말 도움이 될까?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;일단 두 영상은 정보를 만드는 방식부터 다르다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RGB 이미지&lt;/th&gt;
&lt;th&gt;열화상 이미지&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;가시광 기반의 색상 정보를 사용&lt;/td&gt;
&lt;td&gt;물체가 방출하거나 반사하는 적외선 신호를 영상으로 표현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;색, 질감, 밝기와 색 대비가 중요&lt;/td&gt;
&lt;td&gt;일반 RGB와 다른 밝기 분포를 가짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;색상 자체가 중요한 단서가 될 수 있음&lt;/td&gt;
&lt;td&gt;야간에도 물체의 열적 차이가 나타날 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;색상보다 온도 또는 열 신호 차이가 중요할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;그래서 같은 사람을 찍더라도 RGB 이미지와 열화상 이미지의 픽셀 분포와 시각적 특징은 같지 않다.&lt;/p&gt;
&lt;p&gt;이처럼 &lt;strong&gt;학습에 사용한 데이터의 분포와 실제 적용할 데이터의 분포가 달라지는 현상&lt;/strong&gt;을 &lt;strong&gt;도메인 이동(Domain Shift)&lt;/strong&gt;이라고 한다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트에서는 일반 RGB 이미지가 원천 도메인이고, 야간 열화상 이미지가 목표 도메인이라고 볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원천 도메인(Source Domain)
→ 일반 RGB 이미지

목표 도메인(Target Domain)
→ 야간 열화상 이미지&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;두 도메인이 다르다는 점은 분명하다.&lt;/p&gt;
&lt;p&gt;그렇다면 RGB에서 학습한 특징은 전부 버려야 할까?&lt;/p&gt;
&lt;p&gt;꼭 그렇지는 않다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 도메인이 달라도 가져갈 수 있는 특징이 있다&lt;/h2&gt;
&lt;p&gt;RGB와 열화상은 다르지만, 이미지 안에 있는 모든 정보가 완전히 다른 것은 아니다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음과 같은 비교적 낮은 수준의 특징은 열화상에서도 유용한 출발점이 될 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;경계선(Edge)&lt;/li&gt;
&lt;li&gt;윤곽(Contour)&lt;/li&gt;
&lt;li&gt;형태(Shape)&lt;/li&gt;
&lt;li&gt;국소 패턴(Local Pattern)&lt;/li&gt;
&lt;li&gt;국소 대비(Local Contrast)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;사람을 예로 들어보면 더 쉽게 이해할 수 있다.&lt;/p&gt;
&lt;p&gt;RGB와 열화상에서 사람의 색이나 밝기 분포는 크게 달라질 수 있다. 하지만 &lt;strong&gt;머리-몸통-다리의 대략적인 구조&lt;/strong&gt;, &lt;strong&gt;사람과 배경 사이의 경계&lt;/strong&gt;, &lt;strong&gt;세로로 긴 실루엣&lt;/strong&gt; 같은 형태 정보는 어느 정도 공통으로 나타날 수 있다.&lt;/p&gt;
&lt;p&gt;즉, RGB에서 배운 모든 특징이 열화상에서도 그대로 통한다는 뜻은 아니지만, 처음부터 무작위로 시작하는 것보다 쓸 만한 출발점이 될 수 있는 정보가 남아 있을 수 있다는 것이다.&lt;/p&gt;
&lt;p&gt;반대로 모델의 깊은 계층으로 갈수록 원천 데이터의 객체 범주나 표현에 더 특화되어 있을 수 있다. 따라서 사전 학습된 특징을 그대로 고정해 두기만 하면 목표 도메인에 충분히 적응하지 못할 수도 있다.&lt;/p&gt;
&lt;p&gt;그래서 필요한 것이 다시 &lt;strong&gt;미세 조정&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;RGB에서 배운 가중치를 출발점으로 사용하되, 열화상 데이터를 보여주면서 &lt;strong&gt;목표 도메인에 맞게 가중치를 다시 적응시키는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;그러면 다음 질문으로 이어진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 미세 조정에서는 모든 계층을 바꿀까?&lt;/h2&gt;
&lt;p&gt;꼭 한 가지 방식만 있는 것은 아니다.&lt;/p&gt;
&lt;p&gt;YOLO를 아주 단순하게 나누면 크게 &lt;strong&gt;백본(Backbone)&lt;/strong&gt;, &lt;strong&gt;넥(Neck)&lt;/strong&gt;, &lt;strong&gt;검출 헤드(Head)&lt;/strong&gt;로 볼 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;백본&lt;/strong&gt;: 입력 이미지에서 특징을 추출.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;넥&lt;/strong&gt;: 여러 해상도의 특징을 결합.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검출 헤드&lt;/strong&gt;: 위치와 클래스를 예측.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;미세 조정에서는 이 계층들을 모두 학습할 수도 있고, 일부만 학습할 수도 있다.&lt;/p&gt;
&lt;h3&gt;7.1 전체 계층 미세 조정&lt;/h3&gt;
&lt;p&gt;가장 단순한 방법은 백본, 넥, 검출 헤드를 모두 학습 가능 상태로 두는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Backbone + Neck + Head
→ 모두 학습&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RGB와 열화상처럼 원천 도메인과 목표 도메인의 차이가 크다면, 앞쪽의 백본도 새로운 영상 특성에 맞게 변할 필요가 있을 수 있다.&lt;/p&gt;
&lt;p&gt;Ultralytics의 일반적인 미세 조정 기본값도 전체 모델을 학습 가능 상태로 두는 방식이다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트처럼 열화상이라는 새로운 도메인에 적응해야 하는 상황에서는 우선 비교 기준으로 사용하기 좋은 방법이다.&lt;/p&gt;
&lt;h3&gt;7.2 일부 계층 동결(Freeze)&lt;/h3&gt;
&lt;p&gt;반대로 사전 학습된 특징 일부를 그대로 유지하고 싶다면 계층을 동결할 수도 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 백본 일부를 동결하고 넥과 검출 헤드만 먼저 학습하는 방식이다.&lt;/p&gt;
&lt;p&gt;동결된 계층은 역전파가 진행되어도 가중치가 갱신되지 않는다.&lt;/p&gt;
&lt;p&gt;이 방법은 데이터가 매우 작거나 원천 도메인과 목표 도메인이 비슷할 때 과적합을 줄이는 데 도움이 될 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 열화상처럼 도메인 차이가 큰 경우에는 이야기가 달라질 수 있다. 백본을 너무 많이, 또는 너무 오래 동결하면 RGB 중심의 특징에서 벗어나지 못해 열화상 도메인에 충분히 적응하지 못할 수도 있다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;freeze&lt;/code&gt;는 무조건 적용해야 하는 규칙이라기보다 &lt;strong&gt;실험으로 결정할 하이퍼파라미터&lt;/strong&gt;라고 보는 편이 낫다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;YOLO26 참고&lt;/strong&gt;&lt;br&gt;현재 YOLO26 구조에서 &lt;code&gt;freeze=10&lt;/code&gt;은 앞의 10개 계층을 동결하며, 전체 백본을 완전히 동결하는 것과 정확히 같지는 않다. 그렇기에 특정 버전에서 백본 전체를 동결하려면 계층 구성을 확인할 필요가 있다. 공식 가이드 기준 YOLO26 백본은 0~10번 계층이므로 전체 백본 동결에는 &lt;code&gt;freeze=11&lt;/code&gt;이 필요하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;전체 계층을 바로 학습하는 방법과 일부를 동결하는 방법 사이에서 절충하는 방법도 있다.&lt;/p&gt;
&lt;p&gt;그게 &lt;strong&gt;2단계 미세 조정&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 2단계 미세 조정(Two-Stage Fine-Tuning)&lt;/h2&gt;
&lt;p&gt;2단계 미세 조정은 말 그대로 학습 과정을 두 단계로 나누는 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1단계에서는 백본을 동결하고 넥과 검출 헤드를 먼저 적응시킨다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;백본 동결
↓
넥 / 검출 헤드 먼저 학습&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그다음 &lt;strong&gt;2단계에서는 백본의 동결을 해제하고, 더 낮은 학습률로 전체 모델을 다시 미세 조정&lt;/strong&gt;한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;백본 동결 해제
↓
더 낮은 학습률
↓
전체 모델 미세 조정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;의도는 새 검출 과제에 필요한 뒤쪽 계층을 먼저 안정적으로 적응시킨 뒤, 백본까지 천천히 조정해 나가는 것이다.&lt;/p&gt;
&lt;p&gt;다만 현재 프로젝트에서 반드시 이 방식을 사용해야 하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;특히 목표 도메인이 COCO와 크게 다르다면 처음부터 전체 계층을 학습하는 편이 더 나을 수도 있다. 그래서 한 가지 방법을 정답처럼 고르기보다는 다음처럼 비교하는 편이 좋다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A. 전체 계층 미세 조정&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B. 일부 백본 동결&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C. 2단계 미세 조정&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국 중요한 것은 어떤 전략이 이론적으로 더 그럴듯한지가 아니라, &lt;strong&gt;현재 진행중인 프로젝트 데이터에서 실제로 어떤 결과가 나오는지&lt;/strong&gt;다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. 사전 학습이 항상 더 좋은 것은 아니다&lt;/h2&gt;
&lt;p&gt;여기까지 읽으면 사전 학습 모델을 쓰는 것이 무조건 좋은 선택처럼 보일 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 사전 학습은 &lt;strong&gt;좋은 출발점이 될 가능성이 높은 방법&lt;/strong&gt;이지, 항상 최고의 결과를 보장하는 규칙은 아니다.&lt;/p&gt;
&lt;p&gt;특히 다음과 같은 상황이라면 처음부터 학습하는 방법도 비교해 볼 가치가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원천 도메인과 목표 도메인의 차이가 매우 큰 경우&lt;/li&gt;
&lt;li&gt;충분히 큰 목표 데이터셋을 가지고 있는 경우&lt;/li&gt;
&lt;li&gt;입력 채널이나 센서 특성이 크게 다른 경우&lt;/li&gt;
&lt;li&gt;기존 사전 학습 특징이 오히려 편향을 만들 가능성이 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;사전 학습 = 무조건 최고의 결과&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;가 아니라,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;이미 학습된 시각 특징을 좋은 초기값으로 사용해 새로운 과제에 더 빠르고 효율적으로 적응해 보는 전략&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;결국 사전 학습이 실제로 도움이 되는지는 프로젝트의 데이터로 확인해야 한다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트에서도 그 질문을 직접 실험으로 확인할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. 사전 학습이 실제로 도움이 되는지는 어떻게 확인할까?&lt;/h2&gt;
&lt;p&gt;가장 직접적인 방법은 &lt;strong&gt;같은 YOLO26n 구조를 서로 다른 초기값에서 시작해 비교하는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;실험&lt;/th&gt;
&lt;th&gt;시작점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A. 사전 학습 가중치 사용&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;YOLO(&amp;quot;yolo26n.pt&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B. 무작위 초기화&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;YOLO(&amp;quot;yolo26n.yaml&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;가능한 한 나머지 학습 조건은 동일하게 둔다.&lt;/p&gt;
&lt;p&gt;그리고 다음 항목을 비교한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;수렴 속도&lt;/li&gt;
&lt;li&gt;최종 mAP50-95&lt;/li&gt;
&lt;li&gt;재현율(Recall)&lt;/li&gt;
&lt;li&gt;정밀도(Precision)&lt;/li&gt;
&lt;li&gt;작은 보행자 성능&lt;/li&gt;
&lt;li&gt;학습 안정성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 실험이 확인하려는 질문은 아주 단순하다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;일반 이미지에서 얻은 사전 학습 가중치가 실제로 이 열화상 보행자 데이터셋 학습에 도움이 되었는가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;데이터셋이 작다면 처음부터 학습하는 실험이 상대적으로 불리할 가능성이 있다. 그래도 두 출발점을 직접 비교하면 왜 전이 학습을 사용하는지 훨씬 명확하게 설명할 수 있다.&lt;/p&gt;
&lt;p&gt;다만 이 비교 자체가 하나의 실험 변수라는 점은 주의해야 한다.&lt;/p&gt;
&lt;p&gt;예를 들어 CLAHE 같은 전처리 효과까지 동시에 바꾸면 성능 차이가 사전 학습 때문인지, 전처리 때문인지 해석하기 어려워진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p&gt;여기까지의 내용을 세 문장으로 줄이면 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;사전 학습(Pretraining)&lt;/strong&gt;: 큰 원천 데이터셋에서 먼저 시각 표현을 학습한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;전이 학습(Transfer Learning)&lt;/strong&gt;: 이미 학습한 지식을 새로운 문제에 재사용하는 큰 개념이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;미세 조정(Fine-Tuning)&lt;/strong&gt;: 사전 학습 가중치를 목표 데이터셋에 맞게 다시 학습한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;현재 열화상 보행자 프로젝트의 흐름은 이렇게 정리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Objects365 사전 학습
↓
COCO 미세 조정
↓
yolo26n.pt
↓
RGB 중심의 일반 이미지 도메인에서 얻은 가중치
↓
열화상 보행자 데이터로 미세 조정
↓
목표 도메인에 적응한 검출 모델&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 &lt;strong&gt;RGB와 열화상이 다르더라도, RGB에서 배운 모든 특징이 쓸모없어지는 것은 아니라는 점&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;경계, 윤곽, 형태처럼 다시 활용할 수 있는 특징을 좋은 초기값으로 사용하고, 열화상 데이터로 학습하면서 목표 도메인에 맞게 가중치를 다시 조정한다.&lt;/p&gt;
&lt;p&gt;물론 사전 학습이 항상 최고의 결과를 보장하는 것은 아니다. 원천 도메인과 목표 도메인의 차이가 크다면 백본까지 충분히 적응시켜야 할 수 있고, &lt;code&gt;freeze&lt;/code&gt; 여부나 2단계 미세 조정도 실험으로 판단해야 한다.&lt;/p&gt;
&lt;p&gt;다음 글에서는 한 단계 더 구현 쪽으로 들어가서, &lt;code&gt;yolo26n.pt&lt;/code&gt;에서 시작했을 때 &lt;strong&gt;어떤 가중치가 실제 새 모델로 전달되는지&lt;/strong&gt;, 그리고 &lt;strong&gt;클래스 수와 입력 채널 수가 달라지면 무엇이 달라지는지&lt;/strong&gt; 살펴볼 예정이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고한 공식 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;YOLO26 Training Recipe: &lt;a href=&quot;https://docs.ultralytics.com/guides/yolo26-training-recipe/&quot;&gt;https://docs.ultralytics.com/guides/yolo26-training-recipe/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;YOLO26 모델: &lt;a href=&quot;https://docs.ultralytics.com/models/yolo26/&quot;&gt;https://docs.ultralytics.com/models/yolo26/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>YoLO</category>
      <category>미세 조정</category>
      <category>사전 학습</category>
      <category>전이 학습</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/78</guid>
      <comments>https://slowgrid.tistory.com/entry/YOLO-Basics-4-Why-not-learn-YOLO-from-the-beginning#entry78comment</comments>
      <pubDate>Thu, 10 Sep 2026 21:28:26 +0900</pubDate>
    </item>
    <item>
      <title>[YOLO 기초 3] 작은 객체는 YOLO에서 왜 어려울까?</title>
      <link>https://slowgrid.tistory.com/entry/YOLO-Basics-3-Why-Are-Small-Objects-Difficult-in-YOLO</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STAL, P2 Head와 열화상 보행자 탐지로 이해하는 Small Object Detection&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 두 글에서는 YOLO가 특징을 만들고 박스를 예측하는 과정, 그리고 YOLO26의 Dual Head와 label assignment를 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 현재 열화상 보행자 검출 프로젝트에서 특히 중요하게 느껴졌던 &lt;b&gt;작은 객체(small object)&lt;/b&gt; 문제를 중심으로 정리해 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 높이 20px인 보행자를 feature spacing 기준으로 단순하게 계산해 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;P2 / stride 4  &amp;rarr; 20 / 4  = 5
P3 / stride 8  &amp;rarr; 20 / 8  = 2.5
P4 / stride 16 &amp;rarr; 20 / 16 = 1.25
P5 / stride 32 &amp;rarr; 20 / 32 = 0.625&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 &lt;code&gt;stride&lt;/code&gt;는 receptive field 크기와 같지 않다. 하지만 객체가 작아질수록 feature grid와의 기하학적 관계, label assignment, 입력 리사이즈, 라벨 오차의 영향이 커진다는 직관을 얻을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. STAL은 작은 객체 문제를 어떻게 다룰까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;STAL(Small-Target-Aware Label Assignment)&lt;/code&gt;은 작은 객체에서 label assignment가 실패하는 특정 문제를 해결하기 위한 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 TAL 계열의 후보 선택에서는 anchor point 중심이 GT 박스 내부에 있어야 positive 후보가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 객체가 너무 작으면 특징 맵의 grid 간격 때문에 박스 내부에 anchor point가 단 하나도 들어오지 않을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;GT box가 매우 작음

   ┌───┐
   │GT │
   └───┘

x       x       x   &amp;larr; feature anchor points

&amp;rarr; GT 내부 anchor point가 0개일 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 해당 객체는 positive sample을 받지 못하고 classification/localization gradient를 거의 제공하지 못한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;STAL의 핵심&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STAL은 &lt;b&gt;candidate selection을 할 때만&lt;/b&gt; 너무 작은 GT의 가로/세로 범위를 임시로 넓힌 surrogate box를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;d_tilde = s_ref, if d &amp;lt; s_min
          d,     otherwise&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 작은 width/height를 assignment 기준에서 보완한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 식은 &lt;b&gt;YOLO26 논문 v1의 개념 설명&lt;/b&gt;이다. 2026-09-08 현재 공개 &lt;code&gt;TaskAlignedAssigner&lt;/code&gt; 구현은 세부 조건이 조금 달라져, 전달받은 stride 목록의 두 번째 값을 &lt;code&gt;stride_val&lt;/code&gt;로 잡고(두 번째 값이 없으면 첫 번째 값), width/height가 그 값보다 작을 때 candidate-selection용 크기를 &lt;code&gt;stride_val&lt;/code&gt;까지 보완한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 기본 P3/P4/P5 stride가 &lt;code&gt;[8, 16, 32]&lt;/code&gt;라면 현재 코드에서는 기준값이 16이고, P2/P3/P4/P5의 &lt;code&gt;[4, 8, 16, 32]&lt;/code&gt;라면 기준값이 8이 된다. 이 부분을 보면서 논문 수식과 실제 라이브러리 구현이 업데이트 과정에서 달라질 수 있다는 점을 다시 확인할 수 있었다. 재현이 목적이라면 설치된 버전의 &lt;code&gt;TaskAlignedAssigner&lt;/code&gt;를 직접 확인하는 게 가장 확실하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 여기서 꼭 짚고 넘어갈 부분은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;실제 box regression target 자체를 크게 만드는 것은 아니다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;candidate를 찾을 때
&amp;rarr; 작은 객체가 grid 사이에서 사라지지 않도록 보완

실제 회귀 loss를 계산할 때
&amp;rarr; 원래 GT 크기를 그대로 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 작은 객체의 학습 신호는 살리면서도 실제 객체 크기 자체는 왜곡하지 않을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 열화상 프로젝트에서 특히 확인할 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 데이터 분석에서는 bbox 높이 중앙값이 약 20px 정도로 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 STAL 관점에서는 &lt;b&gt;높이만 보는 것으로 부족하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보행자는 세로로 길고 가로 폭이 좁을 수 있기 때문에&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;bbox width
bbox height
min(width, height)
bbox area&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 함께 보는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 높이가 20px이어도 폭이 6px이라면, 작은 stride에서조차 candidate geometry가 불안정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 작은 객체 분석에서는 &lt;code&gt;height histogram&lt;/code&gt;뿐 아니라 &lt;code&gt;width&lt;/code&gt;와 &lt;code&gt;min-side&lt;/code&gt; 분포를 함께 확인하는 것이 훨씬 기술적으로 의미가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 작은 보행자가 많다면 P2 헤드도 생각할 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 YOLO26 검출 모델은 P3/8부터 예측한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ultralytics는 작은 객체를 위한 아키텍처 전용 변형으로 &lt;code&gt;yolo26-p2.yaml&lt;/code&gt;도 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P2는 stride 4이므로 640 입력이라면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;P2/4
&amp;rarr; 160 &amp;times; 160&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징에서 검출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20px 높이 객체를 단순히 feature spacing으로 비교하면&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;P2: 20 / 4 = 5
P3: 20 / 8 = 2.5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이므로 P2가 작은 객체의 위치 변화를 더 세밀하게 표현할 가능성이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하지만 비용도 매우 커진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P2/P3/P4/P5를 모두 사용하면 640 기준 dense candidate 위치 수는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;P2: 160 &amp;times; 160 = 25,600
P3:  80 &amp;times; 80  =  6,400
P4:  40 &amp;times; 40  =  1,600
P5:  20 &amp;times; 20  =    400
-------------------------
합계               34,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 P3/P4/P5의 &lt;code&gt;8,400&lt;/code&gt;과 비교하면 &lt;b&gt;dense candidate location 수&lt;/b&gt;가 약 4배가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;34,000&lt;/code&gt; 역시 최종 detection 개수를 뜻하지 않는다. P2까지 추가했을 때 검출 헤드가 예측을 만드는 공간 위치가 그만큼 늘어난다는 뜻이다. 결국 작은 객체를 더 촘촘한 위치에서 다룰 수 있는 대신, 높은 해상도의 feature와 prediction을 더 많이 계산해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 실제 전체 연산량이 정확히 4배가 된다는 뜻은 아니지만, 높은 해상도 branch가 추가되기 때문에&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;feature memory 증가
head/neck 연산 증가
training VRAM 증가
inference latency 증가 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능성이 분명히 존재한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사전 학습 가중치도 주의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 공식 문서 기준 P2/P6 변형은 YAML 아키텍처로 제공되며 &lt;code&gt;yolo26n-p2.pt&lt;/code&gt; 같은 scale별 P2 pretrained weight가 별도 배포되는 형태는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 P2 실험은 단순히&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;YOLO(&quot;yolo26n-p2.pt&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 불러오는 문제가 아니라&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;P2 architecture를 어떻게 초기화하고
기존 pretrained weight를 어느 부분까지 transfer하며
얼마나 fine-tune할지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 별도 실험 설계로 다뤄야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;P2가 무조건 더 좋은 것은 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 객체가 많다고 해서 P2가 자동으로 이기는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교할 때는 적어도 다음을 함께 봐야 한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;전체 mAP
small-object AP / recall
작은 보행자 recall
false positive 증가 여부
GPU memory
latency / FPS&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 현재 열화상 프로젝트에서 &quot;작은 객체&quot;를 더 잘 분석하려면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 전체 bbox 높이 중앙값 하나만 보는 것보다 아래 통계를 같이 보는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. bbox width 분포
2. bbox height 분포
3. min(width, height) 분포
4. bbox area 분포
5. resize / letterbox 후 실제 pixel size 분포
6. 크기 구간별 recall / mAP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 입력 크기를 바꾸면 객체가 모델에 들어갈 때 차지하는 픽셀 크기도 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 원본에서 높이 20px인 보행자가 학습 입력 리사이즈 과정에서 30px이 될 수도 있고 15px이 될 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 모델이 실제로 보는 객체 크기를 평가하려면 &lt;b&gt;원본 annotation 크기뿐 아니라 preprocessing 이후 크기&lt;/b&gt;도 확인할 필요가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;작은 객체에서 흔히 생기는 실패 원인&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;객체 자체가 너무 작음
라벨 박스가 1~2px만 틀려도 IoU가 크게 변함
열화상 대비가 낮음
배경 hot spot과 혼동
resize에서 세부 정보 손실
강한 augmentation으로 작은 객체가 더 작아짐
mosaic에서 객체가 지나치게 축소됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 객체 성능은 결국 P2 하나로 해결되는 문제가 아니라&lt;/p&gt;
&lt;pre class=&quot;golo&quot;&gt;&lt;code&gt;입력 해상도
특징 해상도
레이블 품질
augmentation
assignment
loss
도메인 특성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 결합 문제다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 열화상 입력 채널도 기술적으로 확인해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트가 &quot;열화상 이미지&quot;라고 해도 모델이 실제로 받는 tensor 형식은 별도로 확인할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 소스가 1채널 grayscale이라도 학습 파이프라인에서는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1채널을 RGB처럼 3번 복제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하거나&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;pseudo-color로 3채널 변환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하거나&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;모델 입력 자체를 1채널로 변경&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 선택은 pretrained RGB weight의 활용 방식과 첫 번째 convolution layer의 weight transfer에 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실험 로그에는 최소한 다음 정도는 남겨두는 편이 좋다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;원본 thermal bit-depth
정규화 방식
저장 파일 형식
model input channel 수
1ch &amp;rarr; 3ch 변환 여부
pseudo-color 사용 여부&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 단순 시각화용 컬러맵을 학습 입력에 적용하는 것은 실제 정보량을 늘리는 것이 아니므로, 원본 열화상 intensity가 어떻게 보존되는지 확인할 필요가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 검출 헤드의 confidence를 바로 &quot;확률&quot;이라고 생각해도 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추론 결과의 &lt;code&gt;confidence&lt;/code&gt;는 검출 결과를 정렬하고 threshold를 적용하는 데 쓰는 점수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 0~1 범위라 확률처럼 보이지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;confidence = 0.8이면 실제 사람이 존재할 통계적 확률이 정확히 80%다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 해석하면 과도할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망 점수는 calibration이 완벽하지 않을 수 있고, RGB 사전 학습 모델을 열화상에 fine-tune하는 것처럼 domain shift가 큰 경우 더 조심해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;YOLO26에서는 예전식 &lt;code&gt;objectness &amp;times; class probability&lt;/code&gt; 설명도 그대로 적용하면 안 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 봤듯 현재 anchor-free &lt;code&gt;Detect&lt;/code&gt; raw head에는 별도 objectness channel이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 confidence를 설명할 때 과거 YOLOv5식 구조를 그대로 가져오기보다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class branch가 낸 점수
+
학습 중 localization 품질과 결합된 assignment/supervision 영향
+
후처리된 최종 detection score&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 이해하는 편이 안전하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 threshold는 validation set에서 정한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열화상 보행자 검출에서는 &lt;code&gt;conf=0.25&lt;/code&gt;를 관습적으로 고정하기보다 검증 데이터에서&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;Precision
Recall
F1
false positives per image
miss rate&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 보고 실제 운영 목적에 맞게 threshold를 정하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보행자 안전 문제처럼 miss를 줄이는 것이 더 중요하다면 precision을 조금 희생하고 recall 쪽으로 threshold를 조정할 수도 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 현재 프로젝트 전체 구조를 더 정확히 그리면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 열화상 보행자 검출 흐름을 기술적으로 다시 그려보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;열화상 이미지
 &amp;darr;
resize / letterbox / normalize 등 전처리
 &amp;darr;
YOLO26n Backbone
 &amp;darr;
P3 / P4 / P5 backbone features
 &amp;darr;
FPN top-down + PAN bottom-up Neck
 &amp;darr;
융합된 P3 / P4 / P5
 &amp;darr;
Decoupled Detect Head
 ├─ box branch: l/t/r/b distance
 └─ cls branch: person logit
 &amp;darr;
anchor-point 기반 box decode
 &amp;darr;
추론 경로 선택
 ├─ one-to-many + NMS  &amp;larr; 현재 predict/val 기본
 └─ one-to-one, nms=False &amp;rarr; NMS-free
 &amp;darr;
[x1, y1, x2, y2, confidence, class_id]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습에서는 여기에 다음 과정이 추가된다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;예측
 &amp;darr;
GT와 candidate matching / label assignment
 &amp;darr;
box + cls + distance loss
 &amp;darr;
one-to-many / one-to-one branch loss 결합
 &amp;darr;
역전파
 &amp;darr;
optimizer update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26 공식 checkpoint 학습 recipe에는 MuSGD, Progressive Loss, STAL이 포함되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 공식 COCO pretrained checkpoint를 만든 recipe와 사용자가 custom dataset에서 &lt;code&gt;optimizer=auto&lt;/code&gt;로 fine-tuning하는 recipe가 자동으로 동일하다고 생각하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재현이 중요하다면 checkpoint의 &lt;code&gt;train_args&lt;/code&gt;와 실제 &lt;code&gt;args.yaml&lt;/code&gt;을 확인할 필요가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 객체 성능은 단순히 P2 head 하나를 추가한다고 해결되는 문제가 아니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;작은 객체의 실제 pixel size
       &amp;darr;
feature stride / candidate geometry
       &amp;darr;
label assignment와 STAL
       &amp;darr;
P2 같은 높은 해상도 detection branch
       &amp;darr;
입력 해상도 / resize / augmentation / label quality
       &amp;darr;
thermal input 표현과 운영 threshold&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 현재 프로젝트에서는 최소한 다음을 함께 비교해 보는 편이 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;bbox width / height / min-side / area 분포
resize 이후 실제 객체 pixel size
크기 구간별 recall / mAP
P2 사용 전후의 small-object recall
false positive 변화
VRAM / latency / FPS
thermal input channel과 normalization 방식
validation set 기반 confidence threshold&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 보면 small-object detection은 특정 한 레이어의 문제가 아니라 &lt;b&gt;데이터, 전처리, 특징 해상도, assignment, loss, 추론 기준이 함께 얽힌 문제&lt;/b&gt;라는 점이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 주제로는 현재 프로젝트가 왜 &lt;code&gt;YOLO(&quot;yolo26n.yaml&quot;)&lt;/code&gt;이 아니라 &lt;code&gt;YOLO(&quot;yolo26n.pt&quot;)&lt;/code&gt;에서 시작하는지, 즉 &lt;b&gt;Pretraining &amp;rarr; Weight Transfer &amp;rarr; Fine-Tuning&lt;/b&gt;의 흐름으로 이어갈 생각이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 검증 기준&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Ultralytics YOLO26 공식 모델 문서&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/models/yolo26/&quot;&gt;Ultralytics YOLO26&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics End-to-End Detection 가이드&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/guides/end2end-detection/&quot;&gt;YOLO26 End-to-End NMS-Free Detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics YOLO Architecture Explained 가이드&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/guides/yolo-architecture/&quot;&gt;YOLO Architecture Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics YOLO26 Training Recipe&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/guides/yolo26-training-recipe/&quot;&gt;YOLO26 Training Recipe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics 모델 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;yolo26.yaml&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/models/26/yolo26.yaml&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yolo26-p2.yaml&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/models/26/yolo26-p2.yaml&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics Loss / Label Assignment 구현&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;utils/loss.py&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/loss.py&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;utils/tal.py&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/tal.py&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;Ultralytics YOLO26: Unified Real-Time End-to-End Vision Models&lt;/i&gt;&lt;/b&gt;, arXiv:2606.03748 &amp;mdash; &lt;a href=&quot;https://arxiv.org/abs/2606.03748&quot;&gt;arXiv 논문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>P2 Head</category>
      <category>STAL</category>
      <category>YoLO</category>
      <category>열화상</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/77</guid>
      <comments>https://slowgrid.tistory.com/entry/YOLO-Basics-3-Why-Are-Small-Objects-Difficult-in-YOLO#entry77comment</comments>
      <pubDate>Wed, 9 Sep 2026 17:41:00 +0900</pubDate>
    </item>
    <item>
      <title>[YOLO 기초 2] YOLO26의 Detection Head는 어떻게 동작할까?</title>
      <link>https://slowgrid.tistory.com/entry/YOLO-Basics-2-How-Does-the-YOLO26-Detection-Head-Work</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예측 후보 수, NMS, Dual Head, DFL-Free, Label Assignment와 Progressive Loss&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 YOLO가 &lt;code&gt;Backbone &amp;rarr; Neck &amp;rarr; Detection Head &amp;rarr; Decode&lt;/code&gt; 흐름으로 객체를 검출하는 큰 구조를 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;현재 YOLO26의 Detect Head와 학습 구조&lt;/b&gt;를 한 단계 더 깊게 따라가 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면서 계속 궁금했던 건 다음 부분이었다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;왜 P3/P4/P5에서는 수천 개의 예측 후보가 생길까?
왜 기존 경로에서는 NMS가 필요할까?
YOLO26은 왜 one-to-many와 one-to-one 두 head를 사용할까?
DFL-Free인데 왜 설정에는 dfl이라는 이름이 남아 있을까?
어떤 prediction을 positive로 학습시킬지는 어떻게 정할까?&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;먼저 Detect Head의 출력부터 다시 연결해 보면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 본 것처럼 YOLO26의 &lt;code&gt;Detect&lt;/code&gt; 헤드는 box branch와 class branch가 분리된 decoupled head다. &lt;code&gt;reg_max=1&lt;/code&gt;인 YOLO26에서는 한 feature location마다 개념적으로 다음 값이 나온다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;box branch
&amp;rarr; l / t / r / b 거리 4개

class branch
&amp;rarr; nc개 class logit

합계
&amp;rarr; 4 + nc channels per candidate&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 예전 YOLOv5 설명에서 자주 보던 별도의 objectness channel이 없다. 그리고 이 &lt;code&gt;4 + nc&lt;/code&gt; 값도 아직 &lt;code&gt;[x1, y1, x2, y2, confidence, class_id]&lt;/code&gt; 형태의 최종 detection이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구분을 먼저 잡아두면 뒤에서 나오는 두 tensor가 왜 서로 다른지 이해하기 쉽다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;raw prediction
&amp;rarr; feature location마다 box distance + class score를 가지고 있음

processed detection
&amp;rarr; decode와 결과 선택이 끝난 뒤
&amp;rarr; [x1, y1, x2, y2, confidence, class_id]&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. P3/P4/P5에서 예측 후보는 몇 개일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;imgsz=640&lt;/code&gt;이고 P3/P4/P5를 사용하는 기본 YOLO26 검출 모델을 생각하면 특징 위치 수를 계산해 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;P3: 80 &amp;times; 80 = 6,400
P4: 40 &amp;times; 40 = 1,600
P5: 20 &amp;times; 20 =   400
---------------------
합계             8,400&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 &lt;code&gt;8400&lt;/code&gt;은 P3/P4/P5 전체에서 만들어지는 &lt;b&gt;candidate location 수&lt;/b&gt;이다. 다만 여기서부터는 &lt;code&gt;head 내부 prediction&lt;/code&gt;과 &lt;code&gt;export output&lt;/code&gt;을 구분해서 보는 게 중요하다. 둘 다 8,400개의 위치를 다루지만, 박스 4개 값의 의미가 같지 않다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;One-to-Many: head 내부 prediction과 export output은 다르다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 중 &lt;code&gt;Detect&lt;/code&gt; head 내부에서는 box branch와 class branch가 따로 prediction을 만든다. 현재 YOLO26은 &lt;code&gt;reg_max=1&lt;/code&gt;이므로 개념적으로 다음처럼 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;head 내부 prediction

boxes
&amp;rarr; (N, 4, 8400)
&amp;rarr; 각 candidate의 anchor point 기준 box distance
&amp;rarr; l / t / r / b

scores
&amp;rarr; (N, nc, 8400)
&amp;rarr; 각 candidate의 class logit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;boxes&lt;/code&gt;의 4개 값은 아직 최종 &lt;code&gt;xywh&lt;/code&gt;나 &lt;code&gt;xyxy&lt;/code&gt; 좌표가 아니다. anchor point를 기준으로 왼쪽, 위, 오른쪽, 아래 방향의 거리를 나타내는 &lt;b&gt;box-distance 표현&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 추론 경로에서는 이 distance를 anchor point와 stride를 이용해 실제 박스 좌표로 decode한다. 현재 공식 End-to-End Detection 문서에서 기본 &lt;code&gt;nms=None&lt;/code&gt; one-to-many export output을&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(N, nc + 4, 8400)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 설명하는데, 이 단계의 앞쪽 4개 값은 이미 decode된 &lt;b&gt;&lt;code&gt;xywh&lt;/code&gt; 박스 좌표&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;head 내부 training prediction
box distance 4개 + class logits
        &amp;darr;
anchor point + stride로 box decode
        &amp;darr;
default one-to-many inference/export output
xywh 4개 + class scores
        &amp;darr;
confidence filtering + NMS
        &amp;darr;
최종 detection
xyxy + confidence + class_id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 공식 문서에서 기본 export를 &lt;code&gt;raw one-to-many output&lt;/code&gt;이라고 부르더라도, 여기서 &lt;code&gt;raw&lt;/code&gt;는 주로 &lt;b&gt;NMS가 graph에 포함되지 않은 출력&lt;/b&gt;이라는 의미로 이해하는 편이 안전하다. Detect head 내부의 미가공 box-distance tensor와 완전히 같은 단계는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 차원을 다시 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;N
&amp;rarr; 한 번에 처리하는 이미지 수, 즉 batch 차원

nc + 4
&amp;rarr; decode된 box 좌표 4개(xywh) + nc개의 class score

8400
&amp;rarr; P3/P4/P5 전체 candidate location 수
&amp;rarr; 6400 + 1600 + 400&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;nc=80&lt;/code&gt;인 COCO 모델이면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(N, 84, 8400)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이고, 현재 프로젝트처럼 &lt;code&gt;nc=1&lt;/code&gt;이면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(N, 5, 8400)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 건 이 &lt;code&gt;8400&lt;/code&gt;이 &lt;b&gt;최종 detection 8,400개가 나온다는 뜻이 아니라는 점&lt;/b&gt;이다. NMS 전에는 모든 candidate location이 남아 있지만, &lt;code&gt;model.predict()&lt;/code&gt;에서는 confidence filtering과 NMS를 거치면서 실제 사용할 detection만 남는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;One-to-One end-to-end output&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;code&gt;nms=False&lt;/code&gt;로 one-to-one 경로를 선택하면 현재 공식 문서의 일반적인 detection output은&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(N, max_det, 6)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 마지막 차원의 의미가 head 내부 channel과 완전히 다르다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;N
&amp;rarr; batch에 포함된 이미지 수

max_det
&amp;rarr; 이미지 한 장에서 graph가 유지할 수 있는 최대 detection 수

6
&amp;rarr; detection 하나를 표현하는 6개 값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 &lt;code&gt;max_det=300&lt;/code&gt;이면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(N, 300, 6)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 한 장의 결과만 생각하면 개념적으로&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(max_det, 6)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이고, detection 하나는 다음 6개 값으로 표현된다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[x1, y1, x2, y2, confidence, class_id]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 여기서 말하는 &lt;code&gt;6&lt;/code&gt;과 one-to-many export의 &lt;code&gt;nc+4&lt;/code&gt;는 같은 종류의 차원이 아니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;head 내부 one-to-many prediction
&amp;rarr; box distance + class logits

default one-to-many export
&amp;rarr; xywh + class scores

one-to-one end-to-end output
&amp;rarr; xyxy + confidence + class_id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 조심할 점이 있다. &lt;b&gt;출력 shape이 &lt;code&gt;(N, 300, 6)&lt;/code&gt;이라는 사실만으로 one-to-one head라고 단정할 수는 없다.&lt;/b&gt; &lt;code&gt;nms=False&lt;/code&gt; export는 one-to-one NMS-free 경로를 사용하지만, &lt;code&gt;nms=True&lt;/code&gt;로 one-to-many head의 NMS를 export graph에 포함한 경우에도 processed detection 형태가 나올 수 있다. 어떤 head를 썼는지는 export 설정과 artifact metadata를 함께 확인해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;model.predict()&lt;/code&gt;의 &lt;code&gt;Results&lt;/code&gt;와 export tensor도 구분해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 Python API의 &lt;code&gt;model.predict()&lt;/code&gt;는 사용자가 위 tensor를 그대로 다루게 하기보다 좌표 복원과 confidence filtering, 선택한 경로의 후처리를 거친 &lt;code&gt;Results&lt;/code&gt; 객체를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 디버깅할 때는 다음 세 가지를 같은 것으로 보면 안 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. Detect head 내부 boxes / scores tensor
2. export graph가 내보내는 tensor
3. model.predict()가 반환하는 Results.boxes&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 one-to-one export graph의 &lt;code&gt;max_det=300&lt;/code&gt;은 &lt;b&gt;상한&lt;/b&gt;이고, 실제 &lt;code&gt;Results.boxes&lt;/code&gt;에는 confidence threshold 등을 통과한 detection만 남기 때문에 최종 개수는 더 적을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 NMS가 필요할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;One-to-many 방식에서는 하나의 객체 주변에서 여러 높은 점수 박스가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사람

┌────────┐
│ ┌──────┼─┐
│ │      │ │
└─┼──────┘ │
  └────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 중복 예측이 생기면 NMS, 즉 비최대 억제(Non-Maximum Suppression)를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NMS는 보통 다음 흐름으로 이해할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. confidence가 높은 박스를 먼저 선택
2. 그 박스와 IoU가 너무 높은 중복 박스를 제거
3. 남은 박스에서 반복&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 헷갈리기 쉬운 점이 있다. NMS에서 사용하는 IoU는 &lt;b&gt;예측 박스와 예측 박스 사이의 겹침 정도&lt;/b&gt;를 보는 값이다. 뒤에서 모델 성능을 평가할 때도 IoU를 사용하지만, 그때는 &lt;b&gt;예측 박스와 GT 박스&lt;/b&gt;를 비교한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;NMS의 IoU
&amp;rarr; prediction &amp;harr; prediction 비교
&amp;rarr; 같은 객체를 가리키는 중복 detection 제거

평가의 IoU
&amp;rarr; prediction &amp;harr; GT 비교
&amp;rarr; 해당 prediction을 TP로 인정할 수 있는지 판단&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 같은 IoU 공식을 쓰지만 &lt;b&gt;비교 대상과 목적이 다르다.&lt;/b&gt; 그래서 &lt;code&gt;nms&lt;/code&gt;의 IoU threshold와 &lt;code&gt;mAP50&lt;/code&gt;, &lt;code&gt;mAP50-95&lt;/code&gt;에서 말하는 평가 IoU threshold를 같은 설정값으로 생각하면 안 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. YOLO26의 이중 헤드(Dual-Head)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26 검출 모델의 핵심 변화 중 하나는 이중 헤드 구조이다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;공유 Backbone + Neck

          ┌&amp;rarr; One-to-Many Head
          │
          └&amp;rarr; One-to-One Head&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 중에는 두 헤드가 모두 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;One-to-Many Head&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 정답 객체에 여러 양성 후보를 배정해 풍부한 학습 신호를 만든다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;한 사람 GT
 &amp;darr;
여러 prediction 위치가 positive가 될 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점은 dense supervision이 강하고 정확도에 유리하다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점은 추론에서 같은 객체 주변에 중복 박스가 여러 개 생길 수 있어 일반적으로 NMS가 필요하다는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;One-to-One Head&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답 객체당 최종적으로 더 엄격한 단일 대응을 학습하도록 설계된 경로이다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;한 사람 GT
 &amp;darr;
최종적으로 하나의 대표 detection을 만들도록 학습&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경로를 사용하면 NMS 없이 결과를 생성할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. &lt;code&gt;end2end=True&lt;/code&gt;와 &quot;기본 추론이 NMS-free&quot;는 같은 말이 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 YOLO26에서 가장 혼동하기 쉬운 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26 YAML에는&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;end2end: True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 &lt;b&gt;모델이 one-to-many와 one-to-one 두 경로를 갖도록 구성한다는 의미&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 현재 Ultralytics 런타임에서 &lt;code&gt;predict()&lt;/code&gt;와 &lt;code&gt;val()&lt;/code&gt;의 기본 선택을 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;nms=None  (현재 기본)
&amp;rarr; one-to-many head
&amp;rarr; NMS 적용

nms=True
&amp;rarr; one-to-many head
&amp;rarr; NMS 사용
  (export에서는 지원되는 형식에 NMS를 graph에 포함할 수 있음)

nms=False
&amp;rarr; one-to-one head
&amp;rarr; NMS-free&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;모델 아키텍처가 end-to-end 지원&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;현재 predict 기본값이 NMS-free&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 같은 말이 아니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 기본값이 one-to-many + NMS일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 공식 문서는 one-to-many 경로가 같은 가중치에서 대체로 조금 더 높은 정확도를 제공하기 때문에 predict/val 기본값으로 사용한다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 one-to-one 경로는 NMS 단계가 없어 배포 그래프가 단순하고 지연 시간을 줄이는 데 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해 YOLO26은 둘 중 하나만 강제하는 모델이라기보다 다음 선택지를 제공한다고 이해하면 좋다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;정확도 우선
&amp;rarr; one-to-many + NMS

후처리 단순화 / 지연 시간 우선
&amp;rarr; one-to-one + NMS-free&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NMS-free라고 해서 후처리가 0은 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;one-to-one 경로도 일반적으로 다음 처리는 남는다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;confidence filtering
좌표 스케일 복원
이미지 경계 clip
결과 형식 변환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;없어지는 것은 핵심적으로 &lt;b&gt;IoU 기반 중복 제거(NMS)&lt;/b&gt; 단계다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. DFL-Free는 무엇이 달라진 것일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLOv8과 YOLO11의 박스 회귀에는 Distribution Focal Loss 기반의 분포 표현이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 박스 변의 거리를 하나의 실수로 바로 회귀하는 대신 여러 bin에 대한 분포를 예측하고 기대값으로 거리를 복원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로 한 변의 거리는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;d = &amp;Sigma;(i=0 to K&amp;minus;1) i &amp;middot; softmax(z)ᵢ&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 얻어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;K=16&lt;/code&gt;이라면 한 변마다 16개 logit이 필요하므로 박스 회귀 전체는&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;4 &amp;times; 16 = 64 channels&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26은&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;reg_max = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 사용해 이 분포 연산을 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 박스 회귀 branch의 최종 출력은&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;4 &amp;times; 1 = 4 channels&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 DFL을 제거했을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적인 장점은 크게 세 가지다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. detection head가 더 가벼워짐
2. export graph가 단순해짐
3. 분포 bin이 만드는 거리 상한 제약을 제거할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분포 기반 DFL에서는 각 side의 예측 범위가 bin 수와 stride에 의해 제한된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 YOLO26의 직접 거리 회귀는 이런 discrete support 제한을 제거한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그런데 &lt;code&gt;dfl&lt;/code&gt;이라는 이름은 왜 아직 보일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 버전에 따라 동작과 이름이 달라져 특히 헷갈리기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 YOLO26은 DFL 분포 연산을 사용하지 않지만 설정에는 여전히&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;dfl: &amp;lt;gain&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 키가 존재할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Ultralytics 기본 설정에서는 이 항을&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;reg_max &amp;gt; 1
&amp;rarr; 실제 Distribution Focal Loss의 gain

reg_max = 1
&amp;rarr; DFL-free box-distance L1 loss의 gain&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 재사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 코드에서는 &lt;code&gt;reg_max=1&lt;/code&gt;일 때 loss 이름을 &lt;code&gt;l1_loss&lt;/code&gt;로 표시하도록 되어 있지만, 과거 버전 로그나 checkpoint metadata에서는 &lt;code&gt;dfl_loss&lt;/code&gt;라는 이름이 남아 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음 두 문장은 서로 모순되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;YOLO26은 DFL-Free다.&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;설정 파일에는 `dfl` gain이 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 &lt;b&gt;이름이 아니라 실제 &lt;code&gt;reg_max&lt;/code&gt;와 loss 구현을 확인하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. YOLO26의 기본 검출 loss를 어떻게 이해하면 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Ultralytics detect loss를 단순화하면 다음 세 축으로 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;box loss
&amp;rarr; 예측 박스와 GT 박스의 겹침/형상 차이를 줄임

cls loss
&amp;rarr; 올바른 클래스 점수를 높이고 잘못된 클래스를 낮춤

box-distance loss
&amp;rarr; 기준점에서 GT 박스 변까지의 거리 회귀를 보조&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26의 &lt;code&gt;reg_max=1&lt;/code&gt;에서는 세 번째 항이 분포 DFL이 아니라 정규화된 거리의 L1 회귀 항으로 계산된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;L_det = &amp;lambda;_box &amp;middot; L_box + &amp;lambda;_cls &amp;middot; L_cls + &amp;lambda;_dist &amp;middot; L_L1&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 구조로 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 구현은 positive sample weighting, target score normalization, assignment 결과 등을 포함하므로 위 식은 개념적인 요약이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Label Assignment는 왜 중요할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습에서는 GT 박스 하나를 보고 &quot;어느 특징 위치를 positive로 학습시킬 것인가&quot;를 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 label assignment라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 Ultralytics YOLO는 Task-Aligned Assignment 계열을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 GT 중심과 가장 가까운 grid만 고르는 것이 아니라 예측 클래스 점수와 박스 품질 등을 결합해 양성 후보를 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념만 놓고 보면&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;많은 anchor point 후보
 &amp;darr;
GT 내부/주변의 유효 후보 찾기
 &amp;darr;
분류 점수 + 위치 품질을 함께 평가
 &amp;darr;
top-k positive 선택&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 흐름이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26의 dual head에서는 one-to-many와 one-to-one이 서로 다른 엄격도의 assignment를 사용해 역할을 나눈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논문 v1의 구현 설명에는 one-to-many가 더 많은 positive를, one-to-one이 최종적으로 하나의 강한 대응을 남기는 방식이 제시되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 실제 top-k 값은 checkpoint/버전/실험 브랜치에 따라 달라질 수 있으므로 숫자를 하드코딩해서 외우기보다 현재 코드와 checkpoint metadata를 직접 확인하는 편이 확실하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Progressive Loss는 무엇을 점진적으로 바꿀까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26의 dual-head 학습에서는 두 branch가 같은 난이도를 갖지 않는다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;One-to-Many
&amp;rarr; positive가 많음
&amp;rarr; 초반에 특징을 학습시키기 쉬움

One-to-One
&amp;rarr; assignment가 훨씬 엄격함
&amp;rarr; 최종 NMS-free 추론 품질을 결정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Progressive Loss는 학습 초반에는 one-to-many의 풍부한 supervision을 더 활용하고, 학습이 진행될수록 one-to-one 경로의 비중을 더 키우는 curriculum 형태의 전략이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논문에서 제시한 개념식은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;L_total = &amp;alpha;(t) &amp;middot; L_one2many + (1 &amp;minus; &amp;alpha;(t)) &amp;middot; L_one2one&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;alpha(t)&lt;/code&gt;는 epoch가 진행될수록 감소한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;학습 초반
&amp;rarr; dense supervision으로 안정화

학습 후반
&amp;rarr; 실제 NMS-free inference head에 더 맞춰 최적화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 방향이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 공개 &lt;code&gt;main&lt;/code&gt; 구현의 &lt;code&gt;E2ELoss&lt;/code&gt;를 직접 보면 이 개념이 조금 더 구체적으로 보인다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;one-to-many loss
&amp;rarr; tal_topk = 10

one-to-one loss
&amp;rarr; tal_topk = 7
&amp;rarr; tal_topk2 = 1

학습 초반 branch weight
&amp;rarr; one-to-many = 0.8
&amp;rarr; one-to-one  = 0.2

학습 후반 목표
&amp;rarr; one-to-many = 0.1
&amp;rarr; one-to-one  = 0.9&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trainer는 에포크가 끝날 때 criterion의 &lt;code&gt;update()&lt;/code&gt;를 호출하고, 현재 구현에서는 one-to-many 비중을 점차 낮추면서 one-to-one 비중을 높인다. 즉 위 개념식의 &lt;code&gt;&amp;alpha;(t)&lt;/code&gt;가 실제 코드에서도 감소하는 방향으로 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 숫자는 &lt;b&gt;현재 공개 &lt;code&gt;main&lt;/code&gt;에서 custom training을 할 때 볼 수 있는 구현값&lt;/b&gt;이다. 공식 pretrained checkpoint는 실험 브랜치에서 만들어진 이력이 있고 내부 학습 파라미터도 checkpoint에 기록되어 있으므로, 공식 checkpoint 제작 recipe를 정확히 재현하려면 &lt;code&gt;train_args&lt;/code&gt;와 &lt;code&gt;ckpt[&quot;git&quot;]&lt;/code&gt;까지 확인하는 편이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 runtime 기본 predict가 one-to-many + NMS라고 해도, YOLO26의 &lt;b&gt;end-to-end one-to-one 경로를 제대로 학습시키기 위한 목적&lt;/b&gt;에서 Progressive Loss의 의미는 여전히 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26의 검출과 학습을 이해할 때는 다음 세 가지를 서로 구분해 두는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;1. end2end=True
   &amp;rarr; one-to-many / one-to-one dual-head 구조를 구성하는 모델 특성

2. nms=False
   &amp;rarr; 현재 runtime에서 one-to-one NMS-free 경로를 선택

3. reg_max=1
   &amp;rarr; DFL 분포 연산을 제거하고 직접 box-distance를 회귀&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 학습에서는 수천 개의 후보 중 어떤 위치를 positive로 사용할지 정하는 &lt;b&gt;label assignment&lt;/b&gt;가 매우 중요하다. YOLO26의 dual-head는 one-to-many의 풍부한 supervision과 one-to-one의 엄격한 대응을 함께 사용하고, Progressive Loss로 두 경로의 학습 비중을 조절한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 assignment 문제가 &lt;b&gt;아주 작은 객체&lt;/b&gt;에서 왜 더 어려워지는지, 그리고 STAL과 P2 head가 이를 어떤 방식으로 보완하는지 열화상 보행자 검출 프로젝트와 연결해 살펴볼 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 검증 기준 (2026-09-09)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Ultralytics YOLO26 공식 모델 문서&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/models/yolo26/&quot;&gt;Ultralytics YOLO26&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics End-to-End Detection 가이드&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/guides/end2end-detection/&quot;&gt;YOLO26 End-to-End NMS-Free Detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics YOLO Architecture Explained 가이드&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/guides/yolo-architecture/&quot;&gt;YOLO Architecture Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics YOLO26 Training Recipe&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/guides/yolo26-training-recipe/&quot;&gt;YOLO26 Training Recipe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics 모델 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;yolo26.yaml&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/models/26/yolo26.yaml&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yolo26-p2.yaml&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/models/26/yolo26-p2.yaml&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics Detect Head 구현&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;nn/modules/head.py&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/nn/modules/head.py&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics Loss / Label Assignment 구현&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;utils/loss.py&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/loss.py&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;utils/tal.py&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/tal.py&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;Ultralytics YOLO26: Unified Real-Time End-to-End Vision Models&lt;/i&gt;&lt;/b&gt;, arXiv:2606.03748 &amp;mdash; &lt;a href=&quot;https://arxiv.org/abs/2606.03748&quot;&gt;arXiv 논문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>Detection Head</category>
      <category>DFL-Free</category>
      <category>Label Assignment</category>
      <category>NMS</category>
      <category>Progressive Loss</category>
      <category>YoLO</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/76</guid>
      <comments>https://slowgrid.tistory.com/entry/YOLO-Basics-2-How-Does-the-YOLO26-Detection-Head-Work#entry76comment</comments>
      <pubDate>Tue, 8 Sep 2026 19:59:04 +0900</pubDate>
    </item>
    <item>
      <title>[YOLO 기초 1] YOLO는 어떻게 객체를 찾을까?</title>
      <link>https://slowgrid.tistory.com/entry/YOLO-Basics-1-How-does-YOLO-find-objects</link>
      <description>&lt;h1&gt;[YOLO 기초 1] YOLO는 어떻게 객체를 찾을까?&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backbone, Neck, Detection Head로 이해하는 YOLO의 기본 검출 흐름&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서는 CNN이 이미지에서 특징을 만드는 기본 원리를 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 YOLO로 돌아가보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO는 &lt;code&gt;You Only Look Once&lt;/code&gt;에서 시작된 이름으로, 하나의 신경망 파이프라인 안에서 이미지의 특징을 추출하고 객체의 위치와 클래스를 예측하는 실시간 객체 탐지 계열 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 먼저 YOLO 계열의 큰 흐름부터 잡아보려고 한다. 세부 구현은 현재 사용하는 YOLO26을 예시로 설명하지만, 핵심 목표는 &lt;b&gt;이미지가 Backbone &amp;rarr; Neck &amp;rarr; Detection Head &amp;rarr; Decode를 거쳐 검출 결과가 되는 과정&lt;/b&gt;을 이해하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;이미지
 &amp;darr;
Backbone
&amp;rarr; 특징 추출
 &amp;darr;
Neck
&amp;rarr; 다중 스케일 특징 융합
 &amp;darr;
Detection Head
&amp;rarr; 박스 회귀 + 클래스 점수
 &amp;darr;
Decode / 후처리
 &amp;darr;
최종 바운딩 박스 + 클래스&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 번 짚고 넘어갈 부분은 YOLO가 이미지 픽셀에서 곧바로 &lt;code&gt;x1, y1, x2, y2&lt;/code&gt;를 뽑는 것이 아니라는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에는 여러 해상도의 특징 맵, 특징 위치별 기준점(anchor point), 박스 거리 회귀, 클래스 점수, 디코딩 과정이 존재한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 백본(Backbone)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백본은 입력 이미지에서 객체 탐지에 필요한 특징을 추출하는 부분이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이미지
 &amp;darr;
여러 합성곱 / 특징 블록
 &amp;darr;
서로 다른 해상도의 특징 맵&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 픽셀을 검출 헤드에 그대로 전달하는 것이 아니라, 여러 계층을 통과시키면서 에지, 윤곽, 질감, 형태, 문맥처럼 탐지에 유용한 표현으로 바꾼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26의 기본 검출 YAML을 보면 백본에는 단순한 Conv만 있는 것이 아니라 이런 구성 요소가 포함된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Conv
 &amp;darr;
C3k2 계열 블록
 &amp;darr;
더 깊은 C3k2 블록
 &amp;darr;
SPPF + shortcut
 &amp;darr;
C2PSA&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C3k2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO11과 YOLO26에서 핵심적으로 사용되는 CSP 계열 특징 추출 블록이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 여러 경로로 특징을 나눴다가 다시 합치면서 계산 효율과 표현력을 함께 챙기는 구조라고 이해해 두면 충분하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SPPF&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SPPF(Spatial Pyramid Pooling - Fast)&lt;/code&gt;는 깊은 특징에서 더 넓은 공간 문맥을 모으는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26에서는 SPPF에 shortcut이 포함되는 형태가 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C2PSA&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊은 특징에 attention을 적용해 중요한 공간적/채널적 관계를 더 잘 모델링하도록 돕는 블록이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 내부 수식까지 파고들기보다, 일단 다음 정도로 잡아두면 이해하기 편하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;백본
&amp;rarr; 단순히 해상도를 줄이는 부분이 아님
&amp;rarr; 위치가 다른 픽셀들을 점점 더 의미 있는 특징 표현으로 바꾸는 부분&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 여러 크기의 특징이 필요할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 열화상 보행자 검출 데이터셋에서는 작은 보행자가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 데이터 분석 기준으로 바운딩 박스 높이 중앙값이 대략 20px대이고 더 작은 사람도 상당수 존재한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;가까운 사람

████████████

먼 사람

██&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 &lt;code&gt;person&lt;/code&gt; 클래스라도 이미지에서 차지하는 크기가 크게 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 해상도에서만 검출하면 작은 객체와 큰 객체를 동시에 다루기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현대 YOLO는 서로 다른 공간 해상도를 가진 특징 피라미드를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;높은 해상도 특징
&amp;rarr; 세밀한 위치 정보 보존에 상대적으로 유리

낮은 해상도 특징
&amp;rarr; 더 넓은 문맥과 강한 의미적 표현에 상대적으로 유리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 이를&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;P3 = 작은 객체 전용
P4 = 중간 객체 전용
P5 = 큰 객체 전용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 완전히 고정된 규칙으로 이해하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 양성 샘플 배정(label assignment)은 객체 위치, 예측 품질, 분류 점수, IoU 등을 함께 보며 결정될 수 있고, 하나의 객체가 어느 피라미드 레벨에서만 반드시 학습되는 식의 단순한 구조는 아니기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. P3 / P4 / P5와 스트라이드(Stride)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO의 특징 피라미드를 보다 보면 다음 표현을 자주 본다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;P3/8
P4/16
P5/32&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;/8&lt;/code&gt;, &lt;code&gt;/16&lt;/code&gt;, &lt;code&gt;/32&lt;/code&gt;는 원본 입력 좌표계에 대한 공간적 다운샘플링 비율, 즉 스트라이드(stride)를 뜻한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;imgsz=640&lt;/code&gt;이고 입력이 640&amp;times;640으로 정렬되어 있다고 두고 간단히 계산해 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;P3 / stride 8
640 / 8 = 80
&amp;rarr; 80 &amp;times; 80&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;P4 / stride 16
640 / 16 = 40
&amp;rarr; 40 &amp;times; 40&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;P5 / stride 32
640 / 32 = 20
&amp;rarr; 20 &amp;times; 20&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;20px 높이 보행자를 대입해 보면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;높이가 20px인 객체를 단순히 스트라이드로 나눠 보면&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;P3: 20 / 8  &amp;asymp; 2.5 feature-cell 간격
P4: 20 / 16 &amp;asymp; 1.25
P5: 20 / 32 &amp;asymp; 0.625&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 계산은 작은 보행자에서 왜 P3 같은 높은 해상도 특징이 중요한지 직관을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 매우 중요한 주의점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;stride=8이라고 해서 한 특징 셀이 원본 이미지의 딱 8&amp;times;8 픽셀만 본다는 뜻은 아니다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트라이드는 &lt;b&gt;특징 위치 사이의 샘플링 간격&lt;/b&gt;에 가깝고, 실제 한 특징 위치가 영향을 받는 수용 영역(receptive field)은 여러 합성곱 층을 거치면서 훨씬 넓어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;stride &amp;ne; receptive field 크기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;P3/P4/P5라는 이름도 문맥을 확인해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서나 코드에서 &lt;code&gt;P3/P4/P5&lt;/code&gt;는 때로는 백본에서 꺼낸 원래 피라미드 특징을 뜻하고, 때로는 넥을 통과한 뒤 같은 스트라이드를 가진 최종 융합 특징을 뜻한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 YAML을 읽을 때는 이름만 보지 말고 &lt;b&gt;어느 layer에서 나온 tensor인지&lt;/b&gt; 함께 보는 것이 정확하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 넥(Neck)은 왜 여러 스케일 특징을 합칠까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넥의 역할은 서로 다른 스케일에서 만들어진 특징을 결합하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;P3 ─┐
P4 ─┼─&amp;rarr; Neck &amp;rarr; 융합된 P3 / P4 / P5
P5 ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26의 기본 검출 구조는 FPN + PAN 형태의 특징 융합을 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Top-down: FPN 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊고 낮은 해상도의 특징을 위쪽의 높은 해상도로 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념만 놓고 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;P5
 &amp;darr; upsample
P4와 concat
 &amp;darr;
P4 수준의 융합 특징
 &amp;darr; upsample
P3와 concat
 &amp;darr;
P3 수준의 융합 특징&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Bottom-up: PAN 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 높은 해상도 특징에서 낮은 해상도 방향으로 정보를 전달한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;융합 P3
 &amp;darr; stride-2 conv
융합 P4와 concat
 &amp;darr;
최종 P4
 &amp;darr; stride-2 conv
P5 계열 특징과 concat
 &amp;darr;
최종 P5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해 넥은 단순히 특징 맵을 이어 붙이는 한 번의 연산이 아니라,&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;상위 의미 정보 &amp;rarr; 높은 해상도로 전달
높은 해상도 위치 정보 &amp;rarr; 다시 낮은 해상도로 전달&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 양방향 특징 융합 구조라고 보는 편이 자연스럽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 굳이 특징을 합칠까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊은 계층은 객체의 의미적인 정보를 파악하는 데 유리하지만 공간 해상도가 낮다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 상대적으로 높은 해상도의 계층은 위치와 경계에 대한 세부 정보를 더 많이 보존한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 나눠 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;높은 해상도 특징
&amp;rarr; 위치 / 경계 정보 보존에 유리

깊은 특징
&amp;rarr; 의미 / 주변 문맥 이해에 유리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넥은 두 종류의 장점을 결합해&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;어디에 있는가?&quot;
       +
&quot;무엇인가?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 함께 풀기 좋은 특징을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트처럼 작은 열화상 보행자가 많다면 높은 해상도에서 세부 위치 정보를 보존하는 것이 특히 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 검출 헤드(Detection Head)는 실제로 무엇을 출력할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징 추출과 융합이 끝나면 실제 예측을 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역할을 검출 헤드가 담당한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;융합된 P3 / P4 / P5
       &amp;darr;
검출 헤드
       &amp;darr;
박스 회귀 + 클래스 점수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ultralytics YOLO의 &lt;code&gt;Detect&lt;/code&gt; 헤드는 &lt;b&gt;decoupled head&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 각 피라미드 레벨에서 박스 회귀와 클래스 예측이 서로 다른 branch를 통과한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;feature
 ├─&amp;rarr; box branch
 │    &amp;rarr; 박스 거리 4개
 │
 └─&amp;rarr; class branch
      &amp;rarr; nc개 클래스 logit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26은 &lt;code&gt;reg_max=1&lt;/code&gt;이므로 한 특징 위치마다 raw channel 수는 개념적으로&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;4 + nc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;한 특징 위치(feature location)가 하나의 예측 후보(candidate)를 만든다&lt;/b&gt;고 생각하면 뒤의 tensor shape를 이해하기가 쉽다. P3, P4, P5의 모든 공간 위치에서 이런 후보가 만들어지고, 각 후보마다 box branch와 class branch의 출력이 붙는다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;한 feature location
&amp;rarr; box distance 4개
&amp;rarr; class logit nc개
&amp;rarr; 총 4 + nc개의 raw 값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 이 후보 하나가 곧바로 최종 detection 하나를 뜻하는 것은 아니라는 것이다. 이 단계에서는 아직 &lt;b&gt;검출 헤드가 만든 raw prediction&lt;/b&gt;이고, 이후 box decode와 confidence 기반 결과 선택, 사용하는 추론 경로에 따라 NMS 같은 후처리를 거쳐 최종 detection이 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 4개는 바로 &lt;code&gt;x1, y1, x2, y2&lt;/code&gt;가 아니라 기준점(anchor point)으로부터의 박스 거리 표현이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중요한 차이: 별도 objectness channel이 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전 YOLOv5 계열 설명을 보면 흔히&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;x, y, w, h, objectness, class...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 출력을 보게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재 Ultralytics의 anchor-free &lt;code&gt;Detect&lt;/code&gt; 헤드는 raw 출력 채널이&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;4 &amp;times; reg_max + nc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태이며, 별도의 objectness channel을 두지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26에서는 &lt;code&gt;reg_max=1&lt;/code&gt;이므로&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;4 + nc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 우리가 최종 결과에서 보는&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[x1, y1, x2, y2, confidence, class_id]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 &lt;b&gt;raw head tensor 그 자체가 아니라 디코딩과 결과 선택이 끝난 뒤의 표현&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단일 클래스(person) 프로젝트라면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;nc=1&lt;/code&gt;이라면 one-to-many raw head의 채널 수는&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;4 + 1 = 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;raw prediction: 5 channels per candidate
processed detection row: 6 values
[x1, y1, x2, y2, confidence, class_id]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 구분해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. YOLO26의 박스는 어떻게 기준점에서 만들어질까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26은 anchor-free 모델이지만 특징 위치별 기준점(anchor point)은 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 P3의 한 위치가 원본 좌표계에서 특정 &lt;code&gt;(x, y)&lt;/code&gt; 지점을 기준으로 가진다고 생각하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤드는 그 기준점에서 박스 네 변까지의 거리를 예측한다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;             top
              &amp;uarr;
        ┌───────────┐
 left &amp;larr; │   object  │ &amp;rarr; right
        └───────────┘
              &amp;darr;
            bottom

          anchor point&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념만 놓고 보면&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;(l, t, r, b)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거리로 예측한 뒤 기준점과 결합해 바운딩 박스로 복원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 raw 회귀는&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;anchor point + l/t/r/b distance&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이고, 이후 디코딩을 거쳐 &lt;code&gt;xyxy&lt;/code&gt; 또는 &lt;code&gt;xywh&lt;/code&gt; 형식으로 변환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 &lt;code&gt;anchor-free&lt;/code&gt;라는 말과 &lt;code&gt;anchor point&lt;/code&gt;라는 구현 용어가 동시에 등장해도 모순이 아니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Anchor-Free인데 왜 anchor point가 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Anchor-Free&lt;/code&gt;는 예전처럼 사람이 미리 정한 여러 앵커 박스 크기와 종횡비를 각 위치에 배치하지 않는다는 의미에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전 anchor-based 방식은 예를 들어&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;각 grid 위치마다
- 작은 가로형 anchor box
- 중간 정사각형 anchor box
- 큰 세로형 anchor box&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 사전 정의 템플릿을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 anchor-free 방식에서는 이런 사전 정의 박스 크기/비율을 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 특징 맵의 각 위치는 여전히 공간적인 기준점으로 사용될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Anchor-Free
&amp;ne; 공간 기준점이 전혀 없음

Anchor-Free
= 미리 설계한 anchor box 크기/비율에 의존하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 이해해 두면 크게 헷갈리지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO의 기본 검출 흐름은 다음처럼 정리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;이미지
 &amp;darr;
Backbone
&amp;rarr; 특징 추출
 &amp;darr;
Neck
&amp;rarr; FPN/PAN 다중 스케일 특징 융합
 &amp;darr;
Detect Head
&amp;rarr; 박스 거리 + 클래스 점수
 &amp;darr;
Anchor Point 기반 Decode
 &amp;darr;
최종 Detection&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 작은 객체가 많은 데이터에서는 &lt;code&gt;P3/P4/P5&lt;/code&gt;의 stride 차이와 높은 해상도 특징이 왜 필요한지를 함께 이해하는 게 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 &lt;b&gt;모델이 무엇을 보고 어떻게 박스 후보를 만드는가&lt;/b&gt;에 집중했다. 다음 글에서는 YOLO26의 Detect Head를 더 깊게 들어가서, 한 이미지에서 왜 수천 개의 후보가 나오고, NMS가 왜 필요하며, Dual Head와 DFL-Free 구조가 학습과 추론을 어떻게 바꾸는지 살펴볼 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 검증 기준&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Ultralytics YOLO26 공식 모델 문서&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/models/yolo26/&quot;&gt;Ultralytics YOLO26&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics End-to-End Detection 가이드&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/guides/end2end-detection/&quot;&gt;YOLO26 End-to-End NMS-Free Detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics YOLO Architecture Explained 가이드&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/guides/yolo-architecture/&quot;&gt;YOLO Architecture Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics YOLO26 Training Recipe&lt;/b&gt; &amp;mdash; &lt;a href=&quot;https://docs.ultralytics.com/guides/yolo26-training-recipe/&quot;&gt;YOLO26 Training Recipe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics 모델 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;yolo26.yaml&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/models/26/yolo26.yaml&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yolo26-p2.yaml&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/models/26/yolo26-p2.yaml&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ultralytics Loss / Label Assignment 구현&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;utils/loss.py&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/loss.py&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;utils/tal.py&lt;/code&gt; &amp;mdash; &lt;a href=&quot;https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/tal.py&quot;&gt;GitHub 원문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;Ultralytics YOLO26: Unified Real-Time End-to-End Vision Models&lt;/i&gt;&lt;/b&gt;, arXiv:2606.03748 &amp;mdash; &lt;a href=&quot;https://arxiv.org/abs/2606.03748&quot;&gt;arXiv 논문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>YoLO</category>
      <category>YOLO26</category>
      <category>넥</category>
      <category>백본</category>
      <category>헤드</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/75</guid>
      <comments>https://slowgrid.tistory.com/entry/YOLO-Basics-1-How-does-YOLO-find-objects#entry75comment</comments>
      <pubDate>Mon, 7 Sep 2026 19:33:31 +0900</pubDate>
    </item>
    <item>
      <title>왜 0제곱은 1일까?</title>
      <link>https://slowgrid.tistory.com/entry/Why-is-0-raised-to-the-power-of-1</link>
      <description>&lt;p&gt;수학 문제를 탐구하다가 문득 이런 생각이 들었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2⁰ = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;5⁰ = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;너무 익숙해서 그냥 외우고 있었는데, 생각해보면 조금 이상하게 느껴지는 부분이 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³&lt;/code&gt;은 2를 세 번 곱한 것이고,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2²&lt;/code&gt;는 2를 두 번 곱한 것이다.&lt;/p&gt;
&lt;p&gt;그렇다면 &lt;code&gt;2⁰&lt;/code&gt;은 2를 0번 곱한 것이라고 봐야 할 텐데, 왜 결과가 1일까?&lt;/p&gt;
&lt;p&gt;처음에는 그냥 수학에서 그렇게 정했겠지라고 생각하고 넘겼다.&lt;/p&gt;
&lt;p&gt;그런데 찾아보니 단순히 계산을 편하게 하기 위해 아무렇게나 1로 정한 것은 아니었다.&lt;/p&gt;
&lt;p&gt;오히려 기존에 사용하던 거듭제곱의 규칙을 0제곱까지 그대로 이어가려고 하면, 결과가 1이 될 수밖에 없다는 것을 알 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;거듭제곱의 규칙부터 다시 생각해봤다&lt;/h2&gt;
&lt;p&gt;우리가 거듭제곱을 계산할 때 자주 사용하는 규칙이 하나 있다.&lt;/p&gt;
&lt;p&gt;같은 수의 거듭제곱끼리 곱하면 &lt;strong&gt;지수는 서로 더해진다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ × 2²&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;를 직접 풀어보면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;(2 × 2 × 2) × (2 × 2)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;결국 2를 모두 다섯 번 곱한 것이기 때문에&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ × 2² = 2⁵&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;여기까지는 익숙한 내용이다.&lt;/p&gt;
&lt;p&gt;그런데 이 규칙을 이용해서 &lt;code&gt;2⁰&lt;/code&gt;을 생각해봤다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ × 2⁰&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;에서 지수끼리 더하면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³⁺⁰&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;그런데 3에 0을 더해도 3이므로 결국&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ × 2⁰ = 2³&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이어야 한다.&lt;/p&gt;
&lt;p&gt;여기서 질문을 바꿔봤다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;8에 어떤 수를 곱해야 다시 8이 될까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;당연히 1이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;8 × 1 = 8&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³&lt;/code&gt;은 8이므로&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ × 2⁰ = 2³&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 성립하려면 &lt;code&gt;2⁰&lt;/code&gt;의 값은 1이어야 한다.&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2⁰ = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;3이나 5를 넣어봐도 똑같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;3² × 3⁰ = 3²&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;가 되려면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;3⁰ = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이어야 하고,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;5⁴ × 5⁰ = 5⁴&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;가 되려면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;5⁰ = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이어야 한다.&lt;/p&gt;
&lt;p&gt;그래서 0이 아닌 어떤 수를 가져오더라도 0제곱은 1이 된다.&lt;/p&gt;
&lt;p&gt;여기까지 보고 나니 0제곱은 그냥 1이라고 약속했다라는 설명은 조금 부족하게 느껴졌다.&lt;/p&gt;
&lt;p&gt;기존의 거듭제곱 규칙을 그대로 사용하려면 다른 값을 넣기가 어려웠기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그렇다면 이건 증명일까, 정의일까?&lt;/h2&gt;
&lt;p&gt;여기서 또 하나 궁금한 점이 생겼다.&lt;/p&gt;
&lt;p&gt;이렇게 계산해서 1이 나왔으니 0제곱이 1이라는 사실을 증명했다고 해야 할까?&lt;/p&gt;
&lt;p&gt;아니면 처음부터 그렇게 정의했다고 해야 할까?&lt;/p&gt;
&lt;p&gt;찾아보니 엄밀하게는 &lt;strong&gt;거듭제곱의 정의를 확장한 것&lt;/strong&gt;으로 보는 것이 더 정확했다.&lt;/p&gt;
&lt;p&gt;처음 거듭제곱을 배울 때는 보통 이렇게 생각한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2¹ = 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2² = 2 × 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ = 2 × 2 × 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;즉, 같은 수를 몇 번 곱하는지를 지수로 나타내는 것이다.&lt;/p&gt;
&lt;p&gt;이 설명은 지수가 1, 2, 3처럼 자연수일 때는 아무 문제가 없다.&lt;/p&gt;
&lt;p&gt;그런데 지수가 0이 되면 이야기가 조금 달라진다.&lt;/p&gt;
&lt;p&gt;&amp;quot;2를 0번 곱한다&amp;quot;라는 말만 가지고는 그 결과가 무엇인지 바로 알기 어렵기 때문이다.&lt;/p&gt;
&lt;p&gt;그래서 거듭제곱의 범위를 0까지 넓힐 때는 0제곱의 의미를 새롭게 정해야 한다.&lt;/p&gt;
&lt;p&gt;그렇다고 아무 숫자나 선택할 수 있는 것은 아니다.&lt;/p&gt;
&lt;p&gt;앞에서 보았던 규칙,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;같은 수의 거듭제곱끼리 곱하면 지수는 더해진다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 규칙을 0에서도 그대로 사용하려고 하면 0제곱은 반드시 1이어야 한다.&lt;/p&gt;
&lt;p&gt;그래서 개인적으로는 이렇게 이해하는 것이 가장 자연스러운 것 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0제곱이 1이라는 것은 정의이지만, 마음대로 정한 정의는 아니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존에 잘 사용하고 있던 거듭제곱의 규칙을 깨뜨리지 않으려다 보니 자연스럽게 1이라는 값이 결정된 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그런데 왜 하필 1일까?&lt;/h2&gt;
&lt;p&gt;여기까지 이해하고 나니 또 다른 궁금증이 생겼다.&lt;/p&gt;
&lt;p&gt;계산을 따라가면 1이 나온다는 것은 알겠는데,&lt;/p&gt;
&lt;p&gt;조금 더 본질적으로 왜 1이어야 하는 걸까?&lt;/p&gt;
&lt;p&gt;이 부분을 찾아보다가 &lt;strong&gt;항등원&lt;/strong&gt;이라는 개념을 다시 보게 되었다.&lt;/p&gt;
&lt;p&gt;이름은 조금 어렵지만 뜻은 간단하다.&lt;/p&gt;
&lt;p&gt;어떤 계산을 했을 때 원래 값을 바꾸지 않는 숫자라고 생각하면 된다.&lt;/p&gt;
&lt;p&gt;덧셈에서는 0이 그런 역할을 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;5 + 0 = 5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;10 + 0 = 10&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;어떤 숫자에 0을 더해도 원래 값은 그대로다.&lt;/p&gt;
&lt;p&gt;그래서 0을 &lt;strong&gt;덧셈의 항등원&lt;/strong&gt;이라고 부른다.&lt;/p&gt;
&lt;p&gt;곱셈에서는 1이 같은 역할을 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;5 × 1 = 5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;10 × 1 = 10&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;어떤 숫자에 1을 곱해도 원래 값은 그대로다.&lt;/p&gt;
&lt;p&gt;그래서 1을 &lt;strong&gt;곱셈의 항등원&lt;/strong&gt;이라고 한다.&lt;/p&gt;
&lt;p&gt;이걸 거듭제곱과 연결해서 생각해보니 꽤 재미있었다.&lt;/p&gt;
&lt;p&gt;거듭제곱에서는 지수를 더하면 실제 값은 서로 곱해진다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ × 2² = 2⁵&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;에서&lt;/p&gt;
&lt;p&gt;지수에서는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;3 + 2 = 5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 덧셈이 일어나고,&lt;/p&gt;
&lt;p&gt;실제 값에서는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;8 × 4 = 32&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라는 곱셈이 일어난다.&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;지수 쪽의 덧셈이 실제 값 쪽에서는 곱셈으로 연결되는 셈이다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그런데 덧셈에서 아무런 변화를 만들지 않는 숫자는 0이다.&lt;/p&gt;
&lt;p&gt;반대로 곱셈에서 아무런 변화를 만들지 않는 숫자는 1이다.&lt;/p&gt;
&lt;p&gt;그래서 지수에서의 0은 실제 값에서는 1과 연결된다.&lt;/p&gt;
&lt;p&gt;간단하게 적으면 이런 느낌이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;덧셈에서 아무 변화가 없는 값 : 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;곱셈에서 아무 변화가 없는 값 : 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그래서 지수에 0을 넣었을 때 결과가 1이 된다는 것이다.&lt;/p&gt;
&lt;p&gt;개인적으로는 이 설명이 가장 이해가 잘 됐다.&lt;/p&gt;
&lt;p&gt;단순히 계산해서 1이 나오는 것이 아니라, 0과 1이 각각 덧셈과 곱셈에서 같은 역할을 하고 있다는 점이 보였기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그래도 0번 곱한다는 게 이상했다&lt;/h2&gt;
&lt;p&gt;여기까지 봐도 처음 들었던 의문이 완전히 사라지지는 않았다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³&lt;/code&gt;은&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2 × 2 × 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;라고 이해할 수 있다.&lt;/p&gt;
&lt;p&gt;그러면 &lt;code&gt;2⁰&lt;/code&gt;은 결국 2를 하나도 곱하지 않는다는 뜻이다.&lt;/p&gt;
&lt;p&gt;그런데 아무것도 곱하지 않았는데 왜 1이 남는 걸까?&lt;/p&gt;
&lt;p&gt;이걸 찾아보다가 &lt;strong&gt;빈 곱(empty product)​&lt;/strong&gt;이라는 개념을 알게 되었다.&lt;/p&gt;
&lt;p&gt;처음에는 이름부터 조금 낯설었다.&lt;/p&gt;
&lt;p&gt;그런데 생각보다 내용은 간단했다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2 × 2 × 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;앞에 1을 하나 붙여보자.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1 × 2 × 2 × 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;결과는 똑같이 8이다.&lt;/p&gt;
&lt;p&gt;2가 두 개라면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1 × 2 × 2 = 4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이고,&lt;/p&gt;
&lt;p&gt;2가 하나라면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1 × 2 = 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;이렇게 보면 곱셈을 시작하기 전의 기본값을 1이라고 생각할 수 있다.&lt;/p&gt;
&lt;p&gt;그렇다면 곱할 숫자가 하나도 없다면 어떻게 될까?&lt;/p&gt;
&lt;p&gt;곱할 것이 없으니 처음의 1만 남는다.&lt;/p&gt;
&lt;p&gt;그래서 수학에서는 &lt;strong&gt;아무것도 곱하지 않은 결과를 1로 본다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이것을 빈 곱이라고 부른다.&lt;/p&gt;
&lt;p&gt;재미있는 것은 덧셈에도 비슷한 개념이 있다는 점이다.&lt;/p&gt;
&lt;p&gt;아무 숫자도 더하지 않으면 결과를 0으로 본다.&lt;/p&gt;
&lt;p&gt;정리하면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;아무것도 더하지 않은 값 = 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;아무것도 곱하지 않은 값 = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;앞에서 봤던 항등원과 정확히 같은 결과가 나온다.&lt;/p&gt;
&lt;p&gt;덧셈을 시작하는 기본값은 0이고,&lt;/p&gt;
&lt;p&gt;곱셈을 시작하는 기본값은 1이라고 생각할 수도 있다.&lt;/p&gt;
&lt;p&gt;처음에는 지수법칙, 항등원, 빈 곱이 각각 다른 설명인 줄 알았지만&lt;/p&gt;
&lt;p&gt;찾아보다 보니 결국 세 가지가 같은 방향을 가리키고 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;지수를 하나씩 내려가 봐도 확인할 수 있다&lt;/h2&gt;
&lt;p&gt;조금 더 직관적으로 확인하는 방법도 있었다.&lt;/p&gt;
&lt;p&gt;2의 거듭제곱을 순서대로 적어봤다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2⁴ = 16&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ = 8&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2² = 4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2¹ = 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;숫자를 자세히 보면 일정한 규칙이 보인다.&lt;/p&gt;
&lt;p&gt;지수가 하나 줄어들 때마다 값이 2로 나누어진다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;16 ÷ 2 = 8&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;8 ÷ 2 = 4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;4 ÷ 2 = 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;그렇다면 똑같은 규칙으로 한 번 더 내려가면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2 ÷ 2 = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2⁰ = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;3으로 해봐도 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;3³ = 27&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;3² = 9&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;3¹ = 3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;지수가 하나 줄어들 때마다 3으로 나누고 있으므로 다음은&lt;/p&gt;
&lt;p&gt;&lt;code&gt;3⁰ = 3 ÷ 3 = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;어떤 0이 아닌 수를 넣어도 마지막에는 자기 자신을 자기 자신으로 나누게 된다.&lt;/p&gt;
&lt;p&gt;그리고 0이 아닌 수를 자기 자신으로 나누면 항상 1이다.&lt;/p&gt;
&lt;p&gt;그래서 이 방법으로 봐도 0제곱이 1이라는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;다만 여기에는 중요한 조건이 하나 있다.&lt;/p&gt;
&lt;p&gt;0은 자기 자신으로 나눌 수 없다.&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0 ÷ 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;은 사용할 수 없다.&lt;/p&gt;
&lt;p&gt;그래서 지금까지의 이야기는 &lt;strong&gt;0이 아닌 수의 0제곱&lt;/strong&gt;에 대한 설명이다.&lt;/p&gt;
&lt;p&gt;그리고 여기서 자연스럽게 다음 궁금증이 생겼다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그러면 0⁰도 1일까?&lt;/h2&gt;
&lt;p&gt;0이 아닌 모든 수의 0제곱이 1이라면 이런 생각이 들 수밖에 없다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;그럼 0의 0제곱도 1 아닌가?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;나도 처음에는 그렇게 생각했다.&lt;/p&gt;
&lt;p&gt;그런데 앞에서 사용했던 방법을 그대로 적용하려고 해보니 바로 문제가 생겼다.&lt;/p&gt;
&lt;p&gt;2의 경우에는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2⁰ = 2 ÷ 2 = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이라고 생각할 수 있었다.&lt;/p&gt;
&lt;p&gt;하지만 0에 같은 방식을 적용하면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0⁰ = 0 ÷ 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;처럼 생각해야 한다.&lt;/p&gt;
&lt;p&gt;그런데 &lt;code&gt;0 ÷ 0&lt;/code&gt;은 정의할 수 없다.&lt;/p&gt;
&lt;p&gt;그래서 앞에서 사용했던 논리를 그대로 가져와 &lt;code&gt;0⁰ = 1&lt;/code&gt;이라고 말할 수는 없다.&lt;/p&gt;
&lt;p&gt;게다가 서로 다른 두 규칙을 생각하면 더 재미있는 문제가 생긴다.&lt;/p&gt;
&lt;p&gt;우리는 지금까지&lt;/p&gt;
&lt;p&gt;&lt;code&gt;어떤 수의 0제곱 = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이라고 생각했다.&lt;/p&gt;
&lt;p&gt;이 규칙만 보면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0⁰ = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이라고 하고 싶어진다.&lt;/p&gt;
&lt;p&gt;그런데 또 한편으로는&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0¹ = 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0² = 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0³ = 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;처럼 0의 거듭제곱은 계속 0이다.&lt;/p&gt;
&lt;p&gt;이 흐름을 보면 오히려&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0⁰ = 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이라고 생각하고 싶어진다.&lt;/p&gt;
&lt;p&gt;같은 지점에서 두 규칙이 서로 다른 방향을 가리키는 것이다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;0⁰&lt;/code&gt;은 일반적인 0제곱보다 조금 더 조심해서 다뤄야 한다.&lt;/p&gt;
&lt;p&gt;미적분에서 어떤 극한이 &lt;code&gt;0⁰&lt;/code&gt;과 같은 형태로 나타나는 경우에는 이를 &lt;strong&gt;부정형&lt;/strong&gt;이라고 부른다.&lt;/p&gt;
&lt;p&gt;반대로 조합론이나 이산수학처럼 어떤 분야에서는 여러 공식이 자연스럽게 유지되도록 &lt;code&gt;0⁰ = 1&lt;/code&gt;로 정의해서 사용하는 경우도 있다.&lt;/p&gt;
&lt;p&gt;결국 &lt;code&gt;0⁰&lt;/code&gt;은 단순히 &amp;quot;1이다&amp;quot; 또는 &amp;quot;0이다&amp;quot;라고 말하기보다 어떤 상황에서 등장한 것인지를 같이 살펴봐야 하는 문제였다.&lt;/p&gt;
&lt;p&gt;이 부분은 0제곱 자체보다 이야기가 훨씬 길어질 것 같아서 따로 정리해봐도 재미있을 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;정리하면서&lt;/h2&gt;
&lt;p&gt;처음에는 정말 단순한 궁금증이었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2⁰ = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;너무 자주 봤던 식이라 당연하게 생각했는데, 막상 왜 1이냐고 물으면 제대로 설명하기 어려웠다.&lt;/p&gt;
&lt;p&gt;처음에는 그냥 수학에서 그렇게 정한 약속이라고 생각했다.&lt;/p&gt;
&lt;p&gt;그런데 직접 하나씩 살펴보니 생각보다 여러 개념이 연결되어 있었다.&lt;/p&gt;
&lt;p&gt;가장 먼저 알게 된 것은 거듭제곱의 규칙이었다.&lt;/p&gt;
&lt;p&gt;같은 수의 거듭제곱끼리 곱하면 지수는 더해진다.&lt;/p&gt;
&lt;p&gt;그래서&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ × 2⁰&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;은 지수만 보면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;3 + 0 = 3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이기 때문에 결과가 다시 &lt;code&gt;2³&lt;/code&gt;이어야 한다.&lt;/p&gt;
&lt;p&gt;그러려면 &lt;code&gt;2⁰&lt;/code&gt;은 1이어야 한다.&lt;/p&gt;
&lt;p&gt;다른 숫자를 넣어도 결과는 같았다.&lt;/p&gt;
&lt;p&gt;그리고 항등원이라는 개념으로 생각해보니 조금 더 명확해졌다.&lt;/p&gt;
&lt;p&gt;덧셈에서 아무런 변화를 만들지 않는 숫자는 0이고,&lt;/p&gt;
&lt;p&gt;곱셈에서 아무런 변화를 만들지 않는 숫자는 1이다.&lt;/p&gt;
&lt;p&gt;거듭제곱은 지수의 덧셈을 실제 값의 곱셈과 연결하기 때문에 지수에서의 0이 실제 값에서는 1과 연결된다.&lt;/p&gt;
&lt;p&gt;빈 곱이라는 개념에서도 같은 결과가 나왔다.&lt;/p&gt;
&lt;p&gt;아무것도 더하지 않으면 0이고,&lt;/p&gt;
&lt;p&gt;아무것도 곱하지 않으면 1이다.&lt;/p&gt;
&lt;p&gt;마지막으로 지수를 하나씩 줄여봐도&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2³ = 8&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2² = 4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2¹ = 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2⁰ = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;처럼 같은 결론에 도착했다.&lt;/p&gt;
&lt;p&gt;결국 여러 방향에서 생각해봤지만 모두 같은 곳을 가리키고 있었다.&lt;/p&gt;
&lt;p&gt;그래서 지금은 이렇게 이해하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0제곱이 1이라는 것은 단순히 외워야 하는 약속이 아니다. 기존의 거듭제곱 규칙을 지수 0까지 자연스럽게 이어가려고 하면 1이라는 값이 필요해진다.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;즉, 정의이기는 하지만 마음대로 정한 정의는 아니다.&lt;/p&gt;
&lt;p&gt;처음에는 &lt;code&gt;0제곱 = 1&lt;/code&gt;이라는 작은 규칙 하나가 궁금했을 뿐인데,&lt;/p&gt;
&lt;p&gt;그 이유를 따라가다 보니 거듭제곱의 규칙부터 항등원, 빈 곱까지 서로 연결되어 있다는 것을 알 수 있었다.&lt;/p&gt;</description>
      <category>CS/수학</category>
      <category>0</category>
      <category>거듭제곱</category>
      <category>로그</category>
      <category>수학</category>
      <category>제곱</category>
      <category>지수</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/74</guid>
      <comments>https://slowgrid.tistory.com/entry/Why-is-0-raised-to-the-power-of-1#entry74comment</comments>
      <pubDate>Sun, 6 Sep 2026 18:54:08 +0900</pubDate>
    </item>
    <item>
      <title>[딥러닝 기초 3] CNN은 이미지를 어떻게 볼까?</title>
      <link>https://slowgrid.tistory.com/entry/Deep-Learning-Basics-3-How-Does-a-CNN-View-Images</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;합성곱(Convolution), 커널(Kernel), 스트라이드(Stride), 특징 맵(Feature Map) 이해하기&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;지금까지는 일반적인 신경망(Neural Network)이 어떻게 학습하는지를 살펴봤다.&lt;/p&gt;
&lt;p&gt;하지만 YOLO가 다루는 입력은 단순한 숫자 몇 개가 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지 = 높이 × 너비 × 채널&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;형태의 공간 구조를 가지고 있다.&lt;/p&gt;
&lt;p&gt;이미지에서는 가까이 있는 픽셀끼리 관계가 있고, 경계와 모양, 질감 등의 공간적인 패턴이 중요하다.&lt;/p&gt;
&lt;p&gt;이런 이미지 처리에서 오랫동안 핵심적으로 사용되어 온 연산이 합성곱(Convolution)이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. CNN&lt;/h2&gt;
&lt;p&gt;CNN은 합성곱 신경망(Convolutional Neural Network)의 약자다.&lt;/p&gt;
&lt;p&gt;합성곱 연산을 이용해 이미지에서 다양한 특징(Feature)을 추출하는 신경망이라고 생각할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지
 ↓
합성곱
 ↓
특징 맵
 ↓
합성곱
 ↓
더 복잡한 특징&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;2. 합성곱(Convolution)은 무엇을 하는가?&lt;/h2&gt;
&lt;p&gt;이미지를 작은 영역으로 나눠서 보면서 특정 패턴에 얼마나 반응하는지 계산한다고 생각하면 이해하기 쉽다.&lt;/p&gt;
&lt;p&gt;예를 들어 이미지 위에 3×3 크기의 작은 필터가 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지

┌──────────────┐
│ ■ ■ ■        │
│ ■ ■ ■        │
│ ■ ■ ■        │
│              │
│              │
└──────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 3×3 영역의 픽셀과 필터의 가중치를 곱하고 더해서 하나의 값을 만든다.&lt;/p&gt;
&lt;p&gt;그리고 필터를 옆으로 이동한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;첫 위치
↓
계산

오른쪽으로 이동
↓
계산

다시 이동
↓
계산&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이미지 전체를 훑은 결과 새로운 숫자 배열이 만들어진다.&lt;/p&gt;
&lt;p&gt;이 결과가 특징 맵이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 커널(Kernel) 또는 필터(Filter)&lt;/h2&gt;
&lt;p&gt;이미지 위를 이동하는 작은 가중치 집합을 커널 또는 필터라고 부른다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;3 × 3 커널&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 말할 때 &lt;code&gt;3 × 3&lt;/code&gt;은 우선 &lt;strong&gt;공간적인 크기&lt;/strong&gt;를 뜻한다.&lt;/p&gt;
&lt;p&gt;하지만 실제 합성곱 커널은 입력 채널 방향까지 함께 본다.&lt;/p&gt;
&lt;p&gt;예를 들어 RGB 입력에서 하나의 일반적인 3×3 필터는 개념적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;3 × 3 × 3(RGB 채널)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;영역의 값을 사용해 하나의 출력 채널 값을 만든다.&lt;/p&gt;
&lt;p&gt;따라서 &amp;quot;3×3 커널에는 가중치가 9개뿐이다&amp;quot;라고 이해하면 실제 CNN 구조를 지나치게 단순화하게 된다.&lt;/p&gt;
&lt;p&gt;중요한 것은 사람이&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이 필터는 사람 머리 전용
이 필터는 다리 전용&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 직접 만들어주는 것이 아니라는 것이다.&lt;/p&gt;
&lt;p&gt;학습 과정에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;순전파
→ 손실
→ 역전파
→ 가중치 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 반복되면서 커널의 가중치 역시 검출에 유용한 방향으로 학습된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 스트라이드(Stride)&lt;/h2&gt;
&lt;p&gt;커널이 이미지 위를 몇 칸씩 이동할지를 스트라이드라고 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stride = 1
→ 한 칸씩 이동

stride = 2
→ 두 칸씩 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;스트라이드가 커지면 출력 특징 맵의 너비와 높이가 줄어들 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 여러 다운샘플링 계층을 지나면서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;640 × 640
 ↓
320 × 320
 ↓
160 × 160
 ↓
80 × 80&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 공간 해상도가 감소한다.&lt;/p&gt;
&lt;p&gt;이 개념은 나중에 YOLO의&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;P3/8
P4/16
P5/32&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 이해할 때 중요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 패딩(Padding)&lt;/h2&gt;
&lt;p&gt;합성곱을 그대로 수행하면 이미지 가장자리 부분을 처리하면서 출력 크기가 줄어들 수 있다.&lt;/p&gt;
&lt;p&gt;패딩은 이미지 주변에 값을 추가해 특징 맵의 공간 크기를 조절하는 방법이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;패딩
→ 가장자리 처리
→ 출력 공간 크기 조절&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도로 우선 이해하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 채널(Channel)&lt;/h2&gt;
&lt;p&gt;이미지에는 공간 크기뿐 아니라 채널도 존재한다.&lt;/p&gt;
&lt;p&gt;일반적인 RGB 이미지라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;빨강
초록
파랑&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3개의 채널이 있다.&lt;/p&gt;
&lt;p&gt;합성곱을 통과하면 채널 수가 증가할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;입력

640 × 640 × 3

↓

합성곱

↓

320 × 320 × 64&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 64개의 채널은 원래 RGB 색상 64개를 의미하는 것이 아니다.&lt;/p&gt;
&lt;p&gt;모델이 학습한 서로 다른 특징 표현을 담는 여러 채널이라고 생각할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 특징 맵(Feature Map)&lt;/h2&gt;
&lt;p&gt;특징 맵은 합성곱 등을 통과해 만들어진 중간 표현이다.&lt;/p&gt;
&lt;p&gt;초기 계층에서는 상대적으로 단순한 구조에 반응하는 특징이 형성될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;경계
방향
밝기 변화
간단한 질감&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여러 계층을 통과하면 이러한 특징을 다시 조합한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;단순 특징
 ↓
부분적인 형태
 ↓
더 복잡한 구조
 ↓
객체 검출에 유용한 특징&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다만&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;특징 맵 1 = 머리
특징 맵 2 = 팔&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 각 채널에 사람이 명확한 이름을 붙일 수 있다고 생각하면 지나친 단순화가 된다.&lt;/p&gt;
&lt;p&gt;실제로는 많은 특징 채널이 함께 작동하면서 예측을 만든다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 왜 특징 맵 크기를 줄일까?&lt;/h2&gt;
&lt;p&gt;특징 맵의 공간 크기를 줄이면 몇 가지 효과가 있다.&lt;/p&gt;
&lt;p&gt;첫째, 연산량이 감소한다.&lt;/p&gt;
&lt;p&gt;둘째, 더 깊은 계층의 하나의 위치가 원본 이미지에서 더 넓은 범위를 바라볼 수 있게 된다.&lt;/p&gt;
&lt;p&gt;이를 수용 영역(Receptive Field) 관점에서 이해할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;초기 계층
→ 작은 지역 패턴

깊은 계층
→ 더 넓은 이미지 영역의 정보&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;객체 전체의 구조를 판단하려면 주변 맥락 역시 중요하기 때문에 이런 계층적인 특징 추출이 필요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. 열화상 이미지(Thermal Image)에서는 무엇이 달라질까?&lt;/h2&gt;
&lt;p&gt;현재 프로젝트의 입력은 일반적인 RGB 사진이 아니라 열화상 이미지다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;RGB
→ 색상 기반 정보가 풍부함

열화상
→ 온도와 복사에너지에 대응하는 신호 기반&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 데이터셋에서는 원본 &lt;code&gt;uint16&lt;/code&gt; 열화상 TIFF에 백분위 정규화(Percentile Normalization) 등을 적용한다.&lt;/p&gt;
&lt;p&gt;CLAHE 실험에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;열화상 이미지
 ↓
백분위 정규화
 ↓
CLAHE
 ↓
YOLO 입력&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 사용한다.&lt;/p&gt;
&lt;p&gt;CLAHE는 국소적인 대비를 조정하므로 사람과 주변 배경의 표현 역시 달라진다.&lt;/p&gt;
&lt;p&gt;따라서 이미지 전처리는 단순히&lt;/p&gt;
&lt;p&gt;사람이 보기 좋은 사진을 만든다가 아니다.&lt;/p&gt;
&lt;p&gt;CNN이 특징을 추출할 입력 분포 자체를 변화시킨다고 볼 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. CNN 학습과 특징&lt;/h2&gt;
&lt;p&gt;중요한 점은 특징 추출기 역시 학습 대상이라는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;커널 가중치
 ↓
특징 맵
 ↓
예측
 ↓
손실
 ↓
역전파
 ↓
커널 가중치 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 모델은&lt;/p&gt;
&lt;p&gt;사람은 머리와 팔이 있으니 머리를 찾는 필터를 만들어라라는 명령을 받지 않는다.&lt;/p&gt;
&lt;p&gt;라벨과 손실을 기반으로 검출 성능에 유용한 특징을 만들어가도록 가중치가 최적화된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11. 합성곱 가중치의 실제 형태(Shape)&lt;/h2&gt;
&lt;p&gt;PyTorch의 일반적인 &lt;code&gt;Conv2d&lt;/code&gt; 가중치 형태는 개념적으로 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[C_out, C_in, K_h, K_w]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;입력 채널 = 3
출력 채널 = 64
커널 = 3 × 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면 가중치는 대략&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[64, 3, 3, 3]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;구조를 가진다.&lt;/p&gt;
&lt;p&gt;즉 64개의 출력 특징 채널 각각이 입력 채널 전체를 보면서 서로 다른 특징을 만든다.&lt;/p&gt;
&lt;p&gt;이 관점에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;640 × 640 × 3
→ Conv
→ 320 × 320 × 64&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;는 단순히 이미지 크기만 줄이는 것이 아니라 &lt;strong&gt;공간 정보와 채널 표현을 동시에 변환하는 연산&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;12. 출력 특징 맵 크기는 어떻게 계산될까?&lt;/h2&gt;
&lt;p&gt;1차원 크기만 단순하게 보면 합성곱 출력 크기는 다음 관계로 계산할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;출력 크기 = floor((입력 크기 + 2×패딩 - 커널 크기) / 스트라이드) + 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;입력 크기   = 640
커널 크기   = 3
스트라이드  = 2
패딩        = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;floor((640 + 2 - 3) / 2) + 1 = 320&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;3×3, stride=2, padding=1&lt;/code&gt; 같은 합성곱을 지나면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;640
→ 320&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로 줄어드는 구조를 자주 볼 수 있다.&lt;/p&gt;
&lt;p&gt;실제 공식에는 팽창률(Dilation)도 포함될 수 있지만, YOLO 구조를 읽기 시작할 때는 이 단순식만 알아도 계층별 특징 해상도를 따라가기 쉬워진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;13. 1×1 합성곱은 왜 사용할까?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;1×1 Conv&lt;/code&gt;는 공간적으로는 한 픽셀 위치만 보지만 채널 방향의 정보를 섞을 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;80 × 80 × 256
 ↓
1 × 1 Conv
 ↓
80 × 80 × 128&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 너비와 높이는 유지하면서 채널 수를 바꿀 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 1×1 Conv는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;채널 투영(Channel Projection)
채널 혼합(Channel Mixing)
연산량 조절&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;에 유용하다.&lt;/p&gt;
&lt;p&gt;반대로 채널별 합성곱(Depthwise Convolution)은 채널별로 공간 합성곱을 분리해 연산량을 줄이는 데 유리하다.&lt;/p&gt;
&lt;p&gt;현대 YOLO의 일부 블록과 헤드를 보면 일반 Conv, 1×1 Conv, Depthwise Conv가 목적에 따라 조합되는 이유가 여기에 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;14. 다운샘플링은 작은 객체 정보도 잃을 수 있다&lt;/h2&gt;
&lt;p&gt;특징 맵 크기를 줄이는 것은 연산량과 수용 영역 측면에서 장점이 있지만 장점만 있는 것은 아니다.&lt;/p&gt;
&lt;p&gt;예를 들어 입력에서 높이가 20px인 사람을 생각해보자.&lt;/p&gt;
&lt;p&gt;스트라이드 8 특징 맵에서는 개념적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;20 / 8 ≈ 2.5 셀&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도의 높이에 대응한다.&lt;/p&gt;
&lt;p&gt;스트라이드 32 특징 맵에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;20 / 32
&amp;lt; 1 셀&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;수준이다.&lt;/p&gt;
&lt;p&gt;물론 실제 CNN 특징은 단순히 픽셀을 셀 하나로 압축하는 방식은 아니고 주변 맥락과 여러 계층 정보가 함께 작동한다.&lt;/p&gt;
&lt;p&gt;하지만 작은 객체일수록 높은 해상도 특징이 중요한 이유를 직관적으로 보여준다.&lt;/p&gt;
&lt;p&gt;이 때문에 다음 글에서 다룰 P3/P4/P5 같은 다중 스케일 특징이 중요하고, 매우 작은 객체 문제에서는 더 높은 해상도의 P2 특징을 고려하기도 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;15. 열화상 전처리(Preprocessing)에서 특히 조심할 점&lt;/h2&gt;
&lt;p&gt;열화상 이미지는 일반 RGB와 달리 원본 값의 물리적인 의미가 중요할 수 있다.&lt;/p&gt;
&lt;p&gt;현재처럼 &lt;code&gt;uint16&lt;/code&gt; 값을 백분위 정규화한 뒤 CLAHE를 적용하면 CNN이 보는 대비는 크게 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;여기서 몇 가지를 구분해야 한다.&lt;/p&gt;
&lt;h3&gt;이미지별 정규화&lt;/h3&gt;
&lt;p&gt;각 이미지를 독립적으로 최솟값-최댓값(Min-Max) 또는 백분위(Percentile) 기준으로 늘리면 같은 픽셀 값이 이미지마다 다른 최종 밝기로 매핑될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;장점
→ 장면별 대비 확보

주의
→ 이미지 사이의 절대적인 온도 척도 정보가 약해질 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;원본 TIFF가 방사측정 온도(Radiometric Temperature)의 의미를 보존하는 데이터라면 이 선택은 더 중요하다.&lt;/p&gt;
&lt;h3&gt;CLAHE&lt;/h3&gt;
&lt;p&gt;CLAHE는 국소 대비(Local Contrast)를 강화하므로 작은 보행자 경계를 강조할 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 동시에&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;센서 잡음(Sensor Noise)
배경의 작은 밝기 변화
고온점(Hot Spot)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;도 강조할 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 &amp;quot;사람이 보기 더 선명하다&amp;quot;보다&lt;/p&gt;
&lt;p&gt;&amp;quot;같은 데이터 분할(Split)과 같은 학습 조건에서 검출 지표(Detection Metric)가 실제로 좋아지는가?&amp;quot;로 판단해야 한다.&lt;/p&gt;
&lt;h3&gt;1채널과 3채널&lt;/h3&gt;
&lt;p&gt;사전 학습된(Pretrained) RGB YOLO는 일반적으로 3채널 입력을 전제로 한다.&lt;/p&gt;
&lt;p&gt;열화상 그레이스케일을 3채널로 복제하면 모델 구조를 바꾸지 않고 사전 학습된 가중치를 사용할 수 있지만, 세 채널에 같은 값이 들어가므로 RGB의 색상 차이는 존재하지 않는다.&lt;/p&gt;
&lt;p&gt;반대로 모델 입력 자체를 1채널로 바꾸면 첫 합성곱의 가중치 형태가 달라져 사전 학습된 가중치를 그대로 재사용하는 방식도 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;이 차이는 미세 조정(Fine-Tuning)을 해석할 때 중요한 조건이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p&gt;CNN을 아주 단순화하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지
 ↓
커널을 이용한 합성곱
 ↓
특징 맵
 ↓
계층 반복
 ↓
복잡한 특징&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;그리고&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;커널
스트라이드
패딩
채널
특징 맵&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;개념을 알게 되면 YOLO 내부 구조를 읽는 것이 조금씩 가능해진다.&lt;/p&gt;
&lt;p&gt;다음 글에서는 드디어 YOLO 안으로 들어가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;백본(Backbone)
넥(Neck)
P3 / P4 / P5
검출 헤드(Detection Head)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 각각 어떤 역할을 하는지 살펴보려고 한다.&lt;/p&gt;</description>
      <category>AI &amp;middot; Data/Deep Learning</category>
      <category>CNN</category>
      <category>딥러닝</category>
      <category>스트라이드</category>
      <category>채널</category>
      <category>커널</category>
      <category>특징 맵</category>
      <category>패딩</category>
      <category>합성곱</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/73</guid>
      <comments>https://slowgrid.tistory.com/entry/Deep-Learning-Basics-3-How-Does-a-CNN-View-Images#entry73comment</comments>
      <pubDate>Sat, 5 Sep 2026 15:57:26 +0900</pubDate>
    </item>
    <item>
      <title>[딥러닝 기초 2] 에포크와 배치는 무엇이 다를까? &amp;mdash; 실제 학습 반복 구조 이해하기</title>
      <link>https://slowgrid.tistory.com/entry/Deep-Learning-Basics-2-What-Is-the-Difference-Between-Epochs-and-Batches-Understanding-the-Structure-of-Actual-Training-Iterations</link>
      <description>&lt;p&gt;지난 글에서는 딥러닝 학습의 기본 흐름을 살펴봤다.&lt;/p&gt;
&lt;p&gt;그런데 실제 데이터셋에는 이미지 한 장만 존재하지 않는다.&lt;/p&gt;
&lt;p&gt;현재 열화상 보행자 검출(Thermal Pedestrian Detection) 프로젝트에는 학습 이미지가 2,110장 있다.&lt;/p&gt;
&lt;p&gt;그렇다고 이 이미지 2,110장을 한꺼번에 GPU에 넣는 것은 아니다.&lt;/p&gt;
&lt;p&gt;여기서 에포크(Epoch)와 배치(Batch)가 등장한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 에포크(Epoch)&lt;/h2&gt;
&lt;p&gt;에포크는 전체 학습 데이터셋을 한 번 학습에 사용하는 단위다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트를 예로 들면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 이미지 = 2,110&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이므로 개념적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;2,110장의 학습 이미지를
한 번 모두 사용

= 1 에포크&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;50 에포크라면 전체 데이터셋을 50번 반복해서 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;전체 데이터셋
 ↓
에포크 1

전체 데이터셋
 ↓
에포크 2

...

전체 데이터셋
 ↓
에포크 50&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 이미지를 여러 번 본다고 해서 매번 완전히 동일한 계산이 이루어지는 것은 아니다.&lt;/p&gt;
&lt;p&gt;에포크가 진행되면서 가중치(Weight)가 바뀌고, 데이터 증강(Data Augmentation)이 적용될 수도 있기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 배치(Batch)&lt;/h2&gt;
&lt;p&gt;2,110장의 이미지를 한꺼번에 GPU 메모리에 올리는 것은 부담이 크다.&lt;/p&gt;
&lt;p&gt;그래서 학습 데이터셋을 작은 그룹으로 나눈다.&lt;/p&gt;
&lt;p&gt;이 묶음이 배치이다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;batch = 16&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 사용한다.&lt;/p&gt;
&lt;p&gt;개념적으로는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지 1 ~ 16
 ↓
순전파
 ↓
손실
 ↓
역전파
 ↓
가중치 갱신

이미지 17 ~ 32
 ↓
순전파
 ↓
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 진행된다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;에포크
→ 데이터셋 전체를 몇 번 학습하는가?

배치
→ 한 번의 계산에 몇 개의 샘플을 함께 사용하는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 구분할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 반복 단계(Iteration 또는 Step)&lt;/h2&gt;
&lt;p&gt;에포크와 배치까지만 알고 있었는데 실제 학습 로그(Training Log)를 이해하려면 하나가 더 필요했다.&lt;/p&gt;
&lt;p&gt;바로 &lt;code&gt;Iteration&lt;/code&gt; 또는 &lt;code&gt;Step&lt;/code&gt;, 즉 반복 단계다.&lt;/p&gt;
&lt;p&gt;보통 하나의 배치를 처리하는 반복을 &lt;code&gt;Iteration&lt;/code&gt; 또는 &lt;code&gt;Batch Step&lt;/code&gt;이라고 부른다.&lt;/p&gt;
&lt;p&gt;다만 &lt;strong&gt;배치 하나를 처리했다고 해서 항상 매개변수(Parameter) 갱신이 즉시 한 번 일어나는 것은 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;기울기 누적(Gradient Accumulation)을 사용하면 여러 배치의 기울기를 누적한 뒤 한 번의 &lt;code&gt;optimizer.step()&lt;/code&gt;을 수행할 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;현재 데이터셋이 2,110장이고 배치 크기(Batch Size)가 16이라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;2110 ÷ 16
≈ 131.9&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이므로 대략 132개의 배치가 필요하다.&lt;/p&gt;
&lt;p&gt;즉 개념적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;약 132회 배치 반복
≈ 1 에포크&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;50 에포크라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;132 × 50
≈ 6,600회 배치 반복&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도가 발생한다.&lt;/p&gt;
&lt;p&gt;하지만 이것을 그대로 &lt;code&gt;6,600번의 매개변수 갱신&lt;/code&gt;이라고 부르면 정확하지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;기울기 누적(Gradient Accumulation)을 사용하면 여러 배치 반복마다 한 번씩 옵티마이저(Optimizer) 갱신이 발생하기 때문이다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;배치 반복 횟수 ≠ 항상 옵티마이저 단계 수&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이며 실제 값은 마지막 배치 처리 방식, 분산 학습(Distributed Training), 워밍업(Warmup), 기울기 누적 설정 등에 따라 달라질 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 배치 크기가 크면 무조건 좋을까?&lt;/h2&gt;
&lt;p&gt;처음에는 GPU 메모리만 충분하면 배치 크기를 크게 하면 좋다고 생각했다.&lt;/p&gt;
&lt;p&gt;하지만 배치 크기도 학습 특성에 영향을 준다.&lt;/p&gt;
&lt;h3&gt;작은 배치&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;장점
→ GPU 메모리 사용량 감소

특징
→ 각 배치가 데이터셋 전체를 덜 대표하므로
   기울기의 변동이 상대적으로 커질 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;큰 배치&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;장점
→ 더 많은 샘플을 이용해 기울기 계산
→ 상대적으로 안정적인 기울기 추정 가능

단점
→ GPU 메모리 사용량 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 배치 크기 역시 모델, 데이터, GPU 메모리 등을 고려해서 결정해야 하는 학습 하이퍼파라미터(Hyperparameter)라고 볼 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 이미지는 GPU에 어떤 형태로 들어갈까?&lt;/h2&gt;
&lt;p&gt;컴퓨터 입장에서 이미지는 결국 숫자 배열이다.&lt;/p&gt;
&lt;p&gt;딥러닝 프레임워크에서는 이러한 다차원 숫자 배열을 텐서(Tensor)라고 부른다.&lt;/p&gt;
&lt;p&gt;PyTorch의 이미지 배치는 흔히&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[B, C, H, W]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;형태인데&lt;/p&gt;
&lt;p&gt;여기에서 BCHW는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;B = 배치(Batch)
C = 채널(Channel)
H = 높이(Height)
W = 너비(Width)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;예를 들어 RGB 이미지 16장을 640×640 크기로 사용한다면 개념적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[16, 3, 640, 640]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;크기의 텐서가 모델에 전달될 수 있다.&lt;/p&gt;
&lt;p&gt;열화상 이미지라 하더라도 실제 YOLO 파이프라인(Pipeline)에 입력하는 형태에 따라 3채널 형태로 변환될 수 있다.&lt;/p&gt;
&lt;p&gt;중요한 점은 YOLO가 이미지를 우리가 보는 사진 그대로 이해하는 것이 아니라 &lt;strong&gt;매우 큰 숫자 배열을 입력받아 계산한다&lt;/strong&gt;는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 왜 GPU를 사용할까?&lt;/h2&gt;
&lt;p&gt;딥러닝에서는 엄청나게 많은 곱셈과 덧셈이 반복된다.&lt;/p&gt;
&lt;p&gt;특히 이미지에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;픽셀 × 필터&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 연산을 공간 전체에 반복한다.&lt;/p&gt;
&lt;p&gt;학습할 때는 여기에&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;순전파 + 손실 + 역전파 + 매개변수 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;까지 수행한다.&lt;/p&gt;
&lt;p&gt;GPU는 이런 대규모 병렬 연산에 적합하다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트의 환경에서는 NVIDIA GPU와 &lt;code&gt;CUDA:0&lt;/code&gt;을 이용해 학습을 수행한다.&lt;/p&gt;
&lt;p&gt;CUDA는 NVIDIA GPU를 범용 병렬 연산에 사용할 수 있도록 제공되는 플랫폼과 프로그래밍 모델(Programming Model)이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. GPU 메모리에는 무엇이 올라갈까?&lt;/h2&gt;
&lt;p&gt;GPU 메모리에는 단순히 이미지 배치만 존재하는 것이 아니다.&lt;/p&gt;
&lt;p&gt;학습할 때는 개념적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;모델 매개변수 + 입력 배치 + 중간 특징 맵 + 기울기 + 옵티마이저 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;등이 함께 메모리를 사용한다.&lt;/p&gt;
&lt;p&gt;그래서 배치 크기를 두 배로 늘렸다고 해서 전체 GPU 메모리가 정확하게 두 배가 되는 것은 아니지만 일반적으로 배치 크기가 커질수록 메모리 사용량도 증가한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 에포크가 많으면 무조건 좋은 모델일까?&lt;/h2&gt;
&lt;p&gt;아니다.&lt;/p&gt;
&lt;p&gt;에포크가 너무 적으면 모델이 충분히 학습되지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;이를 과소적합(Underfitting) 관점에서 생각할 수 있다.&lt;/p&gt;
&lt;p&gt;반대로 지나치게 많은 에포크 동안 학습 데이터셋에 맞추다 보면 새로운 데이터에 대한 성능이 떨어질 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;에포크 증가

초반
→ 패턴 학습

적절한 시점
→ 검증(Validation) 성능 개선

지나친 학습
→ 학습 데이터셋에 과하게 적응할 가능성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 문제는 뒤에서 과적합(Overfitting)을 공부할 때 다시 살펴볼 예정이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;현재 프로젝트에 대입하면&lt;/h2&gt;
&lt;p&gt;현재 열화상 보행자 검출 프로젝트의 주요 설정은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;학습 이미지 = 2,110
에포크       = 50
배치 크기    = 16&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;따라서 개념적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;16장
 ↓
순전파
 ↓
손실
 ↓
역전파
 ↓
기울기 누적 가능
 ↓
필요한 시점에 옵티마이저 갱신

× 약 132회 배치 반복

= 1 에포크

× 50

= 전체 학습&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;python train.py&lt;/code&gt; 한 줄 뒤에서 이런 과정이 수천 번 반복되고 있는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. 배치 크기와 유효 배치 크기는 다를 수 있다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;batch=16&lt;/code&gt;이라고 설정하면 한 번의 순전파/역전파에 16개 샘플이 들어간다는 뜻으로 이해할 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 옵티마이저가 실제로 한 번 갱신될 때 반영되는 샘플 수는 더 클 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 기울기 누적(Gradient Accumulation)을 4번 한다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;마이크로 배치 = 16
누적 횟수 = 4

16 × 4 = 약 64개 샘플&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;의 기울기가 한 번의 옵티마이저 갱신에 반영되는 효과를 낼 수 있다.&lt;/p&gt;
&lt;p&gt;이를 흔히 유효 배치 크기(Effective Batch Size) 관점에서 본다.&lt;/p&gt;
&lt;p&gt;다중 GPU(Multi-GPU)까지 포함하면 개념적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;유효 배치 크기(Effective Batch Size)
≈
GPU당 배치 크기
× GPU 수
× 누적 횟수&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 생각할 수 있다.&lt;/p&gt;
&lt;p&gt;현재 Ultralytics 학습 구현에는 &lt;code&gt;nbs&lt;/code&gt;라는 기준 배치 크기(Nominal Batch Size) 개념이 있고 기본값은 64다.&lt;/p&gt;
&lt;p&gt;실제 Trainer는 배치 크기와 &lt;code&gt;nbs&lt;/code&gt;를 이용해 기울기 누적(Gradient Accumulation)을 사용할 수 있으므로 &lt;code&gt;batch=16&lt;/code&gt;이라고 해서 매 16장마다 반드시 가중치가 갱신된다고 생각하면 안 된다.&lt;/p&gt;
&lt;p&gt;워밍업 동안 누적 횟수가 달라질 수도 있으므로 정확한 옵티마이저 단계 수는 사용 중인 Ultralytics 버전과 실행 로그를 기준으로 확인하는 것이 가장 안전하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. 마지막 배치는 16장이 아닐 수도 있다&lt;/h2&gt;
&lt;p&gt;2,110장을 배치 크기 16으로 나누면 정확히 나누어떨어지지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;16 × 131 = 2,096

남는 샘플 = 14&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 DataLoader 설정에 따라 마지막 배치가 14장처럼 더 작을 수 있다.&lt;/p&gt;
&lt;p&gt;일부 학습 파이프라인은 마지막 불완전 배치를 버릴 수도 있지만, 그렇지 않다면 에포크 안의 마지막 배치 크기는 앞의 배치와 다를 수 있다.&lt;/p&gt;
&lt;p&gt;그래서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;데이터셋 크기 ÷ 배치 크기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;는 반복 단계 수를 이해하는 좋은 근사이지만, 실제 DataLoader 길이는 코드 설정을 확인해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11. DataLoader가 하는 일&lt;/h2&gt;
&lt;p&gt;학습 반복 구조 앞에는 DataLoader가 있다.&lt;/p&gt;
&lt;p&gt;DataLoader는 단순히 파일을 읽는 역할만 하는 것이 아니다.&lt;/p&gt;
&lt;p&gt;개념적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지 / 정답 라벨 경로 읽기
 ↓
샘플 불러오기
 ↓
전처리 / 데이터 증강
 ↓
텐서 변환
 ↓
배치로 묶기
 ↓
GPU에 전달&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;과정을 준비한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;workers&lt;/code&gt; 값을 늘리면 여러 CPU 워커가 다음 배치를 미리 준비해 GPU가 데이터를 기다리는 시간을 줄일 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 너무 많은 워커는 CPU/RAM 사용량을 늘리고, 저장장치 속도가 느리면 오히려 병목이 될 수도 있다.&lt;/p&gt;
&lt;p&gt;즉 GPU 사용률이 낮다고 해서 항상 모델 연산이 느린 것은 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;디스크 입출력
CPU 데이터 증강
DataLoader 워커
GPU 연산&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중 어디가 병목인지 구분해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;12. 데이터 순서는 왜 섞어야 할까? (Shuffle)&lt;/h2&gt;
&lt;p&gt;학습 데이터셋은 보통 에포크마다 샘플 순서를 섞는다.&lt;/p&gt;
&lt;p&gt;만약 데이터가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;맑은 장면 100장
→ 비슷한 장면 100장
→ 또 다른 장면 100장&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 정렬되어 있다면 순서를 섞지 않을 경우 연속 배치가 지나치게 비슷해질 수 있다.&lt;/p&gt;
&lt;p&gt;데이터 순서를 섞는 Shuffle을 사용하면 각 배치가 다양한 샘플을 포함할 가능성이 커진다.&lt;/p&gt;
&lt;p&gt;다만 영상에서 추출한 연속 프레임(Frame)이 데이터셋에 많다면 순서를 섞는 것만으로 데이터 누수(Data Leakage)가 해결되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;학습/검증 데이터 분할(Train/Validation Split) 자체를 장면(Scene) 또는 시퀀스(Sequence) 단위로 나눠야 유사 프레임이 양쪽에 섞이는 문제를 막을 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;13. AMP는 왜 GPU 메모리를 줄일 수 있을까?&lt;/h2&gt;
&lt;p&gt;최근 딥러닝 학습에서는 혼합 정밀도(Mixed Precision)를 자주 사용한다.&lt;/p&gt;
&lt;p&gt;예를 들어 일부 연산을 FP16 또는 BF16으로 수행하면 FP32만 사용할 때보다&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;텐서 메모리 사용량 감소
+
지원 GPU에서 연산 속도 향상&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;효과를 얻을 수 있다.&lt;/p&gt;
&lt;p&gt;Ultralytics의 &lt;code&gt;amp&lt;/code&gt; 옵션도 이 목적과 관련된다.&lt;/p&gt;
&lt;p&gt;다만 정밀도(Precision)가 낮아지면 수치적으로 매우 작은 기울기가 표현되지 못할 수 있어 프레임워크는 손실 스케일링(Loss Scaling) 같은 기법을 함께 사용한다.&lt;/p&gt;
&lt;p&gt;즉 GPU 메모리를 이해할 때는 배치 크기뿐 아니라&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;입력 크기
모델 크기
특징 맵
기울기
옵티마이저 상태
정밀도(FP32 / FP16 / BF16)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;도 함께 봐야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;글을 마치며&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;에포크(Epoch)
→ 데이터셋 전체를 한 번 사용하는 단위

배치(Batch)
→ 한 번에 처리하는 샘플 묶음

반복 단계(Step / Iteration)
→ 배치 하나를 처리하는 학습 단위&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 이미지 배치는 텐서가 되어 GPU에서 대규모 병렬 계산에 사용된다.&lt;/p&gt;
&lt;p&gt;그런데 아직 중요한 질문이 남는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이미지라는 숫자 배열에서 모델은 어떻게 경계, 모양, 사람 같은 특징을 찾아낼까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;다음에는 CNN과 합성곱(Convolution)을 살펴보려고 한다.&lt;/p&gt;</description>
      <category>AI &amp;middot; Data/Deep Learning</category>
      <category>딥러닝</category>
      <category>배치</category>
      <category>에포크</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/72</guid>
      <comments>https://slowgrid.tistory.com/entry/Deep-Learning-Basics-2-What-Is-the-Difference-Between-Epochs-and-Batches-Understanding-the-Structure-of-Actual-Training-Iterations#entry72comment</comments>
      <pubDate>Fri, 4 Sep 2026 23:18:14 +0900</pubDate>
    </item>
    <item>
      <title>OpenAI, GPT-6 Astra 공개...컴퓨터 직접 다루는 차세대 AI 등장</title>
      <link>https://slowgrid.tistory.com/entry/OpenAI-GPT-6-Astra-ReleaseNext-Generation-AI-That-Direct-Works-with-Computers-Emerges</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI가 차세대 인공지능 모델 &lt;b&gt;GPT-6 Astra&lt;/b&gt;를 공개했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-6 Astra의 핵심은 단순히 질문에 더 정확하게 답하거나 코드를 더 잘 작성하는 데 있지 않습니다. OpenAI가 이번 모델에서 전면에 내세운 것은 &lt;b&gt;컴퓨터 사용(Computer Use)​&lt;/b&gt;과 &lt;b&gt;복잡한 업무를 처음부터 끝까지 수행하는 능력&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 9월 3일 공개된 Astra는 컴퓨터 사용과 웹 탐색, 소프트웨어 개발, 사이버보안, 과학 연구, 전문 업무 등을 하나의 모델에서 처리하는 것을 목표로 합니다. OpenAI는 제한된 조직부터 Astra 배포를 시작했으며, 이후 ChatGPT Plus&amp;middot;Pro&amp;middot;Business&amp;middot;Enterprise와 API 등으로 순차 확대할 예정이라고 밝혔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 'GPT의 숫자가 6으로 올라갔다'고 보기보다는, &lt;b&gt;생성형 AI의 역할이 답변 생성에서 실제 업무 수행으로 이동하고 있다는 점&lt;/b&gt;을 보여주는 모델에 가깝습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;질문에 답하는 AI에서 &amp;lsquo;일을 끝내는 AI&amp;rsquo;로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 생성형 AI의 대표적인 사용 방식은 질문에 답하거나 글을 쓰고, 코드를 만들어 주는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-6 Astra가 지향하는 방식은 조금 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 최종적으로 원하는 결과를 알려주면 AI가 필요한 과정을 나누고, 여러 도구와 프로그램을 사용하면서 실제 결과물이 완성될 때까지 작업하는 &lt;b&gt;엔드투엔드(end-to-end) 업무 수행​&lt;/b&gt;에 초점이 맞춰져 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI가 공개한 예시를 보면 범위가 상당히 넓습니다. Astra는 온라인 양식을 작성하거나 CRM의 고객 정보를 업데이트하고, 캘린더를 정리하는 작업을 수행할 수 있습니다. 웹에서 자료를 조사한 뒤 이메일이나 문서 편집기에 요약문을 작성하거나, 데이터를 분석해 그래프를 만들고 웹사이트를 제작한 뒤 화면을 직접 조작하며 기능이 제대로 작동하는지 QA 테스트하는 작업도 가능합니다. 소프트웨어 설치와 테스트, 화면에 나타난 문제를 보고 해결하는 작업까지 컴퓨터 사용 영역에 포함됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 사용자가 &amp;ldquo;어떻게 해야 하나요?&amp;rdquo;라고 물었을 때 방법을 설명하는 것에서 끝나는 것이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;이 작업을 완료해 줘&amp;rdquo;라고 지시하면 AI가 직접 도구를 사용해 결과를 만들어내는 방향​&lt;/b&gt;으로 발전하고 있는 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPT-6 Astra에서 &amp;lsquo;컴퓨터 사용&amp;rsquo;이 중요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 에이전트가 웹사이트나 프로그램을 사용하는 것 자체가 완전히 새로운 개념은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI는 2025년 이미 Computer-Using Agent(CUA)와 Operator를 공개했습니다. 당시 CUA는 화면을 스크린샷으로 인식하고 가상의 마우스와 키보드를 이용해 버튼을 클릭하거나 글자를 입력하고 페이지를 스크롤하는 방식을 사용했습니다. 별도의 전용 API가 없어도 사람이 사용하는 GUI를 그대로 이용할 수 있다는 것이 특징이었습니다. 이후 Operator 기능은 ChatGPT의 에이전트 기능으로 통합됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Astra에서는 이 컴퓨터 사용 능력이 한층 더 핵심적인 모델 능력으로 들어왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI가 공개한 OSWorld 2.0 평가에서 Astra는 &lt;b&gt;72.6%&lt;/b&gt;를 기록해 GPT-5.6 Sol의 &lt;b&gt;65.7%&lt;/b&gt;를 앞섰습니다. 화면을 보고 필요한 위치를 판단하는 ScreenSpot-Pro에서는 &lt;b&gt;92.7% 대 76.9%&lt;/b&gt;, 전문적인 컴퓨터 업무를 평가하는 AutomationBench에서는 &lt;b&gt;41.4% 대 18.1%​&lt;/b&gt;를 기록했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속도도 중요한 변화입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI의 OSWorld 지연 시간 시뮬레이션에서 Astra는 작업당 약 40분이 걸렸고 GPT-5.6 Sol은 약 75분이 걸렸습니다. OpenAI는 이를 Astra가 약 &lt;b&gt;47% 적은 시간으로 더 높은 점수&lt;/b&gt;를 기록한 결과라고 설명합니다. Codex의 컴퓨터 사용 환경까지 함께 개선하면서 Mind2Web 평가에서는 기존 GPT-5.6 Sol 환경보다 작업 완료 속도가 약 &lt;b&gt;1.9배&lt;/b&gt; 빨라졌다고 밝혔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 에이전트가 실제 업무에서 사용되려면 단순히 &amp;ldquo;할 수 있다&amp;rdquo;는 것만으로는 부족합니다. 사람이 직접 처리하는 것보다 지나치게 오래 걸린다면 자동화의 의미가 줄어들기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Astra에서는 &lt;b&gt;정확도뿐 아니라 작업 완료 속도 자체가 중요한 경쟁 요소로 등장했다&lt;/b&gt;고 볼 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문서/엑셀/프레젠테이션도 &amp;lsquo;초안&amp;rsquo;이 아니라 결과물 중심으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-6 Astra에서 눈여겨볼 또 다른 부분은 전문적인 업무 결과물 제작 능력입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI는 Astra가 문서와 스프레드시트, 프레젠테이션을 만드는 것뿐 아니라 &lt;b&gt;기업이나 사용자가 기존에 사용하던 템플릿과 작성 방식에 맞춰 결과물을 만드는 능력&lt;/b&gt;을 강화했다고 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 회사에서 사용하던 보고서 양식이나 발표 자료의 구조를 참고해 같은 형식으로 문서를 작성하거나, 기존 자료의 문체와 시각적인 스타일을 유지하면서 새로운 결과물을 만들 수 있다는 것입니다. 필요한 정보만 선별해 결과물에 반영하고 불필요한 내용을 반복하는 것도 줄이도록 훈련됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 변화는 AI 업무 자동화에서 상당히 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 회사 업무에서는 내용을 생성하는 것보다 &lt;b&gt;회사 양식에 맞추고, 파일을 수정하고, 여러 프로그램을 오가며 최종 제출 가능한 상태로 정리하는 과정​&lt;/b&gt;에 더 많은 시간이 들어가는 경우가 많기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Astra가 목표로 하는 자동화는 바로 이 마지막 단계까지 포함합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;벤치마크 성능은 얼마나 올라갔나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI가 공개한 주요 평가 결과를 GPT-5.6 Sol과 비교하면 다음과 같습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;평가&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;GPT-6 Astra&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;GPT-5.6 Sol&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OSWorld 2.0&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;72.6%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;65.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScreenSpot-Pro&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;92.7%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;76.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutomationBench&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;41.4%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;18.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-Bench Science 0.1&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;64.6%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;22.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FrontierMath Tier 4 (v2)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;97.6%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;83.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-Bench 4.0&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;57.9%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;37.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BenchCAD&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;95.9%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;83.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HealthBench Professional&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;63.4%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;60.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARC-AGI-3&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;99.9%&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;7.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 AutomationBench와 Terminal-Bench Science에서 격차가 크고, 추상적 환경에서 학습과 문제 해결 능력을 평가하는 ARC-AGI-3에서는 &lt;b&gt;7.8%에서 99.9%&lt;/b&gt;라는 극단적인 차이가 나타났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 수치는 그대로 &amp;ldquo;GPT-6가 GPT-5.6보다 12배 똑똑하다&amp;rdquo;는 식으로 해석해서는 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI는 ARC-AGI-3 평가에서 Astra에 자사의 Responses API 하네스를 사용했으며, 실제 사용 환경에 더 가깝게 만들기 위해 두 가지 설정을 변경했다고 명시했습니다. 이 설정이 ARC-AGI-3만을 겨냥한 것은 아니라고 설명하지만, 평가 환경이 결과에 영향을 미칠 수 있다는 점은 함께 볼 필요가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 OpenAI가 공개한 표의 점수는 각 모델에서 여러 추론 강도를 시험한 뒤 얻은 최대 점수이며, 연구 환경이나 API에서 얻은 결과가 실제 ChatGPT에서 동일하게 나타난다고 보장되는 것도 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 벤치마크의 의미는 숫자 하나보다는 &lt;b&gt;Astra가 장시간&amp;middot;다단계 작업과 컴퓨터를 사용하는 에이전트 작업에서 큰 폭으로 발전했다는 흐름&lt;/b&gt;에 두는 것이 적절합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;긴 작업을 이어가는 방식도 달라진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 업무를 AI에게 맡길 때 발생하는 문제 중 하나는 작업이 길어질수록 이전 내용을 잊어버린다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 모델은 컨텍스트가 길어지면 과거 작업 내용을 압축해 요약하는 방식을 많이 사용했습니다. 하지만 이 과정에서 &amp;ldquo;어떤 방법을 시도했다가 왜 실패했는지&amp;rdquo;, &amp;ldquo;이전에 확인했던 테스트 결과가 무엇인지&amp;rdquo; 같은 세부 정보가 빠질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI는 Astra를 사용하는 Codex에서 과거 작업 내용을 메모 형태로 보존하고, 이전 컨텍스트도 다시 검색할 수 있는 실험적인 방식을 도입하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 코드베이스 수정이나 장시간 디버깅처럼 수십 단계 이상의 작업을 이어갈 때 AI가 이전 작업 맥락을 유지하기 위한 변화입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사이버보안 능력은 처음으로 &amp;lsquo;Critical&amp;rsquo; 단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Astra 발표에서 성능 못지않게 중요하게 다뤄진 부분이 사이버보안입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI는 GPT-6 Astra가 자사의 Preparedness Framework에서 사이버보안 능력 &lt;b&gt;Critical&lt;/b&gt; 기준에 도달한 첫 번째 모델이라고 밝혔습니다. 적절한 도구와 접근 권한이 제공되는 경우 사람이 각 단계를 지시하지 않아도 알려지지 않은 보안 취약점을 찾고 공격 방법을 개발할 수 있는 수준이라는 설명입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Astra는 ExploitBench에서 &lt;b&gt;100%&lt;/b&gt;를 기록했고 GPT-5.6 Sol은 78.5%였습니다. 최근 취약점을 사용해 별도로 구성한 평가 과정에서는 기존에 알려지지 않았던 제로데이 취약점 두 개를 찾아 실제로 이용하기도 했으며, OpenAI는 해당 취약점을 관련 개발사에 전달하고 있다고 밝혔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 능력은 보안 연구자 입장에서는 취약점을 더 빠르게 찾아 수정할 수 있는 강력한 도구가 될 수 있지만, 반대로 공격에 악용될 가능성도 함께 커졌다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 현재 공개되는 Astra에는 고급 공격 코드 작성 등 일부 사이버보안 작업을 제한하는 보호 장치가 적용됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 강력해진 만큼 &amp;lsquo;시키지 않은 일을 하지 않는 능력&amp;rsquo;도 중요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 직접 컴퓨터를 사용할 수 있게 되면 새로운 문제가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 판단으로 파일을 삭제하거나, 허용되지 않은 시스템에 접근하거나, 사용자가 의도하지 않은 결제를 실행하는 것처럼 &lt;b&gt;답변 오류가 실제 행동의 오류로 이어질 수 있기 때문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Astra 발표에서 OpenAI가 반복적으로 강조한 또 하나의 키워드가 &lt;b&gt;Alignment, 즉 사용자 의도에 맞춰 행동하는 능력​&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI는 어려운 업무를 주었을 때 모델이 목표를 달성하기 위해 허가된 범위를 넘어가는지를 확인하는 평가도 진행했습니다. 별도의 운영 안전장치를 제외한 조건에서 GPT-5.6 Sol은 불가능한 사이버 작업을 해결하려고 하면서 허가된 목표 범위를 넘어선 경우가 있었던 반면 Astra에서는 해당 평가에서 이를 관찰하지 않았다고 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Astra에는 실제 서비스 환경에서도 모델의 추론과 행동을 확인해 허가되지 않은 행동 가능성이 감지되면 작업을 중단하거나 사용자의 확인을 요구하는 추가 모니터링이 적용됩니다. 이 때문에 정상적인 작업도 경우에 따라 일시 중지될 수 있다고 OpenAI는 설명하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 컴퓨터를 직접 사용하는 AI에서는 &lt;b&gt;얼마나 많은 일을 할 수 있느냐만큼, 어디까지 해야 하는지를 정확하게 판단하는 능력도 핵심 성능​&lt;/b&gt;이 된 셈입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPT-6 Astra API 사양과 가격&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자용 GPT-6 Astra의 주요 사양은 다음과 같습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;GPT-6 Astra&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;모델 ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpt-6-astra&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨텍스트 윈도우&lt;/td&gt;
&lt;td&gt;1,050,000 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대 출력&lt;/td&gt;
&lt;td&gt;128,000 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지식 기준일&lt;/td&gt;
&lt;td&gt;2026년 4월 30일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;입력 가격&lt;/td&gt;
&lt;td&gt;100만 토큰당 $10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;캐시된 입력&lt;/td&gt;
&lt;td&gt;100만 토큰당 $1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;캐시 쓰기&lt;/td&gt;
&lt;td&gt;100만 토큰당 $12.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출력 가격&lt;/td&gt;
&lt;td&gt;100만 토큰당 $50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Responses API에서는 웹 검색, 파일 검색, 이미지 생성, Code Interpreter, Hosted Shell, Apply Patch, Computer Use, MCP, Skills 등의 도구를 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 105만 토큰 규모의 컨텍스트는 대규모 문서나 코드베이스, 장시간 업무를 한 번의 작업 흐름에서 다루기 위한 기반으로 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 입력이 27만2천 토큰을 넘어가면 별도의 장문 컨텍스트 요금 체계가 적용되므로 API에서 대량의 자료를 사용하는 경우에는 단순한 토큰 단가뿐 아니라 실제 작업당 비용을 함께 계산할 필요가 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 바로 모든 사용자가 사용할 수 있는 것은 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 9월 4일 기준 GPT-6 Astra는 발표됐지만 모든 사용자에게 한꺼번에 공개된 상태는 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI는 먼저 제한된 조직을 대상으로 배포하고 있으며, 이후 며칠에 걸쳐 ChatGPT Plus, Pro, Business, Enterprise 사용자와 OpenAI API 등으로 접근 범위를 확대할 예정이라고 안내하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 계정에서 아직 Astra가 보이지 않는다고 해서 이상한 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 출시는 &amp;lsquo;발표와 동시에 모든 사람이 사용하는 전면 출시&amp;rsquo;라기보다는 &lt;b&gt;발표 후 단계적으로 접근 권한을 확대하는 롤아웃 방식&lt;/b&gt;에 가깝습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결국 중요한 것은 GPT-6라는 이름이 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-6 Astra 발표에서 가장 주목해야 할 부분은 단순히 GPT의 세대가 하나 올라갔다는 사실이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 담당하는 업무 범위 자체가 달라지고 있다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기의 생성형 AI가&lt;br /&gt;&lt;b&gt;질문 &amp;rarr; 답변&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방식이었다면, 앞으로의 AI 에이전트는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목표 전달 &amp;rarr; 계획 &amp;rarr; 정보 수집 &amp;rarr; 프로그램 조작 &amp;rarr; 코드 실행 &amp;rarr; 결과 확인 &amp;rarr; 수정 &amp;rarr; 최종 결과물 제작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과 같은 긴 작업 흐름을 수행하는 방향으로 발전하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-6 Astra는 이런 변화가 상당히 구체적인 단계까지 왔음을 보여주는 모델입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹사이트를 찾아 정보를 읽는 것을 넘어 직접 버튼을 누르고, 전문 소프트웨어를 사용하고, 코드를 실행하고, 문서와 스프레드시트&amp;middot;프레젠테이션을 완성하고, 만들어진 결과가 제대로 동작하는지 다시 확인하는 것까지 하나의 AI가 담당하는 방향입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 Astra가 사이버보안 분야에서 처음으로 Critical 능력에 도달했다는 사실은 AI의 능력이 높아질수록 &lt;b&gt;권한 관리와 안전장치, 행동 모니터링의 중요성 역시 함께 커진다​&lt;/b&gt;는 점을 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 앞으로의 AI 경쟁은 단순히&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;누가 더 좋은 답변을 만드는가&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;누가 실제 업무를 더 정확하고 빠르고 안전하게 끝낼 수 있는가&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 겨루는 방향으로 이동할 가능성이 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-6 Astra는 그 변화를 보여주는 가장 분명한 사례 중 하나라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenAI &amp;mdash; &lt;i&gt;GPT-6 Astra: A new generation of intelligence&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;OpenAI &amp;mdash; &lt;i&gt;Safety overview: GPT-6 Astra&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;OpenAI &amp;mdash; &lt;i&gt;Path to Astra: critical capabilities and frontier safeguards&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;OpenAI Developers &amp;mdash; &lt;i&gt;GPT-6 Astra Model&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;OpenAI Developers &amp;mdash; &lt;i&gt;Model guidance&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;OpenAI Help Center &amp;mdash; &lt;i&gt;ChatGPT Release Notes&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성 기준일: &lt;b&gt;2026년 9월 4일&lt;/b&gt;&lt;/p&gt;</description>
      <category>AI News</category>
      <category>AGI</category>
      <category>Astra</category>
      <category>GPT-6</category>
      <category>openai</category>
      <category>지피티</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/71</guid>
      <comments>https://slowgrid.tistory.com/entry/OpenAI-GPT-6-Astra-ReleaseNext-Generation-AI-That-Direct-Works-with-Computers-Emerges#entry71comment</comments>
      <pubDate>Fri, 4 Sep 2026 12:44:56 +0900</pubDate>
    </item>
    <item>
      <title>Galaxy Book4 Pro 360의 화면은 대체 어디로 연결되어 있을까?</title>
      <link>https://slowgrid.tistory.com/entry/Where-on-earth-is-the-Galaxy-Book-4-Pro-360s-screen-connected</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;작은 디스플레이 케이블 하나에서 시작한 eDP와 고속 디지털 통신 이야기&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Galaxy Book4 Pro 360을 사용하다가 내부 구조를 자세히 들여다볼 일이 생겼다.&lt;/p&gt;
&lt;p&gt;메인보드와 디스플레이는 화면을 띄워야하기에 분명 서로 연결되어 있어야 한다. GPU가 만든 영상 데이터가 결국 상판의 OLED 패널까지 전달되어야 하기 때문이다. 그런데 처음 내부를 봤을 때는 조금 이상했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;생각보다 메인보드와 디스플레이 사이에 눈에 띄는 연결부가 거의 없어 보였다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그래서 가장 먼저 든 생각은 단순했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;“메인보드와 디스플레이가 이렇게 떨어져 있는데, 화면 데이터는 대체 어디로 가는 거지?”&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;메인보드 주변과 힌지 쪽을 다시 자세히 살펴보니, 처음에는 접지나 실드 구조물의 일부처럼 보였던 곳 근처로 &lt;strong&gt;아주 얇은 디스플레이 케이블&lt;/strong&gt;이 지나가고 있었다.&lt;/p&gt;
&lt;p&gt;그제야 첫 번째 의문은 풀렸다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;“아, 화면은 이쪽 케이블을 통해 연결되어 있구나.”&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 곧바로 두 번째 의문이 생겼다.&lt;/p&gt;
&lt;p&gt;케이블이 너무 작고 얇았다.&lt;/p&gt;
&lt;p&gt;Galaxy Book4 Pro 360 16인치 모델의 디스플레이는 &lt;strong&gt;2880 × 1800 해상도와 최대 120Hz Adaptive Refresh Rate&lt;/strong&gt;를 지원한다. 한 화면에 약 518만 개의 픽셀이 있고, 최대 주사율에서는 그 화면을 초당 120번 갱신한다.&lt;/p&gt;
&lt;p&gt;단순히 RGB 8bit만 가정해도 눈에 보이는 픽셀 데이터만 약 &lt;strong&gt;14.93Gbps&lt;/strong&gt;에 이른다.&lt;/p&gt;
&lt;p&gt;그런데 이 많은 정보가, 눈으로 보기에는 몇 가닥의 얇은 패턴이 들어 있는 작은 케이블 하나를 통해 이동한다.&lt;/p&gt;
&lt;p&gt;그래서 이 글의 진짜 질문은 여기서 시작됐다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;“이렇게 작은 선이 어떻게 2880 × 1800, 120Hz 화면을 띄울 수 있는 걸까?”&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;처음에는 단순한 분해 과정에서 생긴 궁금증이었지만, 이 질문을 따라가다 보면 &lt;strong&gt;eDP(Embedded DisplayPort), 고속 직렬 통신, 차동 신호, 전송선 이론, 신호 무결성, Link Training, TCON과 패널 드라이버&lt;/strong&gt;까지 이어진다.&lt;/p&gt;
&lt;p&gt;이 글에서는 Galaxy Book4 Pro 360을 보며 생겼던 이 의문을 출발점으로, 작은 디스플레이 연결부 안에서 실제로 어떤 공학이 동작하는지 하나씩 살펴보려고 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 518만 개의 픽셀을 518만 개의 선으로 연결하지 않는다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDULL8/dJMcabli96O/YYTCsccaDIxu2srKiLCoQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDULL8/dJMcabli96O/YYTCsccaDIxu2srKiLCoQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDULL8/dJMcabli96O/YYTCsccaDIxu2srKiLCoQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDULL8%2FdJMcabli96O%2FYYTCsccaDIxu2srKiLCoQK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Figure 1. 메인보드와 디스플레이 사이의 고속 디지털 링크를 단순화한 개념도&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가장 먼저 버려야 할 생각은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;“픽셀이 많으니 그만큼 많은 전선이 필요하지 않을까?”&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 직관이다.&lt;/p&gt;
&lt;p&gt;실제 시스템은 완전히 다르게 동작한다.&lt;/p&gt;
&lt;p&gt;메인보드의 GPU가 518만 개의 픽셀 각각에 직접 전선을 연결하는 것이 아니라, 한 프레임의 영상 정보를 &lt;strong&gt;고속 디지털 데이터 스트림&lt;/strong&gt;으로 만든 다음 소수의 초고속 통신 채널을 통해 디스플레이로 전송한다.&lt;/p&gt;
&lt;p&gt;네트워크와 비슷하게 생각하면 이해하기 쉽다.&lt;/p&gt;
&lt;p&gt;인터넷으로 수십 GB의 파일을 보낸다고 해서 파일 안의 바이트 수만큼 전선이 필요한 것은 아니다. 하나의 통신 채널에서 시간에 따라 데이터를 빠르게 연속 전송하면 된다.&lt;/p&gt;
&lt;p&gt;디스플레이도 같은 원리다.&lt;/p&gt;
&lt;p&gt;노트북 내부에서는 이러한 역할을 하는 대표적인 인터페이스 중 하나가 &lt;strong&gt;eDP, Embedded DisplayPort&lt;/strong&gt;다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. eDP는 메인보드와 패널 사이의 초고속 데이터 링크&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lXBt4/dJMcabyHFb6/1XcCc377zKmWB1H9e2tmU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lXBt4/dJMcabyHFb6/1XcCc377zKmWB1H9e2tmU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lXBt4/dJMcabyHFb6/1XcCc377zKmWB1H9e2tmU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlXBt4%2FdJMcabyHFb6%2F1XcCc377zKmWB1H9e2tmU1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Figure 2. eDP Source에서 패널의 픽셀까지 이어지는 논리적 데이터 경로. 실제 Galaxy Book4 Pro 360 회로도나 핀아웃을 의미하는 것은 아님.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;eDP는 &lt;strong&gt;Embedded DisplayPort&lt;/strong&gt;의 약자다.&lt;/p&gt;
&lt;p&gt;외부 모니터 연결용으로 익숙한 DisplayPort 기술을 노트북, 태블릿, 일체형 PC와 같은 내장 디스플레이 환경에 맞춰 최적화한 인터페이스라고 이해하면 된다.&lt;/p&gt;
&lt;p&gt;구조를 아주 단순하게 표현하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CPU / GPU
   │
   ▼
eDP Source / Transmitter
   │
   │  Main Link
   │  AUX Channel
   │  Control
   ▼
Display Cable
   │
   ▼
eDP Receiver
   │
   ▼
Timing / Display Control Logic
   │
   ▼
Panel Drivers
   │
   ▼
OLED Pixel Matrix&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기에서 주인공은 &lt;strong&gt;Main Link&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;실제 영상 데이터의 대부분이 Main Link를 통해 전송된다.&lt;/p&gt;
&lt;p&gt;그리고 별도로 &lt;strong&gt;AUX Channel&lt;/strong&gt;이라는 저속 양방향 통신 채널이 존재한다. AUX는 화면을 직접 그리기 위한 엄청난 양의 픽셀 데이터를 운반하는 채널이라기보다, GPU와 디스플레이가 서로 상태와 능력을 확인하고 링크를 관리하는 제어 채널에 가깝다.&lt;/p&gt;
&lt;p&gt;쉽게 표현하면,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Main Link = 화물을 실어 나르는 고속도로&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AUX Channel = 고속도로를 관리하는 관제망&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이라고 생각할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 얇은 케이블 안에는 실제로 여러 개의 전송선이 숨어 있다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GpavV/dJMcacRRLST/Ut9b6M6D55n74sKCoXa571/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GpavV/dJMcacRRLST/Ut9b6M6D55n74sKCoXa571/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GpavV/dJMcacRRLST/Ut9b6M6D55n74sKCoXa571/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGpavV%2FdJMcacRRLST%2FUt9b6M6D55n74sKCoXa571%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Figure 3. 고속 디스플레이 케이블을 FPC 형태로 단순화해 표현한 개념도. 실제 Galaxy Book4 Pro 360의 핀아웃이 아님.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;겉으로 보면 하나의 얇은 케이블처럼 보이지만 내부를 전기적으로 생각하면 여러 신호가 동시에 지나간다.&lt;/p&gt;
&lt;p&gt;eDP Main Link는 일반적으로 여러 개의 &lt;strong&gt;Lane&lt;/strong&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 4 Lane 구성이라면 개념적으로 다음과 같은 신호가 존재할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Lane 0 + / -
Lane 1 + / -
Lane 2 + / -
Lane 3 + / -

AUX + / -
Control
Power
Ground
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 눈여겨볼 부분은 각각의 Lane 뒤에 붙어 있는 &lt;strong&gt;&lt;code&gt;+&lt;/code&gt;와 &lt;code&gt;-&lt;/code&gt;&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;한 Lane을 한 가닥의 전선으로 만드는 것이 아니라 &lt;strong&gt;두 개의 선을 하나의 쌍(pair)으로 사용한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이를 &lt;strong&gt;Differential Pair&lt;/strong&gt;, 즉 차동 신호쌍이라고 한다.&lt;/p&gt;
&lt;p&gt;그리고 이 얇은 패턴 한 쌍이 초당 수십억 비트 수준의 데이터를 전달할 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 DisplayPort의 HBR3 전송률은 Lane 하나당 &lt;strong&gt;8.1Gbps&lt;/strong&gt;다. 4 Lane의 raw link rate는 32.4Gbps가 되며, HBR3의 8b/10b line encoding을 고려한 payload 대역폭은 25.92Gbps다.&lt;/p&gt;
&lt;p&gt;다만 이 수치가 &lt;strong&gt;Galaxy Book4 Pro 360이 실제로 HBR3 4-Lane으로 동작한다는 뜻은 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Figure 3의 HBR3 표시는 작은 고속 케이블이 어느 정도의 전송률을 처리할 수 있는지를 설명하기 위한 &lt;strong&gt;공학적 예시&lt;/strong&gt;이다. 실제 제품의 Lane 수, Link Rate, 핀아웃, DSC 사용 여부 등은 패널과 시스템 설계에 따라 달라질 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 실제로 2880 × 1800 120Hz에는 얼마나 많은 데이터가 필요할까?&lt;/h2&gt;
&lt;p&gt;Galaxy Book4 Pro 360의 해상도는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;2880 × 1800
= 5,184,000 pixels&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;약 &lt;strong&gt;518만 픽셀&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;120Hz라면 초당 처리하는 Active Pixel 수만 계산해도 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;5,184,000 × 120
= 622,080,000 pixels/s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;약 &lt;strong&gt;6억 2,208만 픽셀/초&lt;/strong&gt;가 된다.&lt;/p&gt;
&lt;p&gt;RGB 각 채널을 8bit로 표현한다고 단순화하면 한 픽셀은 24bit이므로,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;622,080,000 × 24
= 14,929,920,000 bit/s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;약 &lt;strong&gt;14.93Gbps&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;RGB 각 채널을 10bit로 계산하면 픽셀당 30bit이므로,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;622,080,000 × 30
= 18.66Gbps&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;여기서 14.93Gbps와 18.66Gbps는 &lt;strong&gt;실제 화면에 나타나는 Active Pixel 데이터만 단순 계산한 값&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;실제 링크에서는 영상 타이밍, blanking, 전송 방식의 오버헤드 등을 추가로 고려해야 한다.&lt;/p&gt;
&lt;p&gt;그럼에도 우리가 육안으로 보는 몇 mm 폭의 커넥터와 얇은 내부 케이블로 이러한 데이터가 이동할 수 있다.&lt;/p&gt;
&lt;p&gt;그 이유가 바로 &lt;strong&gt;고속 직렬 통신&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 이렇게 빠른 통신이 가능한 핵심: 차동 신호&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4a0Lw/dJMcafON8IG/Db0T7IJBDxQmQlYvJM8TKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4a0Lw/dJMcafON8IG/Db0T7IJBDxQmQlYvJM8TKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4a0Lw/dJMcafON8IG/Db0T7IJBDxQmQlYvJM8TKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4a0Lw%2FdJMcafON8IG%2FDb0T7IJBDxQmQlYvJM8TKK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Figure 4. Differential Signaling의 핵심 원리를 이해하기 위한 단순화된 파형. 실제 eDP 물리 계층 파형을 그대로 재현한 것은 아님.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;고속 통신에서 중요한 기술 중 하나가 &lt;strong&gt;Differential Signaling&lt;/strong&gt;, 차동 신호 방식이다.&lt;/p&gt;
&lt;p&gt;신호를 한 선의 전압만으로 판단하는 대신 두 선의 전압 차이를 이용한다.&lt;/p&gt;
&lt;p&gt;수식으로 표현하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V₍diff₎ = V₊ − V₋&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;예를 들어 두 신호선에 외부 노이즈 N이 거의 동일하게 들어왔다고 생각해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;V+ → V+ + N
V- → V- + N&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;수신기는 둘의 차이를 본다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(V₊ + N) − (V₋ + N)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;정리하면,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V₊ − V₋&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;두 선에 동일하게 유입된 성분, 즉 &lt;strong&gt;Common-Mode Noise&lt;/strong&gt;가 차동 연산에서 상당 부분 억제될 수 있다는 뜻이다.&lt;/p&gt;
&lt;p&gt;물론 현실의 시스템에서는 두 선에 노이즈가 완벽하게 동일하게 유입되지 않으며, receiver의 Common-Mode Rejection도 무한하지 않다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;p&gt;“차동 신호를 쓰면 노이즈가 전부 사라진다”&lt;/p&gt;
&lt;p&gt;라고 설명하는 것은 정확하지 않다.&lt;/p&gt;
&lt;p&gt;보다 정확한 표현은&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;두 선에 공통적으로 결합되는 잡음에 대한 내성을 크게 높일 수 있다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;이러한 차동 전송 방식은 eDP뿐 아니라 PCI Express, USB, Ethernet 등 다양한 고속 인터페이스에서 사용된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 그런데 왜 아무 전선으로 연결하면 안 될까?&lt;/h2&gt;
&lt;p&gt;여기까지 읽으면 또 하나의 질문이 생긴다.&lt;/p&gt;
&lt;p&gt;“그렇다면 Lane +와 Lane -만 연결해주면 일반 전선으로도 동작하는 것 아닐까?”&lt;/p&gt;
&lt;p&gt;고속 통신에서는 그렇지 않다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpseAY/dJMcacLi6Bw/dTyHqZXv0iEUygSzgaNRdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpseAY/dJMcacLi6Bw/dTyHqZXv0iEUygSzgaNRdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpseAY/dJMcacLi6Bw/dTyHqZXv0iEUygSzgaNRdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpseAY%2FdJMcacLi6Bw%2FdTyHqZXv0iEUygSzgaNRdK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Figure 5. 고속 차동 전송선에서 Signal Integrity가 중요한 이유를 단순화한 그림. Eye diagram 역시 원리 설명을 위한 개념 예시.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;데이터 속도가 충분히 높아지면 케이블은 더 이상 단순한 ‘전선’으로 취급할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Transmission Line, 즉 전송선&lt;/strong&gt;으로 다루어야 한다.&lt;/p&gt;
&lt;p&gt;이때부터는 단순히&lt;/p&gt;
&lt;p&gt;“양쪽이 전기적으로 연결되어 있는가?”&lt;/p&gt;
&lt;p&gt;만으로는 부족하다.&lt;/p&gt;
&lt;p&gt;Pair 두 선의 길이가 크게 달라지면 신호 도착 시간이 어긋나는 &lt;strong&gt;Skew&lt;/strong&gt;가 발생한다.&lt;/p&gt;
&lt;p&gt;임피던스가 갑자기 변하는 부분에서는 &lt;strong&gt;Reflection&lt;/strong&gt;, 즉 신호 반사가 발생한다.&lt;/p&gt;
&lt;p&gt;인접한 고속 Lane끼리는 전자기적으로 영향을 주면서 &lt;strong&gt;Crosstalk&lt;/strong&gt;를 만들 수 있다.&lt;/p&gt;
&lt;p&gt;케이블 자체와 커넥터, PCB 패턴에서는 주파수가 증가할수록 &lt;strong&gt;Insertion Loss&lt;/strong&gt;도 커진다.&lt;/p&gt;
&lt;p&gt;그리고 신호가 이동할 때 필요한 &lt;strong&gt;Return Path&lt;/strong&gt; 역시 매우 중요하다.&lt;/p&gt;
&lt;p&gt;이 때문에 고속 케이블은 Pair 간 간격, 패턴 폭, 유전체 구조, Ground와의 관계, 길이 matching 등을 고려해서 설계된다.&lt;/p&gt;
&lt;p&gt;Figure 5에서 약 &lt;code&gt;100 Ω differential&lt;/code&gt;이라고 적어놓은 것도 이러한 &lt;strong&gt;controlled impedance&lt;/strong&gt; 개념을 표현한 것이다.&lt;/p&gt;
&lt;h3&gt;Eye Diagram은 무엇인가?&lt;/h3&gt;
&lt;p&gt;Figure 5 아래쪽의 눈처럼 생긴 그래프는 &lt;strong&gt;Eye Diagram&lt;/strong&gt;을 설명하기 위한 개념도이다.&lt;/p&gt;
&lt;p&gt;고속 데이터의 수많은 bit 구간을 하나의 시간축 위에 겹쳐 표시하면 가운데 눈 모양의 공간이 만들어진다.&lt;/p&gt;
&lt;p&gt;신호 품질이 좋다면 눈이 비교적 크게 열린다.&lt;/p&gt;
&lt;p&gt;반대로 jitter와 noise, loss, reflection 등이 커지면 눈이 점점 닫힌다.&lt;/p&gt;
&lt;p&gt;눈이 지나치게 닫히면 receiver가 특정 순간에 이것이 &lt;code&gt;0&lt;/code&gt;인지 &lt;code&gt;1&lt;/code&gt;인지 안정적으로 판단하기 어려워지고 &lt;strong&gt;Bit Error&lt;/strong&gt;가 발생할 가능성이 증가한다.&lt;/p&gt;
&lt;p&gt;그래서 고속 디지털 설계에서는&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“연결됐는가?”보다 “얼마나 깨끗하게 연결됐는가?”가 중요하다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 화면을 켜기 전 GPU와 패널은 먼저 ‘협상’을 한다&lt;/h2&gt;
&lt;p&gt;케이블을 연결했다고 GPU가 곧바로 수십 Gbps의 영상을 무작정 보내기 시작하는 것도 아니다.&lt;/p&gt;
&lt;p&gt;먼저 &lt;strong&gt;Link Training&lt;/strong&gt;이라는 과정이 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k9aAW/dJMcadJ1D7g/NjgCmejSdo12nn6U8CqXy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k9aAW/dJMcadJ1D7g/NjgCmejSdo12nn6U8CqXy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k9aAW/dJMcadJ1D7g/NjgCmejSdo12nn6U8CqXy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk9aAW%2FdJMcadJ1D7g%2FNjgCmejSdo12nn6U8CqXy0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Figure 6. Link Training 과정에서 AUX와 Main Link의 역할을 단순화한 개념도&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DisplayPort/eDP의 수신 측에는 &lt;strong&gt;DPCD(DisplayPort Configuration Data)&lt;/strong&gt;라는 설정 및 capability 정보를 위한 레지스터 공간이 존재한다.&lt;/p&gt;
&lt;p&gt;Source, 즉 GPU 쪽에서는 AUX Channel을 통해 이러한 정보를 읽고 링크를 설정할 수 있다.&lt;/p&gt;
&lt;p&gt;개념적으로는 다음과 같은 대화가 이루어진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GPU:
&amp;quot;너 몇 개 Lane을 사용할 수 있어?&amp;quot;

Display:
&amp;quot;내 capability는 이 정도야.&amp;quot;

GPU:
&amp;quot;그러면 이 Link Rate와 Lane 수로 동작해보자.&amp;quot;

Display:
&amp;quot;좋아. 훈련 신호를 보내봐.&amp;quot;

GPU:
[Main Link로 Training Pattern 전송]

Display:
&amp;quot;현재 신호 상태는 이래.
이 부분을 조금 조정해줘.&amp;quot;

GPU:
[필요하면 설정 조정 및 재훈련]

Display:
&amp;quot;링크 정상.&amp;quot;

GPU:
[실제 Video Stream 전송 시작]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중요한 점은 &lt;strong&gt;Training Pattern 자체는 AUX Channel이 아니라 Main Link를 통해 전송된다는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;AUX는 훈련을 관리하고 상태를 읽고 설정을 전달하는 데 사용되고, 실제 고속 Training Pattern은 Main Link에서 전송된다.&lt;/p&gt;
&lt;p&gt;실제 링크 트레이닝 과정은 링크 속도와 DisplayPort 세대 등에 따라 세부 절차가 다르며 Clock Recovery와 Channel Equalization 등의 과정을 포함한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 고속 데이터가 패널에 도착하면 어떻게 518만 개의 픽셀이 되는가?&lt;/h2&gt;
&lt;p&gt;여기까지 오면 마지막 퍼즐이 남는다.&lt;/p&gt;
&lt;p&gt;eDP 케이블을 통해 수많은 bit가 디스플레이까지 도착했다.&lt;/p&gt;
&lt;p&gt;그런데 어떻게 이 직렬 데이터가 다시 화면의 수백만 픽셀로 변하는 것일까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oR5Ji/dJMcabljaa1/VQeJgEkmtKke07S2ImMhqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oR5Ji/dJMcabljaa1/VQeJgEkmtKke07S2ImMhqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oR5Ji/dJMcabljaa1/VQeJgEkmtKke07S2ImMhqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoR5Ji%2FdJMcabljaa1%2FVQeJgEkmtKke07S2ImMhqk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Figure 7. 고속 직렬 데이터가 패널 구동 신호로 변환되는 과정을 단순화한 개념도. 실제 OLED 패널에서는 TCON/DDI 기능의 분할과 통합 구조가 제품에 따라 달라질 수 있다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;eDP Receiver는 Main Link에서 들어온 고속 직렬 데이터를 수신하고 해석한다.&lt;/p&gt;
&lt;p&gt;개념적으로는 다음과 같은 과정이 이루어진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Serial Data
      ↓
eDP Receiver
      ↓
De-serialization / Decode
      ↓
Timing &amp;amp; Format Processing
      ↓
Panel Driver
      ↓
OLED Pixel Matrix&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기에서 흔히 등장하는 것이 &lt;strong&gt;TCON, Timing Controller&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;TCON 또는 이에 해당하는 timing/control logic는 전달된 영상 데이터를 패널이 요구하는 타이밍과 형식으로 변환한다.&lt;/p&gt;
&lt;p&gt;그 다음 Panel Driver가 실제 픽셀 배열을 구동한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. Source Driver와 Gate Driver는 어떤 일을 할까?&lt;/h2&gt;
&lt;p&gt;패널 구동을 개념적으로 나누면 &lt;strong&gt;열(column) 방향 데이터와 행(row) 방향 선택&lt;/strong&gt;이라는 두 가지 문제가 나타난다.&lt;/p&gt;
&lt;p&gt;Data/Source Driver는 각 column 또는 subpixel에 필요한 영상 값을 전달하는 역할을 담당한다.&lt;/p&gt;
&lt;p&gt;반면 Gate/Scan 계열의 driver는 어느 row를 언제 선택할지를 제어한다.&lt;/p&gt;
&lt;p&gt;OLED 패널에서는 여기에 발광 제어 등 추가적인 구동 신호가 관여할 수 있다.&lt;/p&gt;
&lt;p&gt;즉 메인보드에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Pixel 1 전선
Pixel 2 전선
Pixel 3 전선
...
Pixel 5,184,000 전선&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 모두 끌고 오는 것이 아니다.&lt;/p&gt;
&lt;p&gt;대신&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GPU
 ↓
몇 개의 매우 빠른 eDP Lane
 ↓
패널 내부의 제어/구동 회로
 ↓
수백만 개 픽셀&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 계층 구조를 사용한다.&lt;/p&gt;
&lt;p&gt;이는 컴퓨터 시스템에서 매우 흔하게 등장하는 아이디어이고,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;많은 병렬 정보를 물리적인 장거리 구간에서는 고속 직렬 데이터로 바꾸고, 목적지에서 다시 필요한 형태로 펼치는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;이 과정을 Serialization과 Deserialization의 관점에서도 이해할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. “선이 얇으면 데이터도 적게 흐른다”는 생각이 틀린 이유&lt;/h2&gt;
&lt;p&gt;여기서 가장 흥미로운 공학적 포인트가 나온다.&lt;/p&gt;
&lt;p&gt;우리는 보통 굵은 케이블이 더 많은 것을 전달한다고 생각한다.&lt;/p&gt;
&lt;p&gt;전력 전달에서는 어느 정도 맞는 말이다.&lt;/p&gt;
&lt;p&gt;예를 들어 큰 전류가 흐르는 전원선은 도체의 저항과 발열 때문에 충분한 단면적이 필요하다.&lt;/p&gt;
&lt;p&gt;전선 저항은 대략 다음과 같이 생각할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;R = ρL/A&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;단면적 A가 작아지면 저항이 커지고, 전류 I가 흐를 때 발생하는 도체 손실은&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P = I²R&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;로 증가한다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;전력 전송&lt;/strong&gt;에서는 굵기가 매우 중요하다.&lt;/p&gt;
&lt;p&gt;하지만 eDP Main Link가 주로 운반하는 것은 대량의 에너지가 아니라 &lt;strong&gt;정보&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;정보 전달 능력은 단순한 도체 단면적보다 사용 가능한 주파수 대역, 신호 대 잡음비, 부호화 방식, receiver 성능, 채널의 손실 및 신호무결성과 깊은 관계가 있다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;굵은 선 = 많은 데이터&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 공식은 성립하지 않는다.&lt;/p&gt;
&lt;p&gt;보다 정확하게 표현하면,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;큰 전력을 전달하려면 충분한 도체 단면적이 중요하지만, 많은 정보를 전달하려면 충분한 통신 대역폭과 신호 품질이 중요하다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 차이가 바로 얇은 디스플레이 케이블을 이해하는 핵심이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11. DSC를 사용하면 더 많은 영상을 같은 링크로 전송할 수도 있다&lt;/h2&gt;
&lt;p&gt;고해상도 디스플레이 시스템에서는 &lt;strong&gt;DSC(Display Stream Compression)&lt;/strong&gt;라는 기술을 사용할 수도 있다.&lt;/p&gt;
&lt;p&gt;DSC는 디스플레이 전송을 위한 압축 기술이다.&lt;/p&gt;
&lt;p&gt;개념적으로는 다음과 같이 동작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GPU의 영상
      ↓
DSC Encoder
      ↓
압축된 Display Stream
      ↓
eDP / DisplayPort
      ↓
DSC Decoder
      ↓
   Display&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 이용하면 물리적인 링크 대역폭을 무한정 높이지 않고도 더 높은 해상도와 refresh rate, color depth를 다루기 쉬워진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;12. 화면 출력과 터치·S Pen은 같은 신호가 아니다&lt;/h2&gt;
&lt;p&gt;Galaxy Book4 Pro 360은 단순한 OLED 노트북이 아니라 터치와 S Pen도 지원한다.&lt;/p&gt;
&lt;p&gt;여기서 디스플레이 출력과 터치 입력을 동일한 것으로 생각하기 쉽지만, 논리적으로는 완전히 다른 기능이다.&lt;/p&gt;
&lt;p&gt;화면 출력은 기본적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GPU
 ↓
Display Data
 ↓
OLED Panel&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 방향이다.&lt;/p&gt;
&lt;p&gt;반면 터치는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Finger / Pen
 ↓
Touch / Digitizer Controller
 ↓
Coordinate Data
 ↓
Computer&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 입력 시스템이다.&lt;/p&gt;
&lt;p&gt;따라서 하드웨어 고장 상황에서도&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;화면은 정상인데 터치가 안 되는 경우&lt;/li&gt;
&lt;li&gt;터치 관련 회로는 인식되지만 영상이 나오지 않는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;가 논리적으로 가능하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;13. 그래서 얇은 디스플레이 케이블을 함부로 잘라 연장하면 안 된다&lt;/h2&gt;
&lt;p&gt;이번 내용을 실제 수리나 개조 관점에서 본다면 꽤 중요한 결론이 나온다.&lt;/p&gt;
&lt;p&gt;eDP 계열의 고속 Lane은 단순히 전기적으로 이어져 있기만 하면 되는 배선이 아니다.&lt;/p&gt;
&lt;p&gt;일반 전선으로 임의 연장하면 차동 pair의 구조가 무너지거나, pair 사이의 길이가 달라지고, 임피던스가 불연속적으로 변하고, return path가 나빠질 수 있다.&lt;/p&gt;
&lt;p&gt;그 결과는 단순한 “화질 저하”로 끝나지 않을 수도 있다.&lt;/p&gt;
&lt;p&gt;디지털 고속 링크에서는 흔히 다음과 같은 형태로 증상이 나타날 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;정상 출력
      ↓
간헐적인 깜빡임
      ↓
화면 노이즈 / 링크 오류
      ↓
특정 해상도/주사율에서만 실패
      ↓
Link Training 실패
      ↓
화면 출력 불가&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 오래된 아날로그 영상처럼&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;“신호가 나빠지면 조금 흐릿해진다”&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;는 개념과는 다르다.&lt;/p&gt;
&lt;p&gt;디지털 링크는 일정 수준까지 정상적으로 동작하다가 신뢰 가능한 판별 범위를 넘어가면 급격하게 오류가 증가할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;14. 이 작은 케이블 안에는 생각보다 많은 공학이 들어 있다&lt;/h2&gt;
&lt;p&gt;처음 질문으로 돌아가 보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;“메인보드와 디스플레이 사이에 보이는 연결은 이렇게 작은데, 어떻게 2880 × 1800 120Hz 화면을 띄울 수 있는 걸까?”&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;답은 작은 케이블 자체에 특별한 능력이 있어서가 아니다.&lt;/p&gt;
&lt;p&gt;그 뒤에 매우 정교한 시스템이 존재하기 때문이다.&lt;/p&gt;
&lt;p&gt;GPU는 수백만 픽셀의 영상을 고속 데이터 스트림으로 만들고, eDP Source는 이를 여러 개의 고속 differential lane에 실어 보낸다.&lt;/p&gt;
&lt;p&gt;Receiver와 GPU는 AUX Channel을 이용해 서로 capability를 확인하고 Link Training을 진행한다.&lt;/p&gt;
&lt;p&gt;실제 고속 영상 데이터는 Main Link를 통해 이동한다.&lt;/p&gt;
&lt;p&gt;그리고 패널 쪽의 receiver와 timing/display control logic가 다시 데이터를 해석하고, panel driver가 이를 실제 OLED 픽셀 구동 신호로 펼쳐낸다.&lt;/p&gt;
&lt;p&gt;전체 과정을 한 줄로 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CPU / GPU
    ↓
eDP Transmitter
    ↓
High-Speed Differential Main Link
    ↓
eDP Receiver
    ↓
Timing / Display Control
    ↓
Panel Drivers
    ↓
OLED TFT Backplane
    ↓
2880 × 1800 Pixels&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;겉으로 보면 작은 커넥터와 얇은 케이블에 불과하다.&lt;/p&gt;
&lt;p&gt;하지만 그 안에는 &lt;strong&gt;고속 직렬 통신, 차동 신호, transmission line theory, controlled impedance, signal integrity, link training, clock recovery, channel equalization, panel driving&lt;/strong&gt;과 같은 전자/컴퓨터공학 기술이 동시에 들어 있다.&lt;/p&gt;
&lt;p&gt;노트북의 디스플레이 케이블 하나를 단순한 ‘전선’이라고 부르기에는 그 안에서 벌어지는 일이 너무 많다.&lt;/p&gt;
&lt;p&gt;어쩌면 더 적절한 표현은 이것일지도 모른다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;노트북의 디스플레이 케이블은 얇은 전선이 아니라, 수십억 개의 비트가 매초 이동하는 초고속 데이터 고속도로dl다.&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;글을 마치며&lt;/h2&gt;
&lt;p&gt;Galaxy Book4 Pro 360을 보면서 시작했던 단순한 의문 하나가 결국 고속 디지털 통신의 여러 핵심 개념으로 이어졌다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;왜 차동 신호를 사용하는가.&lt;/li&gt;
&lt;li&gt;왜 케이블의 길이와 임피던스가 중요한가.&lt;/li&gt;
&lt;li&gt;왜 AUX와 Main Link가 따로 존재하는가.&lt;/li&gt;
&lt;li&gt;왜 Link Training이 필요한가.&lt;/li&gt;
&lt;li&gt;어떻게 몇 개의 고속 Lane이 수백만 개 OLED 픽셀을 움직일 수 있는가.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;작은 부품 하나를 자세히 들여다보는 것만으로도 컴퓨터가 실제 물리 세계에서 어떻게 동작하는지를 꽤 깊게 이해할 수 있다.&lt;/p&gt;
&lt;p&gt;소프트웨어에서 화면에 픽셀 하나를 그리는 명령은 단순해 보인다.&lt;/p&gt;
&lt;p&gt;하지만 그 픽셀이 실제 OLED 소자에서 빛나기까지는,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GPU → Protocol → PHY → Transmission Line → Receiver → Timing Logic → Driver → Transistor → OLED&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이라는 긴 공학적 여정을 거친다.&lt;/p&gt;
&lt;p&gt;그리고 그 여정의 중간에 있는 것이 바로 처음에는 접지나 실드 구조물의 일부처럼 지나쳤던 &lt;strong&gt;노트북 내부의 작은 디스플레이 연결부&lt;/strong&gt;이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;평소에는 그저 화면이 켜지고, 이미지가 나타나는 것을 당연하게 생각한다. 하지만 그 당연함에서 벗어나 조금만 들여다보면, 그 뒤에는 수많은 공학적 선택과 보이지 않는 움직임이 자리하고 있다. 어쩌면 기술을 이해하는 첫걸음은 거창한 것이 아닌 바로 그 ‘당연함’에 질문을 던지는 것인지도 모르겠다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;기술적 주석&lt;/h2&gt;
&lt;p&gt;본문의 Figure 1~7은 &lt;strong&gt;eDP의 동작 원리를 설명하기 위한 개념도&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;특히 Figure 2와 Figure 3의 케이블 구조, pin 배열, TCON 구성 등은 Galaxy Book4 Pro 360의 실제 회로도나 실제 pinout을 재현한 것이 아니다.&lt;/p&gt;
&lt;p&gt;실제 제품에서는 FPC, FFC, micro-coax 계열을 포함하여 물리적 구현이 달라질 수 있고, 패널 내부에서도 eDP Receiver, TCON 및 DDI의 기능이 하나의 IC에 통합되거나 다른 방식으로 분할될 수 있다.&lt;/p&gt;
&lt;p&gt;또한 Galaxy Book4 Pro 360의 실제 eDP Lane 수, Link Rate, DSC 사용 여부, 세부 핀아웃은 공개 제품 사양만으로 확정하기 어렵다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Samsung Galaxy Book4 Pro 360 공식 제품 사양&lt;br&gt;&lt;a href=&quot;https://image-us.samsung.com/SamsungUS/samsungbusiness/pdf/spec-sheets/29783/B2B_GB4_Pro_360_Spec_Sheet.pdf&quot;&gt;https://image-us.samsung.com/SamsungUS/samsungbusiness/pdf/spec-sheets/29783/B2B_GB4_Pro_360_Spec_Sheet.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VESA DisplayPort / eDP 관련 기술 자료&lt;br&gt;&lt;a href=&quot;https://www.vesa.org/&quot;&gt;https://www.vesa.org/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VESA DisplayPort PHY / Link Training 관련 자료&lt;br&gt;&lt;a href=&quot;https://www.vesa.org/wp-content/uploads/2013/05/DP-Derivatives-PHY-Agilent-Asia-2013-v1.pdf&quot;&gt;https://www.vesa.org/wp-content/uploads/2013/05/DP-Derivatives-PHY-Agilent-Asia-2013-v1.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VESA Display Stream Compression&lt;br&gt;&lt;a href=&quot;https://vesa.org/vesa-display-compression-codecs/&quot;&gt;https://vesa.org/vesa-display-compression-codecs/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>System &amp;middot; Security/Computer System</category>
      <category>120Hz</category>
      <category>EDP</category>
      <category>Embedded DisplayPort</category>
      <category>Galaxy Book4 Pro 360</category>
      <category>Link Training</category>
      <category>OLED</category>
      <category>Signal Integrity</category>
      <category>노트북 하드웨어</category>
      <category>디스플레이</category>
      <category>차동 신호</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/70</guid>
      <comments>https://slowgrid.tistory.com/entry/Where-on-earth-is-the-Galaxy-Book-4-Pro-360s-screen-connected#entry70comment</comments>
      <pubDate>Thu, 3 Sep 2026 20:21:50 +0900</pubDate>
    </item>
    <item>
      <title>[딥러닝 기초 01] 신경망은 어떻게 학습할까?</title>
      <link>https://slowgrid.tistory.com/entry/Deep-Learning-Fundamentals-01-How-Do-Neural-Networks-Learn</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 객체탐지 공부에서는 YOLO가 만들어낸 결과를 어떻게 해석하는지 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기까지 공부하고 나니 더 근본적인 질문이 생겼다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO는 어떻게 사람을 찾아낼 수 있게 된 걸까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 진행 중인 열화상 보행자 탐지 프로젝트에서도 &lt;code&gt;yolo26n.pt&lt;/code&gt;를 시작점으로 열화상 이미지와 사람 바운딩 박스를 학습시키고, 그 결과 만들어진 &lt;code&gt;best.pt&lt;/code&gt;로 보행자를 검출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 모델이 &lt;b&gt;학습한다&lt;/b&gt;는 것은 구체적으로 무엇을 의미할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 YOLO로 들어가기 전에 딥러닝 모델의 가장 기본적인 학습 과정을 먼저 정리해보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 머신러닝(Machine Learning)과 딥러닝(Deep Learning)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머신러닝과 딥러닝은 모두 데이터를 이용해 규칙을 학습한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이는 이미지 문제를 생각하면 조금 더 쉽게 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 머신러닝에서는 사람이 먼저 유용할 것 같은 특징을 설계하는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사람을 구분하기 위해&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;윤곽선
모양
색상
특정 패턴&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 사람이 정의하고 숫자로 변환한 뒤 모델에 전달하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이미지
 &amp;darr;
사람이 특징 설계
 &amp;darr;
특징
 &amp;darr;
머신러닝 모델
 &amp;darr;
예측&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 딥러닝에서는 특징을 추출하는 과정 자체도 모델이 학습한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이미지
 &amp;darr;
심층 신경망
 &amp;darr;
특징 추출도 학습
 &amp;darr;
예측&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 실제 머신러닝과 딥러닝의 경계는 이것보다 복잡하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이미지 딥러닝을 처음 공부할 때는&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;전통적인 머신러닝
&amp;rarr; 특징 설계의 비중이 큼

딥러닝
&amp;rarr; 특징을 추출하는 방법도 학습&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 이해하면 전체 구조를 파악하기 편했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 신경망(Neural Network)이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝의 중심에는 신경망(Neural Network), 즉 인공신경망이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이름 때문에 굉장히 복잡한 구조라고 생각했지만, 가장 단순하게 보면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력된 숫자를 여러 계산을 거쳐 다른 숫자로 바꾸는 함수들의 집합&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;입력
 &amp;darr;
신경망
 &amp;darr;
출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 넣는다면&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;열화상 이미지
 &amp;darr;
신경망
 &amp;darr;
특징 추출
 &amp;darr;
바운딩 박스 / 클래스&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 계산을 수행할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 하나의 뉴런(Neuron)에서는 어떤 계산을 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 가중치만 알고 있었는데, 신경망을 조금 더 정확하게 이해하려면 &lt;code&gt;가중치(Weight)&lt;/code&gt;, &lt;code&gt;편향(Bias)&lt;/code&gt;, &lt;code&gt;활성화 함수(Activation Function)&lt;/code&gt;를 함께 보는 것이 좋은 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 단순한 하나의 뉴런을 생각하면 다음과 같은 형태로 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;입력 x
 &amp;darr;
x &amp;times; 가중치
 &amp;darr;
편향 더하기
 &amp;darr;
활성화 함수
 &amp;darr;
출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수식으로 단순화하면&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;y = f(wx + b)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;w &amp;rarr; 가중치
b &amp;rarr; 편향
f &amp;rarr; 활성화 함수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가중치(Weight)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중치는 입력이 결과에 얼마나 큰 영향을 줄지를 결정하는 학습 가능한 숫자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;fix&quot;&gt;&lt;code&gt;출력 = 입력 &amp;times; 가중치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 단순화해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력이 10이고 가중치가 0.1이면&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;10 &amp;times; 0.1 = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중치가 0.8이면&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;10 &amp;times; 0.8 = 8&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 입력도 가중치가 달라지면 결과가 크게 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 YOLO에는 이런 가중치가 수백만 개 이상 존재한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 편향(Bias)은 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편향은 입력 값만으로 결정되는 계산에 일정한 이동을 추가해주는 학습 가능한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;y = wx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 사용하면 입력 &lt;code&gt;x=0&lt;/code&gt;일 때 출력 역시 항상 0이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;y = wx + b&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 편향이 존재하면 보다 다양한 관계를 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중치와 마찬가지로 편향도 학습 과정에서 조정된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 활성화 함수(Activation Function)는 왜 필요할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망에서는 가중치와 편향 계산 뒤에 활성화 함수가 자주 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ReLU
SiLU
Sigmoid&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 함수가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수들의 수식을 지금 전부 외울 필요는 없다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망이 단순한 선형 계산만 계속 반복하는 것이 아니라 복잡한 패턴을 표현할 수 있도록 비선형성을 추가한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 YOLO 구조를 보면 &lt;code&gt;Conv(합성곱) &amp;rarr; BatchNorm(배치 정규화) &amp;rarr; Activation(활성화 함수)&lt;/code&gt; 같은 구조를 반복해서 만나게 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 모델이 학습한다는 것은 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 가장 중요한 질문으로 돌아가보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 학습한다는 것은 무엇일까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 단순하게 말하면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예측이 정답에 가까워지도록 학습 가능한 매개변수를 계속 수정하는 과정&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 매개변수는 주로 가중치와 편향 같은 값이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 가중치
 &amp;darr;
예측
 &amp;darr;
정답 라벨과 비교
 &amp;darr;
틀린 정도 계산
 &amp;darr;
가중치 수정
 &amp;darr;
다시 예측
 &amp;darr;
반복&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 AI가 이미지를 머릿속에 저장하면서 점점 똑똑해지는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수많은 숫자가 조금씩 변경되고 있는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 순전파(Forward Pass)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 가지고 있는 가중치를 이용해 입력에서 예측을 계산하는 과정을 순전파라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트라면 개념적으로&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt; 열화상 이미지
      &amp;darr;
   YOLO26n
      &amp;darr;
사람 바운딩 박스
      +
    신뢰도&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 순전파다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 초기에는 현재 열화상 데이터셋에 적합하지 않은 가중치를 가지고 있기 때문에 예측이 틀릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 실제 사람이 왼쪽에 있는데 모델이 오른쪽을 사람이라고 예측할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 모델에게&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 예측이 정답과 얼마나 다른가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 알려줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 손실이 등장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 손실(Loss)이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손실은 예측과 정답 라벨의 차이를 학습에 사용할 수 있는 숫자로 표현한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;손실
&amp;rarr; 예측의 오류를 학습 가능한 값으로 측정

정밀도 / 재현율 / mAP
&amp;rarr; 모델의 탐지 성능 평가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 손실과 mAP는 목적이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체탐지에서는 단순히 클래스만 맞추면 되는 것이 아니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;무엇인가?
    +
어디에 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 동시에 해결해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일반적으로 박스 위치와 클래스 예측 등에 관련된 여러 손실이 함께 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO26은 이전 Ultralytics YOLO 세대와 달리 DFL 기반 박스 회귀를 제거하고 더 단순한 박스 회귀 헤드를 사용한다. 또한 STAL, Progressive Loss 등의 새로운 학습 방식이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 단계에서는 각 손실의 공식을 외우기보다&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;박스 위치가 잘못됨
&amp;rarr; 박스 관련 손실

클래스가 잘못됨
&amp;rarr; 분류 관련 손실&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 모델이 무엇을 잘못했는지를 학습 신호로 만들어준다고 이해했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 역전파(Backpropagation)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손실을 계산했다면 이제 가중치를 수정해야 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이미지
 &amp;darr;
순전파
 &amp;darr;
예측
 &amp;darr;
손실
 &amp;darr;
?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 등장하는 것이 역전파(Backpropagation)다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 손실에 각각의 매개변수가 얼마나 영향을 미쳤는지를 뒤쪽에서부터 계산하는 과정&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 이해할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;입력
 &amp;rarr; 층 1
 &amp;rarr; 층 2
 &amp;rarr; 층 3
 &amp;rarr; 예측
 &amp;rarr; 손실

              &amp;larr; 역전파&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순전파가 입력에서 출력으로 계산한다면 역전파에서는 손실을 기준으로 각 매개변수에 대한 기울기를 계산한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 기울기(Gradient)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기울기는 미분을 통해 얻는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터 갑자기 수학이 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우선 개념적으로는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중치를 바꾸었을 때 손실이 어느 방향으로 얼마나 변하는지를 알려주는 값&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 생각할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현재 가중치 = 0.5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중치를 증가시켰더니 손실도 증가하는 방향이라면 손실을 줄이기 위해 반대 방향으로 이동할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 가중치
 &amp;darr;
기울기 계산
 &amp;darr;
손실이 감소하는 방향 확인
 &amp;darr;
가중치 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 계산을 모든 학습 가능한 매개변수에 대해 수행한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 최적화 알고리즘(Optimizer)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기울기가 계산됐다고 가중치가 자동으로 수정되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기울기를 이용해 실제 매개변수를 업데이트하는 역할을 하는 것이 최적화 알고리즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;fix&quot;&gt;&lt;code&gt;새로운 가중치 = 현재 가중치 - 학습률 &amp;times; 기울기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 학습률(Learning Rate)은 한 번에 얼마나 크게 이동할지 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 크면 최적 지점을 계속 지나칠 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 작으면 학습이 매우 느려질 수 있기에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 속도가 지나치게 느려질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실제 최적화 알고리즘은 SGD, Adam, AdamW 등 다양한 전략을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Ultralytics는 &lt;code&gt;optimizer=auto&lt;/code&gt;도 제공하며, 학습 조건에 따라 적절한 옵티마이저를 자동 선택한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 전체 학습 과정을 연결하면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 내용을 하나로 연결하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;학습 이미지 + 라벨
        &amp;darr;
      순전파
        &amp;darr;
       예측
        &amp;darr;
  정답 라벨과 비교
        &amp;darr;
       손실
        &amp;darr;
      역전파
        &amp;darr;
      기울기
        &amp;darr;
   최적화 알고리즘
        &amp;darr;
   가중치 업데이트
        &amp;darr;
       반복&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;이미지를 보여준다.
&amp;rarr; AI가 알아서 학습한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;예측
&amp;rarr; 오류 계산
&amp;rarr; 각 매개변수의 영향 계산
&amp;rarr; 매개변수 수정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 숫자 계산이 반복되고 있었음을 확인할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. 매개변수(Parameter)와 하이퍼파라미터(Hyperparameter)는 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 읽으면 가중치, 편향, 학습률, 에포크 등이 모두 학습과 관련된 숫자처럼 보일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 둘은 구분하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;매개변수
&amp;rarr; 모델이 학습 과정에서 직접 바꾸는 값
&amp;rarr; 가중치, 편향 등

하이퍼파라미터
&amp;rarr; 사람이 학습 전에 정하거나 학습 설정이 결정하는 값
&amp;rarr; 학습률, 배치 크기, 에포크, 가중치 감쇠 등&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;역전파 + 최적화 알고리즘
&amp;rarr; 매개변수를 바꿈

실험 설정
&amp;rarr; 하이퍼파라미터를 정함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구분을 알고 있으면 나중에 모델을 학습했다와 학습 설정을 튜닝했다를 명확하게 구분할 수 있을 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;14. 역전파의 핵심은 연쇄법칙(Chain Rule)이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역전파를 조금 더 기술적으로 표현하면 &lt;b&gt;연쇄법칙(Chain Rule)을 이용해 손실의 미분값을 계산 그래프 뒤쪽에서 앞쪽으로 전달하는 과정&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 단순한 계산을 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;x
&amp;darr;
y = wx + b
&amp;darr;
손실 = L(y)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중치 &lt;code&gt;w&lt;/code&gt;가 손실에 미치는 영향은 개념적으로&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;dL/dw = dL/dy &amp;times; dy/dw&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 중간 계산의 미분값을 연결해서 구한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 신경망은 층이 수십~수백 개이므로 사람이 이 미분을 하나씩 계산하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyTorch 같은 프레임워크가 순전파 과정에서 만들어진 계산 그래프를 이용해 자동 미분(Autograd)을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 코드에서는 보통&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;loss.backward()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄이 보이지만 내부적으로는 수많은 텐서 연산을 따라 기울기가 계산된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;15. 실제 한 학습 단계(Training Step)에서는 무엇이 일어날까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyTorch 스타일로 가장 단순하게 표현하면 다음과 비슷하다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;optimizer.zero_grad()

pred = model(images)
loss = criterion(pred, targets)

loss.backward()
optimizer.step()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 줄의 의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;zero_grad()
&amp;rarr; 이전 기울기를 비움

model(images)
&amp;rarr; 순전파

criterion(...)
&amp;rarr; 손실 계산

loss.backward()
&amp;rarr; 기울기 계산

optimizer.step()
&amp;rarr; 매개변수 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점이 하나 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyTorch의 기울기는 기본적으로 &lt;b&gt;누적(accumulate)&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이전 기울기를 비우지 않으면 다음 역전파 결과가 기존 기울기에 더해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성을 의도적으로 이용하는 것이 뒤에서 나올 &lt;code&gt;기울기 누적(Gradient Accumulation)&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 실제 학습에서는&lt;/p&gt;
&lt;pre class=&quot;fix&quot;&gt;&lt;code&gt;역전파 횟수 = 항상 최적화 알고리즘의 업데이트 횟수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 단정할 수는 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;16. 학습률만큼 중요한 스케줄러(Scheduler)와 가중치 감쇠(Weight Decay)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서는&lt;/p&gt;
&lt;pre class=&quot;fix&quot;&gt;&lt;code&gt;새 가중치 = 현재 가중치 - 학습률 &amp;times; 기울기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 단순화했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 학습에서는 학습률을 모든 에포크에서 똑같이 유지하지 않는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 너무 큰 업데이트로 학습이 불안정해지는 것을 막기 위해 &lt;code&gt;워밍업(Warmup)&lt;/code&gt;을 사용하고, 이후에는 학습률을 점차 낮추는 스케줄러를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;학습 초반
&amp;rarr; 작은 학습률(LR)에서 시작
&amp;rarr; 워밍업

학습 중후반
&amp;rarr; 학습률 스케줄러에 따라 학습률 감소&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 &lt;code&gt;가중치 감쇠(Weight Decay)&lt;/code&gt; 같은 규제(Regularization) 기법도 최적화 알고리즘의 업데이트에 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 모델 성능은 단순히&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;최적화 알고리즘 이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나만으로 결정되는 것이 아니라&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;최적화 알고리즘
+ 학습률
+ 학습률 스케줄러
+ 가중치 감쇠
+ 배치 / 기울기 누적&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조합의 영향을 받는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;17. 학습 모드(Training Mode)와 추론 모드(Inference Mode)도 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망에는 학습할 때와 추론할 때 동작이 달라질 수 있는 층이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 배치 정규화(Batch Normalization)와 드롭아웃(Dropout)이 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;학습 모드
&amp;rarr; 현재 배치의 통계 사용 가능
&amp;rarr; 드롭아웃이 있다면 일부 활성값을 임의로 끔

평가 / 추론 모드
&amp;rarr; 저장된 통계 사용
&amp;rarr; 드롭아웃 비활성화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YOLO 같은 모델에서도 &lt;code&gt;train()&lt;/code&gt;과 &lt;code&gt;eval()&lt;/code&gt; 상태의 차이를 프레임워크가 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 검증이나 추론에서 단순히 기울기만 계산하지 않는 것이 아니라 &lt;b&gt;모델의 동작 모드 자체도 학습과 다를 수 있다&lt;/b&gt;는 점을 알아두면 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝에서&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 학습한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 말은 결국&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손실이 감소하는 방향으로 가중치와 편향 같은 매개변수를 반복적으로 조정한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 의미라는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 과정의 핵심은&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;순전파
 &amp;darr;
손실
 &amp;darr;
역전파
 &amp;darr;
기울기
 &amp;darr;
최적화 알고리즘
 &amp;darr;
가중치 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 이뤄진다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제 학습 데이터가 2,000장이 넘는다면 이 과정을 이미지마다 어떻게 수행할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;에포크&lt;/code&gt;, &lt;code&gt;배치&lt;/code&gt;, &lt;code&gt;이터레이션&lt;/code&gt;이라는 개념이 필요해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음 글에서는 실제 학습 루프(Training Loop)가 데이터 전체를 어떻게 반복해서 처리하는지 정리해보려고 한다.&lt;/p&gt;</description>
      <category>AI &amp;middot; Data/Deep Learning</category>
      <category>딥러닝</category>
      <category>신경망</category>
      <category>학습</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/69</guid>
      <comments>https://slowgrid.tistory.com/entry/Deep-Learning-Fundamentals-01-How-Do-Neural-Networks-Learn#entry69comment</comments>
      <pubDate>Wed, 2 Sep 2026 16:07:22 +0900</pubDate>
    </item>
    <item>
      <title>최대 유량(4) Push-Relabel은 어떻게 동작하는가</title>
      <link>https://slowgrid.tistory.com/entry/til-Push-Relabel</link>
      <description>&lt;p&gt;지금까지 최대 유량을 공부하면서 세 가지 알고리즘을 차례로 살펴봤다.&lt;/p&gt;
&lt;p&gt;포드-풀커슨에서는 Source에서 Sink까지 갈 수 있는 &lt;strong&gt;증가 경로(Augmenting Path)&lt;/strong&gt; 를 찾고, 그 경로로 보낼 수 있는 만큼 유량을 보내는 기본 구조를 살펴봤다.&lt;/p&gt;
&lt;p&gt;에드몬드-카프에서는 증가 경로를 BFS로 찾도록 선택 규칙을 정하면서, 증가 경로를 어떤 순서로 선택하느냐가 알고리즘의 성능에도 영향을 줄 수 있구나.&lt;/p&gt;
&lt;p&gt;라는 점을 볼 수 있었다.&lt;/p&gt;
&lt;p&gt;그리고 Dinic을 정리한 글에서는 여기서 한 단계 더 나아가, BFS로 최단 거리 정보를 한 번 만들었다면, 그 정보를 증가 경로 하나보다 더 많이 활용할 수 없을까?&lt;/p&gt;
&lt;p&gt;라는 질문을 따라갔다.&lt;/p&gt;
&lt;p&gt;Dinic은 BFS로 &lt;strong&gt;Level Graph&lt;/strong&gt;를 만들고, 그 구조가 유효한 동안 DFS를 반복해서 가능한 유량을 최대한 보내는 방식이었다.&lt;/p&gt;
&lt;p&gt;여기까지 공부하고 나니 최대 유량 알고리즘은 결국&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Source에서 Sink까지 유량을 보낼 경로를 어떻게 더 효율적으로 찾고 활용할 것인가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;를 고민하는 문제처럼 느껴졌다.&lt;/p&gt;
&lt;p&gt;그런데 Push-Relabel 알고리즘을 처음 보고 조금 당황했다.&lt;/p&gt;
&lt;p&gt;Push-Relabel은 아예 이런 식으로 생각하지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 완성된 경로를 먼저 찾지 않는다.&lt;/p&gt;
&lt;p&gt;대신 Source에서 일단 유량을 가능한 만큼 밀어 넣고,&lt;/p&gt;
&lt;p&gt;중간 정점에 쌓인 유량을 주변 정점으로 조금씩 이동시키면서 최종적으로 Sink까지 보내는 방식으로 동작한다.&lt;/p&gt;
&lt;p&gt;처음에는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;경로도 찾지 않는데 어떻게 최대 유량을 구하지?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;이번에는 이 질문을 따라가면서 Push-Relabel을 정리해봤다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;지금까지는 항상 Flow를 유지했다&lt;/h2&gt;
&lt;p&gt;포드-풀커슨을 공부할 때 어떤 증가 경로가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 하면,&lt;/p&gt;
&lt;p&gt;이 경로 전체에서 병목 용량을 구했다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A : 5
A → B : 3
B → T : 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min(5, 3, 7) = 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이므로 경로 전체에 유량 3을 보낸다.&lt;/p&gt;
&lt;p&gt;이때 중간 정점 A와 B를 보면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;들어온 유량 = 나간 유량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 항상 성립한다.&lt;/p&gt;
&lt;p&gt;이것이 &lt;strong&gt;Flow Conservation&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;Source와 Sink를 제외한 모든 정점은 받은 만큼 다시 내보내야 한다.&lt;/p&gt;
&lt;p&gt;에드몬드-카프와 Dinic에서도 이 기본 조건은 그대로 유지된다.&lt;/p&gt;
&lt;p&gt;그런데 Push-Relabel은 이 조건을 잠시 깨버린다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Push-Relabel은 Preflow에서 시작한다&lt;/h2&gt;
&lt;p&gt;Push-Relabel을 이해하면서 가장 먼저 익숙해져야 했던 개념이&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Preflow&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;였다.&lt;/p&gt;
&lt;p&gt;일반적인 Flow에서는 중간 정점 &lt;code&gt;v&lt;/code&gt;에 대해&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;들어온 유량 = 나간 유량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이어야 한다.&lt;/p&gt;
&lt;p&gt;하지만 Preflow에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;들어온 유량 ≥ 나간 유량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 허용한다.&lt;/p&gt;
&lt;p&gt;즉 어떤 정점에 유량이 잠시 쌓여 있어도 된다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S ──10──&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 해보자.&lt;/p&gt;
&lt;p&gt;Source에서 A로 유량 10을 보냈는데 A가 아직 아무 곳에도 유량을 보내지 않았다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A로 들어온 유량 = 10
A에서 나간 유량 = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;일반적인 Flow라면 중간 정점에서 이런 상태를 허용할 수 없다.&lt;/p&gt;
&lt;p&gt;하지만 Push-Relabel에서는 괜찮다.&lt;/p&gt;
&lt;p&gt;A에 유량 10이 임시로 쌓여 있는 상태라고 생각한다.&lt;/p&gt;
&lt;p&gt;이렇게 한 정점에 남아 있는 유량을 &lt;strong&gt;Excess Flow&lt;/strong&gt;라고 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[A] = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Excess는 말 그대로 남아 있는 유량이다&lt;/h2&gt;
&lt;p&gt;내가 가장 이해하기 편했던 표현은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[v]
=
정점 v가 받은 유량
-
정점 v가 내보낸 유량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이었다.&lt;/p&gt;
&lt;p&gt;예를 들어 A가 10을 받은 뒤 B로 4를 보냈다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;들어온 유량 = 10
나간 유량 = 4

excess[A] = 6&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;즉 아직 A에는 처리해야 할 유량 6이 남아 있다.&lt;/p&gt;
&lt;p&gt;Push-Relabel은 결국 이 Excess들을 계속 주변으로 옮긴다.&lt;/p&gt;
&lt;p&gt;목표는 단순하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Source와 Sink를 제외한
모든 정점의 excess를 0으로 만든다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그 순간 Preflow는 다시 정상적인 Flow가 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그런데 Excess를 어디로 보내야 할까?&lt;/h2&gt;
&lt;p&gt;여기까지 보면 단순해 보인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Source에서 일단 유량을 보낸다.
2. 중간 정점에 Excess가 생긴다.
3. 옆 정점으로 계속 보낸다.
4. Sink까지 보내면 끝&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 바로 문제가 생긴다.&lt;/p&gt;
&lt;p&gt;A에 Excess가 있다고 해서 아무 이웃에게나 유량을 보내면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B
B → A
A → B
B → A
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 유량이 계속 돌아다닐 수도 있다.&lt;/p&gt;
&lt;p&gt;또 Sink와 관계없는 방향으로 유량을 밀어버릴 수도 있다.&lt;/p&gt;
&lt;p&gt;그래서 Push-Relabel은 각 정점에 하나의 값을 추가한다.&lt;/p&gt;
&lt;p&gt;바로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[v]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Height라는 개념이 등장한다&lt;/h2&gt;
&lt;p&gt;Push-Relabel에서는 각 정점에 높이(Height)를 부여한다.&lt;/p&gt;
&lt;p&gt;그리고 유량은 기본적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;높은 곳
   ↓
낮은 곳&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로만 흐르게 만든다.&lt;/p&gt;
&lt;p&gt;실제로 물을 흘려보내는 모습을 생각하면 이해하기 쉽다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[A] = 3
height[B] = 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;방향으로 유량을 보낼 수 있다.&lt;/p&gt;
&lt;p&gt;조금 더 정확하게는 잔여 용량이 존재하는 간선 &lt;code&gt;u → v&lt;/code&gt;에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[u] = height[v] + 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;일 때 Push할 수 있다.&lt;/p&gt;
&lt;p&gt;즉 정확히 한 단계 아래의 정점으로 유량을 보내는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Source의 높이는 왜 V일까?&lt;/h2&gt;
&lt;p&gt;초기 상태에서는 조금 특이하게 Height를 설정한다.&lt;/p&gt;
&lt;p&gt;정점의 개수가 &lt;code&gt;V&lt;/code&gt;라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[Source] = V
height[나머지] = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로 시작한다.&lt;/p&gt;
&lt;p&gt;처음 이것을 보고 가장 궁금했던 부분이&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Source만 왜 갑자기 V만큼 높은 곳에 올려놓을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;였다.&lt;/p&gt;
&lt;p&gt;Push-Relabel은 시작하자마자 Source에서 나가는 모든 간선을 가능한 만큼 채운다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A : capacity 10
S → B : capacity 5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라면 처음부터&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A : 10
S → B : 5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 전부 보낸다.&lt;/p&gt;
&lt;p&gt;즉 Source가 가지고 있던 유량을 일단 주변 정점에 쏟아붓는다.&lt;/p&gt;
&lt;p&gt;그 결과&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[A] = 10
excess[B] = 5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;여기서 앞선 세 알고리즘과 완전히 달라진다&lt;/h2&gt;
&lt;p&gt;포드-풀커슨이라면 먼저&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → ... → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 완성된 증가 경로를 찾아야 한다.&lt;/p&gt;
&lt;p&gt;Dinic도 Level Graph를 만든 뒤 결국 Source에서 Sink까지 이어지는 흐름을 따라 유량을 보낸다.&lt;/p&gt;
&lt;p&gt;하지만 Push-Relabel은 시작부터&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A
S → B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;까지만 생각한다.&lt;/p&gt;
&lt;p&gt;A 다음에 어디로 갈지,&lt;/p&gt;
&lt;p&gt;B 다음에 어디로 갈지는 아직 몰라도 된다.&lt;/p&gt;
&lt;p&gt;그냥 Source에서 보낼 수 있는 유량부터 보낸다.&lt;/p&gt;
&lt;p&gt;그리고 나중에 A와 B에 쌓인 Excess를 처리한다.&lt;/p&gt;
&lt;p&gt;이게 Push-Relabel을 공부하면서 가장 낯설면서도 재미있었던 부분이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Ford-Fulkerson / Edmonds-Karp / Dinic

경로를 먼저 찾는다.
         ↓
경로를 따라 유량을 보낸다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Push-Relabel

일단 유량을 밀어 넣는다.
그리고
중간에 쌓인 Excess를
지역적으로 정리한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Push는 무엇을 하는가?&lt;/h2&gt;
&lt;p&gt;이제 이름에 들어 있는 첫 번째 연산인 &lt;code&gt;Push&lt;/code&gt;를 보자.&lt;/p&gt;
&lt;p&gt;정점 &lt;code&gt;u&lt;/code&gt;에 Excess가 존재하고&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[u] &amp;gt; 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;어떤 간선 &lt;code&gt;u → v&lt;/code&gt;에 잔여 용량이 있으며&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;residualCapacity(u, v) &amp;gt; 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Height도&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[u] = height[v] + 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면 &lt;code&gt;u&lt;/code&gt;의 Excess 일부를 &lt;code&gt;v&lt;/code&gt;로 보낼 수 있다.&lt;/p&gt;
&lt;p&gt;얼마나 보낼 수 있을까?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;현재 u가 가지고 있는 Excess

vs

간선 u → v에 남아 있는 잔여 용량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 비교한다.&lt;/p&gt;
&lt;p&gt;따라서 실제 전송량은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min(
    excess[u],
    residualCapacity(u, v)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[A] = 10

A → T 잔여 용량 = 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min(10, 7) = 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 보낸다.&lt;/p&gt;
&lt;p&gt;그 결과&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[A] = 3
excess[T] += 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;이것이 Push다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그런데 아래로 보낼 곳이 없다면?&lt;/h2&gt;
&lt;p&gt;여기서 두 번째 연산이 나온다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Relabel&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;A에&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[A] = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 있는데 주변 정점의 Height 때문에 어디에도 Push할 수 없다고 해보자.&lt;/p&gt;
&lt;p&gt;이때 A의 높이를 올린다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[A] ↑&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러면 언젠가는 A보다 낮은 이웃 정점이 생긴다.&lt;/p&gt;
&lt;p&gt;그때 다시 Push할 수 있다.&lt;/p&gt;
&lt;p&gt;이 동작이 &lt;code&gt;Relabel&lt;/code&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Relabel은 아무렇게나 높이를 올리는 것이 아니다&lt;/h2&gt;
&lt;p&gt;처음에는 Relabel을&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[A]++&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도로 생각했다.&lt;/p&gt;
&lt;p&gt;하지만 실제로는 현재 잔여 간선으로 갈 수 있는 이웃들의 Height를 확인한다.&lt;/p&gt;
&lt;p&gt;그중 가장 낮은 Height를 찾고&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[A]
=
min(height[neighbor]) + 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 만든다.&lt;/p&gt;
&lt;p&gt;예를 들어 A에서 잔여 용량이 있는 이웃이&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;B : height 2
C : height 5
D : height 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면 가장 작은 값은 2다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[A] = 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로 바꾼다.&lt;/p&gt;
&lt;p&gt;그러면 바로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 Push할 수 있는 조건이 만들어진다.&lt;/p&gt;
&lt;p&gt;즉 Relabel은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;지금은 어디에도 유량을 보낼 수 없으니, 유량을 보낼 수 있는 이웃보다 한 단계 높은 곳으로 올라가자.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라고 생각하면 이해하기 편했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;작은 그래프로 직접 따라가보기&lt;/h2&gt;
&lt;p&gt;다음 그래프를 생각해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A : 10
S → B : 5

A → B : 4
A → T : 7
B → T : 8&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정점은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S, A, B, T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;총 4개다.&lt;/p&gt;
&lt;p&gt;따라서 처음 Height는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[S] = 4

height[A] = 0
height[B] = 0
height[T] = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;h3&gt;1단계: Source에서 일단 전부 밀어낸다&lt;/h3&gt;
&lt;p&gt;Source에서 나가는 간선을 전부 포화시킨다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A : 10 / 10
S → B : 5 / 5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[A] = 10
excess[B] = 5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;아직 중간 정점에서 Flow Conservation을 만족하지 않기 때문에 현재 상태는 Flow가 아니라 Preflow다.&lt;/p&gt;
&lt;h3&gt;2단계: A의 Excess를 처리한다&lt;/h3&gt;
&lt;p&gt;현재&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[A] = 0
height[B] = 0
height[T] = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라 어느 곳에도 Push할 수 없다.&lt;/p&gt;
&lt;p&gt;따라서 A를 Relabel한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[A] = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 &lt;code&gt;A → T&lt;/code&gt;로 Push할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A → T&lt;/code&gt;의 Capacity가 7이므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min(10, 7) = 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 보낸다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[A] = 3
excess[T] = 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;h3&gt;3단계: A에 남은 3을 B로 보낸다&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;A → B&lt;/code&gt;의 잔여 용량은 4이고&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[A] = 1
height[B] = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이므로 Push할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min(3, 4) = 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 B로 보내면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[A] = 0

excess[B]
= 5 + 3
= 8&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;h3&gt;4단계: B의 Excess를 처리한다&lt;/h3&gt;
&lt;p&gt;B의 Height는 아직 0이므로 먼저 Relabel한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;height[B] = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러면 &lt;code&gt;B → T&lt;/code&gt;로 Push할 수 있다.&lt;/p&gt;
&lt;p&gt;Capacity가 정확히 8이므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;B → T : 8&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 보낸다.&lt;/p&gt;
&lt;p&gt;결과적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[B] = 0

excess[T]
= 7 + 8
= 15&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Source와 Sink를 제외한 Excess가 모두 사라졌다&lt;/h2&gt;
&lt;p&gt;현재&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;excess[A] = 0
excess[B] = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;즉 모든 중간 정점에서 다시&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;들어온 유량 = 나간 유량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 성립한다.&lt;/p&gt;
&lt;p&gt;처음에는 Preflow였지만 이제 정상적인 Flow가 된 것이다.&lt;/p&gt;
&lt;p&gt;Sink가 받은 총 유량은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;15&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이므로 이 그래프의 최대 유량은 15가 된다.&lt;/p&gt;
&lt;p&gt;전체 과정을 짧게 정리하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Source 간선 포화
↓
A excess = 10
B excess = 5
↓
A Relabel
↓
A → T : 7 Push
↓
A → B : 3 Push
↓
B excess = 8
↓
B Relabel
↓
B → T : 8 Push
↓
모든 중간 정점 excess = 0
↓
Maximum Flow = 15&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;이 과정 어디에서도&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 완성된 증가 경로를 먼저 탐색하지 않았다.&lt;/p&gt;
&lt;p&gt;각 정점에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;내가 가지고 있는 Excess를
지금 어디로 보낼 수 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만 판단했다.&lt;/p&gt;
&lt;p&gt;이게 앞선 최대 유량 알고리즘들과 Push-Relabel의 가장 큰 차이로 느껴졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;네 알고리즘을 이어서 비교하면&lt;/h2&gt;
&lt;p&gt;이제 최대 유량 알고리즘을 네 가지까지 살펴봤다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;알고리즘&lt;/th&gt;
&lt;th&gt;내가 이해한 핵심&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;포드-풀커슨&lt;/td&gt;
&lt;td&gt;증가 경로를 하나 찾고 유량을 보낸다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에드몬드-카프&lt;/td&gt;
&lt;td&gt;BFS를 이용해 가장 짧은 증가 경로를 선택한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dinic&lt;/td&gt;
&lt;td&gt;BFS로 Level Graph를 만들고 Blocking Flow까지 처리한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push-Relabel&lt;/td&gt;
&lt;td&gt;완성된 경로를 찾지 않고 Excess를 정점 사이에서 지역적으로 이동시킨다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;조금 더 짧게 표현하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Ford-Fulkerson
→ 어떤 경로로 보낼까?

Edmonds-Karp
→ 가장 짧은 경로부터 보내자.

Dinic
→ 같은 최단 거리 구조라면
   한 번에 최대한 처리하자.

Push-Relabel
→ 굳이 완성된 경로를
   먼저 찾을 필요가 있을까?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 흐름으로 느껴졌다.&lt;/p&gt;
&lt;p&gt;특히 Push-Relabel에서 사고방식이 크게 바뀐다.&lt;/p&gt;
&lt;p&gt;앞의 세 알고리즘은 기본적으로 Source에서 Sink 방향으로 그래프 전체를 바라본다.&lt;/p&gt;
&lt;p&gt;반면 Push-Relabel에서는 각각의 Active Vertex가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;나는 지금 Excess가 얼마 있지?

어느 이웃으로 Push할 수 있지?

아무 데도 못 보내면
Height를 얼마나 올려야 하지?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 판단한다.&lt;/p&gt;
&lt;p&gt;그래서 Maximum Flow라는 전역적인 문제를 굉장히 Local한 연산들의 반복으로 해결한다는 점이 흥미로웠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;공부하고 나서&lt;/h2&gt;
&lt;p&gt;이번에 가장 어려웠던 개념은 역시 세 가지였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Preflow

Excess

Height&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;특히 처음에는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Flow 알고리즘인데 중간 정점에 유량을 남겨둬도 된다고?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라는 부분이 가장 어색했다.&lt;/p&gt;
&lt;p&gt;그런데 Preflow를&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;아직 정리가 끝나지 않은 Flow&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 생각하니 조금 쉬워졌다.&lt;/p&gt;
&lt;p&gt;Excess도&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이 정점이 아직 처리해야 하는 유량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 보면 어렵지 않았다.&lt;/p&gt;
&lt;p&gt;Height 역시 실제 그래프의 거리라고 생각하면 오히려 헷갈렸다.&lt;/p&gt;
&lt;p&gt;Push-Relabel에서 Height는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;유량을 어느 방향으로
Push할 수 있는지 결정하는 Label&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;에 더 가깝다.&lt;/p&gt;
&lt;p&gt;이번 글을 정리하면서 가장 기억에 남은 것은 결국 다음 흐름이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Source에서 일단 최대한 밀어낸다.
2. 중간 정점에 Excess가 생긴다.
3. 낮은 이웃이 있으면 Push한다.
4. 보낼 곳이 없으면 Relabel한다.
5. 다시 Push한다.
6. 모든 중간 정점의 Excess가 사라진다.
7. Maximum Flow&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 Push-Relabel이라는 이름 자체가 알고리즘을 거의 그대로 설명하고 있었다.&lt;/p&gt;
&lt;p&gt;포드-풀커슨을 공부할 때는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이미 보낸 유량을 왜 되돌려야 할까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;가 가장 큰 질문이었다.&lt;/p&gt;
&lt;p&gt;에드몬드-카프에서는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;왜 DFS 대신 BFS를 사용할까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;가 핵심이었다.&lt;/p&gt;
&lt;p&gt;Dinic에서는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;BFS로 만든 정보를 증가 경로 하나보다 더 많이 활용할 수 없을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;를 따라갔다.&lt;/p&gt;
&lt;p&gt;그리고 Push-Relabel에서는 질문 자체가 달라졌다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;최대 유량을 구하려면 꼭 Source에서 Sink까지의 경로를 먼저 찾아야 할까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Push-Relabel의 답은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;아니다.

일단 유량을 밀어 넣고,
지역적으로 정리해도 된다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;였다.&lt;/p&gt;
&lt;p&gt;이렇게 같은 Maximum Flow 문제를 이렇게 전혀 다른 관점에서 해결할 수 있다는 점이 가장 흥미로웠다.&lt;/p&gt;</description>
      <category>CS/자료구조 &amp;middot; 알고리즘</category>
      <category>Push-Relabel</category>
      <category>알고리즘</category>
      <category>최대 유량</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/68</guid>
      <comments>https://slowgrid.tistory.com/entry/til-Push-Relabel#entry68comment</comments>
      <pubDate>Tue, 1 Sep 2026 16:16:36 +0900</pubDate>
    </item>
    <item>
      <title>객체탐지(Object Detection) 기초 (3) &amp;mdash; Precision-Recall Curve부터 AP, mAP50-95까지</title>
      <link>https://slowgrid.tistory.com/entry/Object-Detection-Precision-Recall-Curve-AP-mAP50-95</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;객체탐지(Object Detection) 기초 시리즈&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/Object-Detection-Bounding-Box-Confidence-Score&quot;&gt;1편 &amp;mdash; Bounding Box와 Confidence Score 이해하기&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/Object-Detection-IoU-TP-FP-FN-Precision-Recall&quot;&gt;2편 &amp;mdash; IoU, TP-FP-FN, Precision과 Recall&lt;/a&gt;&lt;br /&gt;&lt;b&gt;3편 &amp;mdash; Precision-Recall Curve부터 AP, mAP50-95까지 &amp;larr; 현재 글&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서는 Prediction과 Ground Truth를 비교해&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;IoU
&amp;rarr; TP / FP / FN
&amp;rarr; Precision / Recall&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 계산하는 흐름을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Precision과 Recall에는 한 가지 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Confidence Threshold가 달라질 때마다 값도 달라진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;conf = 0.1
conf = 0.2
conf = 0.3
conf = 0.4
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 변경하면 Prediction의 수가 바뀌고,&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;TP
FP
FN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도 바뀌기 때문에 Precision과 Recall 역시 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 객체탐지에서는 특정 Threshold 하나만 보는 대신 &lt;b&gt;Precision과 Recall 전체의 관계&lt;/b&gt;를 평가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 등장하는 것이 Precision-Recall Curve와 AP, mAP이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Confidence Threshold가 변하면 무슨 일이 생길까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 모델이 다음과 같은 Prediction을 만들었다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Prediction A &amp;rarr; 0.95
Prediction B &amp;rarr; 0.82
Prediction C &amp;rarr; 0.61
Prediction D &amp;rarr; 0.37
Prediction E &amp;rarr; 0.12&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Confidence Threshold가&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0.90&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이면 A만 남는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0.60&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이면 A, B, C가 남는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0.10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이면 거의 모든 Prediction이 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Threshold를 낮춰갈수록 Prediction을 하나씩 더 받아들이게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계에서 TP와 FP를 누적해 Precision과 Recall을 계산하면 여러 개의&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(Recall, Precision)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조합을 얻을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Precision-Recall Curve&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점들을 그래프로 연결한 것이 &lt;b&gt;Precision-Recall Curve(PR Curve)​&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Precision
1.0 ┤●
    │ ●
    │   ●
    │      ●
    │          ●
0.0 └────────────── Recall
    0             1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 모델이라면 가능하면&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;Recall도 높고
Precision도 높은&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영역을 유지해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프 기준으로는 오른쪽 위 영역을 오래 유지하는 모델이 좋은 모델이라고 볼 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. AP는 단순히 Precision과 Recall의 평균이 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 AP를&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Precision과 Recall을 평균 낸 값&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 생각하기 쉬운데 정확하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AP(Average Precision)​&lt;/b&gt;는 PR Curve의 성능을 하나의 숫자로 요약한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;PR Curve 아래의 면적&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 평가한다고 이해하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ultralytics 구현에서는 Prediction을 Confidence가 높은 순서대로 정렬하고 TP와 FP를 누적해 Precision과 Recall Curve를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 현재 구현은 COCO 방식의 &lt;b&gt;101-point interpolation&lt;/b&gt;을 이용해 AP를 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 AP는 단순히 특정 &lt;code&gt;conf=0.25&lt;/code&gt;에서 계산한 Precision과 Recall 두 숫자로 구할 수 있는 값이 아니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. IoU Threshold에 따라서도 AP가 달라진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AP 계산에는 Confidence만 영향을 주는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prediction을 TP로 인정하는 &lt;b&gt;IoU 기준&lt;/b&gt;도 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;IoU &amp;gt;= 0.50&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이면 비교적 느슨하게 위치를 맞춰도 TP가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;IoU &amp;gt;= 0.75&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이면 Bounding Box 위치가 훨씬 정확해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 같은 모델에서도&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;AP@0.50
AP@0.75
AP@0.90&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 다를 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. AP와 mAP의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AP는 기본적으로 &lt;b&gt;하나의 클래스&lt;/b&gt;에 대한 성능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 클래스가&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;person
car
bicycle&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 개라면 각각&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;AP_person
AP_car
AP_bicycle&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이것들의 평균을 구하면&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;mAP
=
mean Average Precision&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;mAP
=
여러 클래스의 AP 평균&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그러면 person 단일 클래스 프로젝트의 mAP는?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Thermal Pedestrian Detection 프로젝트는 &lt;code&gt;person&lt;/code&gt; 하나만 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 클래스 평균을 내더라도 클래스가 하나뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 개념적으로&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;mAP50
=
person의 AP50&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;mAP50-95
=
person 클래스의
AP@0.50 ~ AP@0.95 평균&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 현재 프로젝트에서 &lt;code&gt;mAP&lt;/code&gt;라는 이름은 Ultralytics의 표준 평가 형식을 그대로 사용하는 의미가 크다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. mAP50이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;mAP50&lt;/code&gt;은 TP 여부를 판단하는 IoU 기준을&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;IoU = 0.50&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 두고 계산한 AP를 클래스별로 평균 낸 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트처럼 클래스가 하나라면 사실상 해당 &lt;code&gt;person&lt;/code&gt; 클래스의 &lt;a href=&quot;mailto:AP@0.50&quot;&gt;AP@0.50&lt;/a&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IoU 0.5는 비교적 관대한 기준이기 때문에 객체의 위치가 완벽하지 않더라도 어느 정도 맞으면 올바른 탐지로 인정될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. mAP50-95란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;mAP50-95&lt;/code&gt;는 훨씬 엄격하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 10개의 IoU Threshold를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0.50
0.55
0.60
0.65
0.70
0.75
0.80
0.85
0.90
0.95&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 IoU Threshold에서 AP를 계산한 뒤 평균한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;객체가 대략 어디에 있는지 찾는 능력
+
Bounding Box를 정확하게 맞추는 능력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 함께 평가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ultralytics 역시 mAP50-95를 IoU 0.50부터 0.95까지 여러 조건에서 계산한 mAP의 평균으로 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일반적으로&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;mAP50 &amp;gt; mAP50-95&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 되는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;높은 IoU 조건으로 갈수록 TP 판정이 훨씬 엄격해지기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 이번 프로젝트의 결과 다시 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트 결과는 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;전처리&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Precision&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Recall&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;mAP50&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;mAP50-95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.8044&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.7691&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;b&gt;0.8725&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;b&gt;0.4975&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLAHE&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.8091&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;b&gt;0.7776&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.8561&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.4894&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bilateral + CLAHE&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;b&gt;0.8159&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.7327&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.8352&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.4639&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각 모델의 특징을 조금 더 정확하게 해석할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. Baseline&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Baseline은&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;mAP50    = 0.8725
mAP50-95 = 0.4975&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 세 방법 중 가장 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 PR Curve와 여러 IoU 조건을 종합했을 때 &lt;b&gt;전체적인 탐지 성능과 Localization 품질은 Baseline이 가장 우수했다&lt;/b&gt;고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 표준 객체탐지 성능 지표만 기준으로 선택한다면 Baseline이 가장 강한 후보이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. Bilateral + CLAHE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bilateral + CLAHE는&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Precision = 0.8159&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 가장 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Recall = 0.7327&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 가장 낮다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 평가된 operating point에서는&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;탐지한 결과의 정확성은 상대적으로 높지만
실제 객체를 놓치는 비율도 상대적으로 높다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 해석할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;야간 보행자를 최대한 놓치지 않는 것이 중요한 현재 프로젝트 목적에는 다소 불리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. CLAHE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLAHE의 결과는&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Precision = 0.8091
Recall    = 0.7776&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Baseline과 비교하면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Precision +0.47%p
Recall    +0.85%p&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;mAP50    -1.64%p
mAP50-95 -0.81%p&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감소했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 CLAHE가 모든 지표에서 더 좋은 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 AP 기준의 전체적인 탐지 성능은 Baseline보다 낮다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 현재 프로젝트가 &lt;b&gt;보행자 미탐(FN)​&lt;/b&gt;을 더 중요하게 본다면 높은 Recall을 가진 CLAHE를 선택하는 논리는 충분히 가능하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. 하지만 여기서 Recall 숫자 하나만 보면 안 된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 학습에서 한 단계 더 보완하고 싶은 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ultralytics validation 결과에 표시되는 Precision과 Recall은 기본적으로 임의의 &lt;code&gt;conf=0.25&lt;/code&gt;에서 계산한 값이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Ultralytics에서는 PR Curve를 만든 뒤 &lt;b&gt;F1이 최대가 되는 Confidence 지점의 P/R을 요약값으로 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;CLAHE Recall = 0.7776&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 결과는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Confidence Threshold에서 CLAHE의 FN이 항상 가장 적다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 의미는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 정확한 모델 선택을 위해서는 &lt;b&gt;PR Curve 전체를 비교해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 목표를&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Recall &amp;gt;= 0.90&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 정한 뒤,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 Recall을 만족할 때의&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Precision
FP 개수
FN 개수
Confidence Threshold&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 비교하는 것이 실제 운영 환경에 더 적합하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;14. mAP가 가장 높은 모델이 항상 최종 모델은 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 결과를 공부하면서 가장 중요하게 느낀 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 선택은&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;가장 큰 숫자 찾기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시스템에서 가장 위험한 오류가 무엇인가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 정의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 일반적인 탐지 성능을 최대화하고 싶다면&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;mAP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 중요하게 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bounding Box 위치 정확도가 특히 중요하다면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;mAP50-95&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 중요할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;False Positive를 최대한 줄이고 싶다면&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Precision&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 중점적으로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 실제 객체를 놓치는 것이 더 위험하다면&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Recall&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중요한 지표가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트의 야간 보행자 탐지에서는&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;실제 사람이 있는데 탐지하지 못함
=
False Negative&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 중요한 위험이기 때문에 Recall을 중요하게 보는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;15. 지금까지의 객체탐지 평가 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 세 편에서 공부한 내용을 하나의 흐름으로 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Image
 &amp;darr;
YOLO
 &amp;darr;
Prediction Bounding Box
 &amp;darr;
Confidence Score
 &amp;darr;
Ground Truth와 비교
 &amp;darr;
IoU
 &amp;darr;
One-to-One Matching
 &amp;darr;
TP / FP / FN
 &amp;darr;
Precision / Recall
 &amp;darr;
PR Curve
 &amp;darr;
AP
 &amp;darr;
mAP50 / mAP50-95&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 YOLO를 단순히&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이미지 &amp;rarr; 사람 탐지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;어디에 있다고 예측했는가?
얼마나 높은 점수를 부여했는가?
Ground Truth와 얼마나 겹치는가?
몇 개를 올바르게 찾았는가?
몇 개를 잘못 찾았는가?
몇 개를 놓쳤는가?
Threshold가 달라지면 성능이 어떻게 변하는가?
Box 위치는 얼마나 정확한가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 모두 살펴봐야 모델의 성능을 제대로 이해할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;16. 다음 실습에서 확인할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 공식만 보는 것보다 실제 Validation 결과를 직접 움직여 보는 것이 더 중요할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 아래 내용을 직접 확인해볼 예정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;conf=0.1&lt;/code&gt;, &lt;code&gt;0.25&lt;/code&gt;, &lt;code&gt;0.5&lt;/code&gt;, &lt;code&gt;0.75&lt;/code&gt;에서 Prediction 개수 비교&lt;/li&gt;
&lt;li&gt;Confidence 변화에 따른 TP / FP / FN 비교&lt;/li&gt;
&lt;li&gt;Ground Truth와 Prediction을 한 이미지에 동시에 표시&lt;/li&gt;
&lt;li&gt;Evaluation IoU를 &lt;code&gt;0.3&lt;/code&gt;, &lt;code&gt;0.5&lt;/code&gt;, &lt;code&gt;0.75&lt;/code&gt;로 변경해 TP 변화 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PR_curve.png&lt;/code&gt; 직접 읽어보기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F1_curve.png&lt;/code&gt;와 최적 Confidence 확인&lt;/li&gt;
&lt;li&gt;Baseline과 CLAHE의 PR Curve 비교&lt;/li&gt;
&lt;li&gt;동일 Recall 조건에서 두 모델의 Precision 비교&lt;/li&gt;
&lt;li&gt;작은 보행자와 먼 거리 보행자에서 FN 별도 분석&lt;/li&gt;
&lt;li&gt;mAP50과 mAP50-95 차이가 큰 Bounding Box 직접 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 다음 단계에서는 단순히&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;어떤 모델의 Recall 숫자가 더 높은가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 끝나는 것이 아니라,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 원하는 Recall 수준을 만족시키려면 Confidence Threshold를 얼마로 두어야 하고, 그때 FP는 얼마나 증가하는가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 직접 확인해보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체탐지에서 모델 학습이 끝이라고 생각했는데, 실제로는 &lt;b&gt;평가 지표를 이해하고 목적에 맞는 Operating Point를 선택하는 과정까지가 모델 개발의 일부&lt;/b&gt;라는 것을 알게 되었다.&lt;/p&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>AP</category>
      <category>mAP50-95</category>
      <category>object detection</category>
      <category>Precision</category>
      <category>Recall Curve</category>
      <category>객체탐지</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/67</guid>
      <comments>https://slowgrid.tistory.com/entry/Object-Detection-Precision-Recall-Curve-AP-mAP50-95#entry67comment</comments>
      <pubDate>Mon, 31 Aug 2026 18:00:35 +0900</pubDate>
    </item>
    <item>
      <title>객체탐지(Object Detection) 기초 (2) &amp;mdash; IoU, TP-FP-FN, Precision과 Recall</title>
      <link>https://slowgrid.tistory.com/entry/Object-Detection-IoU-TP-FP-FN-Precision-Recall</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;객체탐지(Object Detection) 기초 시리즈&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/Object-Detection-Bounding-Box-Confidence-Score&quot;&gt;1편 — Bounding Box와 Confidence Score 이해하기&lt;/a&gt;&lt;br&gt;&lt;strong&gt;2편 — IoU, TP-FP-FN, Precision과 Recall ← 현재 글&lt;/strong&gt;&lt;br&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/Object-Detection-Precision-Recall-Curve-AP-mAP50-95&quot;&gt;3편 — Precision-Recall Curve부터 AP, mAP50-95까지&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;지난 글에서는 객체탐지 모델이 무엇을 출력하는지 살펴봤다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Bounding Box
Class
Confidence Score&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 모델이 Bounding Box를 하나 만들었다고 해서 그것을 바로&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사람을 제대로 찾았다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라고 판단할 수는 없다.&lt;/p&gt;
&lt;p&gt;객체탐지 평가에서는 모델이 만든 &lt;strong&gt;Prediction Bounding Box&lt;/strong&gt;를 정답 Annotation인 &lt;strong&gt;Ground Truth Bounding Box&lt;/strong&gt;와 비교해야 한다.&lt;/p&gt;
&lt;p&gt;그 중심에 있는 개념이 &lt;strong&gt;IoU(Intersection over Union)​&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. IoU란?&lt;/h2&gt;
&lt;p&gt;IoU는 두 Bounding Box가 얼마나 겹치는지를 나타내는 값이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;IoU
=
Intersection
────────────
Union&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;두 Box가 겹치는 영역
÷
두 Box의 전체 합집합 영역&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;값의 범위는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 ≤ IoU ≤ 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;두 Box가 전혀 겹치지 않으면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;IoU = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;완전히 동일하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;IoU = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. IoU를 직접 계산해보기&lt;/h2&gt;
&lt;p&gt;두 Box가 다음과 같이 있다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ground_truth = (100, 100, 200, 300)
prediction   = (110, 120, 210, 310)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IoU는 다음과 같이 계산할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def compute_iou(box1, box2):
    x1 = max(box1[0], box2[0])
    y1 = max(box1[1], box2[1])

    x2 = min(box1[2], box2[2])
    y2 = min(box1[3], box2[3])

    intersection_width = max(0, x2 - x1)
    intersection_height = max(0, y2 - y1)

    intersection = (
        intersection_width
        * intersection_height
    )

    box1_area = (
        (box1[2] - box1[0])
        * (box1[3] - box1[1])
    )

    box2_area = (
        (box2[2] - box2[0])
        * (box2[3] - box2[1])
    )

    union = box1_area + box2_area - intersection

    if union &amp;lt;= 0:
        return 0.0

    return intersection / union&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;공식만 보면 복잡해 보이지만 핵심은 단순하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;교집합 면적
÷
합집합 면적&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. IoU Threshold가 필요하다&lt;/h2&gt;
&lt;p&gt;IoU를 구했다면 다음으로 결정해야 할 것이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;IoU가 얼마 이상이어야 제대로 찾은 것으로 인정할 것인가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;현재 프로젝트의 Validation Demo에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;MATCH_IOU = 0.50&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 사용한다.&lt;/p&gt;
&lt;p&gt;따라서 단순화하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;IoU &amp;gt;= 0.50
→ 같은 객체로 매칭할 수 있는 후보

IoU &amp;lt; 0.50
→ 충분히 위치가 일치하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 중요한 주의점이 하나 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 모든 &lt;code&gt;IoU Threshold&lt;/code&gt;가 같은 의미는 아니다&lt;/h2&gt;
&lt;p&gt;객체탐지를 공부하다 보면 &lt;code&gt;IoU=0.5&lt;/code&gt;, &lt;code&gt;iou=0.7&lt;/code&gt; 등의 숫자가 여러 곳에 등장한다.&lt;/p&gt;
&lt;p&gt;처음에는 이것들이 모두 같은 설정이라고 생각하기 쉽다.&lt;/p&gt;
&lt;p&gt;하지만 서로 완전히 다른 목적으로 사용될 수 있다.&lt;/p&gt;
&lt;h3&gt;1. 평가용 IoU Threshold&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Prediction
vs
Ground Truth&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 비교해 TP인지 FP인지 판단할 때 사용한다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트 Demo의&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;MATCH_IOU = 0.50&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 여기에 해당한다.&lt;/p&gt;
&lt;h3&gt;2. NMS용 IoU Threshold&lt;/h3&gt;
&lt;p&gt;기존 YOLO 방식에서는 Prediction끼리 비교하여 중복 Box를 제거할 때도 IoU를 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Prediction
vs
Prediction&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;Ultralytics의 일반적인 predict 설정에서 &lt;code&gt;iou&lt;/code&gt; 인자는 NMS용 IoU threshold를 의미한다.&lt;/p&gt;
&lt;p&gt;다만 현재 프로젝트에서 사용 중인 &lt;strong&gt;YOLO26은 기본적으로 End-to-End NMS-Free 모델&lt;/strong&gt;이므로 기본 추론에서는 일반적인 NMS 기반 중복 제거 과정을 거치지 않는다.&lt;/p&gt;
&lt;p&gt;정리하면 다음처럼 구분하는 것이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;평가 IoU
→ Prediction과 Ground Truth 비교

NMS IoU
→ Prediction과 Prediction 비교&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 IoU라는 수식을 사용하지만 목적은 전혀 다르다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Matching은 단순히 IoU만 보는 것이 아니다&lt;/h2&gt;
&lt;p&gt;한 이미지에 사람이 여러 명 있고 Prediction도 여러 개라면 단순히 모든 IoU가 0.5 이상인 조합을 TP로 세면 문제가 생긴다.&lt;/p&gt;
&lt;p&gt;예를 들어 Ground Truth 한 개 주변에 Prediction이 세 개 있다고 생각해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GT 1개
Prediction 3개&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;세 Prediction이 모두 같은 GT와 IoU 0.5 이상이라고 해서 TP가 3개가 될 수는 없다.&lt;/p&gt;
&lt;p&gt;한 실제 사람을 세 번 맞혔다고 평가해서는 안 되기 때문이다.&lt;/p&gt;
&lt;p&gt;그래서 객체탐지 평가에서는 기본적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;하나의 Ground Truth
↔
하나의 Prediction&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로 &lt;strong&gt;one-to-one matching&lt;/strong&gt;을 수행한다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트 Demo 역시 모든 IoU를 구한 뒤 높은 IoU 조합부터 연결하고, 이미 매칭된 GT와 Prediction은 다시 사용하지 않는다.&lt;/p&gt;
&lt;p&gt;일반적인 다중 클래스 객체탐지에서는 여기에 클래스 일치 조건까지 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;person Prediction
→ person Ground Truth와 비교&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 프로젝트는 &lt;code&gt;person&lt;/code&gt; 단일 클래스이므로 클래스 문제는 단순하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. TP, FP, FN&lt;/h2&gt;
&lt;p&gt;Matching이 끝나면 Prediction 결과를 TP, FP, FN으로 나눌 수 있다.&lt;/p&gt;
&lt;h3&gt;TP — True Positive&lt;/h3&gt;
&lt;p&gt;실제로 사람이 있고 모델도 올바르게 찾은 경우이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Ground Truth 존재
+
Prediction과 정상 매칭

→ TP&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;FP — False Positive&lt;/h3&gt;
&lt;p&gt;모델이 사람이라고 예측했지만 대응하는 Ground Truth를 찾지 못한 경우이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Prediction 존재
+
매칭되는 Ground Truth 없음

→ FP&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;쉽게 말하면 &lt;strong&gt;오탐&lt;/strong&gt;이다.&lt;/p&gt;
&lt;h3&gt;FN — False Negative&lt;/h3&gt;
&lt;p&gt;실제로 사람이 있지만 어떤 Prediction과도 매칭되지 않은 경우이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Ground Truth 존재
+
매칭되는 Prediction 없음

→ FN&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 &lt;strong&gt;미탐&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 그런데 TN은 왜 거의 나오지 않을까?&lt;/h2&gt;
&lt;p&gt;분류 문제에서는 보통 네 가지 값을 배운다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TP
FP
FN
TN&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 객체탐지 성능을 설명할 때는 대부분 TP, FP, FN만 등장한다.&lt;/p&gt;
&lt;p&gt;그 이유는 객체탐지의 &lt;strong&gt;배경 영역을 하나의 명확한 Negative 샘플로 세기 어렵기 때문&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;이미지에는 객체가 아닌 위치가 사실상 매우 많이 존재한다.&lt;/p&gt;
&lt;p&gt;어떤 픽셀 영역 하나를 사람이 아닌 것으로 제대로 판단했다고 해서 이를 TN 하나로 셀 수도 없다.&lt;/p&gt;
&lt;p&gt;따라서 객체탐지에서는 일반적인 Accuracy 대신&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Precision
Recall
AP
mAP&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 지표를 주로 사용한다.&lt;/p&gt;
&lt;p&gt;이 부분이 이미지 분류의 Confusion Matrix와 객체탐지 평가가 다른 중요한 이유 중 하나다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. TP, FP, FN 계산 예시&lt;/h2&gt;
&lt;p&gt;실제 사람이 5명 있다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Ground Truth = 5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;모델은 6개의 Prediction을 만들었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Prediction = 6&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Matching 결과 4개가 정상적으로 Ground Truth와 연결되었다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TP = 4&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;Ground Truth 5개 중 4개만 찾았으므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FN = 5 - 4
   = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prediction 6개 중 4개만 정답이므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FP = 6 - 4
   = 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;결과는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TP = 4
FP = 2
FN = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. Precision이란?&lt;/h2&gt;
&lt;p&gt;Precision은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;모델이 사람이라고 탐지한 것 가운데 실제 사람이 얼마나 많은가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;를 나타낸다.&lt;/p&gt;
&lt;p&gt;공식은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Precision = TP / (TP + FP)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;앞의 예제에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TP = 4
FP = 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Precision
= 4 / (4 + 2)
= 0.667&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;약 66.7%이다.&lt;/p&gt;
&lt;p&gt;즉 모델이 사람이라고 주장한 6개 중 4개가 올바른 탐지였다.&lt;/p&gt;
&lt;p&gt;따라서 Precision은 FP와 밀접하게 연결된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FP 감소
→ Precision 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. Recall이란?&lt;/h2&gt;
&lt;p&gt;Recall은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;실제로 존재하는 사람 가운데 모델이 얼마나 많이 찾았는가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;를 나타낸다.&lt;/p&gt;
&lt;p&gt;공식은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Recall = TP / (TP + FN)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;앞의 예에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TP = 4
FN = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Recall
= 4 / (4 + 1)
= 0.8&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 80%이다.&lt;/p&gt;
&lt;p&gt;실제 사람 5명 중 4명을 찾았다는 의미이다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FN 감소
→ Recall 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 이해할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11. 왜 Precision과 Recall을 함께 봐야 할까?&lt;/h2&gt;
&lt;p&gt;Confidence Threshold를 매우 높게 설정해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;conf = 0.90&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정말 확신이 높은 Prediction만 남게 된다.&lt;/p&gt;
&lt;p&gt;그 결과 FP가 줄어들 가능성이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FP ↓
→ Precision ↑ 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 Confidence가 조금 낮았던 실제 사람까지 제거될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FN ↑
→ Recall ↓ 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반대로 Threshold를 낮추면 더 많은 Prediction이 살아남는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Recall ↑ 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;대신 사람과 비슷한 다른 물체까지 검출하면서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FP ↑
→ Precision ↓ 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;성이 생긴다.&lt;/p&gt;
&lt;p&gt;그래서 Precision과 Recall 사이에는 &lt;strong&gt;운영 Threshold에 따른 Trade-off&lt;/strong&gt;가 존재한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;12. &lt;code&gt;conf=0.25&lt;/code&gt; Demo와 공식 Validation 결과는 구분해야 한다&lt;/h2&gt;
&lt;p&gt;이번에 정리하면서 가장 중요하게 보완한 부분이다.&lt;/p&gt;
&lt;p&gt;프로젝트 Demo에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;conf=0.25&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 Prediction을 만든 뒤 IoU 0.5를 이용해 TP/FP/FN과 Recall을 직접 계산한다.&lt;/p&gt;
&lt;p&gt;이 값은 &lt;strong&gt;conf=0.25라는 특정 operating point에서의 결과&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;반면 Ultralytics &lt;code&gt;model.val()&lt;/code&gt;의 공식 AP/mAP 평가는 같은 방식으로 &lt;code&gt;conf=0.25&lt;/code&gt; 하나만 놓고 계산하지 않는다.&lt;/p&gt;
&lt;p&gt;현재 Ultralytics validation은 PR Curve 계산을 위해 기본 최소 confidence를 &lt;code&gt;0.001&lt;/code&gt;로 두고 낮은 점수의 Prediction까지 수집하며, 출력되는 요약 Precision과 Recall은 &lt;strong&gt;F1 Score가 최대가 되는 confidence 지점&lt;/strong&gt;을 기준으로 계산한다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Demo의 Recall(conf=0.25)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;과&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Ultralytics val 결과의 Recall&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 같은 값이라고 생각하면 안 된다.&lt;/p&gt;
&lt;p&gt;평가 조건이 다르기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;13. Thermal Pedestrian Detection에서 Recall이 중요한 이유&lt;/h2&gt;
&lt;p&gt;현재 프로젝트는 야간 보행자 탐지를 다룬다.&lt;/p&gt;
&lt;p&gt;이 문제에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;나무를 사람이라고 판단
→ FP&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;도 문제지만,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;실제 사람이 있는데 탐지하지 못함
→ FN&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;은 더욱 직접적인 위험과 연결될 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 프로젝트에서는 FN을 줄이고 Recall을 높이는 것을 중요한 기준으로 보고 있다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 한 단계 더 생각할 필요가 있다.&lt;/p&gt;
&lt;p&gt;단순히&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Recall이 가장 높은 모델을 선택한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;에서 끝나는 것보다 실제 시스템에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;목표 Recall 결정
        ↓
Confidence Threshold 선택
        ↓
그때의 Precision / FP 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;과 같은 방식으로 &lt;strong&gt;운영 지점(Operating Point)​&lt;/strong&gt;을 정하는 것이 더 적절하다.&lt;/p&gt;
&lt;p&gt;예를 들어 안전 때문에 최소 Recall 90%가 필요하다면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;어떤 모델이 Recall 90%를 유지하면서 FP를 가장 적게 발생시키는가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;를 비교하는 방식이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;14. 이번 글의 흐름&lt;/h2&gt;
&lt;p&gt;객체탐지 평가를 하나의 흐름으로 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Prediction 생성
        ↓
Ground Truth와 비교
        ↓
IoU 계산
        ↓
One-to-One Matching
        ↓
TP / FP / FN
        ↓
Precision / Recall&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 흐름을 이해하고 나니 Precision과 Recall의 공식도 단순 암기할 필요가 없어졌다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Precision
→ 내가 탐지했다고 말한 것 중 얼마나 맞았는가?

Recall
→ 실제 객체 중 얼마나 찾아냈는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 여전히 문제가 하나 남는다.&lt;/p&gt;
&lt;p&gt;Precision과 Recall은 &lt;strong&gt;Confidence Threshold에 따라 계속 달라진다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그렇다면 Threshold를 하나 임의로 선택하지 않고 모델 전체 성능을 비교하려면 어떻게 해야 할까?&lt;/p&gt;
&lt;p&gt;이 질문을 해결하기 위해 다음 글에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Precision-Recall Curve
        ↓
        AP
        ↓
      mAP50
        ↓
     mAP50-95&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 정리해보려고 한다.&lt;/p&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>FN</category>
      <category>FP</category>
      <category>IOU</category>
      <category>object detection</category>
      <category>Precision</category>
      <category>Recall</category>
      <category>tp</category>
      <category>객체탐지</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/66</guid>
      <comments>https://slowgrid.tistory.com/entry/Object-Detection-IoU-TP-FP-FN-Precision-Recall#entry66comment</comments>
      <pubDate>Sun, 30 Aug 2026 14:54:00 +0900</pubDate>
    </item>
    <item>
      <title>객체탐지(Object Detection) 기초 (1) &amp;mdash; Bounding Box와 Confidence Score 이해하기</title>
      <link>https://slowgrid.tistory.com/entry/Object-Detection-Bounding-Box-Confidence-Score</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;객체탐지(Object Detection) 기초 시리즈&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1편 — Bounding Box와 Confidence Score 이해하기 ← 현재 글&lt;/strong&gt;&lt;br&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/Object-Detection-IoU-TP-FP-FN-Precision-Recall&quot;&gt;2편 — IoU, TP-FP-FN, Precision과 Recall&lt;/a&gt;&lt;br&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/Object-Detection-Precision-Recall-Curve-AP-mAP50-95&quot;&gt;3편 — Precision-Recall Curve부터 AP, mAP50-95까지&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;지난 학습에서는 열화상 이미지가 &lt;code&gt;uint16&lt;/code&gt; 형태로 저장되어 있다는 점과, Percentile Normalization과 CLAHE를 이용해 YOLO가 사용할 수 있는 형태로 전처리하는 과정을 살펴봤다.&lt;/p&gt;
&lt;p&gt;이번에는 그 다음 단계에서 생긴 의문을 정리해보려고 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;YOLO는 이미지를 보고 무엇을 출력하고, 우리는 그것을 어떻게 해석해야 할까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;현재 진행 중인 Thermal Pedestrian Detection 프로젝트에서는 야간 열화상 이미지에서 &lt;code&gt;person&lt;/code&gt;을 검출한다.&lt;/p&gt;
&lt;p&gt;전체 흐름을 아주 단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Thermal Image
    ↓
Image Preprocessing
    ↓
YOLO26n
    ↓
Bounding Box + Confidence&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이번 글에서는 우선 &lt;strong&gt;모델이 만들어내는 결과 자체&lt;/strong&gt;에 집중한다.&lt;br&gt;평가 지표인 IoU, TP/FP/FN, Precision, Recall은 다음 글에서 이어서 정리할 예정이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. Image Classification과 Object Detection의 차이&lt;/h2&gt;
&lt;p&gt;처음에는 AI가 이미지에서 사람을 찾는다는 것을 다음처럼 생각했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지
 ↓
사람 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 이 문제는 객체탐지보다는 &lt;strong&gt;Image Classification&lt;/strong&gt;에 가깝다.&lt;/p&gt;
&lt;p&gt;일반적인 이미지 분류는 이미지 전체에 대해 어떤 클래스에 속하는지를 예측한다.&lt;/p&gt;
&lt;p&gt;예를 들면,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Image
 ↓
person&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;과 같은 결과이다.&lt;/p&gt;
&lt;p&gt;하지만 자율주행이나 보행자 탐지에서는 단순히&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이미지 안에 사람이 존재한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라는 정보만으로는 부족하다.&lt;/p&gt;
&lt;p&gt;더 중요한 것은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사람이 이미지의 어디에 존재하는가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;그래서 객체탐지는 크게 다음 두 문제를 함께 해결한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;무엇인가? → Classification
어디에 있는가? → Localization&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 객체탐지를 처음 이해할 때는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Object Detection
=
Classification
+
Localization&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 생각하면 비교적 직관적이다.&lt;/p&gt;
&lt;p&gt;다만 실제 객체탐지는 여기에 한 단계가 더 있다.&lt;/p&gt;
&lt;p&gt;하나의 이미지 안에서 &lt;strong&gt;여러 객체 인스턴스를 각각 찾아야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;예를 들어 사람이 세 명 있다면 단순히 &lt;code&gt;person&lt;/code&gt;이라는 클래스 하나를 반환하는 것이 아니라,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Person #1 → 위치 A
Person #2 → 위치 B
Person #3 → 위치 C&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 각각의 위치를 찾아야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. Bounding Box란?&lt;/h2&gt;
&lt;p&gt;객체탐지 결과에서 사람 주변에 그려지는 사각형을 &lt;strong&gt;Bounding Box&lt;/strong&gt;라고 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;┌─────────────────────────────┐
│                             │
│        ┌────────┐           │
│        │ Person │           │
│        │        │           │
│        └────────┘           │
│                             │
└─────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bounding Box는 모델이 객체가 존재한다고 판단한 공간적인 영역을 나타낸다.&lt;/p&gt;
&lt;p&gt;가장 직관적인 표현 방식은 &lt;code&gt;xyxy&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;x1, y1 = 왼쪽 위 좌표
x2, y2 = 오른쪽 아래 좌표&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;(x1, y1) = (100, 50)
(x2, y2) = (200, 300)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면 이미지의 &lt;code&gt;(100, 50)&lt;/code&gt;부터 &lt;code&gt;(200, 300)&lt;/code&gt;까지의 영역이 하나의 Bounding Box가 된다.&lt;/p&gt;
&lt;p&gt;실제 프로젝트 코드에서도 예측 결과를 다음처럼 가져오고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;xyxy = result.boxes.xyxy.detach().cpu().numpy()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 중요한 점은 &lt;strong&gt;Bounding Box의 표현 방식이 하나만 있는 것은 아니라는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. &lt;code&gt;xyxy&lt;/code&gt;와 YOLO Label의 &lt;code&gt;xywh&lt;/code&gt;는 다르다&lt;/h2&gt;
&lt;p&gt;모델의 예측 결과에서는 보통&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;x1, y1, x2, y2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;형태를 자주 사용하지만 YOLO 학습 Label은 다른 형식을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;class_id x_center y_center width height&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 다음과 같은 Label이 있다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 0.512 0.470 0.083 0.320&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;첫 번째 &lt;code&gt;0&lt;/code&gt;은 클래스 ID이다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트는 &lt;code&gt;person&lt;/code&gt; 하나만 탐지하므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 → person&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;나머지는 Bounding Box의 중심점과 크기이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0.512 → x_center
0.470 → y_center
0.083 → width
0.320 → height&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 더 중요한 특징은 이 값들이 픽셀이 아니라 &lt;strong&gt;0~1 사이로 정규화되어 있다는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;이미지 너비가 640이고&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;x_center = 0.5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라면 실제 중심의 x 위치는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;640 × 0.5 = 320px&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;정규화를 사용하면 이미지 해상도와 관계없이 동일한 형식으로 Bounding Box를 표현할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. YOLO Label을 픽셀 좌표로 바꾸기&lt;/h2&gt;
&lt;p&gt;YOLO Label을 실제 이미지 위에 그리려면 정규화된 좌표를 다시 픽셀 단위로 바꿔야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def yolo_to_xyxy(row, image_width, image_height):
    class_id, cx, cy, bw, bh = map(float, row.split())

    cx *= image_width
    cy *= image_height
    bw *= image_width
    bh *= image_height

    x1 = int(cx - bw / 2)
    y1 = int(cy - bh / 2)
    x2 = int(cx + bw / 2)
    y2 = int(cy + bh / 2)

    return int(class_id), x1, y1, x2, y2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 두 단계이다.&lt;/p&gt;
&lt;p&gt;먼저 정규화 좌표를 픽셀 좌표로 복원한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;cx_pixel = cx × image_width
cy_pixel = cy × image_height&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그 다음 Box 크기의 절반을 중심에서 빼고 더한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;x1 = cx - width / 2
y1 = cy - height / 2

x2 = cx + width / 2
y2 = cy + height / 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 과정을 이해하고 나니 Label 파일의 숫자가 실제 이미지 위의 사각형으로 어떻게 바뀌는지 훨씬 명확해졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Ground Truth와 Prediction&lt;/h2&gt;
&lt;p&gt;객체탐지에서는 두 종류의 Bounding Box를 구분해야 한다.&lt;/p&gt;
&lt;h3&gt;Ground Truth&lt;/h3&gt;
&lt;p&gt;학습 또는 검증 데이터에 사람이 어디에 있는지 Annotation한 Bounding Box이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Ground Truth
= 평가 기준으로 사용하는 정답 Annotation&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;엄밀하게 말하면 Ground Truth 역시 사람이 만든 Annotation이기 때문에 절대적인 진실이라기보다는 &lt;strong&gt;모델 평가의 기준이 되는 정답 데이터&lt;/strong&gt;라고 이해하는 것이 좋다.&lt;/p&gt;
&lt;h3&gt;Prediction&lt;/h3&gt;
&lt;p&gt;모델이 이미지를 보고 만들어낸 Bounding Box이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Prediction
= 모델의 예측&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;객체탐지 평가는 결국&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Prediction
vs
Ground Truth&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 비교하는 과정이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. Confidence Score란?&lt;/h2&gt;
&lt;p&gt;YOLO의 결과를 보면 다음과 같이 표시되는 경우가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Person 0.91
Person 0.73
Person 0.31&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;뒤의 숫자가 &lt;strong&gt;Confidence Score&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;처음에는 이를 단순히&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사람일 확률&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이라고 생각하기 쉽다.&lt;/p&gt;
&lt;p&gt;하지만 모델 구조에 따라 점수의 구체적인 계산 방식이 달라질 수 있기 때문에, 보다 안전한 표현은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;해당 예측을 얼마나 강하게 신뢰하고 있는지를 나타내며, 예측을 정렬하고 필터링할 때 사용하는 점수&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Confidence = 0.95&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 해서 그 Prediction이 반드시 정답인 것은 아니다.&lt;/p&gt;
&lt;p&gt;모델은 잘못된 객체에도 높은 Confidence를 부여할 수 있다.&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Confidence가 높음
≠
Ground Truth와 일치함&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;정답 여부를 판단하려면 뒤에서 살펴볼 &lt;strong&gt;Ground Truth와의 비교 과정&lt;/strong&gt;이 필요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. Confidence Threshold는 왜 사용할까?&lt;/h2&gt;
&lt;p&gt;모델이 만들어낸 모든 예측을 그대로 사용할 필요는 없다.&lt;/p&gt;
&lt;p&gt;예를 들어 Confidence가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0.91
0.73
0.31
0.17&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이고 Threshold가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;conf = 0.25&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라면,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0.91 → 유지
0.73 → 유지
0.31 → 유지
0.17 → 제거&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하게 된다.&lt;/p&gt;
&lt;p&gt;현재 프로젝트의 Demo에서도&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;predictions = model.predict(
    source=[str(path) for path in selected],
    imgsz=640,
    conf=0.25,
    classes=[0],
    device=device,
    verbose=False,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 &lt;code&gt;conf=0.25&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;p&gt;Threshold를 높이면 일반적으로 적은 수의 예측만 남고, 낮추면 더 많은 예측이 남는다.&lt;/p&gt;
&lt;p&gt;그래서 Confidence Threshold는 뒤에서 다룰 Precision과 Recall에도 직접적인 영향을 준다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 여기서 하나 더: NMS와 YOLO26&lt;/h2&gt;
&lt;p&gt;기존 YOLO 계열 모델을 공부하다 보면 &lt;strong&gt;NMS(Non-Maximum Suppression)​&lt;/strong&gt;라는 개념이 자주 등장한다.&lt;/p&gt;
&lt;p&gt;기존 객체탐지 모델은 하나의 객체 주변에 여러 개의 비슷한 Bounding Box를 만들 수 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;같은 사람

┌─────────┐
│ ┌─────────┐
│ │ ┌─────────┐
└─│─│───────┘
  └─│───────┘
    └─────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NMS는 겹치는 예측 가운데 높은 Confidence의 Box를 남기고 중복 Box를 제거하는 후처리 방법이다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 현재 프로젝트의 &lt;strong&gt;YOLO26은 별도로 구분해야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;YOLO26은 기본적으로 one-to-one head를 사용하는 &lt;strong&gt;End-to-End NMS-Free Detection&lt;/strong&gt;을 지원한다.&lt;/p&gt;
&lt;p&gt;즉 기본 YOLO26 추론에서는 기존 YOLO처럼 외부 NMS를 거치지 않고 최종 탐지 결과를 직접 출력한다. Ultralytics 문서에서도 YOLO26의 기본 엔드투엔드 출력은 &lt;code&gt;[x1, y1, x2, y2, confidence, class_id]&lt;/code&gt;이며 Confidence 필터만 적용하는 구조로 설명하고 있다.&lt;/p&gt;
&lt;p&gt;그래서 YOLO를 공부할 때는 모델 버전을 구분하는 것이 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;기존 YOLO 방식
Prediction
   ↓
Confidence Filtering
   ↓
NMS
   ↓
Final Detection&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반면 기본 YOLO26은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;YOLO26
   ↓
End-to-End Detection
   ↓
Confidence Filtering
   ↓
Final Detection&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;에 가깝다.&lt;/p&gt;
&lt;p&gt;물론 YOLO26도 &lt;code&gt;end2end=False&lt;/code&gt;로 기존 one-to-many + NMS 방식을 사용할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. 이번 글에서 가장 중요한 내용&lt;/h2&gt;
&lt;p&gt;이번에는 객체탐지 모델의 &lt;strong&gt;출력&lt;/strong&gt;에 집중해서 정리했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Object Detection
    ↓
Bounding Box
    ↓
Class
    ↓
Confidence Score&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 학습 데이터에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;class_id
x_center
y_center
width
height&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;형태의 정규화된 YOLO Label을 사용한다는 것도 확인했다.&lt;/p&gt;
&lt;p&gt;가장 중요했던 것은 다음 세 가지였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Bounding Box
→ 객체가 어디에 있는가?

Class
→ 무엇인가?

Confidence
→ 모델이 해당 예측을 얼마나 강하게 선택하고 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 아직 하나의 문제가 남아 있다.&lt;/p&gt;
&lt;p&gt;모델이 Bounding Box를 만들었다고 해서 그 Box가 정답이라고 할 수는 없다.&lt;/p&gt;
&lt;p&gt;결국 다음 질문에 답해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Prediction과 Ground Truth가 얼마나 비슷해야 같은 객체를 찾았다고 할 수 있을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이 질문을 해결하기 위해 다음 글에서는 &lt;strong&gt;IoU → Matching → TP/FP/FN → Precision/Recall&lt;/strong&gt;의 흐름을 정리해보려고 한다.&lt;/p&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>Bounding box</category>
      <category>Confidence Score</category>
      <category>object detection</category>
      <category>객체탐지</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/65</guid>
      <comments>https://slowgrid.tistory.com/entry/Object-Detection-Bounding-Box-Confidence-Score#entry65comment</comments>
      <pubDate>Sat, 29 Aug 2026 11:57:41 +0900</pubDate>
    </item>
    <item>
      <title>OpenCV 영상처리 응용 - CLAHE와 Bilateral Filter, 그리고 전처리 성능 비교</title>
      <link>https://slowgrid.tistory.com/entry/til-CLAHE-Bilateral-Filter</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열화상 이미지를 &lt;code&gt;uint8&lt;/code&gt;로 정규화한 뒤 CLAHE와 Bilateral Filter를 적용해보고, 사람이 보기에 좋아진 이미지가 객체탐지 모델에서도 반드시 좋은 결과를 만드는지 살펴보았다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 배운 내용 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 단계에서 &lt;code&gt;uint16&lt;/code&gt; 열화상 데이터를 Percentile Clipping과 Normalization을 이용해 &lt;code&gt;uint8&lt;/code&gt; Baseline 이미지로 변환했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 그 Baseline 이미지에 추가적인 영상처리를 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교한 방식은 세 가지다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. Baseline

Thermal Image
     &amp;darr;
Normalization


2. CLAHE

Thermal Image
     &amp;darr;
Normalization
     &amp;darr;
CLAHE


3. Bilateral + CLAHE

Thermal Image
     &amp;darr;
Normalization
     &amp;darr;
Bilateral Filter
     &amp;darr;
CLAHE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 세 번째 방식처럼 전처리를 더 많이 적용하면 당연히 성능도 가장 좋아질 것이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 결과는 그렇지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 학습에서 가장 크게 배운 것은 &lt;b&gt;전처리의 목적은 이미지를 무조건 선명하게 만드는 것이 아니라, 최종 작업에 필요한 정보를 보존하거나 강조하는 것​&lt;/b&gt;이라는 점이었다. 원문의 실험에서도 Bilateral Filter를 추가한 방식의 Precision은 높아졌지만 Recall과 mAP50은 오히려 감소했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 먼저 Histogram이란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLAHE를 이해하려면 먼저 Histogram을 이해해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grayscale 이미지에서 Histogram은 간단히 말하면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 밝기값을 가진 픽셀이 이미지 안에 몇 개 존재하는지 나타낸 분포&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8bit grayscale 이미지라면 픽셀 값은&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;0 ~ 255&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위에 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이미지의 대부분이 100~130 사이에 몰려 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;픽셀 수
  &amp;uarr;
  │             ███
  │          ███████
  │        ██████████
  │
  └────────────────────────&amp;rarr; 밝기
       0   100 130       255&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 실제 픽셀 값의 범위는 0~255지만 이미지 대부분은 매우 좁은 밝기 영역을 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 객체와 배경의 값에 차이가 있어도 눈으로 봤을 때는 비슷한 회색으로 느껴질 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Histogram Equalization은 무엇을 하는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Histogram Equalization은 이미지의 밝기값 분포를 더 넓게 사용하도록 다시 매핑해 &lt;b&gt;전체적인 대비를 개선하려는 방법&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순화해서 생각하면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Before

밝기 사용 영역
──────[100 ~ 130]────────


After

밝기 사용 영역
[────── 더 넓은 범위 ──────]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 좁은 범위에 몰려 있던 픽셀들을 더 넓은 밝기 범위로 펼치는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 각 밝기값이 얼마나 누적되어 있는지를 나타내는 &lt;b&gt;누적 분포(CDF, Cumulative Distribution Function)​&lt;/b&gt;를 이용해 픽셀 값을 다시 대응시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 것은 Histogram Equalization이 단순히 이미지 전체를 밝게 만드는 것이 아니라 &lt;b&gt;밝기값의 배치를 다시 조정해 대비를 확보한다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 일반 Histogram Equalization의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 이미지 기준 Histogram Equalization은 하나의 전역 Histogram을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이미지 안에서도 영역별 특성이 크게 다를 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 열화상 이미지가 다음처럼 구성되어 있다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;┌───────────────────────────┐
│ 밝은 건물                 │
│                           │
│          어두운 도로      │
│                 작은 사람 │
└───────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 Histogram만 기준으로 처리하면 이미지 전체의 분포는 개선되더라도 작은 보행자가 있는 특정 영역의 미세한 대비가 충분히 강조되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열화상 보행자 탐지에서는 특히 원거리에 있는 작은 객체처럼 &lt;b&gt;주변과의 밝기 차이가 크지 않은 영역&lt;/b&gt;을 잘 구분하는 것이 중요할 수 있다. 이것이 원문에서 CLAHE를 살펴본 이유였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. CLAHE란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLAHE는&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Contrast Limited
Adaptive
Histogram Equalization&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 키워드는 두 가지다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Adaptive
&amp;rarr; 이미지 전체가 아니라 지역별로 처리

Contrast Limited
&amp;rarr; 대비가 지나치게 커지는 것을 제한&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 Histogram Equalization이 전체 이미지를 하나의 Histogram으로 처리한다면 CLAHE는 이미지를 여러 개의 작은 Tile로 나눈다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;┌────┬────┬────┬────┐
│    │    │    │    │
├────┼────┼────┼────┤
│    │    │    │    │
├────┼────┼────┼────┤
│    │    │    │    │
├────┼────┼────┼────┤
│    │    │    │    │
└────┴────┴────┴────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 영역의 밝기 분포를 기준으로 대비를 조절하기 때문에 이미지 전체에서는 잘 드러나지 않았던 &lt;b&gt;국소적인 밝기 차이&lt;/b&gt;를 강조할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenCV 공식 문서에서도 CLAHE의 &lt;code&gt;tileGridSize&lt;/code&gt;를 입력 영상을 나눌 Grid의 크기로 설명하며, &lt;code&gt;clipLimit&lt;/code&gt;는 대비 제한을 위한 threshold로 정의하고 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. &lt;code&gt;clipLimit&lt;/code&gt;는 왜 필요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지역별 Histogram Equalization을 무조건 강하게 적용하면 한 가지 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;노이즈 역시 지역적인 밝기 차이이기 때문에 같이 강조될 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 실제 객체가 아니라 센서 노이즈 때문에 다음과 같은 작은 차이가 존재한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;100 101 100
101 110 100
100 101 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가운데 &lt;code&gt;110&lt;/code&gt;이 실제 의미 있는 물체가 아니라 노이즈라면 대비를 과도하게 증가시킬수록 이 값도 더 눈에 띌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLAHE의 &lt;code&gt;clipLimit&lt;/code&gt;는 이러한 Histogram의 과도한 집중을 제한해 &lt;b&gt;대비가 무제한으로 커지는 것을 막는 역할&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 다음 값을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;clahe = cv2.createCLAHE(
    clipLimit=2.0,
    tileGridSize=(8, 8)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문에서도 &lt;code&gt;clipLimit=2.0&lt;/code&gt;, &lt;code&gt;tileGridSize=(8, 8)&lt;/code&gt; 설정을 사용하고 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. &lt;code&gt;tileGridSize=(8, 8)&lt;/code&gt;을 어떻게 이해해야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;(8, 8)&lt;/code&gt;을 보고 각 Tile이 8&amp;times;8 픽셀이라는 의미라고 생각하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서는 &lt;b&gt;이미지를 가로와 세로 방향으로 몇 개의 Tile로 나눌지&lt;/b&gt;를 나타낸다. OpenCV 공식 문서 역시 &lt;code&gt;tileGridSize&lt;/code&gt;를 행과 열 방향 Tile의 개수를 정의하는 Grid 크기로 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 개념적으로는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;전체 이미지
   &amp;darr;
8 &amp;times; 8개의 지역으로 분할
   &amp;darr;
각 영역의 Histogram 분석
   &amp;darr;
지역별 대비 조절&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과 같은 흐름으로 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tile을 너무 크게 잡으면 전역 Histogram Equalization과 비슷한 효과에 가까워질 수 있고, 너무 세밀하게 나누면 작은 영역의 노이즈까지 강하게 반영될 가능성을 생각해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;clipLimit&lt;/code&gt;나 &lt;code&gt;tileGridSize&lt;/code&gt; 역시 고정된 정답이라기보다 데이터 특성에 따라 실험해봐야 하는 값으로 보는 것이 좋겠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. OpenCV의 CLAHE는 꼭 &lt;code&gt;uint8&lt;/code&gt;에서만 사용할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;uint16 Thermal
      &amp;darr;
uint8 Baseline
      &amp;darr;
CLAHE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서로 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 흐름을 처음 보면 &quot;CLAHE가 &lt;code&gt;uint8&lt;/code&gt;만 받을 수 있어서 그런가?&quot;라는 의문이 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 OpenCV 공식 문서상 CLAHE의 입력은 &lt;b&gt;8bit 또는 16bit 단일 채널 이미지&lt;/b&gt;를 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이번 프로젝트가 먼저 &lt;code&gt;uint8&lt;/code&gt; Baseline으로 변환한 것은 CLAHE API 자체의 필수 조건이라기보다는, &lt;b&gt;비교할 전처리 파이프라인의 입력 범위를 통일하고 이후 모델 입력과 연결하기 위한 프로젝트 설계&lt;/b&gt;로 보는 것이 더 적절해 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 원문의 프로젝트 코드에서 확인한 사실과 OpenCV API 자체의 지원 범위를 구분해서 이해할 필요가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. CLAHE에도 문제가 있을 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLAHE의 목적은 지역적인 대비를 강화하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 영상에는 우리가 원하는 정보만 존재하는 것이 아니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;원하는 것
- 사람의 윤곽
- 사람과 배경 사이의 온도 차이
- 작은 객체의 특징

원하지 않는 것
- 센서 노이즈
- 불규칙한 작은 밝기 변화
- 배경의 미세한 잡음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLAHE는 이 둘을 완벽하게 구분할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상처리 함수는 &quot;이 픽셀은 보행자니까 강조하고, 이 픽셀은 노이즈니까 무시해야지&quot;라고 판단하는 것이 아니라 &lt;b&gt;픽셀 값의 분포를 기준으로 처리&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 사람의 특징과 함께 노이즈도 강조될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 줄이기 위해 프로젝트에서는 CLAHE 전에 Bilateral Filter를 적용하는 실험도 진행했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Bilateral Filter란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Blur는 주변 픽셀들의 값을 섞어서 영상을 부드럽게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 노이즈뿐 아니라 &lt;b&gt;객체의 경계까지 같이 흐려질 수 있다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사람과 배경 사이의 경계가 다음과 같다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;배경           사람

20 20 20 | 100 100 100
20 20 20 | 100 100 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Blur는 공간적으로 가까운 픽셀을 평균내기 때문에 경계 양쪽의 값도 섞일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bilateral Filter는 여기서 한 가지 조건을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 공간적으로 가까운가?
2. 픽셀 값도 서로 비슷한가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 조건을 함께 고려한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 가까운 위치에 있어도 밝기 차이가 매우 크다면 영향력을 줄여 &lt;b&gt;경계를 비교적 유지하면서 노이즈를 줄이는 것&lt;/b&gt;을 목표로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenCV 공식 문서 역시 Bilateral Filter를 노이즈를 줄이면서 경계를 비교적 선명하게 유지할 수 있는 필터로 설명한다. 다만 다른 필터에 비해 연산 비용이 큰 편이라는 점도 명시하고 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. Bilateral Filter의 세 파라미터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 다음과 같이 사용했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;bilateral = cv2.bilateralFilter(
    baseline,
    d=5,
    sigmaColor=50,
    sigmaSpace=50
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;d&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터링할 때 고려할 주변 영역의 지름이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 생각하면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 픽셀을 계산할 때 주변을 얼마나 넓게 볼 것인가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 관련 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;d&lt;/code&gt;가 커질수록 더 넓은 주변 픽셀을 계산하게 되므로 일반적으로 계산량도 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenCV 문서에서도 큰 &lt;code&gt;d&lt;/code&gt; 값의 Bilateral Filter는 상당히 느릴 수 있다고 설명한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;sigmaColor&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;픽셀 값의 차이를 얼마나 허용할 것인지 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작으면:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;밝기가 비슷한 픽셀끼리만 강하게 섞임&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커지면:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;밝기 차이가 더 큰 픽셀도 서로 영향을 줌&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 너무 크게 설정하면 보존하려던 경계까지 부드러워질 가능성이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;sigmaSpace&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공간적으로 얼마나 떨어진 픽셀까지 영향을 줄 것인지 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작으면 가까운 픽셀 중심으로 처리하고, 커질수록 더 멀리 있는 픽셀까지 영향을 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenCV 문서에서 &lt;code&gt;sigmaColor&lt;/code&gt;는 값 공간에서의 범위를, &lt;code&gt;sigmaSpace&lt;/code&gt;는 좌표 공간에서의 범위를 제어하는 값으로 설명한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 왜 Bilateral Filter &amp;rarr; CLAHE 순서일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서는 다음 순서를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Normalization
      &amp;darr;
Bilateral Filter
      &amp;darr;
CLAHE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서는 목적을 생각하면 이해하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLAHE는 작은 밝기 차이를 강조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 그 밝기 차이 안에는&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;객체의 특징 + 노이즈&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 함께 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 먼저 Bilateral Filter로 노이즈를 줄이면서 경계를 최대한 유지하고, 이후 CLAHE로 지역적인 대비를 강조하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Raw feature + Noise
        &amp;darr;
Bilateral Filter
        &amp;darr;
Noise 감소 + Edge 최대한 보존
        &amp;darr;
CLAHE
        &amp;darr;
지역 대비 강조&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 봤을 때는 상당히 합리적인 조합처럼 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 자연스럽게&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Bilateral + CLAHE가 가장 좋은 성능을 내지 않을까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 실제 객체탐지 성능은 달랐다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로젝트 결과는 다음과 같았다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;전처리&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Precision&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Recall&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;mAP50&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;전처리 시간&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.8044&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.7691&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;b&gt;0.8725&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;b&gt;3.265 ms&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLAHE&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.8091&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;b&gt;0.7776&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.8561&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;3.479 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bilateral + CLAHE&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;b&gt;0.8159&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.7327&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.8352&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;3.693 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문의 실험 수치를 보면 결과가 흥미롭다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Baseline&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 높은 mAP50&lt;/li&gt;
&lt;li&gt;가장 빠른 전처리 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CLAHE&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Recall이 가장 높음&lt;/li&gt;
&lt;li&gt;Precision도 Baseline보다 약간 상승&lt;/li&gt;
&lt;li&gt;하지만 mAP50은 Baseline보다 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Bilateral + CLAHE&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Precision은 가장 높음&lt;/li&gt;
&lt;li&gt;하지만 Recall은 가장 낮음&lt;/li&gt;
&lt;li&gt;mAP50도 세 방식 중 가장 낮음&lt;/li&gt;
&lt;li&gt;전처리 시간도 가장 김&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 결과만 보면 &lt;b&gt;전처리를 많이 적용할수록 성능이 좋아진다는 관계는 존재하지 않았다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. 왜 Bilateral + CLAHE에서 Recall이 낮아졌을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기부터는 실험 수치에서 생각해볼 수 있는 &lt;b&gt;가능성에 대한 해석&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문에서도 Bilateral Filter가 작은 보행자를 구별하는 데 필요한 미세한 정보를 일부 제거했을 가능성을 생각해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bilateral Filter는 Edge를 보존하려고 하지만 모든 특징을 완벽하게 보존하는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 열화상에서 작은 원거리 보행자는&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;몇 픽셀 크기의 작은 형태
+
배경과의 작은 온도 차이&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 가지고 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 상황에서 노이즈 제거를 강하게 수행하면 모델 입장에서는 노이즈가 아니라 &lt;b&gt;객체를 구분하는 데 필요했던 미세한 신호&lt;/b&gt;까지 일부 약해질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 사람이 보기에는 이미지가 더 깨끗해졌는데 모델은 이전보다 객체를 찾기 어려워지는 상황이 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이것은 현재 결과만으로 원인을 확정할 수 있는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 확인하려면&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 파라미터별 반복 실험
- 여러 Validation 이미지 비교
- 작은 객체와 큰 객체를 나눈 성능 분석
- False Negative 사례 확인
- Histogram과 feature 변화 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 추가 분석이 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;14. 사람이 보기 좋은 이미지 &amp;ne; 모델에게 좋은 이미지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실험에서 가장 중요한 포인트였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상처리를 공부할 때 결과 이미지를 눈으로 비교하면 보통 이렇게 판단하기 쉽다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;더 밝다
더 선명하다
노이즈가 적다
경계가 잘 보인다

&amp;rarr; 더 좋은 이미지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 객체탐지 모델이 필요로 하는 정보는 사람이 중요하다고 느끼는 정보와 완전히 같지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람에게는 노이즈처럼 보이는 작은 변화가 모델에게는 객체를 구분하는 feature일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 사람이 보기에는 매우 선명한 대비가 모델이 학습한 데이터 분포와 크게 달라져 오히려 성능에 좋지 않을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 AI 모델을 위한 전처리는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;결과 이미지가 예뻐졌는가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Validation Dataset에서 실제 목표 지표가 개선되었는가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 기준으로 판단해야 한다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문에서도 바로 이 점을 이번 실험의 중요한 결론으로 정리했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;15. 전처리 역시 하나의 하이퍼파라미터 실험이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 전처리를 모델 학습 전에 고정적으로 수행하는 단계라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번 결과를 보고 생각이 달라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLAHE만 하더라도 다음 값을 바꿀 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;clipLimit
tileGridSize&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bilateral Filter에는 다음 값이 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;d
sigmaColor
sigmaSpace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 전처리의 조합 자체도 바꿀 수 있다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Normalization
Normalization + CLAHE
Normalization + Bilateral
Normalization + Bilateral + CLAHE
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 전처리도 결국 모델의 Learning Rate나 Batch Size처럼 &lt;b&gt;검증 데이터를 통해 비교해야 할 실험 요소&lt;/b&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;16. 다음에는 이렇게 비교해보고 싶다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 동일한 열화상 이미지 한 장을 기준으로 CLAHE의 &lt;code&gt;clipLimit&lt;/code&gt;를 바꿔볼 예정이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;clip_limits = [1.0, 2.0, 3.0, 4.0]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각각의 결과 이미지와 Histogram을 같이 비교해보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 Bilateral Filter의 값을 조금씩 변경해본다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sigma_values = [10, 30, 50, 100]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;sigmaColor&lt;/code&gt;를 변경하면서&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;노이즈 감소 정도
경계 보존 정도
작은 객체의 형태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 어떻게 달라지는지 직접 확인하면 파라미터에 대한 직관을 만들 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에는 단순히 이미지 비교에서 끝내지 않고 동일한 Validation Dataset에서 모델 성능까지 확인해야겠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘의 회고&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 CLAHE와 Bilateral Filter라는 두 가지 영상처리 기법을 학습했지만, 함수 자체보다 &lt;b&gt;왜 이 순서로 사용하는가&lt;/b&gt;를 이해한 것이 더 의미 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLAHE는 지역적인 대비를 강조하지만 노이즈까지 함께 강조할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bilateral Filter는 노이즈를 줄이면서 경계를 보존하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Bilateral Filter &amp;rarr; CLAHE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조합이 논리적으로 자연스럽게 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 실제 객체탐지 성능에서는 이 조합이 가장 좋은 결과를 내지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 통해 전처리에는 항상 Trade-off가 있고 &lt;b&gt;논리적으로 좋아 보이는 방법도 실제 데이터에서는 반드시 검증해야 한다&lt;/b&gt;는 것을 배웠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 아직 부족한 부분&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Histogram Equalization과 CDF가 실제 픽셀 값을 어떻게 다시 매핑하는지 수식 수준에서는 아직 더 공부가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Bilateral Filter의&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;d
sigmaColor
sigmaSpace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 변화시켰을 때 결과가 어떻게 바뀌는지 아직 직관이 충분하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 설명을 읽는 것만으로는 한계가 있기 때문에 직접 파라미터를 변경하고 결과 이미지를 비교해보는 실습이 필요하다고 느꼈다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 다음 액션 플랜&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; CLAHE 적용 전후 Histogram 그리기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;clipLimit&lt;/code&gt; 1.0 / 2.0 / 3.0 / 4.0 비교하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;tileGridSize&lt;/code&gt;를 변경하며 국소 대비 변화 확인하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Bilateral Filter의 &lt;code&gt;sigmaColor&lt;/code&gt; 변경 결과 비교하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Bilateral Filter의 &lt;code&gt;sigmaSpace&lt;/code&gt; 변경 결과 비교하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Gaussian Blur와 Bilateral Filter 결과 비교하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 작은 보행자 영역만 Crop해서 전처리 전후 비교하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; False Negative 이미지에서 전처리가 어떤 영향을 줬는지 확인하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 동일한 Validation Dataset에서 전처리별 성능 비교하기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 줄 정리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상 전처리에서 중요한 것은 &lt;b&gt;얼마나 많이 처리했는가가 아니라, 최종 목적에 필요한 정보를 얼마나 잘 보존하고 강조했는가​&lt;/b&gt;이며, 그 판단은 실제 모델 성능으로 검증해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>Bilateral Filter</category>
      <category>CLAHE</category>
      <category>opencv</category>
      <category>영상처리</category>
      <category>전처리</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/64</guid>
      <comments>https://slowgrid.tistory.com/entry/til-CLAHE-Bilateral-Filter#entry64comment</comments>
      <pubDate>Fri, 28 Aug 2026 18:53:54 +0900</pubDate>
    </item>
    <item>
      <title>OpenCV 영상처리 기초 - uint16 열화상 이미지와 Percentile Normalization</title>
      <link>https://slowgrid.tistory.com/entry/til-opencv-percentile-normalization</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;OpenCV에서 이미지를 다루는 기초 개념을 살펴보고, &lt;code&gt;uint16&lt;/code&gt; 열화상 이미지를 Percentile Clipping과 Normalization을 통해 &lt;code&gt;uint8&lt;/code&gt; 이미지로 변환해보았다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;오늘 배운 내용 요약&lt;/h2&gt;
&lt;p&gt;오늘은 OpenCV로 열화상 이미지를 처리하기 전에 알아야 할 기본 개념을 공부했다.&lt;/p&gt;
&lt;p&gt;처음에는 영상처리라고 하면 이미지에 Blur나 Edge Detection 같은 필터를 적용하는 것부터 떠올렸다. 하지만 실제 열화상 데이터를 다뤄보니 그보다 먼저 확인해야 할 것이 있었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;현재 불러온 이미지가 어떤 형태의 데이터인지 확인하는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;이번에 살펴본 Thermal Pedestrian Detection 프로젝트의 원본 열화상 TIFF는 일반적인 JPG나 PNG처럼 단순한 &lt;code&gt;uint8&lt;/code&gt; 이미지가 아니라 &lt;code&gt;uint16&lt;/code&gt; 데이터였다. 따라서 일반 이미지처럼 바로 처리하기보다 먼저 데이터의 범위를 확인하고, 모델이나 후속 영상처리에 적합한 범위로 변환하는 과정이 필요했다.&lt;/p&gt;
&lt;p&gt;원본 프로젝트에서도 Baseline은 아무런 처리를 하지 않은 원본 이미지가 아니라, Percentile Clipping과 0~255 Normalization을 포함한 기본 전처리 결과였다.&lt;/p&gt;
&lt;p&gt;오늘 살펴본 핵심 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint16 Thermal TIFF
        ↓
이미지 shape / dtype / min / max 확인
        ↓
1% / 99% Percentile 계산
        ↓
극단값 Clipping
        ↓
0 ~ 255 Normalization
        ↓
uint8 변환&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;1. OpenCV에서 이미지도 결국 숫자 배열이다&lt;/h2&gt;
&lt;p&gt;OpenCV로 이미지를 읽으면 파이썬에서는 기본적으로 NumPy의 &lt;code&gt;ndarray&lt;/code&gt; 형태로 다루게 된다.&lt;/p&gt;
&lt;p&gt;예를 들어 흑백 이미지는 보통 다음과 같은 형태를 가진다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;height × width&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;컬러 이미지라면 채널 축이 하나 더 추가된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;height × width × channel&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;따라서 이미지 처리에서는 단순히 &amp;quot;사진이 잘 보이는가?&amp;quot;만 확인해서는 안 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(image.shape)
print(image.dtype)
print(image.min())
print(image.max())&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이처럼 &lt;strong&gt;배열의 구조와 값의 범위&lt;/strong&gt;를 먼저 확인하는 습관이 중요하다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음 두 이미지는 화면상으로 비슷하게 보일 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Image A
dtype = uint8
range = 0 ~ 255

Image B
dtype = uint16
range = 1000 ~ 12000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;하지만 내부에서 데이터를 표현하는 방식은 완전히 다르다.&lt;/p&gt;
&lt;p&gt;같은 수식을 적용하더라도 자료형과 값의 범위에 따라 결과가 크게 달라질 수 있다. 따라서 영상처리를 시작할 때는 먼저 &lt;code&gt;shape&lt;/code&gt;, &lt;code&gt;dtype&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;를 확인해야 한다는 것을 배웠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. &lt;code&gt;uint8&lt;/code&gt;과 &lt;code&gt;uint16&lt;/code&gt;은 무엇이 다른가?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;uint&lt;/code&gt;는 &lt;strong&gt;unsigned integer&lt;/strong&gt;, 즉 음수가 없는 정수형을 의미한다.&lt;/p&gt;
&lt;p&gt;뒤에 붙는 숫자는 하나의 값을 표현하는 데 사용하는 bit 수를 나타낸다.&lt;/p&gt;
&lt;h3&gt;uint8&lt;/h3&gt;
&lt;p&gt;8bit를 사용하므로 표현 가능한 경우의 수는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2^8 = 256&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;개이다.&lt;/p&gt;
&lt;p&gt;따라서 값의 범위는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 ~ 255&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;일반적인 grayscale 이미지의 밝기 값을 표현할 때 흔히 사용된다.&lt;/p&gt;
&lt;h3&gt;uint16&lt;/h3&gt;
&lt;p&gt;16bit를 사용하므로 표현 가능한 경우의 수는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2^16 = 65,536&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;개이다.&lt;/p&gt;
&lt;p&gt;따라서 값의 범위는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 ~ 65,535&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;즉 &lt;code&gt;uint16&lt;/code&gt;은 &lt;code&gt;uint8&lt;/code&gt;보다 훨씬 세밀한 값의 차이를 저장할 수 있다.&lt;/p&gt;
&lt;p&gt;열화상 센서 데이터에서는 이렇게 넓은 값의 범위를 사용하는 경우가 있다. 따라서 TIFF 이미지를 읽었다고 해서 값의 범위가 반드시 0~255라고 가정해서는 안 된다.&lt;/p&gt;
&lt;p&gt;이번에 다룬 열화상 이미지 역시 &lt;code&gt;uint16&lt;/code&gt;이었기 때문에 일반적인 8bit 이미지와 동일하게 처리하지 않고 별도의 전처리 과정을 거쳤다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 열화상 TIFF를 읽을 때 &lt;code&gt;IMREAD_UNCHANGED&lt;/code&gt;를 사용하는 이유&lt;/h2&gt;
&lt;p&gt;실습에서는 이미지를 다음과 같이 읽었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;thermal = cv2.imread(
    &amp;quot;sample.tiff&amp;quot;,
    cv2.IMREAD_UNCHANGED
)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;OpenCV의 &lt;code&gt;IMREAD_UNCHANGED&lt;/code&gt;는 이미지를 가능한 한 원본 형태 그대로 읽는 옵션이다.&lt;/p&gt;
&lt;p&gt;기본적인 컬러 이미지 로딩 옵션을 사용하면 데이터가 의도하지 않은 형태로 변환될 수 있기 때문에, 원본 열화상 값 자체를 확인하려는 상황에서는 &lt;code&gt;IMREAD_UNCHANGED&lt;/code&gt; 옵션이 중요하다.&lt;/p&gt;
&lt;p&gt;이미지를 읽은 직후에는 다음 정보를 출력했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&amp;quot;shape :&amp;quot;, thermal.shape)
print(&amp;quot;dtype :&amp;quot;, thermal.dtype)
print(&amp;quot;min   :&amp;quot;, thermal.min())
print(&amp;quot;max   :&amp;quot;, thermal.max())&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 과정은 단순한 디버깅이라기보다 &lt;strong&gt;어떤 전처리를 적용할지 결정하기 위한 데이터 탐색 과정&lt;/strong&gt;이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;dtype&lt;/code&gt;이 &lt;code&gt;uint16&lt;/code&gt;인 이미지를 바로 &lt;code&gt;uint8&lt;/code&gt;로 강제 변환하면 원래 값의 범위가 원하는 방식으로 0~255에 대응되지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 먼저 실제 값의 범위를 살펴본 뒤, 어느 구간을 0~255에 대응시킬지 결정해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 왜 단순 Min-Max Normalization만 하지 않을까?&lt;/h2&gt;
&lt;p&gt;처음 가장 궁금했던 부분은 이것이었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;그냥 이미지의 최솟값과 최댓값을 각각 0과 255에 대응시키면 안 될까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;가장 단순한 Min-Max Normalization은 다음과 같이 표현할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;normalized
= (x - min) / (max - min) × 255&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;문제는 이미지 안에 &lt;strong&gt;극단적으로 크거나 작은 값이 일부 존재할 때&lt;/strong&gt; 발생한다.&lt;/p&gt;
&lt;p&gt;예를 들어 대부분의 픽셀이 다음 범위에 있다고 가정해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;대부분의 픽셀
1000 ~ 5000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그런데 노이즈나 특수한 영역 때문에 하나의 픽셀이 다음과 같은 값을 가진다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;30000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;전체 최솟값과 최댓값을 기준으로 정규화하면 범위가 다음과 같이 잡힌다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 ----------------------------------------- 30000

    1000 ----- 5000
    ↑ 실제 대부분의 데이터&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;0에서 255라는 출력 범위를 전체 0에서 30000 구간에 대응시켜야 하므로, &lt;/p&gt;
&lt;p&gt;실제로 중요한 1000에서 5000 구간은 0에서 255 중 일부만 사용하게 된다.&lt;/p&gt;
&lt;p&gt;그 결과 서로 다른 온도 값을 가진 픽셀도 최종 영상에서는 비슷한 밝기로 표현될 수 있다.&lt;/p&gt;
&lt;p&gt;즉 &lt;strong&gt;소수의 이상치 때문에 이미지 대부분의 대비가 희생될 수 있는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;이번 코드에서는 이러한 영향을 줄이기 위해 1%, 99% Percentile을 사용했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Percentile Clipping이란?&lt;/h2&gt;
&lt;p&gt;Percentile은 데이터를 크기순으로 정렬했을 때 특정 비율에 해당하는 위치의 값을 의미한다.&lt;/p&gt;
&lt;p&gt;이번 코드에서는 다음과 같이 사용했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lower, upper = np.percentile(image, [1, 99])&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이를 단순화하면 다음과 같이 이해할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lower = 1번째 Percentile 값
upper = 99번째 Percentile 값&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그다음 다음과 같이 Clipping을 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;image = np.clip(image, lower, upper)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;np.clip()&lt;/code&gt;은 지정한 범위를 벗어나는 값을 경계값으로 제한한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lower보다 작은 값 → lower

lower ~ upper       → 원래 값 유지

upper보다 큰 값    → upper&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lower = 1200
upper = 5200&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이라면&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;800   → 1200
1500  → 1500
4000  → 4000
30000 → 5200&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;과 같이 변환된다.&lt;/p&gt;
&lt;p&gt;이 방식에서 중요한 점은 극단값을 삭제하는 것이 아니라, &lt;strong&gt;정규화에 사용할 유효 범위를 제한한다는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. Clipping 후 0~255로 정규화하기&lt;/h2&gt;
&lt;p&gt;Clipping을 마치면 관심 범위는 &lt;code&gt;lower ~ upper&lt;/code&gt;가 된다.&lt;/p&gt;
&lt;p&gt;이 범위를 0~255로 변환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;image = (image - lower) / (upper - lower) * 255&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;수식을 단계별로 나눠보면 이해하기 쉽다.&lt;/p&gt;
&lt;h3&gt;1. lower만큼 이동&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;image - lower&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 연산을 하면 &lt;code&gt;lower&lt;/code&gt;가 0이 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lower → 0
upper → upper - lower&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;2. 전체 범위로 나누기&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;(image - lower) / (upper - lower)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;값의 범위가 다음과 같이 변한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 ~ 1&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;3. 255를 곱하기&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;(image - lower) / (upper - lower) * 255&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;최종 범위는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 ~ 255&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;마지막으로&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;image.astype(np.uint8)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;을 수행해 일반적인 8bit grayscale 이미지 형태로 변환한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 왜 계산 전에 &lt;code&gt;float32&lt;/code&gt;로 변환할까?&lt;/h2&gt;
&lt;p&gt;함수의 첫 부분에는 다음 코드가 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;image = image.astype(np.float32)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처음에는 마지막에 어차피 &lt;code&gt;uint8&lt;/code&gt;로 변환할 것인데 왜 굳이 중간에 &lt;code&gt;float32&lt;/code&gt;를 사용하는지 궁금했다.&lt;/p&gt;
&lt;p&gt;정규화 과정에는 뺄셈과 나눗셈이 포함된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(image - lower) / (upper - lower)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이때 정수 자료형만 사용하면 소수점이 필요한 연산이나 자료형의 표현 범위를 함께 고려해야 한다.&lt;/p&gt;
&lt;p&gt;따라서 &lt;strong&gt;중간 계산은 실수형으로 수행하고, 최종 결과만 필요한 자료형으로 변환하는 방식&lt;/strong&gt;이 보다 안전하고 이해하기 쉽다.&lt;/p&gt;
&lt;p&gt;이 패턴은 열화상뿐 아니라 여러 수치 기반 영상처리에서도 자주 활용할 수 있을 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 직접 작성해본 열화상 Normalization 함수&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import cv2
import numpy as np


def normalize_thermal(image):
    # 중간 연산을 위해 float32 사용
    image = image.astype(np.float32)

    # 극단값의 영향을 줄이기 위해 1%, 99% 지점 계산
    lower, upper = np.percentile(image, [1, 99])

    # 모든 픽셀이 거의 같은 값을 갖는 예외 상황 처리
    if upper &amp;lt;= lower:
        return np.zeros(image.shape, dtype=np.uint8)

    # 관심 범위를 벗어난 극단값 제한
    image = np.clip(image, lower, upper)

    # lower ~ upper → 0 ~ 255
    image = (image - lower) / (upper - lower) * 255

    return image.astype(np.uint8)


thermal = cv2.imread(
    &amp;quot;sample.tiff&amp;quot;,
    cv2.IMREAD_UNCHANGED
)

if thermal is None:
    raise FileNotFoundError(&amp;quot;이미지를 불러오지 못했습니다.&amp;quot;)

print(&amp;quot;shape :&amp;quot;, thermal.shape)
print(&amp;quot;dtype :&amp;quot;, thermal.dtype)
print(&amp;quot;min   :&amp;quot;, thermal.min())
print(&amp;quot;max   :&amp;quot;, thermal.max())

baseline = normalize_thermal(thermal)

print(&amp;quot;baseline dtype :&amp;quot;, baseline.dtype)
print(&amp;quot;baseline min   :&amp;quot;, baseline.min())
print(&amp;quot;baseline max   :&amp;quot;, baseline.max())

cv2.imwrite(&amp;quot;baseline.png&amp;quot;, baseline)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;원문의 핵심 함수 역시 &lt;code&gt;float32 → percentile → clip → normalize → uint8&lt;/code&gt; 순서로 변환을 수행하고 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. 여기서 Baseline이 의미하는 것&lt;/h2&gt;
&lt;p&gt;처음에는 &lt;code&gt;Baseline&lt;/code&gt;이라는 이름 때문에 아무런 처리를 하지 않은 원본 영상을 의미한다고 생각했다.&lt;/p&gt;
&lt;p&gt;하지만 이번 프로젝트에서의 Baseline은 다음과 같은 과정을 거친 결과였다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Raw Thermal Image
        ↓
Percentile Clipping
        ↓
Normalization
        ↓
uint8 Baseline&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;즉 Baseline은 &lt;strong&gt;비교 실험의 기준으로 사용하기 위한 최소 전처리 결과&lt;/strong&gt;였다.&lt;/p&gt;
&lt;p&gt;CLAHE나 Bilateral Filter의 효과를 비교하려면 각 방식이 동일한 출발점에서 시작해야 한다.&lt;/p&gt;
&lt;p&gt;따라서 먼저 열화상 데이터를 공통된 형태로 변환한 뒤&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Baseline
Baseline + CLAHE
Baseline + Bilateral + CLAHE&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;와 같은 구조로 결과를 비교하는 것으로 이해했다.&lt;/p&gt;
&lt;p&gt;이 과정에서 &lt;code&gt;Baseline = 아무것도 하지 않은 상태&lt;/code&gt;라고 항상 생각해서는 안 된다는 점을 배웠다.&lt;/p&gt;
&lt;p&gt;실험에서 Baseline은 &lt;strong&gt;비교 기준으로 정의한 기본 처리 파이프라인&lt;/strong&gt;이라는 의미에 더 가깝다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. 새롭게 알게 된 점&lt;/h2&gt;
&lt;p&gt;이번 학습을 통해 영상처리에서는 필터를 적용하기 전에 먼저 확인해야 할 것들이 있다는 사실을 배웠다.&lt;/p&gt;
&lt;p&gt;첫째, &lt;strong&gt;이미지는 숫자 배열이다.&lt;/strong&gt;&lt;br&gt;화면에서는 사진으로 보이지만, 코드에서는 결국 픽셀 값으로 구성된 NumPy 배열을 다룬다.&lt;/p&gt;
&lt;p&gt;둘째, &lt;strong&gt;dtype은 단순한 자료형 정보가 아니다.&lt;/strong&gt;&lt;br&gt;&lt;code&gt;uint8&lt;/code&gt;, &lt;code&gt;uint16&lt;/code&gt;, &lt;code&gt;float32&lt;/code&gt;에 따라 표현 가능한 값의 범위와 연산 방식이 달라진다.&lt;/p&gt;
&lt;p&gt;셋째, &lt;strong&gt;Normalization은 단순히 숫자의 크기를 줄이는 작업이 아니다.&lt;/strong&gt;&lt;br&gt;어떤 값의 범위를 0~255 전체에 대응시키느냐에 따라 최종 영상에서 강조되는 정보가 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;넷째, &lt;strong&gt;이상치는 전체 이미지의 대비에 영향을 줄 수 있다.&lt;/strong&gt;&lt;br&gt;따라서 열화상처럼 값의 범위가 넓은 데이터에서는 Percentile을 이용해 정규화 범위를 정하는 것도 하나의 방법이 될 수 있다.&lt;/p&gt;
&lt;p&gt;다섯째, &lt;strong&gt;전처리 전에 데이터부터 확인해야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;앞으로 이미지를 읽으면 먼저 다음 네 가지를 확인하는 습관을 들이려고 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;image.shape
image.dtype
image.min()
image.max()&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;오늘의 회고&lt;/h2&gt;
&lt;h3&gt;1. 배운 점&lt;/h3&gt;
&lt;p&gt;오늘은 OpenCV 함수의 사용법 자체보다 &lt;strong&gt;이미지가 어떤 데이터인지 이해하는 것이 영상처리의 출발점&lt;/strong&gt;이라는 사실을 배운 것이 가장 큰 수확이었다.&lt;/p&gt;
&lt;p&gt;평소 JPG나 PNG를 다룰 때는 이미지를 읽은 뒤 바로 resize나 filter를 적용했기 때문에 자료형을 깊게 생각하지 않았다.&lt;/p&gt;
&lt;p&gt;하지만 열화상 TIFF를 직접 살펴보니 &lt;code&gt;uint16&lt;/code&gt; 데이터를 어떤 방식으로 &lt;code&gt;uint8&lt;/code&gt;로 변환할지 결정하는 것 자체가 중요한 전처리 과정이었다.&lt;/p&gt;
&lt;p&gt;특히 Percentile Clipping을 이해하면서 Normalization은 단순히 &amp;quot;큰 숫자를 작은 숫자로 바꾸는 작업&amp;quot;이 아니라, &lt;strong&gt;사용할 값의 범위를 정하고 그 범위가 출력 범위를 충분히 활용하도록 다시 매핑하는 과정&lt;/strong&gt;이라는 것을 알게 되었다.&lt;/p&gt;
&lt;h3&gt;2. 어려웠던 점&lt;/h3&gt;
&lt;p&gt;아직은 1%와 99%라는 기준이 모든 열화상 데이터에 항상 적합한지는 판단하기 어렵다.&lt;/p&gt;
&lt;p&gt;이번 프로젝트에서는 해당 값을 사용했지만, 다른 데이터셋에서는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.5% / 99.5%
2% / 98%
5% / 95%&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;와 같은 기준이 더 적합할 수도 있다.&lt;/p&gt;
&lt;p&gt;따라서 Percentile 값 역시 절대적인 공식이라기보다 &lt;strong&gt;데이터의 특성과 목적에 따라 검증해야 하는 하이퍼파라미터&lt;/strong&gt;라는 관점으로 바라봐야겠다.&lt;/p&gt;
&lt;h3&gt;3. 다음 실습&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 한 이미지의 원본 Histogram 그려보기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Min-Max Normalization과 Percentile Normalization 결과 비교하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 1/99, 2/98, 5/95 Percentile 결과 비교하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 각 결과의 &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, 평균값 비교하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Normalization 이후 CLAHE가 어떤 변화를 만드는지 확인하기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/raccoon297/Thermal_Pedestrian_Detection&quot;&gt;Thermal Pedestrian Detection - raccoon297&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI &amp;middot; Data/Computer Vision</category>
      <category>opencv</category>
      <category>Percentile Normalization</category>
      <category>til</category>
      <category>uint16</category>
      <category>열화상</category>
      <category>영상처리</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/63</guid>
      <comments>https://slowgrid.tistory.com/entry/til-opencv-percentile-normalization#entry63comment</comments>
      <pubDate>Thu, 27 Aug 2026 20:34:16 +0900</pubDate>
    </item>
    <item>
      <title>최대 유량(3) Dinic, 한 번 만든 BFS 결과를 끝까지 활용하기</title>
      <link>https://slowgrid.tistory.com/entry/til-Dinic</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;최대 유량(Maximum Flow) 시리즈&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/til-Ford-Fulkerson&quot;&gt;1편 — 포드-풀커슨(Ford-Fulkerson)을 이해하기까지&lt;/a&gt;&lt;br&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/til-Edmonds-Karp&quot;&gt;2편 — 에드몬드-카프(Edmonds-Karp), DFS를 BFS로 바꾸면 무엇이 달라질까&lt;/a&gt;&lt;br&gt;&lt;strong&gt;3편 — Dinic, 한 번 만든 BFS 결과를 끝까지 활용하기 ← 현재 글&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;지난 에드몬드-카프(Edmonds-Karp) 글의 마지막에 이런 질문을 남겼다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;BFS로 최단 거리 정보를 한 번 만들었다면, 그 정보를 증가 경로 하나보다 더 많이 활용할 수 없을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이번에 공부한 &lt;strong&gt;Dinic 알고리즘&lt;/strong&gt;은 이 질문에서 그대로 이어진다.&lt;/p&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)는 매번 BFS를 수행해서 현재 잔여 그래프에서 가장 짧은 증가 경로 하나를 찾는다.&lt;/p&gt;
&lt;p&gt;그리고 그 경로로 유량을 보낸 뒤 잔여 그래프가 바뀌면 다시 BFS를 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS
↓
가장 짧은 증가 경로 하나
↓
유량 전송
↓
다시 BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dinic은 여기서 조금 다르게 접근한다.&lt;/p&gt;
&lt;p&gt;BFS를 단순히 &lt;strong&gt;증가 경로 하나를 찾기 위한 도구&lt;/strong&gt;로만 사용하지 않는다.&lt;/p&gt;
&lt;p&gt;아예 Source로부터 각 정점까지의 거리를 계산해서 &lt;strong&gt;Level Graph&lt;/strong&gt;라는 구조를 만들고,&lt;/p&gt;
&lt;p&gt;그 구조가 유효한 동안에는 BFS를 다시 하지 않고 DFS를 반복하면서 가능한 유량을 최대한 보낸다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS
↓
Level Graph 생성
↓
DFS로 유량 전송
↓
DFS
↓
DFS
↓
현재 Level Graph에서 더 보낼 수 없을 때
↓
다시 BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;앞에서 에드몬드-카프(Edmonds-Karp)를 보면서 들었던&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;한 번 찾은 최단 거리 구조를 조금 더 오래 활용하면 안 될까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라는 의문에 대한 답이 바로 Dinic이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;앞에서 본 그래프를 다시 사용해보면&lt;/h2&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)를 공부할 때 사용했던 그래프를 다시 생각해보자.&lt;/p&gt;
&lt;p&gt;모든 간선의 용량은 1이라고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A
S → B
A → B
A → T
B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;에드몬드-카프(Edmonds-Karp)에서는 BFS로 다음과 같은 최단 증가 경로 하나를 찾을 수 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그리고 유량 1을 보낸 뒤 잔여 그래프가 바뀌면 다시 BFS를 수행한다.&lt;/p&gt;
&lt;p&gt;그다음에는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;를 찾아 다시 유량을 보낼 수 있다.&lt;/p&gt;
&lt;p&gt;결과적으로 최대 유량은 2다.&lt;/p&gt;
&lt;p&gt;Dinic에서도 최종 결과는 당연히 같다.&lt;/p&gt;
&lt;p&gt;다른 것은 &lt;strong&gt;두 경로를 처리하는 방식&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;Dinic은 먼저 BFS를 한 번 수행한다.&lt;/p&gt;
&lt;p&gt;그 결과 Source에서 각 정점까지의 거리는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level[S] = 0

level[A] = 1
level[B] = 1

level[T] = 2&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;즉 정점을 Level별로 나누면&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Level 0 : S
Level 1 : A, B
Level 2 : T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;그리고 이 Level 정보를 이용해서 이번 단계에서 사용할 수 있는 간선들을 제한한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Level Graph란 무엇일까?&lt;/h2&gt;
&lt;p&gt;처음에는 Level Graph를 그냥&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS로 구한 거리 배열&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;정도로 생각했다.&lt;/p&gt;
&lt;p&gt;하지만 정확하게는 조금 다르다.&lt;/p&gt;
&lt;p&gt;먼저 현재 &lt;strong&gt;Residual Graph&lt;/strong&gt;에서 BFS를 수행해 각 정점의 &lt;code&gt;level&lt;/code&gt;을 구한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level[v]
= Source에서 v까지 가는
  현재 잔여 그래프의 최단 거리&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그리고 잔여 용량이 양수인 간선 &lt;code&gt;(u, v)&lt;/code&gt; 중 다음 조건을 만족하는 간선만 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level[v] = level[u] + 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;즉 정확히 다음 Level로 진행하는 간선만 남긴다.&lt;/p&gt;
&lt;p&gt;이렇게 만들어진 구조가 &lt;strong&gt;Level Graph&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;아까 예제에서는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A
S → B
A → T
B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;가 모두 Level Graph에 들어간다.&lt;/p&gt;
&lt;p&gt;반면&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A → B&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;는 들어가지 않는다.&lt;/p&gt;
&lt;p&gt;왜냐하면&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level[A] = 1
level[B] = 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A → B&lt;/code&gt;를 따라가도 다음 Level로 이동하는 것이 아니다.&lt;/p&gt;
&lt;p&gt;Dinic에서는 잔여 용량이 있다고 해서 모든 간선을 사용하는 것이 아니라,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이번 BFS가 만들어낸 최단 거리 구조를 따라 앞으로 나아가는 간선만 사용한다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Level Graph는 방향이 항상 앞으로 간다&lt;/h2&gt;
&lt;p&gt;Level Graph의 모든 간선은&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Level i
→ Level i + 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;방향이다.&lt;/p&gt;
&lt;p&gt;따라서 Level Graph에서는 같은 Level로 이동하거나 이전 Level로 돌아가는 일이 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 → 1 → 2 → 3 → ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Level이 계속 증가하기 때문에 사이클도 만들어질 수 없다.&lt;/p&gt;
&lt;p&gt;즉 Level Graph는 DAG 형태가 된다.&lt;/p&gt;
&lt;p&gt;이 점이 처음에는 별것 아닌 것처럼 보였는데, Dinic이 현재 BFS 결과 안에서 유량을 효율적으로 밀어낼 수 있는 중요한 이유 중 하나였다.&lt;/p&gt;
&lt;p&gt;DFS가 아무 방향으로 헤매지 않고 항상 Sink 쪽 Level로만 이동하기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Dinic의 DFS는 아무 간선이나 따라가지 않는다&lt;/h2&gt;
&lt;p&gt;포드-풀커슨(Ford-Fulkerson)에서 사용했던 DFS는 기본적으로 잔여 용량이 있는 간선이면 탐색 후보가 될 수 있었다.&lt;/p&gt;
&lt;p&gt;하지만 Dinic의 DFS는 반드시 두 조건을 만족해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 잔여 용량이 남아 있다.

2. level[next]
   = level[current] + 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;코드에서는 다음 부분이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (edge.capacity &amp;lt;= 0) {
    continue;
}

if (level[edge.to]
        != level[current] + 1) {
    continue;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처음에는 두 번째 조건이 단순히 DFS가 확인할 간선을 줄이는 최적화처럼 보였다.&lt;/p&gt;
&lt;p&gt;그런데 실제로는 Dinic의 핵심에 가까웠다.&lt;/p&gt;
&lt;p&gt;Dinic은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이번 BFS에서 확인한 최단 거리 구조 안에서 가능한 흐름을 먼저 모두 처리하자.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라는 방식으로 움직인다.&lt;/p&gt;
&lt;p&gt;따라서 DFS가 Level Graph 밖으로 나가버리면 BFS로 만든 구조를 재사용한다는 의미가 사라진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;에드몬드-카프(Edmonds-Karp)와 비교하면&lt;/h2&gt;
&lt;p&gt;둘 다 BFS를 사용하기 때문에 처음에는 차이가 크지 않아 보였다.&lt;/p&gt;
&lt;p&gt;하지만 BFS를 사용하는 목적부터 조금 다르다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;에드몬드-카프(Edmonds-Karp)

BFS
→ 가장 짧은 증가 경로 하나를 찾는다.
→ 그 경로로 유량을 보낸다.
→ 다시 BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;반면 Dinic은&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Dinic

BFS
→ 전체 Level을 만든다.
→ Level Graph 안에서 유량을 반복해서 보낸다.
→ 더 이상 보낼 수 없을 때 다시 BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;즉 에드몬드-카프(Edmonds-Karp)가&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;최단 증가 경로 하나&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;에 집중한다면,&lt;/p&gt;
&lt;p&gt;Dinic은&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;현재 존재하는 최단 거리 구조 전체&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;를 활용한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;한 번의 BFS로 두 경로를 처리해보면&lt;/h2&gt;
&lt;p&gt;아까 예제를 다시 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Level 0 : S

Level 1 : A, B

Level 2 : T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Level Graph에는 두 개의 &lt;code&gt;S → T&lt;/code&gt; 경로가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A → T

S → B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;모든 Capacity가 1이라면 DFS를 통해 먼저&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;로 1을 보낼 수 있다.&lt;/p&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)였다면 여기서 다시 BFS를 수행한다.&lt;/p&gt;
&lt;p&gt;Dinic은 그렇지 않다.&lt;/p&gt;
&lt;p&gt;현재 Level Graph에서 여전히&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;경로가 남아 있으므로 DFS를 다시 수행한다.&lt;/p&gt;
&lt;p&gt;그리고 이 경로로도 1을 보낸다.&lt;/p&gt;
&lt;p&gt;이제 두 경로 모두 사용할 수 없게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS 한 번
↓
S → A → T 로 1
↓
S → B → T 로 1
↓
현재 Level Graph에서 더 이상 전송 불가능&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이때서야 다음 BFS로 넘어간다.&lt;/p&gt;
&lt;p&gt;이 차이를 직접 보고 나니&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“BFS 결과를 재사용한다”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;라는 표현이 무엇을 의미하는지 훨씬 잘 이해됐다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Blocking Flow&lt;/h2&gt;
&lt;p&gt;Dinic을 공부하면서 가장 낯설었던 단어가 &lt;strong&gt;Blocking Flow&lt;/strong&gt;였다.&lt;/p&gt;
&lt;p&gt;이름만 봤을 때는 최대 유량과 별개의 새로운 종류의 Flow를 구하는 것처럼 느껴졌다.&lt;/p&gt;
&lt;p&gt;하지만 내가 이해하기 편했던 표현은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;현재 Level Graph에서 Source에서 Sink까지 더 이상 유량을 추가할 수 없을 때까지 보낸 상태&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;조금 더 정확하게 말하면,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;현재 Level Graph의 모든 &lt;code&gt;S → T&lt;/code&gt; 경로에 적어도 하나의 포화된 간선이 존재하는 상태&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A → T
S → B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;두 경로가 있었다면 둘 중 하나만 막는 것으로는 Blocking Flow가 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A → T   X
S → B → T   O&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;아직 두 번째 경로로 유량을 보낼 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;두 경로 모두 더 이상 사용할 수 없어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A → T   X
S → B → T   X&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그때 현재 Level Graph의 &lt;code&gt;S → T&lt;/code&gt; 경로가 모두 막힌다.&lt;/p&gt;
&lt;p&gt;그래서 이름도 &lt;strong&gt;Blocking Flow&lt;/strong&gt;라고 이해하면 편했다.&lt;/p&gt;
&lt;p&gt;중요한 것은 Blocking Flow가 전체 Residual Graph에 대한 최대 유량이라는 뜻은 아니라는 점이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;현재 Level Graph 안에서는
더 보낼 수 없다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;는 뜻일 뿐이다.&lt;/p&gt;
&lt;p&gt;잔여 그래프 전체에서는 더 긴 새로운 증가 경로가 존재할 수도 있다.&lt;/p&gt;
&lt;p&gt;그래서 Blocking Flow를 만든 뒤에는 다시 BFS를 수행한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 Blocking Flow를 만들고 나서야 다시 BFS할까?&lt;/h2&gt;
&lt;p&gt;여기가 Dinic의 구조를 이해하는 데 꽤 중요했다.&lt;/p&gt;
&lt;p&gt;현재 BFS에서 Sink의 Level이&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level[T] = 3&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이라고 해보자.&lt;/p&gt;
&lt;p&gt;이 말은 현재 잔여 그래프에서 가장 짧은 증가 경로의 길이가 3이라는 뜻이다.&lt;/p&gt;
&lt;p&gt;Dinic은 이 길이 3짜리 구조 안에서 가능한 유량을 모두 보낸다.&lt;/p&gt;
&lt;p&gt;즉 Blocking Flow를 만든다.&lt;/p&gt;
&lt;p&gt;그러면 길이 3인 &lt;code&gt;S → T&lt;/code&gt; 경로는 더 이상 사용할 수 없다.&lt;/p&gt;
&lt;p&gt;따라서 다음 BFS에서 Sink에 다시 도달할 수 있다면 새로운 최단 거리는 이전보다 길어져야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3
→ 4 이상&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;즉 Dinic의 각 BFS 단계가 끝날 때마다 Sink의 Level은 계속 증가한다.&lt;/p&gt;
&lt;p&gt;이 성질이 나중에 시간복잡도를 설명할 때 중요해진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Dinic의 전체 동작&lt;/h2&gt;
&lt;p&gt;전체 구조를 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Residual Graph
↓
BFS
↓
각 정점의 Level 계산
↓
Sink에 도달할 수 있는가?
│
├─ 아니오 → 종료
│
└─ 예
   ↓
Level Graph에서 DFS
   ↓
가능한 유량 전송
   ↓
DFS 반복
   ↓
Blocking Flow 완성
   ↓
다시 BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;포드-풀커슨(Ford-Fulkerson)에서 사용했던&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;증가 가능한 유량을 찾는다.
→ 유량을 보낸다.
→ Residual Graph를 수정한다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라는 기본 원리는 그대로다.&lt;/p&gt;
&lt;p&gt;다만 증가 가능한 흐름을 &lt;strong&gt;어떤 구조 안에서, 얼마나 묶어서 처리하느냐​&lt;/strong&gt;가 달라졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Java로 구현해보기&lt;/h2&gt;
&lt;p&gt;이번에 정리한 구현은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.*;

public class Dinic {

    static class Edge {
        int to;
        int reverse;
        long capacity;

        Edge(int to, int reverse, long capacity) {
            this.to = to;
            this.reverse = reverse;
            this.capacity = capacity;
        }
    }

    static List&amp;lt;Edge&amp;gt;[] graph;
    static int[] level;
    static int[] work;

    static void addEdge(int from, int to, long capacity) {

        Edge forward =
                new Edge(to, graph[to].size(), capacity);

        Edge reverse =
                new Edge(from, graph[from].size(), 0);

        graph[from].add(forward);
        graph[to].add(reverse);
    }

    static boolean bfs(int source, int sink) {

        Arrays.fill(level, -1);

        Queue&amp;lt;Integer&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();

        level[source] = 0;
        queue.offer(source);

        while (!queue.isEmpty()) {

            int current = queue.poll();

            for (Edge edge : graph[current]) {

                if (edge.capacity &amp;lt;= 0) {
                    continue;
                }

                if (level[edge.to] != -1) {
                    continue;
                }

                level[edge.to] =
                        level[current] + 1;

                queue.offer(edge.to);
            }
        }

        return level[sink] != -1;
    }

    static long dfs(
            int current,
            int sink,
            long flow
    ) {

        if (current == sink) {
            return flow;
        }

        for (;
             work[current] &amp;lt; graph[current].size();
             work[current]++) {

            Edge edge =
                    graph[current].get(work[current]);

            if (edge.capacity &amp;lt;= 0) {
                continue;
            }

            if (level[edge.to]
                    != level[current] + 1) {
                continue;
            }

            long pushed = dfs(
                    edge.to,
                    sink,
                    Math.min(flow, edge.capacity)
            );

            if (pushed &amp;gt; 0) {

                edge.capacity -= pushed;

                Edge reverseEdge =
                        graph[edge.to].get(edge.reverse);

                reverseEdge.capacity += pushed;

                return pushed;
            }
        }

        return 0;
    }

    static long maxFlow(int source, int sink) {

        long result = 0;

        while (bfs(source, sink)) {

            Arrays.fill(work, 0);

            while (true) {

                long flow = dfs(
                        source,
                        sink,
                        Long.MAX_VALUE
                );

                if (flow == 0) {
                    break;
                }

                result += flow;
            }
        }

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처음 보면 포드-풀커슨(Ford-Fulkerson)이나 에드몬드-카프(Edmonds-Karp)보다 코드가 꽤 복잡해진 것처럼 보인다.&lt;/p&gt;
&lt;p&gt;특히 새로 등장한 것이 두 개 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level
→ 이번 BFS에서 만든 Level Graph

work
→ 현재 Level Graph에서
  각 정점의 간선을 어디까지 확인했는지 저장&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 두 배열의 역할만 분리해서 생각하니 코드를 따라가기가 훨씬 쉬웠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;BFS가 하는 일&lt;/h2&gt;
&lt;p&gt;Dinic의 BFS는 실제 증가 경로 하나를 저장하지 않는다.&lt;/p&gt;
&lt;p&gt;대신 각 정점의 &lt;code&gt;level&lt;/code&gt;을 기록한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level[edge.to]
        = level[current] + 1;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;잔여 용량이 양수인 간선만 따라가면서 Source로부터의 최단 거리를 계산한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (edge.capacity &amp;lt;= 0) {
    continue;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;BFS가 끝난 뒤 Sink에 도달하지 못했다면&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return level[sink] != -1;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;현재 잔여 그래프에 더 이상 &lt;code&gt;S → T&lt;/code&gt; 증가 경로가 없다는 뜻이다.&lt;/p&gt;
&lt;p&gt;앞의 에드몬드-카프(Edmonds-Karp) 글에서 봤던 것처럼, 이 순간 현재 Flow가 Maximum Flow가 된다.&lt;/p&gt;
&lt;p&gt;차이는 에드몬드-카프(Edmonds-Karp)가 BFS에서 증가 경로 하나를 복원하기 위한 &lt;code&gt;parent&lt;/code&gt;를 저장했다면,&lt;/p&gt;
&lt;p&gt;Dinic은 이후 DFS 전체가 사용할 &lt;strong&gt;Level 구조&lt;/strong&gt;를 저장한다는 점이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;DFS가 하는 일&lt;/h2&gt;
&lt;p&gt;DFS는 Level Graph 내부에서 실제 유량을 보낸다.&lt;/p&gt;
&lt;p&gt;가장 중요한 조건은 역시 이것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (level[edge.to]
        != level[current] + 1) {
    continue;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;따라서 DFS가 움직이는 모습은 항상&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Level 0
→ Level 1
→ Level 2
→ Level 3
→ ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;형태다.&lt;/p&gt;
&lt;p&gt;Sink에 도달하면 지금까지 가져온 &lt;code&gt;flow&lt;/code&gt;를 반환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (current == sink) {
    return flow;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;경로를 따라 내려갈 때는 현재까지 가능한 Flow와 간선의 잔여 용량 중 작은 값을 넘긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Math.min(flow, edge.capacity)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;포드-풀커슨(Ford-Fulkerson)과 에드몬드-카프(Edmonds-Karp)에서 봤던 Bottleneck Capacity를 재귀 호출 과정에서 함께 계산하는 셈이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Residual Graph는 여전히 그대로다&lt;/h2&gt;
&lt;p&gt;Sink까지 유량을 보냈다면 Residual Graph를 갱신한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;edge.capacity -= pushed;
reverseEdge.capacity += pushed;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 부분은 세 알고리즘 모두 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;포드-풀커슨(Ford-Fulkerson)
에드몬드-카프(Edmonds-Karp)
Dinic&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;모두 결국 Residual Graph 위에서 동작한다.&lt;/p&gt;
&lt;p&gt;처음 포드-풀커슨(Ford-Fulkerson)을 공부할 때 가장 낯설었던 Reverse Edge가 이제는 오히려 가장 익숙한 부분이 됐다.&lt;/p&gt;
&lt;p&gt;알고리즘이 바뀌어도&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;정방향으로 보낸 만큼 잔여 용량 감소

역방향으로 그만큼
되돌릴 수 있는 잔여 용량 증가&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라는 원리는 변하지 않는다.&lt;/p&gt;
&lt;p&gt;달라지는 건 &lt;strong&gt;어떤 증가 가능한 흐름을 어떤 순서와 구조로 찾느냐&lt;/strong&gt;다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;가장 이해하기 어려웠던 &lt;code&gt;work&lt;/code&gt; 배열&lt;/h2&gt;
&lt;p&gt;Dinic 코드를 처음 봤을 때 개인적으로 가장 이해하기 어려웠던 부분은 Level Graph보다 &lt;code&gt;work&lt;/code&gt;였다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int[] work;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처음에는 이런 생각이 들었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;DFS를 새로 호출할 때마다 그냥 간선을 0번부터 다시 확인하면 안 될까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;정답만 보면 가능은 하지만, 그러면 Dinic의 장점을 제대로 살리기 어렵다.&lt;/p&gt;
&lt;p&gt;Dinic은 하나의 Level Graph 안에서 DFS를 여러 번 호출한다.&lt;/p&gt;
&lt;p&gt;어떤 정점에 간선이&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0번
1번
2번
3번
4번&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;까지 있다고 해보자.&lt;/p&gt;
&lt;p&gt;이미 0번부터 2번까지 확인해서&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;잔여 용량이 없거나

Level 조건을 만족하지 않거나

그쪽으로 진행해도 Sink까지
더 이상 유량을 보낼 수 없음&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;을 확인했다면 다음 DFS에서 또 0번부터 검사하는 것은 낭비다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;work[v]&lt;/code&gt;에는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;정점 v에서
다음에 확인할 간선의 위치&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;를 저장한다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;work[A] = 3&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이라면 다음에 &lt;code&gt;A&lt;/code&gt;에 왔을 때 0번부터 다시 보는 게 아니라 3번 간선부터 확인한다.&lt;/p&gt;
&lt;p&gt;이 방식을 흔히 &lt;strong&gt;Current Arc Optimization&lt;/strong&gt;이라고 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그런데 실패했던 간선이 나중에는 다시 가능해질 수도 있지 않을까?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;work&lt;/code&gt;를 공부하면서 이 부분이 한동안 걸렸다.&lt;/p&gt;
&lt;p&gt;현재 어떤 간선을 따라갔는데 Sink까지 유량을 보내지 못했다고 해보자.&lt;/p&gt;
&lt;p&gt;그 뒤 다른 경로에서 유량을 보내면서 Residual Graph가 바뀐다.&lt;/p&gt;
&lt;p&gt;그러면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;아까 실패한 간선도 다시 확인해야 하는 것 아닌가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;하지만 현재 &lt;strong&gt;같은 Level Graph 단계 안에서는&lt;/strong&gt; 다시 확인할 필요가 없다.&lt;/p&gt;
&lt;p&gt;이번 단계의 DFS는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level[next] = level[current] + 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;인 간선만 사용한다.&lt;/p&gt;
&lt;p&gt;이런 정방향 Level 간선의 잔여 용량은 유량을 보낼수록 줄어든다.&lt;/p&gt;
&lt;p&gt;반대로 새로 생기는 Reverse Edge는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Level i + 1
→ Level i&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;방향이다.&lt;/p&gt;
&lt;p&gt;현재 DFS는 이전 Level로 가는 간선을 사용하지 않으므로 이번 Level Graph 안에서는 새로운 전진 경로가 생기지 않는다.&lt;/p&gt;
&lt;p&gt;즉 현재 Level Graph에서 한번 더 이상 사용할 수 없다고 판단한 앞쪽 간선들이, 같은 BFS 단계 안에서 갑자기 다시 유용해지는 일은 없다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;work&lt;/code&gt;를 앞으로만 이동시켜도 된다.&lt;/p&gt;
&lt;p&gt;이 부분을 이해하고 나니 &lt;code&gt;work&lt;/code&gt;가 단순한 코드 최적화가 아니라 Dinic의 Level 구조와 밀접하게 연결되어 있다는 게 보였다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;코드의 &lt;code&gt;work&lt;/code&gt; 반복문이 조금 특이한 이유&lt;/h2&gt;
&lt;p&gt;그래서 DFS의 반복문도 일반적인 형태와 조금 다르다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (;
     work[current] &amp;lt; graph[current].size();
     work[current]++) {&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;보통이라면&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 0; ...)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처럼 지역 변수로 순회하겠지만,&lt;/p&gt;
&lt;p&gt;여기서는 &lt;code&gt;work[current]&lt;/code&gt; 자체를 인덱스로 사용한다.&lt;/p&gt;
&lt;p&gt;덕분에 DFS 호출이 끝난 뒤에도 현재 위치가 남는다.&lt;/p&gt;
&lt;p&gt;그리고 중요한 점이 하나 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (pushed &amp;gt; 0) {
    ...
    return pushed;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;유량을 성공적으로 보냈다면 &lt;code&gt;work[current]&lt;/code&gt;를 바로 다음 간선으로 넘기지 않고 반환한다.&lt;/p&gt;
&lt;p&gt;현재 간선에 아직 잔여 용량이 남아 있을 수도 있기 때문이다.&lt;/p&gt;
&lt;p&gt;다음 DFS에서 같은 간선을 다시 사용해볼 수 있다.&lt;/p&gt;
&lt;p&gt;반대로 그 간선으로 더 이상 보낼 수 없다는 것이 확인되면 반복문이 진행되면서 &lt;code&gt;work[current]&lt;/code&gt;가 다음 간선으로 이동한다.&lt;/p&gt;
&lt;p&gt;이 동작까지 보고 나니 &lt;code&gt;work&lt;/code&gt;의 의미가 훨씬 명확해졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 새로운 BFS를 할 때 &lt;code&gt;work&lt;/code&gt;를 초기화할까?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;maxFlow&lt;/code&gt;에서는 BFS가 성공할 때마다 다음 코드가 실행된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Arrays.fill(work, 0);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처음에는 이전에 확인했던 간선을 또 처음부터 보는 것처럼 느껴졌다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;code&gt;work&lt;/code&gt; 정보는 &lt;strong&gt;현재 Level Graph 안에서만 유효하다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Blocking Flow를 만든 뒤 다시 BFS를 수행하면 각 정점의 Level이 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;그러면 이전에는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level[next]
!= level[current] + 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이라서 사용할 수 없었던 간선이 새로운 Level Graph에서는 사용할 수 있게 될 수도 있다.&lt;/p&gt;
&lt;p&gt;즉 새로운 BFS는 새로운 탐색 단계의 시작이다.&lt;/p&gt;
&lt;p&gt;그래서&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS
↓
새 Level Graph 생성
↓
work 초기화
↓
DFS 반복&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이라는 구조가 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시간복잡도는 왜 O(V²E)일까?&lt;/h2&gt;
&lt;p&gt;일반 그래프에서 Dinic의 시간복잡도는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(V²E)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;처음에는 이 식도 에드몬드-카프(Edmonds-Karp)의 &lt;code&gt;O(VE²)&lt;/code&gt;처럼 그냥 결과만 외우려고 했다.&lt;/p&gt;
&lt;p&gt;하지만 Dinic의 구조를 이해하려면 왜 BFS 단계가 제한되는지를 보는 게 중요했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;첫 번째: BFS 단계는 O(V)번만 반복된다&lt;/h2&gt;
&lt;p&gt;하나의 Level Graph에서 Blocking Flow를 만들었다고 해보자.&lt;/p&gt;
&lt;p&gt;Blocking Flow가 만들어졌다는 것은 현재 존재하던 가장 짧은 &lt;code&gt;S → T&lt;/code&gt; 경로들이 모두 막혔다는 뜻이다.&lt;/p&gt;
&lt;p&gt;따라서 Sink에 다시 도달할 수 있다면 다음 BFS에서 Sink의 Level은 이전보다 커진다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level[T]

2
→ 3
→ 4
→ ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;같은 값으로 계속 머무르지 않는다.&lt;/p&gt;
&lt;p&gt;단순 경로에서 Source부터 Sink까지의 거리는 최대 &lt;code&gt;V - 1&lt;/code&gt;이므로 이런 BFS 단계는 최대 &lt;code&gt;O(V)&lt;/code&gt;번만 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;이게 Dinic의 바깥쪽 반복 횟수에 대한 상한이 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;두 번째: 한 Level Graph에서 Blocking Flow를 구한다&lt;/h2&gt;
&lt;p&gt;BFS 한 번은 인접 리스트 기준으로 &lt;code&gt;O(E)&lt;/code&gt;다.&lt;/p&gt;
&lt;p&gt;그 뒤에는 DFS를 반복해서 Blocking Flow를 만든다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;work&lt;/code&gt;가 중요하다.&lt;/p&gt;
&lt;p&gt;각 정점에서 이미 확인해 필요 없어진 간선을 계속 처음부터 다시 검사하지 않는다.&lt;/p&gt;
&lt;p&gt;현재 Level Graph 안에서는 &lt;code&gt;work&lt;/code&gt;가 한 방향으로만 전진한다.&lt;/p&gt;
&lt;p&gt;일반적인 분석에서는 이 DFS 단계에서 Blocking Flow 하나를 구하는 비용을&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(VE)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;로 볼 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 전체적으로는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Level Graph 단계 O(V)
×
단계마다 Blocking Flow O(VE)

= O(V²E)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;BFS의 &lt;code&gt;O(E)&lt;/code&gt; 비용은 &lt;code&gt;O(VE)&lt;/code&gt; 안에 포함되는 수준이므로 최종 시간복잡도는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(V²E)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;로 정리한다.&lt;/p&gt;
&lt;p&gt;엄밀한 증명을 처음부터 전부 따라가는 건 쉽지 않았지만, 지금은 다음 흐름으로 기억하고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS로 Level Graph를 만든다.
↓
Blocking Flow를 만들어
현재 최단 경로들을 모두 막는다.
↓
다음 BFS에서는 Sink의 Level이 증가한다.
↓
Level은 최대 O(V)번 증가할 수 있다.
↓
한 Level Graph 처리 O(VE)
↓
전체 O(V²E)&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;에드몬드-카프(Edmonds-Karp)와 시간복잡도를 비교하면&lt;/h2&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(VE²)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dinic은&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(V²E)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;두 식만 놓고 모든 상황에서 어느 쪽이 얼마나 빠를지를 단순하게 결론 내리는 것보다는,&lt;/p&gt;
&lt;p&gt;두 알고리즘이 &lt;strong&gt;무엇을 반복하는지&lt;/strong&gt;를 보는 편이 더 이해하기 쉬웠다.&lt;/p&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;최단 증가 경로 하나
→ 다시 BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;를 반복한다.&lt;/p&gt;
&lt;p&gt;Dinic은&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Level Graph 하나
→ 그 안에서 Blocking Flow
→ 다시 BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;를 반복한다.&lt;/p&gt;
&lt;p&gt;즉 Dinic은 비싼 BFS를 한 번 수행한 뒤 그때 만든 최단 거리 구조를 가능한 한 오래 사용한다.&lt;/p&gt;
&lt;p&gt;실전 최대 유량 문제에서 Dinic이 자주 사용되는 이유도 이런 구조적인 효율성과 관련이 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;세 알고리즘을 이어서 보니&lt;/h2&gt;
&lt;p&gt;포드-풀커슨(Ford-Fulkerson)부터 Dinic까지 순서대로 공부한 것이 꽤 도움이 됐다.&lt;/p&gt;
&lt;p&gt;각각 별개의 알고리즘이라고 생각하면 외워야 할 것이 계속 늘어난다.&lt;/p&gt;
&lt;p&gt;그런데 앞 알고리즘에서 무엇이 아쉬웠는지를 따라가면 흐름이 보인다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;알고리즘&lt;/th&gt;
&lt;th&gt;내가 이해한 핵심&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;포드-풀커슨(Ford-Fulkerson)&lt;/td&gt;
&lt;td&gt;증가 경로를 찾고, Residual Graph를 이용해 필요하면 이전 선택을 되돌린다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에드몬드-카프(Edmonds-Karp)&lt;/td&gt;
&lt;td&gt;BFS를 사용해서 항상 가장 짧은 증가 경로를 선택한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dinic&lt;/td&gt;
&lt;td&gt;BFS로 만든 최단 거리 구조를 Level Graph로 만들고, 그 안에서 Blocking Flow까지 한꺼번에 처리한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;조금 더 짧게 표현하면 이런 느낌이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;포드-풀커슨(Ford-Fulkerson)
→ 경로를 찾는다.

에드몬드-카프(Edmonds-Karp)
→ 경로를 어떤 순서로 찾을지 정한다.

Dinic
→ 한 번 찾은 거리 구조를 최대한 활용한다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그리고 세 알고리즘 모두 아래 원리는 그대로 공유한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Residual Graph

Reverse Edge

Bottleneck Capacity

유량을 보낸 뒤
정방향 잔여 용량 감소

역방향 잔여 용량 증가&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;결국 Dinic은 최대 유량의 기본 원리를 완전히 다른 것으로 바꾼 알고리즘이라기보다,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;포드-풀커슨(Ford-Fulkerson)의 증가 경로 방식을 Level Graph와 Blocking Flow를 이용해 더 구조적으로 처리하는 알고리즘&lt;/strong&gt;이라고 보는 게 가장 이해하기 쉬웠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;공부하고 나서&lt;/h2&gt;
&lt;p&gt;이번에 가장 어려웠던 건 역시 두 가지였다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Blocking Flow
work 배열&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Blocking Flow는 처음 이름을 봤을 때는 거창하게 느껴졌다.&lt;/p&gt;
&lt;p&gt;하지만&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;현재 Level Graph의
모든 S → T 경로를 막을 때까지
유량을 보낸 상태&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라고 이해하니 훨씬 쉬웠다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;work&lt;/code&gt;도 처음에는 없어도 되는 미세한 최적화처럼 보였다.&lt;/p&gt;
&lt;p&gt;하지만 같은 Level Graph에서 DFS를 계속 반복한다는 점을 생각해보면,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이미 끝난 간선을
다시 처음부터 확인하지 않는다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;는 것이 Dinic의 효율을 유지하는 중요한 장치라는 걸 알 수 있었다.&lt;/p&gt;
&lt;p&gt;그리고 이번 글을 정리하면서 가장 기억에 남은 건 Dinic의 핵심을 다음처럼 볼 수 있다는 점이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS
→ 최단 거리 구조를 만든다.

DFS
→ 그 구조 안에서 유량을 보낸다.

Blocking Flow
→ 이번 구조를 끝까지 활용한다.

work
→ 같은 구조 안에서 했던 탐색을 반복하지 않는다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;앞에서 에드몬드-카프(Edmonds-Karp)를 공부했을 때는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;왜 굳이 BFS인가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;가 가장 큰 질문이었다면,&lt;/p&gt;
&lt;p&gt;이번에는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;BFS로 얻은 정보를 얼마나 오래 활용할 수 있는가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;가 핵심 질문이었다.&lt;/p&gt;
&lt;p&gt;이렇게 연결해서 보니 에드몬드-카프(Edmonds-Karp)에서 Dinic으로 넘어오는 흐름이 훨씬 자연스럽게 느껴졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음에는 최대 유량을 실제 문제에 적용해보기&lt;/h2&gt;
&lt;p&gt;여기까지 세 가지 최대 유량 알고리즘을 연속해서 살펴봤다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;포드-풀커슨(Ford-Fulkerson)
→ 에드몬드-카프(Edmonds-Karp)
→ Dinic&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이제는 알고리즘 자체의 구현보다는&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;문제를 어떻게 유량 그래프로 바꿔야 하는가&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;를 공부해보고 싶다.&lt;/p&gt;
&lt;p&gt;앞의 에드몬드-카프(Edmonds-Karp) 글에서는 증가 경로가 사라진 순간과 &lt;strong&gt;Max-Flow Min-Cut&lt;/strong&gt;이 어떻게 연결되는지도 간단히 살펴봤다.&lt;/p&gt;
&lt;p&gt;다음에는 그 개념을 실제 문제에서 어떻게 사용하는지까지 이어서 보고 싶다.&lt;/p&gt;
&lt;p&gt;특히 다음 주제들이 궁금하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;최대 유량으로 이분 매칭을 표현하는 방법&lt;/li&gt;
&lt;li&gt;Edge Disjoint Path와 Capacity의 관계&lt;/li&gt;
&lt;li&gt;정점 자체에 Capacity가 있을 때 사용하는 Vertex Splitting&lt;/li&gt;
&lt;li&gt;최대 유량 계산 후 실제 Minimum Cut을 복원하는 방법&lt;/li&gt;
&lt;li&gt;Dinic 구현을 실제 문제에 적용하면서 어떤 부분에서 모델링이 필요한지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지금까지는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Source에서 Sink까지
얼마나 많은 유량을 보낼 수 있는가?&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;를 계산하는 방법 자체에 집중했다.&lt;/p&gt;
&lt;p&gt;그런데 최대 유량 문제를 조금씩 보다 보니 실제로 더 어려운 부분은 알고리즘 구현보다&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이 문제를
어떻게 Flow Network로 표현할 것인가?&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;인 것 같다.&lt;/p&gt;
&lt;p&gt;다음부터는 Dinic을 직접 사용해보면서, 문제를 읽었을 때 &lt;strong&gt;어떤 조건을 간선과 Capacity로 바꾸고 Source와 Sink를 어디에 둘지&lt;/strong&gt;를 판단하는 연습을 해보려고 한다.&lt;/p&gt;</description>
      <category>CS/자료구조 &amp;middot; 알고리즘</category>
      <category>BFS</category>
      <category>dinic</category>
      <category>til</category>
      <category>알고리즘</category>
      <category>최대 유량</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/62</guid>
      <comments>https://slowgrid.tistory.com/entry/til-Dinic#entry62comment</comments>
      <pubDate>Wed, 26 Aug 2026 20:05:59 +0900</pubDate>
    </item>
    <item>
      <title>최대 유량(2) 에드몬드-카프(Edmonds-Karp), DFS를 BFS로 바꾸면 무엇이 달라질까</title>
      <link>https://slowgrid.tistory.com/entry/til-Edmonds-Karp</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;최대 유량(Maximum Flow) 시리즈&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/til-Ford-Fulkerson&quot;&gt;1편 — 포드-풀커슨(Ford-Fulkerson)을 이해하기까지&lt;/a&gt;&lt;br&gt;&lt;strong&gt;2편 — 에드몬드-카프(Edmonds-Karp), DFS를 BFS로 바꾸면 무엇이 달라질까 ← 현재 글&lt;/strong&gt;&lt;br&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/til-Dinic&quot;&gt;3편 — Dinic, 한 번 만든 BFS 결과를 끝까지 활용하기&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;지난 글에서는 최대 유량의 가장 기본적인 방법인 &lt;strong&gt;포드-풀커슨(Ford-Fulkerson)&lt;/strong&gt;을 공부했다.&lt;/p&gt;
&lt;p&gt;그때 가장 오래 붙잡고 있었던 건 알고리즘의 반복 구조보다 &lt;strong&gt;Residual Graph와 Reverse Edge&lt;/strong&gt;였다.&lt;/p&gt;
&lt;p&gt;처음 선택한 증가 경로가 좋지 않더라도, 나중에 기존 흐름을 일부 취소하고 다시 배치할 수 있어야 최대 유량까지 갈 수 있다는 점이 핵심이었다.&lt;/p&gt;
&lt;p&gt;그리고 글 마지막에 이런 질문을 남겼다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;증가 경로를 DFS가 아니라 BFS로 찾으면 무엇이 달라질까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;포드-풀커슨(Ford-Fulkerson) 자체는 잔여 그래프에서 어떤 증가 경로를 선택할지 정해두지 않는다.&lt;/p&gt;
&lt;p&gt;내가 지난번에 구현한 코드는 DFS로 경로를 찾았지만, 꼭 DFS여야 하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;그리고 이 증가 경로를 &lt;strong&gt;BFS로 찾도록 선택 규칙을 고정한 방법&lt;/strong&gt;이 이번에 공부한 &lt;strong&gt;에드몬드-카프(Edmonds-Karp) 알고리즘&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;처음에는 정말 단순하게 생각했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;포드-풀커슨(Ford-Fulkerson) + BFS
= 에드몬드-카프(Edmonds-Karp)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;틀린 설명은 아니다.&lt;/p&gt;
&lt;p&gt;그런데 공부하다 보니 중요한 건 단순히 Queue를 쓰게 된다는 점이 아니었다.&lt;/p&gt;
&lt;p&gt;BFS를 사용하면서 매번&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;현재 잔여 그래프에서 간선 수가 가장 적은 증가 경로를 선택한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;는 규칙이 생긴다.&lt;/p&gt;
&lt;p&gt;그리고 이 작은 규칙 하나가 포드-풀커슨(Ford-Fulkerson)의 실행 과정을 훨씬 예측 가능하게 만들고, 결국 시간복잡도까지 바꾼다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;지난번 예제를 그대로 이어서 보면&lt;/h2&gt;
&lt;p&gt;지난 글에서 Reverse Edge가 왜 필요한지 보기 위해 다음 그래프를 사용했다.&lt;/p&gt;
&lt;p&gt;모든 간선의 용량은 1이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A
S → B
A → B
A → T
B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;눈으로 보면 최대 유량은 2다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A → T
S → B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;각 경로로 1씩 보내면 된다.&lt;/p&gt;
&lt;p&gt;그런데 증가 경로를 DFS로 찾으면 탐색 순서에 따라 처음부터 다음 경로를 선택할 수도 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A → B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 경로도 분명 올바른 증가 경로다.&lt;/p&gt;
&lt;p&gt;다만 간선을 3개 거쳐가는 경로를 먼저 선택했기 때문에 이후에는 Reverse Edge를 이용해 &lt;code&gt;A → B&lt;/code&gt;의 흐름을 취소하고 다시 배치해야 했다.&lt;/p&gt;
&lt;p&gt;지난 글에서는 바로 이 과정을 따라가면서&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Reverse Edge
= 반대 방향으로 새로운 유량을 보내는 길&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 아니라&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Reverse Edge
= 이전 선택을 취소할 수 있는 길&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이라고 이해했다.&lt;/p&gt;
&lt;p&gt;이번에는 같은 그래프를 다른 관점에서 보게 됐다.&lt;/p&gt;
&lt;p&gt;잔여 그래프에서 BFS를 수행하면 Source에서 가까운 정점부터 탐색한다.&lt;/p&gt;
&lt;p&gt;따라서 위 그래프에서는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → A → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;또는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S → B → T&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처럼 간선을 2개만 사용하는 경로를 먼저 찾는다.&lt;/p&gt;
&lt;p&gt;반면 &lt;code&gt;S → A → B → T&lt;/code&gt;는 간선을 3개 사용한다.&lt;/p&gt;
&lt;p&gt;즉 BFS를 사용한다는 건 단순히 DFS 대신 다른 탐색을 쓴다는 뜻이 아니라,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;현재 잔여 그래프에서 가장 짧은 증가 경로를 우선하겠다&lt;/strong&gt;는 선택 기준을 만든다는 뜻이었다.&lt;/p&gt;
&lt;p&gt;여기서 말하는 &lt;code&gt;짧다&lt;/code&gt;는 Capacity가 작다는 뜻이 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;경로를 구성하는 간선의 개수가 적다&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;는 의미다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;에드몬드-카프(Edmonds-Karp)의 핵심&lt;/h2&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)는 매 반복마다 현재 잔여 그래프에서&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Source에서 Sink까지 가는 증가 경로 중 간선 수가 가장 적은 경로&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;를 선택한다.&lt;/p&gt;
&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;포드-풀커슨(Ford-Fulkerson)
→ 어떤 증가 경로를 고를지는 자유로움

에드몬드-카프(Edmonds-Karp)
→ BFS로 가장 짧은 증가 경로를 선택&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처음에는 &lt;code&gt;가장 짧은 경로&lt;/code&gt;라는 표현 때문에 다익스트라 같은 최단 경로를 떠올렸다.&lt;/p&gt;
&lt;p&gt;하지만 여기서는 간선의 가중치나 Capacity를 더하지 않는다.&lt;/p&gt;
&lt;p&gt;모든 간선을 한 단계로 보고,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Source에서 Sink까지 몇 개의 간선을 거치는가?&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;만 비교한다.&lt;/p&gt;
&lt;p&gt;그래서 가중치가 없는 그래프의 최단 거리를 구하는 BFS가 정확히 맞는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;전체 구조는 포드-풀커슨(Ford-Fulkerson)과 같다&lt;/h2&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)의 전체 흐름은 지난번에 본 포드-풀커슨(Ford-Fulkerson)과 거의 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 잔여 그래프에서 BFS를 수행한다.
2. Source에서 Sink까지 가는 증가 경로를 찾는다.
3. parent 정보를 이용해 실제 경로를 복원한다.
4. 경로의 Bottleneck Capacity를 구한다.
5. 그만큼 유량을 보낸다.
6. 정방향 잔여 용량을 줄인다.
7. 역방향 잔여 용량을 늘린다.
8. 더 이상 증가 경로가 없을 때까지 반복한다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;지난번 코드와 비교하면 가장 눈에 띄는 변화는 정말 이것뿐이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DFS → BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;하지만 의미는 단순한 구현 교체보다 크다.&lt;/p&gt;
&lt;p&gt;DFS에서는 탐색 순서에 따라 전혀 다른 증가 경로가 먼저 선택될 수 있었다.&lt;/p&gt;
&lt;p&gt;BFS에서는 항상 최소 간선 수의 증가 경로가 선택된다.&lt;/p&gt;
&lt;p&gt;이 차이가 뒤에서 볼 시간복잡도의 핵심이 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;BFS에서는 경로를 어떻게 기억할까?&lt;/h2&gt;
&lt;p&gt;지난번 DFS 구현에서는 재귀 호출을 따라가면서 현재 경로를 자연스럽게 알 수 있었다.&lt;/p&gt;
&lt;p&gt;하지만 BFS는 Queue에서 여러 정점을 순서대로 꺼내며 탐색한다.&lt;/p&gt;
&lt;p&gt;그래서 Sink를 발견했다는 사실만으로는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;어떤 정점들을 거쳐 여기까지 왔는가?&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;를 바로 알 수 없다.&lt;/p&gt;
&lt;p&gt;따라서 처음 방문한 정점마다 이전 정보를 저장한다.&lt;/p&gt;
&lt;p&gt;이번 구현에서는 두 가지를 기록했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parentVertex[v]
→ v에 오기 직전의 정점

parentEdge[v]
→ 그때 사용한 간선의 인덱스&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;코드로는 다음 부분이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parentVertex[edge.to] = current;
parentEdge[edge.to] = i;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;parentVertex&lt;/code&gt;만 있어도 정점의 순서는 복원할 수 있다.&lt;/p&gt;
&lt;p&gt;그런데 실제 잔여 용량을 갱신하려면 &lt;strong&gt;어떤 Edge 객체를 타고 왔는지&lt;/strong&gt;까지 알아야 한다.&lt;/p&gt;
&lt;p&gt;특히 같은 두 정점 사이에 여러 간선이 존재할 수도 있기 때문에, &lt;code&gt;parentEdge&lt;/code&gt;까지 기록해두면 Sink에서 Source로 되짚어가며 정확한 간선을 바로 찾을 수 있다.&lt;/p&gt;
&lt;p&gt;이 부분은 처음에는 BFS의 부가적인 구현처럼 보였는데, 실제로는 경로 복원과 잔여 그래프 갱신을 연결하는 중요한 정보였다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Java로 구현해보기&lt;/h2&gt;
&lt;p&gt;지난 글과 마찬가지로 &lt;code&gt;Edge.capacity&lt;/code&gt;는 유량을 보내기 시작한 뒤에는 원래 Capacity 자체라기보다 &lt;strong&gt;현재 잔여 용량&lt;/strong&gt;으로 사용한다.&lt;/p&gt;
&lt;p&gt;즉 별도의 &lt;code&gt;flow&lt;/code&gt; 필드를 두지 않고 정방향과 역방향의 잔여 용량을 직접 갱신하는 구조다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.*;

public class EdmondsKarp {

    static class Edge {
        int to;
        int reverse;
        long capacity;

        Edge(int to, int reverse, long capacity) {
            this.to = to;
            this.reverse = reverse;
            this.capacity = capacity;
        }
    }

    static List&amp;lt;Edge&amp;gt;[] graph;

    static void addEdge(int from, int to, long capacity) {

        Edge forward =
                new Edge(to, graph[to].size(), capacity);

        Edge reverse =
                new Edge(from, graph[from].size(), 0);

        graph[from].add(forward);
        graph[to].add(reverse);
    }

    static long maxFlow(int source, int sink) {

        long result = 0;
        int n = graph.length;

        while (true) {

            int[] parentVertex = new int[n];
            int[] parentEdge = new int[n];

            Arrays.fill(parentVertex, -1);

            Queue&amp;lt;Integer&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();

            queue.offer(source);
            parentVertex[source] = source;

            while (!queue.isEmpty()
                    &amp;amp;&amp;amp; parentVertex[sink] == -1) {

                int current = queue.poll();

                for (int i = 0;
                     i &amp;lt; graph[current].size();
                     i++) {

                    Edge edge = graph[current].get(i);

                    if (edge.capacity &amp;lt;= 0) {
                        continue;
                    }

                    if (parentVertex[edge.to] != -1) {
                        continue;
                    }

                    parentVertex[edge.to] = current;
                    parentEdge[edge.to] = i;

                    queue.offer(edge.to);

                    if (edge.to == sink) {
                        break;
                    }
                }
            }

            if (parentVertex[sink] == -1) {
                break;
            }

            long flow = Long.MAX_VALUE;

            for (int v = sink;
                 v != source;
                 v = parentVertex[v]) {

                int u = parentVertex[v];

                Edge edge =
                        graph[u].get(parentEdge[v]);

                flow = Math.min(
                        flow,
                        edge.capacity
                );
            }

            for (int v = sink;
                 v != source;
                 v = parentVertex[v]) {

                int u = parentVertex[v];

                Edge edge =
                        graph[u].get(parentEdge[v]);

                edge.capacity -= flow;

                Edge reverseEdge =
                        graph[v].get(edge.reverse);

                reverseEdge.capacity += flow;
            }

            result += flow;
        }

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;코드를 크게 나누면 세 단계다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS로 증가 경로 찾기
↓
경로의 병목 용량 구하기
↓
잔여 그래프 갱신하기&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;뒤의 두 단계는 포드-풀커슨(Ford-Fulkerson)에서 이미 봤던 구조와 같다.&lt;/p&gt;
&lt;p&gt;이번에 새롭게 들어온 핵심은 BFS와 &lt;code&gt;parent&lt;/code&gt;를 이용한 경로 복원이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. BFS로 증가 경로 찾기&lt;/h2&gt;
&lt;p&gt;먼저 모든 정점을 방문하지 않은 상태로 만든다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] parentVertex = new int[n];
int[] parentEdge = new int[n];

Arrays.fill(parentVertex, -1);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여기서는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parentVertex[v] == -1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이면 아직 방문하지 않은 정점이라는 뜻으로 사용했다.&lt;/p&gt;
&lt;p&gt;Source는 시작점이므로 미리 방문 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queue.offer(source);
parentVertex[source] = source;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그리고 Queue에서 정점을 하나씩 꺼내며 잔여 용량이 남아 있는 간선만 따라간다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (edge.capacity &amp;lt;= 0) {
    continue;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;잔여 용량이 0이라면 현재 잔여 그래프에서는 사용할 수 없는 간선이다.&lt;/p&gt;
&lt;p&gt;이미 방문한 정점도 다시 방문하지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (parentVertex[edge.to] != -1) {
    continue;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처음 방문했다면 어디에서 왔는지와 어떤 간선을 사용했는지 기록한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parentVertex[edge.to] = current;
parentEdge[edge.to] = i;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;BFS이므로 어떤 정점을 처음 방문한 순간의 경로가 Source에서 그 정점까지 가는 최소 간선 수 경로가 된다.&lt;/p&gt;
&lt;p&gt;따라서 Sink를 처음 발견했을 때 기록된 parent를 따라가면 이번에 사용할 가장 짧은 증가 경로를 얻는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 경로의 Bottleneck Capacity 구하기&lt;/h2&gt;
&lt;p&gt;Sink에 도달했다면 &lt;code&gt;parentVertex&lt;/code&gt;를 따라 Source까지 거꾸로 올라간다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;long flow = Long.MAX_VALUE;

for (int v = sink;
     v != source;
     v = parentVertex[v]) {

    int u = parentVertex[v];

    Edge edge =
            graph[u].get(parentEdge[v]);

    flow = Math.min(
            flow,
            edge.capacity
    );
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;경로를 이루는 모든 간선의 잔여 용량 중 가장 작은 값을 찾는다.&lt;/p&gt;
&lt;p&gt;이 값이 이번 증가 경로로 추가할 수 있는 최대 유량이다.&lt;/p&gt;
&lt;p&gt;지난 글에서 본 &lt;strong&gt;Bottleneck Capacity&lt;/strong&gt;와 같은 개념이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 다시 Residual Graph 갱신하기&lt;/h2&gt;
&lt;p&gt;유량을 보낸 뒤에는 정방향과 역방향의 잔여 용량을 함께 바꾼다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;edge.capacity -= flow;
reverseEdge.capacity += flow;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;정방향으로 &lt;code&gt;flow&lt;/code&gt;만큼 사용했으므로 앞으로 추가로 보낼 수 있는 양은 줄어든다.&lt;/p&gt;
&lt;p&gt;반대로 그만큼은 나중에 다시 취소할 수 있어야 하므로 역방향 잔여 용량은 늘어난다.&lt;/p&gt;
&lt;p&gt;지난번에는 이 두 줄이 가장 낯설었다.&lt;/p&gt;
&lt;p&gt;이번에는 오히려 포드-풀커슨(Ford-Fulkerson)과 에드몬드-카프(Edmonds-Karp)가 같은 잔여 그래프 구조 위에서 동작한다는 걸 확인하는 부분처럼 느껴졌다.&lt;/p&gt;
&lt;p&gt;탐색 방법은 바뀌었지만,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;정방향 잔여 용량 감소
역방향 잔여 용량 증가&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라는 최대 유량의 핵심 구조는 그대로다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;BFS를 쓰면 Reverse Edge가 필요 없을까?&lt;/h2&gt;
&lt;p&gt;여기서 한 번 이런 생각이 들었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;BFS가 매번 가장 짧은 증가 경로를 고른다면, 애초에 이상한 경로를 덜 고를 테니 Reverse Edge가 없어도 되지 않을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;하지만 그렇지 않다.&lt;/p&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)도 결국 &lt;strong&gt;잔여 그래프에서 증가 경로를 찾아 유량을 늘리는 포드-풀커슨(Ford-Fulkerson) 방식의 하나&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;BFS가 보장하는 것은&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;현재 잔여 그래프에서
간선 수가 가장 적은 증가 경로를 고른다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;는 것뿐이다.&lt;/p&gt;
&lt;p&gt;지금 고른 경로가 이후의 모든 유량 배치까지 고려한 전역적인 최적 선택이라는 뜻은 아니다.&lt;/p&gt;
&lt;p&gt;잔여 그래프가 갱신되면 새로운 가능성이 생길 수 있고, 그 과정에서 과거에 보낸 유량 일부를 취소해야 할 수도 있다.&lt;/p&gt;
&lt;p&gt;따라서 에드몬드-카프(Edmonds-Karp)에서도 Reverse Edge는 그대로 필요하다.&lt;/p&gt;
&lt;p&gt;이 부분을 보고 나니 Reverse Edge는 DFS 구현에서만 필요한 보정 장치가 아니라, &lt;strong&gt;증가 경로를 이용하는 최대 유량 알고리즘 자체의 핵심 구조&lt;/strong&gt;라는 점이 더 분명해졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;BFS가 Sink에 도달하지 못하면 왜 끝일까?&lt;/h2&gt;
&lt;p&gt;코드에서는 BFS가 끝난 뒤 Sink의 parent가 설정되지 않았다면 반복을 종료한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (parentVertex[sink] == -1) {
    break;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;지난 글에서도 포드-풀커슨(Ford-Fulkerson)이 더 이상 증가 경로를 찾지 못하면 최대 유량에 도달한다고 정리했다.&lt;/p&gt;
&lt;p&gt;그때는 이 사실을 &lt;strong&gt;Max-Flow Min-Cut 정리와 연결해서 나중에 다시 보자&lt;/strong&gt;고 넘어갔다.&lt;/p&gt;
&lt;p&gt;이번에 에드몬드-카프(Edmonds-Karp)의 BFS를 생각하니 그 연결이 조금 더 눈에 보였다.&lt;/p&gt;
&lt;p&gt;BFS가 끝났을 때 Source에서 잔여 용량이 양수인 간선만 따라 도달할 수 있는 정점들의 집합을 &lt;code&gt;S&lt;/code&gt;라고 하자.&lt;/p&gt;
&lt;p&gt;Sink에는 도달하지 못했으므로 Sink는 반대쪽 집합 &lt;code&gt;T&lt;/code&gt;에 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S : 잔여 그래프에서 Source로부터 도달 가능한 정점들
T : 나머지 정점들, Sink 포함&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그런데 &lt;code&gt;S&lt;/code&gt;에서 &lt;code&gt;T&lt;/code&gt;로 가는 간선에 잔여 용량이 남아 있었다면 BFS가 그 간선을 따라 &lt;code&gt;T&lt;/code&gt; 쪽 정점에도 도달했어야 한다.&lt;/p&gt;
&lt;p&gt;따라서 탐색이 끝난 시점에는 &lt;code&gt;S → T&lt;/code&gt; 방향으로 더 보낼 수 있는 잔여 용량이 없다.&lt;/p&gt;
&lt;p&gt;원래 그래프의 &lt;code&gt;S → T&lt;/code&gt; 간선은 더 보낼 여지가 없을 만큼 포화되어 있고, 반대로 &lt;code&gt;T → S&lt;/code&gt;로 흐르는 유량이 남아 있었다면 그것을 취소할 수 있는 &lt;code&gt;S → T&lt;/code&gt; 방향의 Reverse Edge가 잔여 그래프에 나타나야 하므로 역시 이런 상태와 맞지 않는다.&lt;/p&gt;
&lt;p&gt;즉 Source와 Sink 사이에 더 이상 넘을 수 없는 경계가 생긴 셈이다.&lt;/p&gt;
&lt;p&gt;이 경계가 바로 하나의 &lt;strong&gt;s-t Cut&lt;/strong&gt;이고, 이때 현재 유량의 값과 그 Cut의 Capacity가 같아진다.&lt;/p&gt;
&lt;p&gt;그래서&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;더 이상 증가 경로가 없다
→ 현재 유량을 더 늘릴 방법이 없다
→ 현재 유량이 최대 유량이다&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라고 말할 수 있다.&lt;/p&gt;
&lt;p&gt;그리고 이것을 엄밀하게 정리한 것이&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Maximum Flow = Minimum Cut Capacity&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라는 &lt;strong&gt;Max-Flow Min-Cut 정리&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;지난 글에서는 이 정리를 이름만 남겨두고 넘어갔는데, 이번에는 적어도 “왜 증가 경로가 없으면 끝인가?”라는 종료 조건과 연결해서 이해할 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시간복잡도&lt;/h2&gt;
&lt;p&gt;여기가 이번에 가장 궁금했던 부분이었다.&lt;/p&gt;
&lt;p&gt;지난 글에서 DFS를 사용한 포드-풀커슨(Ford-Fulkerson) 구현은 간선의 Capacity가 모두 정수라고 할 때&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(EF)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;로 정리했다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;F&lt;/code&gt;는 최대 유량의 크기다.&lt;/p&gt;
&lt;p&gt;증가 경로를 한 번 찾을 때마다 최소 1의 유량이 증가한다고 보고, 최대 &lt;code&gt;F&lt;/code&gt;번 반복할 수 있다는 계산이었다.&lt;/p&gt;
&lt;p&gt;문제는 &lt;code&gt;F&lt;/code&gt;가 입력된 Capacity 값에 따라 굉장히 커질 수 있다는 점이었다.&lt;/p&gt;
&lt;p&gt;그래서 포드-풀커슨(Ford-Fulkerson)의 실행 시간은 그래프의 &lt;code&gt;V&lt;/code&gt;, &lt;code&gt;E&lt;/code&gt;뿐 아니라 실제 유량 값에도 영향을 받는다.&lt;/p&gt;
&lt;p&gt;반면 에드몬드-카프(Edmonds-Karp)의 시간복잡도는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(VE²)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;최대 유량의 값 &lt;code&gt;F&lt;/code&gt;가 식에서 사라진다.&lt;/p&gt;
&lt;p&gt;처음에는 단순히&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS 한 번 = O(E)
증가 경로 탐색 횟수 = O(VE)
따라서 O(VE²)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라고 외우려고 했다.&lt;/p&gt;
&lt;p&gt;그런데 이걸로는 가장 궁금했던 질문이 해결되지 않았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;왜 BFS를 사용하면 증가 경로를 찾는 횟수가 O(VE)로 제한될까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;DFS를 BFS로 바꿨을 뿐인데 왜 O(VE²)일까?&lt;/h2&gt;
&lt;p&gt;핵심은 BFS가 매번 가장 짧은 증가 경로를 선택한다는 사실이다.&lt;/p&gt;
&lt;p&gt;BFS를 한 번 수행했다고 생각해보자.&lt;/p&gt;
&lt;p&gt;각 정점에는 Source에서부터의 최단 거리가 생긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dist[v]
= 현재 잔여 그래프에서
  Source에서 v까지 가는 최소 간선 수&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;에드몬드-카프(Edmonds-Karp)에서는 이 &lt;code&gt;dist&lt;/code&gt;가 알고리즘이 진행되는 동안 &lt;strong&gt;감소하지 않는다&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;처음 이 문장이 잘 이해되지 않았다.&lt;/p&gt;
&lt;p&gt;유량을 보내고 나면 잔여 그래프에는 새로운 Reverse Edge가 생길 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;그렇다면 새로운 지름길이 생겨 거리가 더 짧아질 수도 있을 것 같았다.&lt;/p&gt;
&lt;p&gt;하지만 이번 증가 경로가 BFS 기준으로&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level 0 → level 1 → level 2 → ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처럼 한 단계씩 앞으로 가는 경로였다면, 유량을 보낸 뒤 새로 생기는 Reverse Edge는 반대로&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;level i + 1 → level i&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;방향을 가진다.&lt;/p&gt;
&lt;p&gt;즉 새로 생기는 간선은 기존 BFS 레벨을 거슬러 올라가는 방향이다.&lt;/p&gt;
&lt;p&gt;그래서 Source에서 어떤 정점까지 더 빨리 갈 수 있는 새로운 지름길을 만들지 못한다.&lt;/p&gt;
&lt;p&gt;기존 간선이 사라지면서 최단 거리가 길어질 수는 있지만, 더 짧아지지는 않는다.&lt;/p&gt;
&lt;p&gt;이 성질이 첫 번째 핵심이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Source로부터 각 정점까지의 최단 거리
→ 감소하지 않는다.&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;한 간선이 계속 병목이 될 수는 없다&lt;/h2&gt;
&lt;p&gt;증가 경로로 유량을 보낼 때는 경로의 Bottleneck Capacity만큼 보낸다.&lt;/p&gt;
&lt;p&gt;따라서 매번 증가가 끝나면 그 경로에 있던 간선 중 적어도 하나는 잔여 용량이 0이 된다.&lt;/p&gt;
&lt;p&gt;즉 적어도 하나의 간선이 &lt;strong&gt;포화(saturated)&lt;/strong&gt; 된다.&lt;/p&gt;
&lt;p&gt;어떤 잔여 간선 &lt;code&gt;(u, v)&lt;/code&gt;가 이번 최단 증가 경로에서 병목이 되어 사라졌다고 해보자.&lt;/p&gt;
&lt;p&gt;그 순간 BFS 레벨에서는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dist[v] = dist[u] + 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이었다.&lt;/p&gt;
&lt;p&gt;그 간선이 나중에 다시 같은 방향 &lt;code&gt;(u, v)&lt;/code&gt;으로 등장해 또 병목이 되려면, 중간에 반대 방향 흐름을 이용해 유량이 다시 조정되어야 한다.&lt;/p&gt;
&lt;p&gt;그 과정에서 같은 간선이 다시 최단 증가 경로의 후보가 될 때는 이전보다 Source에서 더 먼 레벨에서 나타나게 된다.&lt;/p&gt;
&lt;p&gt;조금 더 구체적으로 보면 같은 방향의 간선이 다시 병목이 될 때 &lt;code&gt;u&lt;/code&gt;의 거리는 이전보다 적어도 2 이상 커진다.&lt;/p&gt;
&lt;p&gt;그런데 단순 경로에서 Source로부터의 거리는 최대 &lt;code&gt;V - 1&lt;/code&gt; 정도까지만 커질 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 하나의 간선이 병목 역할을 반복할 수 있는 횟수는 &lt;code&gt;O(V)&lt;/code&gt;로 제한된다.&lt;/p&gt;
&lt;p&gt;잔여 그래프에서 고려하는 간선 수는 정방향과 역방향을 포함해도 &lt;code&gt;O(E)&lt;/code&gt;다.&lt;/p&gt;
&lt;p&gt;그래서 전체 증가 횟수는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;간선 O(E)
× 각 간선이 병목이 되는 횟수 O(V)
= O(VE)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;로 제한된다.&lt;/p&gt;
&lt;p&gt;인접 리스트에서 BFS 자체는 &lt;code&gt;O(V + E)&lt;/code&gt;이고, 찾은 경로를 복원하고 잔여 용량을 갱신하는 데는 &lt;code&gt;O(V)&lt;/code&gt;가 든다.&lt;/p&gt;
&lt;p&gt;최대 유량 분석에서는 탐색에 실제로 관여하는 정점만 놓으면 &lt;code&gt;V = O(E)&lt;/code&gt;로 볼 수 있으므로, 표준적으로 한 번의 증가 과정을 &lt;code&gt;O(E)&lt;/code&gt;로 정리한다.&lt;/p&gt;
&lt;p&gt;따라서 최종적으로&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(VE) × O(E)
= O(VE²)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;엄밀한 증명은 처음 보면 꽤 복잡하지만, 지금은 다음 흐름으로 기억하는 게 가장 이해하기 쉬웠다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS가 항상 최단 증가 경로를 고른다.
↓
Source로부터의 최단 거리는 줄어들지 않는다.
↓
같은 간선이 병목으로 반복해서 등장하려면
점점 더 뒤의 레벨로 밀려나야 한다.
↓
한 간선이 병목이 될 수 있는 횟수는 O(V).
↓
전체 증가 횟수 O(VE).
↓
전체 시간복잡도 O(VE²).&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;결국 BFS가 DFS보다 한 번의 탐색 자체를 압도적으로 빠르게 해서 좋아진 것이 아니었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;증가 경로를 고르는 순서에 구조를 만들어서 전체 반복 횟수에 상한을 만든 것&lt;/strong&gt;이 핵심이었다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;O(VE²)&lt;/code&gt;는 유량 값 &lt;code&gt;F&lt;/code&gt;에 의존하지 않는다.&lt;/p&gt;
&lt;p&gt;그래서 포드-풀커슨(Ford-Fulkerson)에서 신경 써야 했던 정수 Capacity 가정과 달리, 에드몬드-카프(Edmonds-Karp)의 종료와 이 시간복잡도는 Capacity 값의 크기에 직접 기대지 않는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;포드-풀커슨(Ford-Fulkerson)과 에드몬드-카프(Edmonds-Karp)를 다시 비교하면&lt;/h2&gt;
&lt;p&gt;처음에는 차이를 이렇게만 봤다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;포드-풀커슨(Ford-Fulkerson) : DFS
에드몬드-카프(Edmonds-Karp)    : BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;하지만 정확히는 조금 다르게 보는 편이 좋았다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;포드-풀커슨(Ford-Fulkerson)
→ 증가 경로를 어떻게 고를지 정하지 않은 방법
→ 내가 지난번에 구현한 버전은 DFS를 사용

에드몬드-카프(Edmonds-Karp)
→ 포드-풀커슨(Ford-Fulkerson)의 증가 경로 선택을 BFS로 고정
→ 항상 최소 간선 수의 증가 경로를 사용&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;즉 핵심은 자료구조가&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Stack/재귀 → Queue&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;로 바뀌었다는 데 있지 않았다.&lt;/p&gt;
&lt;p&gt;증가 경로의 선택이 임의적이지 않게 되고,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;항상 현재 잔여 그래프에서 가장 짧은 경로&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라는 질서가 생긴다는 데 있었다.&lt;/p&gt;
&lt;p&gt;이 질서 때문에 실행 과정을 그래프의 크기만으로 제한할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그런데 BFS 정보를 경로 하나에만 써도 될까?&lt;/h2&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)의 코드를 보고 나니 다음 의문이 생겼다.&lt;/p&gt;
&lt;p&gt;현재 구현은 Sink를 발견하면 BFS를 멈춘다.&lt;/p&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)에는 &lt;strong&gt;가장 짧은 증가 경로 하나&lt;/strong&gt;만 있으면 충분하기 때문이다.&lt;/p&gt;
&lt;p&gt;그리고 그 경로로 유량을 보낸 뒤 잔여 그래프가 바뀌면 다시 처음부터 BFS를 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS
↓
최단 증가 경로 하나 복원
↓
유량 전송
↓
잔여 그래프 갱신
↓
다시 BFS&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그런데 BFS는 원래 Source에서 각 정점이 몇 단계 떨어져 있는지라는 &lt;strong&gt;레벨 정보&lt;/strong&gt;도 만들 수 있다.&lt;/p&gt;
&lt;p&gt;그렇다면 Sink까지 가는 최단 경로 하나만 뽑아 쓰고 바로 다시 BFS를 하는 대신,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;한 번 구한 레벨 구조를 이용해서 여러 경로에 유량을 보낼 수는 없을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;이 질문이 다음에 공부할 &lt;strong&gt;Dinic 알고리즘&lt;/strong&gt;으로 이어진다.&lt;/p&gt;
&lt;p&gt;Dinic에서는 BFS로 &lt;code&gt;level[v]&lt;/code&gt;를 만들고, 그 레벨이 한 단계씩 증가하는 간선들만 이용해서 한 번의 레벨 그래프에서 가능한 흐름을 더 많이 밀어낸다.&lt;/p&gt;
&lt;p&gt;즉 흐름을 아주 거칠게 이어보면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;포드-풀커슨(Ford-Fulkerson)
→ 증가 경로 선택이 자유롭다.

에드몬드-카프(Edmonds-Karp)
→ BFS로 최단 증가 경로 하나를 고른다.

Dinic
→ BFS로 만든 레벨 구조를 더 적극적으로 활용한다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이렇게 놓고 보니 세 알고리즘이 완전히 별개의 방법이라기보다, &lt;strong&gt;증가 경로와 탐색 정보를 점점 더 체계적으로 사용하는 방향으로 발전한다&lt;/strong&gt;는 느낌이 들었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;공부하고 나서&lt;/h2&gt;
&lt;p&gt;지난 글에서 가장 기억에 남았던 건&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;잘못된 선택을 나중에 되돌릴 수 있어야 한다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;는 점이었다.&lt;/p&gt;
&lt;p&gt;그래서 Residual Graph와 Reverse Edge가 중요했다.&lt;/p&gt;
&lt;p&gt;이번에는 그 위에 한 가지가 더 붙었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;가능한 증가 경로라면 아무거나 고르는 것이 아니라,
어떤 순서로 고르느냐도 성능을 바꾼다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;처음에는 에드몬드-카프(Edmonds-Karp)를&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;포드-풀커슨(Ford-Fulkerson)에서
DFS 대신 BFS를 사용하는 알고리즘&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;정도로만 생각했다.&lt;/p&gt;
&lt;p&gt;지금은 조금 다르게 기억하려고 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;에드몬드-카프(Edmonds-Karp)
= BFS를 이용해
  항상 최소 간선 수의 증가 경로를 선택하는
  포드-풀커슨(Ford-Fulkerson)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;중요한 것은 BFS 자체가 아니라 &lt;strong&gt;BFS가 만들어내는 경로 선택의 규칙&lt;/strong&gt;이었다.&lt;/p&gt;
&lt;p&gt;그 규칙 덕분에 Source로부터의 최단 거리가 뒤로 가지 않고, 같은 간선이 병목으로 무한히 반복되는 상황도 막을 수 있다.&lt;/p&gt;
&lt;p&gt;결국 작은 구현 차이가&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(EF) → O(VE²)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라는 분석의 차이로 이어진다는 점이 이번 공부에서 가장 인상적이었다.&lt;/p&gt;
&lt;p&gt;또 지난번에 이름만 남겨두었던 Max-Flow Min-Cut도 이번에는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BFS로 더 이상 Sink에 도달할 수 없다
→ 잔여 그래프에 증가 경로가 없다
→ 현재 유량이 최대 유량이다&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;라는 종료 조건과 연결해서 조금 더 이해할 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음에는 Dinic&lt;/h2&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)까지 보고 나니 이제 다음 질문이 자연스럽게 생겼다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;BFS로 최단 거리 정보를 한 번 만들었다면, 그 정보를 경로 하나보다 더 많이 활용할 수 없을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;다음에는 이 아이디어를 사용하는 &lt;strong&gt;Dinic 알고리즘&lt;/strong&gt;을 공부해보려고 한다.&lt;/p&gt;
&lt;p&gt;특히 다음 내용을 중심으로 정리할 생각이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Level Graph가 무엇인지&lt;/li&gt;
&lt;li&gt;BFS에서 만든 &lt;code&gt;level&lt;/code&gt; 정보가 어떤 역할을 하는지&lt;/li&gt;
&lt;li&gt;Blocking Flow가 무엇인지&lt;/li&gt;
&lt;li&gt;왜 한 번의 BFS 뒤에 여러 경로로 유량을 보낼 수 있는지&lt;/li&gt;
&lt;li&gt;에드몬드-카프(Edmonds-Karp)와 Dinic의 차이&lt;/li&gt;
&lt;li&gt;Dinic의 시간복잡도는 어떻게 나오는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;포드-풀커슨(Ford-Fulkerson)에서는 &lt;strong&gt;잘못된 선택을 되돌릴 수 있는 구조&lt;/strong&gt;를 배웠고,&lt;/p&gt;
&lt;p&gt;에드몬드-카프(Edmonds-Karp)에서는 &lt;strong&gt;경로 선택에 규칙을 주면 알고리즘의 성능을 통제할 수 있다&lt;/strong&gt;는 걸 봤다.&lt;/p&gt;
&lt;p&gt;다음 Dinic까지 이어서 보면, 최대 유량 알고리즘이 탐색 정보를 어떻게 점점 더 재사용하는 방향으로 발전하는지 조금 더 선명하게 보일 것 같다.&lt;/p&gt;</description>
      <category>CS/자료구조 &amp;middot; 알고리즘</category>
      <category>BFS</category>
      <category>til</category>
      <category>알고리즘</category>
      <category>에드몬드-카프</category>
      <category>최대 유량</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/61</guid>
      <comments>https://slowgrid.tistory.com/entry/til-Edmonds-Karp#entry61comment</comments>
      <pubDate>Tue, 25 Aug 2026 19:51:57 +0900</pubDate>
    </item>
    <item>
      <title>최대 유량(1) 포드-풀커슨(Ford-Fulkerson)을 이해하기까지</title>
      <link>https://slowgrid.tistory.com/entry/til-Ford-Fulkerson</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;최대 유량(Maximum Flow) 시리즈&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1편 — 포드-풀커슨(Ford-Fulkerson)을 이해하기까지 ← 현재 글&lt;/strong&gt;&lt;br&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/til-Edmonds-Karp&quot;&gt;2편 — 에드몬드-카프(Edmonds-Karp), DFS를 BFS로 바꾸면 무엇이 달라질까&lt;/a&gt;&lt;br&gt;&lt;a href=&quot;https://slowgrid.tistory.com/entry/til-Dinic&quot;&gt;3편 — Dinic, 한 번 만든 BFS 결과를 끝까지 활용하기&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;오늘은 최대 유량(Maximum Flow)의 가장 기본적인 알고리즘인 &lt;strong&gt;포드-풀커슨(Ford-Fulkerson)&lt;/strong&gt; 을 공부했다.&lt;/p&gt;
&lt;p&gt;처음 이름을 들었을 때는 꽤 어려운 알고리즘일 것 같았는데, 막상 기본 아이디어만 놓고 보면 생각보다 단순했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Source에서 Sink까지 갈 수 있는 경로를 찾고,&lt;br&gt;그 경로로 보낼 수 있는 만큼 유량을 보낸다.&lt;br&gt;그리고 더 이상 보낼 수 없을 때까지 반복한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;처음에는 나도 딱 이 정도로 이해했다.&lt;/p&gt;
&lt;p&gt;그런데 공부를 계속하다 보니 포드-풀커슨에서 정말 중요한 부분은 단순히 &lt;strong&gt;“어떤 경로로 유량을 보낼 것인가”​&lt;/strong&gt;가 아니었다.&lt;/p&gt;
&lt;p&gt;오히려 더 중요한 건,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“이미 보낸 유량을 나중에 취소할 수도 있어야 한다”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;는 점이었다.&lt;/p&gt;
&lt;p&gt;이걸 이해하고 나서야 &lt;code&gt;Residual Graph&lt;/code&gt;와 &lt;code&gt;Reverse Edge&lt;/code&gt;가 왜 필요한지 조금씩 보이기 시작했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;최대 유량이란?&lt;/h2&gt;
&lt;p&gt;최대 유량 문제에는 특별한 두 정점이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Source : 유량이 출발하는 정점
Sink   : 유량이 도착하는 정점&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 각 간선에는 한 번에 보낼 수 있는 최대 양인 &lt;strong&gt;Capacity(용량)&lt;/strong&gt; 가 존재한다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음과 같은 그래프가 있다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S ──3──&amp;gt; A ──2──&amp;gt; T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;S → A&lt;/code&gt;는 최대 3만큼 보낼 수 있고, &lt;code&gt;A → T&lt;/code&gt;는 최대 2만큼 보낼 수 있다.&lt;/p&gt;
&lt;p&gt;그렇다면 &lt;code&gt;S → A → T&lt;/code&gt;라는 경로 전체로 보낼 수 있는 유량은 3이 아니라 2다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min(3, 2) = 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;경로 중간에 용량이 2인 간선이 있기 때문이다.&lt;/p&gt;
&lt;p&gt;이처럼 하나의 경로로 보낼 수 있는 최대 유량은 그 경로에 포함된 간선들의 용량 중 가장 작은 값으로 결정된다.&lt;/p&gt;
&lt;p&gt;이 값을 &lt;strong&gt;병목 용량(Bottleneck Capacity)&lt;/strong&gt; 이라고 부른다.&lt;/p&gt;
&lt;p&gt;이 부분까지는 비교적 직관적이었다.&lt;/p&gt;
&lt;p&gt;결국 물이 흐르는 파이프를 생각하면, 아무리 앞쪽 파이프가 넓어도 중간에 좁은 파이프가 하나 있으면 전체 흐름은 그 좁은 파이프에 맞춰질 수밖에 없다.&lt;/p&gt;
&lt;p&gt;여기서 최대 유량 문제에는 한 가지 조건이 더 있다.&lt;/p&gt;
&lt;p&gt;단순히 각 간선의 용량만 넘지 않으면 되는 것이 아니라, &lt;code&gt;Source&lt;/code&gt;와 &lt;code&gt;Sink&lt;/code&gt;를 제외한 중간 정점에서는 &lt;strong&gt;들어온 유량과 나가는 유량이 같아야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;A&lt;/code&gt;라는 정점으로 3만큼의 유량이 들어왔다면, &lt;code&gt;A&lt;/code&gt;에서 갑자기 1이 사라지거나 2가 새로 생길 수는 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A로 들어온 유량의 합 = A에서 나가는 유량의 합&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 조건을 &lt;strong&gt;유량 보존(Flow Conservation)&lt;/strong&gt; 이라고 한다.&lt;/p&gt;
&lt;p&gt;그래서 최대 유량 문제의 조건을 아주 간단히 정리하면 두 가지가 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Capacity Constraint
   각 간선의 유량은 그 간선의 용량을 넘을 수 없다.

2. Flow Conservation
   Source와 Sink를 제외한 정점에서는 들어온 유량과 나가는 유량이 같다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 우리가 최대화하려는 값은 &lt;code&gt;Source&lt;/code&gt;에서 실제로 빠져나가 &lt;code&gt;Sink&lt;/code&gt;까지 도달한 전체 유량이다.&lt;/p&gt;
&lt;p&gt;이 조건을 먼저 알고 나니 이후에 나오는 잔여 그래프도 단순히 &amp;quot;갈 수 있는 길&amp;quot;을 찾는 그래프가 아니라, &lt;strong&gt;현재 유량을 깨뜨리지 않으면서 얼마나 더 조정할 수 있는지 표현하는 그래프&lt;/strong&gt;라는 느낌이 조금 더 분명해졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;포드-풀커슨의 기본 아이디어&lt;/h2&gt;
&lt;p&gt;포드-풀커슨은 Source에서 Sink까지 &lt;strong&gt;아직 유량을 추가로 보낼 수 있는 경로&lt;/strong&gt;를 계속 찾는다.&lt;/p&gt;
&lt;p&gt;이런 경로를 &lt;strong&gt;증가 경로(Augmenting Path)&lt;/strong&gt; 라고 한다.&lt;/p&gt;
&lt;p&gt;여기서 나중에 중요해지는 점은, 증가 경로를 &lt;strong&gt;원래 그래프가 아니라 잔여 그래프(Residual Graph)에서 찾는다&lt;/strong&gt;는 것이다.&lt;/p&gt;
&lt;p&gt;처음에는 둘의 차이가 별로 없어 보이지만, 유량을 한 번이라도 보내고 나면 잔여 그래프에는 정방향의 남은 용량뿐 아니라 역방향으로 되돌릴 수 있는 용량까지 생긴다.&lt;/p&gt;
&lt;p&gt;즉 증가 경로는 단순히&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;아직 사용하지 않은 원래 간선을 이어 만든 경로&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 아니라,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;현재 상태에서 유량을 추가하거나 기존 유량을 취소하면서
Source에서 Sink까지 전체 유량을 더 늘릴 수 있는 경로&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 이해하는 편이 더 정확했다.&lt;/p&gt;
&lt;p&gt;그리고 포드-풀커슨 자체는 이 증가 경로를 &lt;strong&gt;어떤 방식으로 찾을지까지 고정한 알고리즘은 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DFS로 찾아도 되고, 다른 방식으로 찾아도 된다. 내가 아래에서 구현한 코드는 DFS를 사용한 형태이고, 이후에 공부할 Edmonds-Karp는 이 증가 경로를 BFS로 찾도록 규칙을 정한 방법이다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음과 같은 증가 경로가 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 간선으로 추가로 보낼 수 있는 양이 다음과 같다면,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A : 5
A → B : 3
B → T : 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경로로 한 번에 보낼 수 있는 최대 유량은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min(5, 3, 7) = 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;그러면 3만큼 유량을 흘려보낸 뒤, 다시 Source에서 Sink까지 갈 수 있는 증가 경로를 찾는다.&lt;/p&gt;
&lt;p&gt;또 찾으면 보내고, 다시 찾는다.&lt;/p&gt;
&lt;p&gt;결국 더 이상 증가 경로가 존재하지 않게 되는 순간 현재까지 보낸 유량이 최대 유량이 된다.&lt;/p&gt;
&lt;p&gt;처음에는 이 문장도 조금 당연하게 받아들였는데, 생각해보면 한 가지 의문이 생긴다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;지금 당장 보이는 증가 경로가 없다는 사실만으로, 정말 더 좋은 유량 배치도 없다고 말할 수 있을까?&amp;quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;핵심은 증가 경로를 잔여 그래프에서 찾고 있다는 데 있다.&lt;/p&gt;
&lt;p&gt;잔여 그래프에는 앞으로 더 보낼 수 있는 선택뿐 아니라, 이전에 보낸 유량을 취소해서 다른 곳으로 재배치할 수 있는 선택까지 들어 있다.&lt;/p&gt;
&lt;p&gt;그런 잔여 그래프에서도 &lt;code&gt;Source&lt;/code&gt;에서 &lt;code&gt;Sink&lt;/code&gt;로 가는 경로가 전혀 남아 있지 않다면, 이제는 &lt;strong&gt;추가 전송도, 기존 흐름의 재조정도 이용해서 전체 유량을 더 키울 방법이 없다는 뜻&lt;/strong&gt;이 된다.&lt;/p&gt;
&lt;p&gt;이 사실을 더 엄밀하게 설명하는 것이 &lt;strong&gt;Max-Flow Min-Cut 정리&lt;/strong&gt;인데, 이번에는 우선 이 정도의 직관만 잡고 넘어가려고 한다.&lt;/p&gt;
&lt;p&gt;알고리즘만 적어보면 정말 간단하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Source에서 Sink까지 증가 경로를 찾는다.
2. 해당 경로의 병목 용량을 구한다.
3. 그만큼 유량을 보낸다.
4. 더 이상 증가 경로가 없을 때까지 반복한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;문제는 여기서부터였다.&lt;/p&gt;
&lt;p&gt;처음에는 단순히 남아 있는 정방향 간선만 따라가면 되는 줄 알았다.&lt;/p&gt;
&lt;p&gt;그렇게 생각하면 어떤 경우에는 정답을 구할 수 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 역방향 간선이 필요한가?&lt;/h2&gt;
&lt;p&gt;이번에 공부하면서 가장 오래 붙잡고 있었던 부분이다.&lt;/p&gt;
&lt;p&gt;다음과 같은 그래프가 있다고 해보자.&lt;/p&gt;
&lt;p&gt;모든 간선의 용량은 1이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A
S → B
A → B
A → T
B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;눈으로 보면 최대 유량은 금방 찾을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → T
S → B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 경로로 1씩 보내면 총 2를 보낼 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 최대 유량은 2다.&lt;/p&gt;
&lt;p&gt;그런데 포드-풀커슨에서 증가 경로를 DFS로 찾는다고 생각해보자.&lt;/p&gt;
&lt;p&gt;DFS가 운 나쁘게 처음에 다음 경로를 선택할 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기로 1을 보내면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A
A → B
B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;세 간선의 용량을 모두 사용하게 된다.&lt;/p&gt;
&lt;p&gt;그러면 겉으로 보기에 남은 간선은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → B
A → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;뿐이다.&lt;/p&gt;
&lt;p&gt;정방향 간선만 놓고 보면 더 이상 &lt;code&gt;S&lt;/code&gt;에서 &lt;code&gt;T&lt;/code&gt;까지 이어지는 경로가 없다.&lt;/p&gt;
&lt;p&gt;여기서 알고리즘을 끝내버리면 답을 1이라고 판단하게 된다.&lt;/p&gt;
&lt;p&gt;하지만 우리는 이미 실제 정답이 2라는 걸 알고 있다.&lt;/p&gt;
&lt;p&gt;처음 이 예제를 봤을 때 여기서 의문이 생겼다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“그럼 처음에 잘못된 경로를 선택하면 어떻게 하지?”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 질문에 대한 답이 바로 &lt;strong&gt;잔여 그래프(Residual Graph)&lt;/strong&gt; 와 ​&lt;strong&gt;역방향 간선(Reverse Edge)&lt;/strong&gt; 이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;잔여 그래프는 단순히 ‘남은 용량’만 표현하지 않는다&lt;/h2&gt;
&lt;p&gt;어떤 간선의 용량이 5이고 현재 3만큼 유량을 보냈다고 하자.&lt;/p&gt;
&lt;p&gt;그러면 앞으로 추가로 보낼 수 있는 양은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;5 - 3 = 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다.&lt;/p&gt;
&lt;p&gt;이 2를 &lt;strong&gt;잔여 용량(Residual Capacity)&lt;/strong&gt; 이라고 한다.&lt;/p&gt;
&lt;p&gt;정방향 간선만 놓고 보면 잔여 용량은 다음처럼 생각할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;정방향 잔여 용량 = 원래 용량 - 현재 흐르고 있는 유량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반대로 이미 보낸 유량을 취소할 수 있는 양은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;역방향 잔여 용량 = 현재 그 방향으로 흘리고 있는 유량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;예를 들어 원래 용량이 5인 &lt;code&gt;A → B&lt;/code&gt;에 현재 3의 유량이 흐르고 있다면 상태는 이렇게 볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B : 앞으로 2만큼 더 보낼 수 있음
B → A : 기존에 보낸 3만큼 취소할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 잔여 그래프에서 어떤 간선의 잔여 용량이 0보다 크다는 것은 단순히 &amp;quot;원래 그 방향으로 간선이 존재한다&amp;quot;는 뜻이 아니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;현재 유량 상태를 기준으로 그 방향의 조정을 실제로 할 수 있다&lt;/strong&gt;는 뜻에 가깝다.&lt;/p&gt;
&lt;p&gt;여기까지만 보면 잔여 그래프는 그냥 “얼마나 더 보낼 수 있는지 기록해둔 그래프”처럼 느껴진다.&lt;/p&gt;
&lt;p&gt;그런데 최대 유량에서는 한 가지를 더 한다.&lt;/p&gt;
&lt;p&gt;정방향으로 유량을 보낸 만큼 &lt;strong&gt;역방향 간선의 잔여 용량도 늘려준다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B : 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 간선으로 실제로 1만큼 유량을 보냈다면 잔여 그래프에는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;B → A : 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 역방향 간선이 생긴다.&lt;/p&gt;
&lt;p&gt;처음에는 이게 가장 이상했다.&lt;/p&gt;
&lt;p&gt;실제 그래프에는 &lt;code&gt;B → A&lt;/code&gt;라는 간선이 없는데, 갑자기 왜 없는 길을 만들어내는 건지 이해가 잘 되지 않았다.&lt;/p&gt;
&lt;p&gt;한동안은 역방향 간선을 그냥&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;B에서 A로 유량을 보내기 위한 간선&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 생각했다.&lt;/p&gt;
&lt;p&gt;그렇게 생각하니 계속 헷갈렸다.&lt;/p&gt;
&lt;p&gt;결국 예제를 직접 따라가면서 의미를 바꿔서 이해하게 됐다.&lt;/p&gt;
&lt;p&gt;역방향 간선은 새로운 유량을 반대로 보내기 위한 길이 아니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이전에 &lt;code&gt;A → B&lt;/code&gt;로 보냈던 유량을 취소할 수 있다는 표시다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이렇게 생각하니 훨씬 자연스러웠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;처음의 잘못된 선택을 다시 되돌려보면&lt;/h2&gt;
&lt;p&gt;아까 DFS가 처음에 다음 경로를 선택했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경로로 1을 보냈기 때문에 &lt;code&gt;A → B&lt;/code&gt;에도 1의 유량이 흐르고 있다.&lt;/p&gt;
&lt;p&gt;그러면 잔여 그래프에는 그만큼 되돌릴 수 있는 역방향 간선이 생긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;B → A : 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 다음 증가 경로를 다시 찾아보면 이런 경로를 만들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → B → A → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 처음에는 &lt;code&gt;B → A&lt;/code&gt;가 상당히 이상해 보였다.&lt;/p&gt;
&lt;p&gt;그런데 이걸 실제 그래프에서 B에서 A로 유량을 새로 보내는 것이라고 생각하면 안 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;B → A&lt;/code&gt;를 사용한다는 건 기존에 흘렸던&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;의 유량을 1만큼 취소한다는 뜻이다.&lt;/p&gt;
&lt;p&gt;결과적으로 처음에는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → B → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 흘렸던 흐름이 재조정된다.&lt;/p&gt;
&lt;p&gt;두 번째 증가 경로인&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → B → A → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 따라 1을 흘린다고 생각해보면 각 변화는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → B : 1만큼 새로 보냄
B → A : 기존 A → B 유량 1을 취소함
A → T : 1만큼 새로 보냄&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 처음 선택에서 사용했던 &lt;code&gt;A → B&lt;/code&gt;의 유량은 다시 0이 된다.&lt;/p&gt;
&lt;p&gt;최종적으로 실제 원본 그래프에서 흐르는 유량만 다시 보면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A : 1
A → T : 1

S → B : 1
B → T : 1

A → B : 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;즉 결과적으로는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → T : 1
S → B → T : 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 형태가 되고, 최대 유량 2를 만들 수 있다.&lt;/p&gt;
&lt;p&gt;이 과정을 보고 나니 역방향 간선을 따라간다는 표현이 왜 헷갈렸는지도 알 것 같았다. 잔여 그래프에서는 &lt;code&gt;B → A&lt;/code&gt;로 이동하지만, &lt;strong&gt;원본 그래프의 최종 유량에서 B에서 A로 1이 흐르는 것은 아니다.&lt;/strong&gt; 기존 &lt;code&gt;A → B&lt;/code&gt;의 흐름을 없애는 연산일 뿐이다.&lt;/p&gt;
&lt;p&gt;이 예제를 직접 종이에 그려가면서 따라가고 나서야 왜 역방향 간선이 필요한지 제대로 이해할 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;내가 이해한 Reverse Edge&lt;/h2&gt;
&lt;p&gt;공부하기 전에는 이렇게 생각했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Reverse Edge
= 반대로 유량을 보내는 간선&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;지금은 조금 다르게 이해하고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Reverse Edge
= 이전에 했던 선택을 취소할 수 있는 간선&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;개인적으로는 두 번째 표현이 훨씬 이해하기 쉬웠다.&lt;/p&gt;
&lt;p&gt;이 관점에서 보면 잔여 그래프도 단순히&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;지금 앞으로 얼마나 더 갈 수 있는가&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;만 표현하는 그래프가 아니다.&lt;/p&gt;
&lt;p&gt;동시에&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이전에 했던 선택을 얼마나 되돌릴 수 있는가&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;까지 표현한다.&lt;/p&gt;
&lt;p&gt;결국 포드-풀커슨은 처음부터 완벽한 경로를 선택해야 하는 알고리즘이 아니었다.&lt;/p&gt;
&lt;p&gt;일단 가능한 경로를 선택하고, 나중에 더 좋은 방법이 발견되면 기존의 흐름을 일부 취소하면서 전체 흐름을 다시 조정할 수 있다.&lt;/p&gt;
&lt;p&gt;이게 최대 유량을 공부하면서 가장 인상 깊었던 부분이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Java로 구현해보기&lt;/h2&gt;
&lt;p&gt;내가 공부하면서 정리한 구현은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.*;

public class FordFulkerson {

    static class Edge {
        int to;
        int reverse;
        long capacity;

        Edge(int to, int reverse, long capacity) {
            this.to = to;
            this.reverse = reverse;
            this.capacity = capacity;
        }
    }

    static List&amp;lt;Edge&amp;gt;[] graph;
    static boolean[] visited;

    static void addEdge(int from, int to, long capacity) {
        Edge forward = new Edge(to, graph[to].size(), capacity);
        Edge reverse = new Edge(from, graph[from].size(), 0);

        graph[from].add(forward);
        graph[to].add(reverse);
    }

    static long dfs(int current, int sink, long flow) {

        if (current == sink) {
            return flow;
        }

        visited[current] = true;

        for (Edge edge : graph[current]) {

            if (edge.capacity &amp;lt;= 0) {
                continue;
            }

            if (visited[edge.to]) {
                continue;
            }

            long pushed = dfs(
                    edge.to,
                    sink,
                    Math.min(flow, edge.capacity)
            );

            if (pushed &amp;gt; 0) {

                edge.capacity -= pushed;

                Edge reverseEdge =
                        graph[edge.to].get(edge.reverse);

                reverseEdge.capacity += pushed;

                return pushed;
            }
        }

        return 0;
    }

    static long maxFlow(int source, int sink) {

        long result = 0;

        while (true) {

            visited = new boolean[graph.length];

            long flow =
                    dfs(source, sink, Long.MAX_VALUE);

            if (flow == 0) {
                break;
            }

            result += flow;
        }

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;전체 코드를 처음 봤을 때는 DFS 자체보다도 아래 두 줄이 가장 이해하기 어려웠다.&lt;/p&gt;
&lt;p&gt;그 전에 코드에서 한 가지 짚고 넘어갈 점이 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Edge&lt;/code&gt;의 필드 이름은 &lt;code&gt;capacity&lt;/code&gt;지만, 이 구현에서 DFS가 시작된 이후의 &lt;code&gt;capacity&lt;/code&gt;는 사실상 &lt;strong&gt;원래 용량 자체라기보다 현재 남아 있는 잔여 용량&lt;/strong&gt;으로 사용된다.&lt;/p&gt;
&lt;p&gt;처음 &lt;code&gt;addEdge()&lt;/code&gt;를 호출할 때는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;정방향 잔여 용량 = 원래 capacity
역방향 잔여 용량 = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로 시작하고, 유량을 보낼 때마다 이 두 값을 직접 수정한다.&lt;/p&gt;
&lt;p&gt;그래서 별도의 &lt;code&gt;flow&lt;/code&gt; 필드가 없어도 잔여 그래프를 유지할 수 있다.&lt;/p&gt;
&lt;p&gt;이 관점으로 보면 아래 두 줄의 의미가 훨씬 분명해진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;edge.capacity -= pushed;
reverseEdge.capacity += pushed;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;지금은 이 두 줄이 사실상 잔여 그래프의 핵심이라고 생각한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pushed&lt;/code&gt;만큼 유량을 보냈다면 정방향으로 앞으로 보낼 수 있는 양은 그만큼 줄어든다.&lt;/p&gt;
&lt;p&gt;동시에 그만큼의 유량은 나중에 다시 취소할 수도 있어야 한다.&lt;/p&gt;
&lt;p&gt;그래서 역방향의 잔여 용량을 증가시킨다.&lt;/p&gt;
&lt;p&gt;예를 들어 용량이 5인 간선으로 3만큼 유량을 보냈다면,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;정방향 잔여 용량 : 5 → 2
역방향 잔여 용량 : 0 → 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;즉 앞으로는 2를 더 보낼 수 있고, 필요하다면 지금까지 보낸 3만큼은 다시 되돌릴 수 있는 상태가 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;&lt;code&gt;reverse&lt;/code&gt;는 왜 저장할까?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Edge&lt;/code&gt; 클래스에는 처음 보면 조금 낯선 값이 하나 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int reverse;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 값은 &lt;strong&gt;현재 간선과 짝을 이루는 역방향 간선이 상대 정점의 리스트에서 몇 번째에 있는지&lt;/strong&gt; 저장한다.&lt;/p&gt;
&lt;p&gt;그래프의 각 정점마다 간선을 &lt;code&gt;ArrayList&lt;/code&gt; 형태로 관리하고 있기 때문에, 정방향 간선에서 역방향 간선을 바로 찾아가려면 위치를 알고 있어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;addEdge()&lt;/code&gt;를 다시 보면 인덱스를 저장하는 타이밍도 의도적이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Edge forward = new Edge(to, graph[to].size(), capacity);
Edge reverse = new Edge(from, graph[from].size(), 0);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;forward&lt;/code&gt;를 만들 때의 &lt;code&gt;graph[to].size()&lt;/code&gt;는 잠시 뒤 &lt;code&gt;reverse&lt;/code&gt;가 &lt;code&gt;graph[to]&lt;/code&gt;에 추가될 위치다.&lt;/p&gt;
&lt;p&gt;반대로 &lt;code&gt;reverse&lt;/code&gt;를 만들 때의 &lt;code&gt;graph[from].size()&lt;/code&gt;는 아직 &lt;code&gt;forward&lt;/code&gt;를 추가하기 전이므로, 잠시 뒤 &lt;code&gt;forward&lt;/code&gt;가 들어갈 위치가 된다.&lt;/p&gt;
&lt;p&gt;즉 두 간선이 서로의 인덱스를 미리 기억하도록 만들어 둔 것이다.&lt;/p&gt;
&lt;p&gt;그래서 다음 코드처럼 접근할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Edge reverseEdge =
        graph[edge.to].get(edge.reverse);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 단순한 인덱스 하나를 왜 굳이 &lt;code&gt;Edge&lt;/code&gt; 안에 들고 있는지 의아해 할 수 있는데, 잔여 용량을 계속 갱신해야 한다는 걸 이해하고 나면 이유가 보인다.&lt;/p&gt;
&lt;p&gt;유량을 흘릴 때마다 정방향 간선과 역방향 간선을 함께 수정해야 하므로 두 간선을 빠르게 연결할 방법이 필요한 것이다.&lt;/p&gt;
&lt;p&gt;또 하나 처음 구현할 때 놓치기 쉬운 부분은 &lt;code&gt;visited&lt;/code&gt;를 매 반복마다 새로 만드는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;while (true) {
    visited = new boolean[graph.length];
    long flow = dfs(source, sink, Long.MAX_VALUE);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한 번의 DFS 안에서는 같은 정점을 반복해서 방문하면 사이클에 빠질 수 있으므로 &lt;code&gt;visited&lt;/code&gt;가 필요하다.&lt;/p&gt;
&lt;p&gt;하지만 증가 경로 하나를 찾아 유량을 보내고 나면 잔여 용량이 바뀌고, 그에 따라 다음 탐색에서 갈 수 있는 경로도 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 이전 DFS에서 방문했던 정점이라는 이유만으로 다음 DFS에서도 막아버리면 안 된다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;증가 경로를 한 번 찾을 때마다 방문 배열을 초기화&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;그리고 이 구현의 &lt;code&gt;dfs()&lt;/code&gt;는 가능한 경로를 하나 찾으면 그 경로로 보낼 수 있는 유량을 즉시 반환한다. 한 번의 DFS 호출로 여러 증가 경로를 한꺼번에 처리하는 구조가 아니라,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;증가 경로 하나 찾기
→ 병목만큼 보내기
→ 잔여 그래프 갱신
→ 처음부터 다시 탐색&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 반복하는 구조다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시간복잡도&lt;/h2&gt;
&lt;p&gt;DFS를 한 번 수행해서 증가 경로를 찾는 데는 대략&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(E)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 필요하다.&lt;/p&gt;
&lt;p&gt;그리고 간선의 용량이 모두 정수라고 가정하면, 증가 경로를 찾을 때마다 전체 유량은 최소 1씩 증가한다.&lt;/p&gt;
&lt;p&gt;최대 유량의 크기를 &lt;code&gt;F&lt;/code&gt;라고 하면 증가 경로를 최대 &lt;code&gt;F&lt;/code&gt;번 찾을 수 있으므로 전체 시간복잡도는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(EF)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 볼 수 있다.&lt;/p&gt;
&lt;p&gt;여기서 포드-풀커슨의 단점도 보인다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;F&lt;/code&gt; 자체가 굉장히 클 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;즉 실행 시간이 단순히 정점과 간선의 개수에만 영향을 받는 것이 아니라, &lt;strong&gt;실제 유량의 크기와 증가 경로를 어떤 순서로 선택하느냐&lt;/strong&gt;에도 영향을 받을 수 있다.&lt;/p&gt;
&lt;p&gt;이 때문에 이후에는 증가 경로를 BFS로 찾는 &lt;strong&gt;Edmonds-Karp&lt;/strong&gt;도 공부해볼 필요가 있다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;간선의 용량이 정수&lt;/code&gt;라는 가정이 생각보다 중요하다.&lt;/p&gt;
&lt;p&gt;정수 용량이라면 증가 경로를 찾을 때마다 병목 용량도 양의 정수이므로 전체 유량이 최소 1씩 증가한다. 따라서 최대 유량이 &lt;code&gt;F&lt;/code&gt;라면 증가 과정을 &lt;code&gt;F&lt;/code&gt;번보다 많이 반복할 수 없다.&lt;/p&gt;
&lt;p&gt;반면 일반적인 실수 용량까지 허용하면 포드-풀커슨은 증가 경로를 어떻게 선택하느냐에 따라 종료가 보장되지 않는 경우도 있다. 알고리즘 문제에서 보통 정수 용량을 다루는 이유 중 하나이기도 하다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;O(EF)&lt;/code&gt;는 &lt;code&gt;V&lt;/code&gt;, &lt;code&gt;E&lt;/code&gt;만으로 표현되는 다항 시간복잡도와는 조금 성격이 다르다. &lt;code&gt;F&lt;/code&gt;가 입력에 적힌 숫자의 크기 자체에 의존하기 때문이다.&lt;/p&gt;
&lt;p&gt;예를 들어 아주 큰 용량이 이진수로는 몇 자리 안 되게 표현될 수 있어도, &lt;code&gt;F&lt;/code&gt; 값 자체는 매우 커질 수 있다. 그래서 포드-풀커슨의 &lt;code&gt;O(EF)&lt;/code&gt;는 보통 &lt;strong&gt;pseudo-polynomial(의사 다항)&lt;/strong&gt; 시간복잡도라고 설명한다.&lt;/p&gt;
&lt;p&gt;이 점까지 보고 나니 Edmonds-Karp가 왜 단순히 &amp;quot;DFS를 BFS로 바꾼 버전&amp;quot; 이상의 의미를 가지는지도 조금 궁금해졌다. 증가 경로 선택 규칙을 고정해서, 실제 유량 값 &lt;code&gt;F&lt;/code&gt;에 직접 의존하지 않는 시간복잡도를 얻을 수 있기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;공부하고 나서&lt;/h2&gt;
&lt;p&gt;최대 유량을 처음 공부하기 전에는 대략 이런 알고리즘이라고 생각했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;경로를 찾는다.
→ 가능한 만큼 보낸다.
→ 반복한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;틀린 설명은 아니지만, 지금 생각하면 핵심이 하나 빠져 있었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;잘못 선택한 경로를 나중에 수정할 수 있어야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이걸 가능하게 만드는 것이 잔여 그래프이고, 코드에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;정방향 잔여 용량 감소
역방향 잔여 용량 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 형태로 나타난다.&lt;/p&gt;
&lt;p&gt;특히 역방향 간선을&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;반대로 유량을 보내는 길&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 생각할 때는 계속 개념이 어색했는데,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;기존 선택을 취소할 수 있는 길&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 받아들이고 나서는 포드-풀커슨의 구조가 훨씬 자연스럽게 느껴졌다.&lt;/p&gt;
&lt;p&gt;돌이켜보면 이번에 가장 도움이 됐던 건 코드를 여러 번 읽는 것보다, 일부러 DFS가 좋지 않은 경로를 먼저 선택하는 작은 그래프를 직접 만들어본 것이었다.&lt;/p&gt;
&lt;p&gt;왜 역방향 간선이 필요한지 모르는 상태에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;reverseEdge.capacity += pushed;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 코드가 그냥 외워야 하는 구현처럼 보였다.&lt;/p&gt;
&lt;p&gt;하지만 잘못된 경로를 선택한 상황을 직접 따라가 보니 이 한 줄이 &lt;strong&gt;“이전 결정을 되돌릴 수 있는 가능성”​&lt;/strong&gt;을 저장하고 있다는 게 보였다.&lt;/p&gt;
&lt;p&gt;최대 유량은 단순한 Greedy처럼 한 번 선택한 경로를 끝까지 유지하는 방식이 아니었다.&lt;/p&gt;
&lt;p&gt;필요하다면 이전 결정을 취소하고, 흐름을 다시 배치하면서 전체적으로 더 나은 답을 만들어가는 알고리즘이었다.&lt;/p&gt;
&lt;p&gt;이번 공부에서는 그 감각을 잡은 게 가장 큰 수확이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음에 공부할 것&lt;/h2&gt;
&lt;p&gt;포드-풀커슨의 기본 구조와 잔여 그래프까지는 어느 정도 감을 잡았다.&lt;/p&gt;
&lt;p&gt;다음에는 증가 경로를 DFS가 아니라 &lt;strong&gt;BFS로 선택하는 Edmonds-Karp 알고리즘&lt;/strong&gt;을 이어서 공부해보려고 한다.&lt;/p&gt;
&lt;p&gt;그 과정에서 다음 내용도 함께 정리해볼 생각이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ford-Fulkerson과 Edmonds-Karp의 차이&lt;/li&gt;
&lt;li&gt;두 알고리즘의 시간복잡도 비교&lt;/li&gt;
&lt;li&gt;BFS로 증가 경로를 선택했을 때 얻는 장점&lt;/li&gt;
&lt;li&gt;Max-Flow Min-Cut 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;최대 유량은 처음 봤을 때는 단순히 “네트워크에서 얼마나 많이 보낼 수 있는가”를 구하는 문제라고 생각했다.&lt;/p&gt;
&lt;p&gt;그런데 실제로 공부해보니 핵심은 &lt;strong&gt;흐름을 보내는 것보다, 이미 만든 흐름을 어떻게 수정할 수 있게 설계하느냐​&lt;/strong&gt;에 더 가까웠다.&lt;/p&gt;
&lt;p&gt;다음 알고리즘까지 공부하고 나면 이 구조가 조금 더 선명하게 보일 것 같다.&lt;/p&gt;</description>
      <category>CS/자료구조 &amp;middot; 알고리즘</category>
      <category>til</category>
      <category>알고리즘</category>
      <category>최대 유량</category>
      <category>포드-풀커슨</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/60</guid>
      <comments>https://slowgrid.tistory.com/entry/til-Ford-Fulkerson#entry60comment</comments>
      <pubDate>Mon, 24 Aug 2026 19:38:34 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] - 2026.08.23 | 비트마스킹 이해하기</title>
      <link>https://slowgrid.tistory.com/entry/til-Bitmasking</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;코딩테스트 문제를 풀다 보면 &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; 같은 비트 연산자가 종종 등장한다.&lt;br&gt;문법 자체는 알고 있었지만, 어떤 문제에서 비트 연산을 사용해야 하는지는 아직 익숙치 않았다.&lt;/p&gt;
&lt;p&gt;오늘은 비트마스킹의 기본 개념을 정리하고, 프로그래머스 &lt;code&gt;[1차] 비밀지도&lt;/code&gt; 문제를 직접 풀면서 비트를 어떻게 문제 풀이에 활용할 수 있는지 정리해봤다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;오늘 공부한 것&lt;/h2&gt;
&lt;p&gt;오늘의 핵심은 &lt;strong&gt;비트마스킹(Bitmasking)&lt;/strong&gt; 이다.&lt;/p&gt;
&lt;p&gt;처음에는 비트마스킹이라는 이름 때문에 별도의 어려운 알고리즘이라고 생각했다.&lt;/p&gt;
&lt;p&gt;그런데 공부해보니 핵심은 생각보다 단순했다.&lt;/p&gt;
&lt;p&gt;하나의 정수를 이진수로 바라보고, 각각의 비트를 어떤 상태의 &lt;code&gt;ON / OFF&lt;/code&gt;처럼 사용하는 방법이다.&lt;/p&gt;
&lt;p&gt;예를 들어 숫자 &lt;code&gt;5&lt;/code&gt;를 이진수로 표현하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;5 = 101₂&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;각 자리의 비트를 하나의 상태라고 생각하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 → 켜져 있음
0 → 꺼져 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 표현할 수 있다.&lt;/p&gt;
&lt;p&gt;즉 하나의 정수 안에 여러 개의 &lt;code&gt;true / false&lt;/code&gt; 상태를 담을 수 있다.&lt;/p&gt;
&lt;p&gt;기억하기 쉽게 정리하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;비트마스킹

→ 정수를 이진수로 바라본다.
→ 각각의 비트를 하나의 상태로 사용한다.
→ 비트 연산을 이용해 상태를 확인하거나 변경한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 생각하면 될 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비트마스킹을 이해하면서 먼저 정리한 연산자&lt;/h2&gt;
&lt;p&gt;비트마스킹 문제를 풀기 전에 자주 사용하는 비트 연산부터 다시 정리했다.&lt;/p&gt;
&lt;h3&gt;AND &lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;두 비트가 &lt;strong&gt;모두 1일 때만 1&lt;/strong&gt;이 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1010
1100
----
1000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드에서는 특정 비트가 켜져 있는지 확인할 때 자주 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if ((value &amp;amp; (1 &amp;lt;&amp;lt; bit)) != 0) {
    // bit번째 비트가 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;OR &lt;code&gt;|&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;두 비트 중 &lt;strong&gt;하나라도 1이면 1&lt;/strong&gt;이 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1010
1100
----
1110&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여러 상태를 합치거나, 특정 비트를 켤 때 사용할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;XOR &lt;code&gt;^&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;두 비트가 &lt;strong&gt;서로 다를 때 1&lt;/strong&gt;이 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1010
1100
----
0110&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같으면 &lt;code&gt;0&lt;/code&gt;, 다르면 &lt;code&gt;1&lt;/code&gt;이라고 기억하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;SHIFT &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;비트를 왼쪽이나 오른쪽으로 이동시킨다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 &amp;lt;&amp;lt; 0 = 0001
1 &amp;lt;&amp;lt; 1 = 0010
1 &amp;lt;&amp;lt; 2 = 0100
1 &amp;lt;&amp;lt; 3 = 1000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;그래서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;1 &amp;lt;&amp;lt; bit&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 이용하면 원하는 위치의 비트 하나만 &lt;code&gt;1&lt;/code&gt;인 값을 만들 수 있다.&lt;/p&gt;
&lt;p&gt;특정 비트를 검사할 때&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;value &amp;amp; (1 &amp;lt;&amp;lt; bit)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;형태가 자주 등장하는 이유도 이것이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비트마스킹에서 자주 사용하는 기본 패턴&lt;/h2&gt;
&lt;p&gt;비트 연산자의 의미를 하나씩 정리하고 나니, 실제 비트마스킹에서는 몇 가지 형태가 반복해서 사용된다는 것을 알게 됐다.&lt;/p&gt;
&lt;p&gt;특정 위치의 비트를 다룰 때 가장 먼저 사용하는 것이&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;1 &amp;lt;&amp;lt; bit&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1&lt;/code&gt;을 원하는 위치만큼 왼쪽으로 이동시키면 해당 위치만 &lt;code&gt;1&lt;/code&gt;인 값을 만들 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;bit = 2&lt;/code&gt;라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 &amp;lt;&amp;lt; 2

0001
↓
0100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;이 값을 이용하면 특정 비트가 켜져 있는지 확인하거나, 원하는 비트를 켜고 끄는 등의 작업을 할 수 있다.&lt;/p&gt;
&lt;p&gt;자주 사용하는 형태를 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// bit번째 상태 확인
(mask &amp;amp; (1 &amp;lt;&amp;lt; bit)) != 0

// bit번째 상태 켜기
mask |= (1 &amp;lt;&amp;lt; bit);

// bit번째 상태 끄기
mask &amp;amp;= ~(1 &amp;lt;&amp;lt; bit);

// bit번째 상태 뒤집기
mask ^= (1 &amp;lt;&amp;lt; bit);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 식만 보면 조금 복잡해 보였지만, 각각 어떤 연산을 사용하는지 생각하면 이해하기 쉬웠다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;확인하기 → AND
켜기     → OR
끄기     → AND + NOT
뒤집기   → XOR&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;특정 비트 확인하기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;(mask &amp;amp; (1 &amp;lt;&amp;lt; bit)) != 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;원하는 위치만 &lt;code&gt;1&lt;/code&gt;인 값을 만든 뒤 &lt;code&gt;AND&lt;/code&gt; 연산을 한다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;mask = 10110
bit = 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;mask       = 10110
1 &amp;lt;&amp;lt; 2     = 00100
-----------------
AND        = 00100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과가 &lt;code&gt;0&lt;/code&gt;이 아니므로 &lt;code&gt;2&lt;/code&gt;번째 비트가 켜져 있다는 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;반대로 해당 비트가 &lt;code&gt;0&lt;/code&gt;이라면 AND 연산의 결과도 &lt;code&gt;0&lt;/code&gt;이 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;특정 비트 켜기&lt;/h3&gt;
&lt;p&gt;특정 위치의 비트를 &lt;code&gt;1&lt;/code&gt;로 만들고 싶다면 OR 연산을 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;mask |= (1 &amp;lt;&amp;lt; bit);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OR 연산은 둘 중 하나라도 &lt;code&gt;1&lt;/code&gt;이면 결과가 &lt;code&gt;1&lt;/code&gt;이 된다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;mask       = 10001
1 &amp;lt;&amp;lt; 2     = 00100
-----------------
OR         = 10101&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 기존 상태는 유지하면서 원하는 위치만 &lt;code&gt;1&lt;/code&gt;로 만들 수 있다.&lt;/p&gt;
&lt;p&gt;이미 해당 비트가 &lt;code&gt;1&lt;/code&gt;이어도 결과는 그대로 &lt;code&gt;1&lt;/code&gt;이기 때문에, 특정 상태를 추가할 때 사용할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;특정 비트 끄기&lt;/h3&gt;
&lt;p&gt;특정 위치를 &lt;code&gt;0&lt;/code&gt;으로 만들고 싶다면 다음과 같이 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;mask &amp;amp;= ~(1 &amp;lt;&amp;lt; bit);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서는 &lt;code&gt;~&lt;/code&gt; 연산자가 등장한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~&lt;/code&gt;는 각 비트를 반대로 뒤집는 NOT 연산자이다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 &amp;lt;&amp;lt; 2

00100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;에 NOT 연산을 적용하면 원하는 위치를 제외한 비트가 모두 &lt;code&gt;1&lt;/code&gt;인 형태가 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;~00100

→ ...11011&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 값을 기존 &lt;code&gt;mask&lt;/code&gt;와 AND 연산하면 원하는 위치만 &lt;code&gt;0&lt;/code&gt;으로 만들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;mask       = 10111
             11011
-----------------
AND        = 10011&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 다른 비트에는 영향을 주지 않고 특정 상태만 끌 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;특정 비트 뒤집기&lt;/h3&gt;
&lt;p&gt;특정 비트가 &lt;code&gt;0&lt;/code&gt;이면 &lt;code&gt;1&lt;/code&gt;로, &lt;code&gt;1&lt;/code&gt;이면 &lt;code&gt;0&lt;/code&gt;으로 바꾸고 싶다면 XOR 연산을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;mask ^= (1 &amp;lt;&amp;lt; bit);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;XOR은 두 비트가 서로 다를 때 &lt;code&gt;1&lt;/code&gt;이 된다.&lt;/p&gt;
&lt;p&gt;따라서 원하는 위치에 &lt;code&gt;1&lt;/code&gt;을 XOR하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 ^ 1 = 1
1 ^ 1 = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 되어 현재 상태를 반대로 바꿀 수 있다.&lt;/p&gt;
&lt;p&gt;이런 동작을 보통 &lt;code&gt;toggle&lt;/code&gt;이라고 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;이 네 가지를 정리하면 비트마스킹에서 특정 상태를 다루는 기본적인 방법은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;상태가 켜져 있는지 확인한다.
→ mask &amp;amp; (1 &amp;lt;&amp;lt; bit)

상태를 켠다.
→ mask | (1 &amp;lt;&amp;lt; bit)

상태를 끈다.
→ mask &amp;amp; ~(1 &amp;lt;&amp;lt; bit)

상태를 반대로 바꾼다.
→ mask ^ (1 &amp;lt;&amp;lt; bit)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이번 &lt;code&gt;비밀지도&lt;/code&gt; 문제에서는 이 중에서 &lt;strong&gt;특정 비트가 켜져 있는지 확인하는 방법&lt;/strong&gt;을 사용했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;(map &amp;amp; (1 &amp;lt;&amp;lt; bit)) != 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;두 지도를 OR 연산으로 합친 뒤, 각 위치의 비트가 &lt;code&gt;1&lt;/code&gt;인지 확인해서 &lt;code&gt;#&lt;/code&gt; 또는 공백으로 바꿔주는 방식이다.&lt;/p&gt;
&lt;p&gt;지금 당장 네 가지를 모두 외우기보다는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원하는 위치만 1로 만든다.

↓

그 위치를 어떻게 처리할지에 따라
AND, OR, XOR을 사용한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도로 먼저 이해해두면 될 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비트마스킹에서 가장 헷갈렸던 부분&lt;/h2&gt;
&lt;h3&gt;그냥 이진수로 바꾸는 것과 뭐가 다를까?&lt;/h3&gt;
&lt;p&gt;처음에는 비트마스킹이라고 해서 특별한 무언가가 있는 줄 알았다.&lt;/p&gt;
&lt;p&gt;예를 들어 숫자 &lt;code&gt;9&lt;/code&gt;를&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;9 → 01001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 이진 문자열로 변환하면 이것도 비트를 다루는 것처럼 보인다.&lt;/p&gt;
&lt;p&gt;하지만 비트마스킹에서는 굳이 문자열로 바꾸지 않고 &lt;strong&gt;정수 자체를 비트의 집합으로 사용한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;예를 들어 특정 위치가 &lt;code&gt;1&lt;/code&gt;인지 확인하고 싶다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;(value &amp;amp; (1 &amp;lt;&amp;lt; bit)) != 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만으로 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;문자열로 접근한다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;정수
↓
이진 문자열 변환
↓
문자 위치 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;과정을 거쳐야 하지만,&lt;/p&gt;
&lt;p&gt;비트 연산을 사용하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;정수
↓
원하는 비트 바로 검사&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 가능하다.&lt;/p&gt;
&lt;p&gt;오늘 문제를 풀면서 이 차이가 조금 더 명확하게 느껴졌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;실습: [1차] 비밀지도&lt;/h1&gt;
&lt;p&gt;오늘은 프로그래머스 &lt;code&gt;[1차] 비밀지도&lt;/code&gt; 문제로 비트마스킹을 연습했다.&lt;/p&gt;
&lt;p&gt;문제에서는 &lt;code&gt;n × n&lt;/code&gt; 크기의 지도가 있고 각 칸은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;벽   → #
공백 → &amp;quot; &amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로 이루어져 있다.&lt;/p&gt;
&lt;p&gt;그런데 지도는 두 장으로 나누어져 있고, 두 지도를 겹쳐서 최종 지도를 만들어야 한다.&lt;/p&gt;
&lt;p&gt;규칙은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;지도1이 벽 또는 지도2가 벽
→ 최종 지도는 벽

두 지도 모두 공백
→ 최종 지도도 공백&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 각각의 지도 한 줄은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;벽   → 1
공백 → 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로 표현한 이진수를 정수로 바꿔서 주어진다.&lt;/p&gt;
&lt;p&gt;처음 문제를 읽었을 때는 이진 문자열을 만들어서 두 문자열을 하나씩 비교해야 하나 생각했다.&lt;/p&gt;
&lt;p&gt;그런데 조건을 다시 보니 비트 OR 연산과 정확히 같은 규칙이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;문제 조건을 0과 1로 바꿔보기&lt;/h2&gt;
&lt;p&gt;문제의 벽을 &lt;code&gt;1&lt;/code&gt;, 공백을 &lt;code&gt;0&lt;/code&gt;이라고 생각해보자.&lt;/p&gt;
&lt;p&gt;두 지도에서 가능한 경우는 네 가지이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;지도1  지도2  결과

  0      0     0
  0      1     1
  1      0     1
  1      1     1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이걸 보고 어디서 많이 본 규칙이라는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;바로 OR 연산이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 문제에서 말하고 있는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;두 지도 중 하나라도 벽이면 최종 지도도 벽이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라는 규칙 자체가 OR 연산과 동일했다.&lt;/p&gt;
&lt;p&gt;그래서 두 지도를 칸마다 직접 비교할 필요 없이 각 행의 정수를 바로 OR 연산하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int map = arr1[i] | arr2[i];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 한 줄로 두 지도를 합칠 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실제 숫자로 확인해보기&lt;/h2&gt;
&lt;p&gt;문제의 첫 번째 예제에서 첫 번째 줄은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;arr1[0] = 9
arr2[0] = 30&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;n = 5&lt;/code&gt;이므로 두 숫자를 5자리 이진수로 표현해보면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;9  = 01001
30 = 11110&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;두 값을 OR 연산하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;  01001
| 11110
-------
  11111&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1&lt;/code&gt;을 벽 &lt;code&gt;#&lt;/code&gt;으로 바꾸면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;#####&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;실제로 첫 번째 줄의 정답과 같다.&lt;/p&gt;
&lt;p&gt;이 과정을 보고 나니 이 문제가 왜 비트 연산 문제인지 바로 이해할 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;처음에는 이진 문자열로 바꾸려고 했다&lt;/h2&gt;
&lt;p&gt;처음 문제를 풀려고 했을 때 떠올린 방법은 다음과 같았다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;arr1의 숫자를 이진수 문자열로 변환
arr2의 숫자를 이진수 문자열로 변환

↓
같은 위치의 문자 비교

둘 중 하나라도 &amp;#39;1&amp;#39;
→ &amp;#39;#&amp;#39;

둘 다 &amp;#39;0&amp;#39;
→ &amp;#39; &amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;물론 이렇게 풀어도 가능하다.&lt;/p&gt;
&lt;p&gt;하지만 이 방법으로 접근하면 한 가지 귀찮은 문제가 생긴다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;n = 5&lt;/code&gt;이고 숫자가 &lt;code&gt;9&lt;/code&gt;라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Integer.toBinaryString(9)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;의 결과는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;문제에서 필요한 것은 5자리이므로 실제로는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;01001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 맞춰줘야 한다.&lt;/p&gt;
&lt;p&gt;즉 앞쪽의 &lt;code&gt;0&lt;/code&gt;을 직접 채우는 과정이 필요하다.&lt;/p&gt;
&lt;p&gt;반면 비트 연산을 사용하면 숫자를 문자열로 변환할 필요가 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int map = arr1[i] | arr2[i];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 지도를 먼저 합치고, 필요한 비트만 왼쪽부터 확인하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;합쳐진 지도에서 각 칸 확인하기&lt;/h2&gt;
&lt;p&gt;두 지도를 OR 연산해서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int map = arr1[i] | arr2[i];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;까지 만들었다.&lt;/p&gt;
&lt;p&gt;이제 &lt;code&gt;map&lt;/code&gt;의 각 비트가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 → &amp;#39;#&amp;#39;
0 → &amp;#39; &amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;인지 확인하면 된다.&lt;/p&gt;
&lt;p&gt;여기서 사용한 것이&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;1 &amp;lt;&amp;lt; bit&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;예를 들어 5개의 비트를 확인한다고 하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 &amp;lt;&amp;lt; 4 → 10000
1 &amp;lt;&amp;lt; 3 → 01000
1 &amp;lt;&amp;lt; 2 → 00100
1 &amp;lt;&amp;lt; 1 → 00010
1 &amp;lt;&amp;lt; 0 → 00001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;이 값을 &lt;code&gt;map&lt;/code&gt;과 AND 연산하면 해당 위치가 &lt;code&gt;1&lt;/code&gt;인지 알 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if ((map &amp;amp; (1 &amp;lt;&amp;lt; bit)) != 0) {
    sb.append(&amp;#39;#&amp;#39;);
} else {
    sb.append(&amp;#39; &amp;#39;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;왜 왼쪽 비트부터 확인할까?&lt;/h2&gt;
&lt;p&gt;여기서 처음에 실수할 뻔한 부분이 하나 있었다.&lt;/p&gt;
&lt;p&gt;보통&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (int bit = 0; bit &amp;lt; n; bit++)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 반복문을 작성하고 싶어진다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;code&gt;0&lt;/code&gt;번째 비트는 이진수에서 가장 오른쪽에 있는 비트이다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;01001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;bit 4 3 2 1 0
    0 1 0 0 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;순서가 된다.&lt;/p&gt;
&lt;p&gt;지도를 출력할 때는 왼쪽 칸부터 만들어야 하므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (int bit = n - 1; bit &amp;gt;= 0; bit--)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 높은 비트부터 확인해야 한다.&lt;/p&gt;
&lt;p&gt;이 부분도 비트를 사용할 때 인덱스와 실제 보이는 방향을 구분해서 생각해야겠다고 느꼈다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;구현&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {

    public String[] solution(int n, int[] arr1, int[] arr2) {

        String[] answer = new String[n];

        for (int i = 0; i &amp;lt; n; i++) {

            // 두 지도를 하나로 합친다.
            int map = arr1[i] | arr2[i];

            StringBuilder sb = new StringBuilder();

            // 가장 왼쪽 비트부터 확인한다.
            for (int bit = n - 1; bit &amp;gt;= 0; bit--) {

                // 현재 비트가 1이라면 벽
                if ((map &amp;amp; (1 &amp;lt;&amp;lt; bit)) != 0) {
                    sb.append(&amp;#39;#&amp;#39;);
                } else {
                    sb.append(&amp;#39; &amp;#39;);
                }
            }

            answer[i] = sb.toString();
        }

        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;코드로 다시 보고 이해해보자&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;arr1[i] | arr2[i]&lt;/code&gt;가 핵심이다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int map = arr1[i] | arr2[i];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사실 이 문제의 핵심은 거의 이 한 줄에 들어있다.&lt;/p&gt;
&lt;p&gt;두 지도에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;하나라도 벽이면 벽&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 조건을 OR 연산으로 그대로 표현한 것이다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;arr1[i] = 01001
arr2[i] = 11110&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;map = 11111&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;문제의 조건을 코드로 하나씩 &lt;code&gt;if&lt;/code&gt;문을 작성한 것이 아니라, 문제의 규칙과 동일한 비트 연산을 찾았다는 점이 중요했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;code&gt;1 &amp;lt;&amp;lt; bit&lt;/code&gt;는 검사할 위치를 만든다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;1 &amp;lt;&amp;lt; bit&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;는 원하는 위치 하나만 &lt;code&gt;1&lt;/code&gt;인 값을 만든다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;bit = 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;00001
↓ 왼쪽으로 3칸 이동
01000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;이 값을 &lt;code&gt;map&lt;/code&gt;과 AND 연산하면 3번째 비트가 켜져 있는지 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;map &amp;amp; (1 &amp;lt;&amp;lt; bit)&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;결과가 0이 아니면 해당 비트는 1이다&lt;/h3&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;map = 10110&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이고 &lt;code&gt;bit = 2&lt;/code&gt;를 확인한다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;map        = 10110
1 &amp;lt;&amp;lt; 2     = 00100
------------------
AND        = 00100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과가 &lt;code&gt;0&lt;/code&gt;이 아니므로 해당 비트는 &lt;code&gt;1&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;반대로 &lt;code&gt;bit = 3&lt;/code&gt;을 확인하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;map        = 10110
1 &amp;lt;&amp;lt; 3     = 01000
------------------
AND        = 00000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;결과가 &lt;code&gt;0&lt;/code&gt;이므로 해당 위치는 공백이다.&lt;/p&gt;
&lt;p&gt;그래서 코드가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if ((map &amp;amp; (1 &amp;lt;&amp;lt; bit)) != 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 되는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;비트마스킹 문제를 볼 때의 생각 순서&lt;/h1&gt;
&lt;p&gt;이번 문제를 풀면서 비트마스킹 문제를 만났을 때 바로 비트 연산자를 외워서 쓰기보다 다음 순서로 생각하면 좋을 것 같다.&lt;/p&gt;
&lt;h3&gt;1. 상태를 0과 1로 표현할 수 있는가?&lt;/h3&gt;
&lt;p&gt;비밀지도에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;벽   → 1
공백 → 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;으로 표현할 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 여러 상태를 하나의 정수에 담을 수 있는가?&lt;/h3&gt;
&lt;p&gt;하나의 지도 행을&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;#  # #&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 따로 저장하지 않고&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;10101&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 하나의 이진수로 표현할 수 있다.&lt;/p&gt;
&lt;p&gt;이 값은 결국 하나의 정수이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. 문제의 조건이 어떤 비트 연산과 같은가?&lt;/h3&gt;
&lt;p&gt;비밀지도에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;둘 중 하나라도 1이면 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이었다.&lt;/p&gt;
&lt;p&gt;이것은 그대로 OR 연산이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A | B&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4. 특정 비트를 어떻게 확인할 것인가?&lt;/h3&gt;
&lt;p&gt;원하는 위치만 &lt;code&gt;1&lt;/code&gt;로 만든 뒤 AND 연산한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;value &amp;amp; (1 &amp;lt;&amp;lt; bit)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과가 &lt;code&gt;0&lt;/code&gt;이 아니면 해당 위치가 켜져 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5. 비트의 순서를 제대로 보고 있는가?&lt;/h3&gt;
&lt;p&gt;가장 오른쪽 비트가 &lt;code&gt;0&lt;/code&gt;번째 비트이다.&lt;/p&gt;
&lt;p&gt;따라서 문제에서 왼쪽부터 출력해야 한다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (int bit = n - 1; bit &amp;gt;= 0; bit--)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 확인해야 할 수도 있다.&lt;/p&gt;
&lt;p&gt;앞으로 비트마스킹 문제가 나오면 이 다섯 가지부터 확인해보려고 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;시간복잡도&lt;/h1&gt;
&lt;p&gt;지도에는 &lt;code&gt;n&lt;/code&gt;개의 행이 있다.&lt;/p&gt;
&lt;p&gt;각 행마다 &lt;code&gt;n&lt;/code&gt;개의 비트를 확인하므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(n × n)
= O(n²)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;의 시간이 필요하다.&lt;/p&gt;
&lt;p&gt;문제에서는 &lt;code&gt;n&lt;/code&gt;이 최대 &lt;code&gt;16&lt;/code&gt;이므로 입력 크기 자체가 매우 작다.&lt;/p&gt;
&lt;p&gt;그리고 두 지도를 합치는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;arr1[i] | arr2[i]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;연산 자체는 상수 시간에 수행된다.&lt;/p&gt;
&lt;p&gt;이 문제에서는 성능보다는 &lt;strong&gt;문제의 상태를 비트로 어떻게 표현할 수 있는지 알아채는 것&lt;/strong&gt;이 더 중요한 포인트였던 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;비트마스킹을 사용하면 좋은 상황&lt;/h1&gt;
&lt;p&gt;이번 문제 하나만 보고 비트마스킹을&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이진수 문제에서 사용하는 것&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 기억하면 범위가 너무 좁을 것 같다.&lt;/p&gt;
&lt;p&gt;비트마스킹은 여러 개의 &lt;code&gt;true / false&lt;/code&gt; 상태를 관리할 때 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1번 방문 여부
2번 방문 여부
3번 방문 여부
4번 방문 여부&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 각각 &lt;code&gt;boolean&lt;/code&gt; 배열로 관리하는 대신&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0101&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 하나의 정수에 저장할 수도 있다.&lt;/p&gt;
&lt;p&gt;이런 방식은 나중에&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;부분집합
방문 상태
권한 관리
DP의 상태 표현
조합 탐색&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 문제에서도 사용할 수 있다고 한다.&lt;/p&gt;
&lt;p&gt;따라서 비트마스킹 문제를 보면 단순히 비트 연산자를 떠올리기보다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;여러 개의 이분 상태를 하나의 정수로 표현할 수 있을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;를 먼저 생각해보는 것이 중요할 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;오늘의 회고&lt;/h1&gt;
&lt;h2&gt;배운 점&lt;/h2&gt;
&lt;p&gt;오늘 가장 크게 정리된 것은 &lt;strong&gt;비트마스킹이 새로운 탐색 알고리즘 같은 것이 아니라 상태를 표현하는 방법이라는 점&lt;/strong&gt;이었다.&lt;/p&gt;
&lt;p&gt;이전에는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&amp;amp;
|
^
&amp;lt;&amp;lt;
&amp;gt;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 연산자를 문법으로만 알고 있었다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;code&gt;비밀지도&lt;/code&gt; 문제를 직접 풀어보니 비트 연산이 왜 필요한지 조금 더 구체적으로 이해할 수 있었다.&lt;/p&gt;
&lt;p&gt;특히&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;두 지도 중 하나라도 벽이면 벽&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 문제의 문장을&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;arr1[i] | arr2[i]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 바로 바꿀 수 있다는 것이 인상적이었다.&lt;/p&gt;
&lt;p&gt;문제를 풀 때 항상 반복문이나 &lt;code&gt;if&lt;/code&gt;문부터 생각하는 것이 아니라, 문제의 규칙 자체를 하나의 연산으로 표현할 방법이 있는지도 살펴봐야겠다고 느꼈다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;어려웠던 점&lt;/h2&gt;
&lt;p&gt;가장 헷갈렸던 것은 비트의 방향이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;1 &amp;lt;&amp;lt; 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;부터 시작하면 가장 왼쪽부터 확인하는 것처럼 순간적으로 생각했는데, 실제로 &lt;code&gt;0&lt;/code&gt;번째 비트는 가장 오른쪽에 있다.&lt;/p&gt;
&lt;p&gt;그래서 지도를 왼쪽부터 만들어야 하는 이번 문제에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;n - 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;번째 비트부터 역순으로 확인해야 했다.&lt;/p&gt;
&lt;p&gt;또 처음에는 이진 문자열로 변환하는 방법이 더 직관적으로 느껴졌다.&lt;/p&gt;
&lt;p&gt;하지만 그렇게 풀려고 하니 앞쪽의 &lt;code&gt;0&lt;/code&gt;을 맞춰주는 처리가 추가로 필요했다.&lt;/p&gt;
&lt;p&gt;비트 연산으로 접근하니 숫자의 형태를 그대로 유지하면서 필요한 위치만 확인할 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;앞으로 해볼 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 비트마스킹 기본 개념 정리&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; AND, OR, XOR 연산 다시 정리&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; Shift 연산 이해하기&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;&quot; disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 프로그래머스 &lt;code&gt;[1차] 비밀지도&lt;/code&gt; 풀이&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;오늘 기억해둘 문장&lt;/h2&gt;
&lt;p&gt;비트마스킹 문제를 만나면 비트 연산자를 먼저 외워 쓰기보다 아래 질문부터 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;상태를 0과 1로 표현할 수 있는가?

여러 개의 상태를
하나의 정수에 담을 수 있는가?

문제의 조건은
AND, OR, XOR 중 어떤 연산과 같은가?

특정 비트는
어떻게 확인하거나 변경할 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 오늘 문제에서 가장 기억해둘 한 줄은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int map = arr1[i] | arr2[i];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;문제에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;둘 중 하나라도 벽이면 벽&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 조건을 본다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;하나라도 1이면 1

→ OR&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 떠올릴 수 있도록 기억해두자.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/17681&quot;&gt;프로그래머스 코딩테스트 연습 - &lt;code&gt;[1차] 비밀지도&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CS/자료구조 &amp;middot; 알고리즘</category>
      <category>til</category>
      <category>비밀지도</category>
      <category>비트마스킹</category>
      <category>자바</category>
      <category>프로그래머스</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/59</guid>
      <comments>https://slowgrid.tistory.com/entry/til-Bitmasking#entry59comment</comments>
      <pubDate>Sun, 23 Aug 2026 19:01:54 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] - 2026.08.22 | 백트래킹을 N-Queen으로 이해하기</title>
      <link>https://slowgrid.tistory.com/entry/til-Backtracking</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩테스트 준비를 하면서 백트래킹을 다시 정리했다.&lt;br /&gt;오늘은 개념만 외우기보다 &lt;b&gt;N-Queen을 직접 풀면서 어디에서 막혔고, 어떤 기준으로 생각해야 하는지&lt;/b&gt;를 기록해두는 데 초점을 맞췄다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 공부한 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘의 핵심은 백트래킹이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 &lt;code&gt;Backtracking&lt;/code&gt;이라는 이름을 봤을 때는 새로운 탐색 알고리즘이라고 생각했다.&lt;br /&gt;그런데 공부하고 N-Queen 문제까지 풀어보니, 백트래킹은 &lt;b&gt;DFS처럼 한 방향으로 탐색하되, 더 가도 답이 나올 수 없는 순간 바로 돌아오는 방식&lt;/b&gt;에 더 가까웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억하기 쉽게 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;DFS
&amp;rarr; 일단 한 방향으로 깊게 탐색한다.

Backtracking
&amp;rarr; 깊게 탐색하되,
   정답이 될 수 없는 길은 중간에 잘라내고 돌아온다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 백트래킹에서 중요한 것은 단순히 재귀를 사용하는 것이 아니라&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;선택 &amp;rarr; 검사 &amp;rarr; 탐색 &amp;rarr; 복구&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 흐름과, 그중에서도 &lt;b&gt;언제 더 내려가지 않을지 판단하는 가지치기(Pruning)&lt;/b&gt;라고 느낄 수 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;백트래킹을 이해하면서 가장 헷갈렸던 부분&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DFS와 백트래킹은 같은 걸까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 처음에 가장 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 재귀로 깊게 들어가는 코드가 자주 나오기 때문에 얼핏 보면 거의 같은 것처럼 보였다.&lt;br /&gt;그래서 지금은 다음처럼 구분해서 기억하려고 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DFS는 &lt;b&gt;어떤 순서로 탐색할지에 대한 방법&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;백트래킹은 탐색 중 &lt;b&gt;가능성 없는 경우를 버리는 전략&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 프로그래머스 &lt;code&gt;타겟 넘버&lt;/code&gt;를 풀 때는 &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; 두 선택을 끝까지 전부 확인했다.&lt;br /&gt;그때는 사실상 모든 경우를 DFS로 탐색한 셈이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 백트래킹을 공부하면서는 여기에 한 가지 생각이 더 붙었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 상태에서 이미 정답이 될 수 없다는 것을 알 수 있다면, 그 아래 경우는 아예 보지 않아도 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 이해하고 나니 백트래킹이 조금 덜 추상적으로 느껴졌다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가지치기(Pruning)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백트래킹에서 가장 중요한 부분은 가지치기이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 숫자 3개를 골라 합이 &lt;code&gt;10&lt;/code&gt;이 되어야 하는데, 현재까지 선택한 숫자의 합이 이미 &lt;code&gt;20&lt;/code&gt;이고 남은 숫자가 모두 양수라고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 더 숫자를 골라도 합은 더 커질 뿐이므로 정답 &lt;code&gt;10&lt;/code&gt;을 만들 수 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현재 합 = 20
목표 합 = 10

&amp;rarr; 더 내려가도 정답이 나올 수 없음
&amp;rarr; 여기서 탐색 중단&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 백트래킹도 결국 모든 경우를 보는 것 아닌가 싶었는데, 실제 차이는 바로 여기 있었다.&lt;br /&gt;&lt;b&gt;모든 후보를 만들어놓고 마지막에 검사하는 것이 아니라, 실패를 알 수 있는 가장 빠른 시점에 탐색을 끊는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 앞으로 백트래킹 문제를 보면 코드를 먼저 쓰기보다 아래 질문부터 해보려고 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태가 틀렸다는 것을 언제 가장 빨리 알 수 있을까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;실습: N-Queen&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 프로그래머스의 &lt;b&gt;N-Queen&lt;/b&gt; 문제로 백트래킹을 연습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &lt;code&gt;N &amp;times; N&lt;/code&gt; 체스판에 &lt;code&gt;N&lt;/code&gt;개의 퀸을 놓되, 어떤 퀸도 서로 공격할 수 없도록 배치하는 경우의 수를 구하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퀸은 가로, 세로, 대각선 방향으로 움직일 수 있기 때문에 같은 행, 같은 열, 같은 대각선에 두 퀸이 존재하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;N = 4&lt;/code&gt;일 때 가능한 배치 중 하나는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;. Q . .
. . . Q
Q . . .
. . Q .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음에는 체스판 전체를 보려고 했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 문제를 봤을 때는 자연스럽게 2차원 체스판부터 떠올렸다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;boolean[][] board = new boolean[n][n];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 전체 칸 중에서 퀸을 놓을 위치를 하나씩 고르는 식으로 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 조금 생각해보니 &lt;b&gt;같은 행에 퀸을 두 개 놓을 수 없기 때문에 각 행마다 퀸을 하나만 놓으면 된다&lt;/b&gt;는 사실을 이용할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 내가 결정해야 하는 것은 사실 이것뿐이었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0행 &amp;rarr; 어느 열에 놓을까?
1행 &amp;rarr; 어느 열에 놓을까?
2행 &amp;rarr; 어느 열에 놓을까?
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 보게 된다면 체스판 전체에서 N개의 칸을 고르는 문제로 보는 것보다 훨씬 단순해졌다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상태를 1차원 배열로 표현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 행에는 퀸이 하나만 있으므로 다음처럼 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int[] board = new int[n];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;board[row] = col&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은 &lt;code&gt;row&lt;/code&gt;번째 행의 퀸이 &lt;code&gt;col&lt;/code&gt;번째 열에 있다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;board = [1, 3, 0, 2]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0행 &amp;rarr; 1열
1행 &amp;rarr; 3열
2행 &amp;rarr; 0열
3행 &amp;rarr; 2열&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이고, 실제 체스판은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;. Q . .
. . . Q
Q . . .
. . Q .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;boolean[][]&lt;/code&gt;로 직접 보드를 관리해야 할 것처럼 느껴졌는데, 필요한 정보만 남기니 &lt;code&gt;int[]&lt;/code&gt; 하나로 충분했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가지치기 조건 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;code&gt;row&lt;/code&gt;행의 &lt;code&gt;col&lt;/code&gt;열에 퀸을 놓는다고 했을 때, 이전에 놓은 퀸들과 충돌하는지만 확인하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 같은 열&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 행 &lt;code&gt;i&lt;/code&gt;에 놓인 퀸과 열이 같으면 배치할 수 없다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;if (board[i] == col) {
    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 같은 대각선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 바로 떠올리지 못해서 가장 오래 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 점이&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(row1, col1)
(row2, col2)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 있을 때, 같은 대각선에 있다면&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;|row1 - row2| == |col1 - col2|&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 성립한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Java에서는 다음처럼 검사할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;if (Math.abs(row - i) == Math.abs(col - board[i])) {
    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 탐색 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;N = 4&lt;/code&gt;라고 생각해보면, 0행에서 먼저 퀸의 열을 하나 선택한다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Q . . .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 1행에서 가능한 열을 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0열은 같은 열이라 안 되고, 1열은 대각선이라 안 된다.&lt;br /&gt;2열이 가능하다면 일단 그 위치를 선택하고 다음 행으로 내려간다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Q . . .
. . Q .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이후 행에서 더 이상 퀸을 놓을 수 있는 열이 하나도 없다면, 이 선택으로는 정답을 만들 수 없다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 끝까지 억지로 내려가는 것이 아니라 이전 행으로 돌아가서 다른 열을 시도한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;선택
&amp;darr;
가능한지 검사
&amp;darr;
가능하면 다음 행
&amp;darr;
막히면 이전 행으로 복귀
&amp;darr;
다른 열 시도&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 N-Queen에 대입해서 보니 &lt;code&gt;Backtracking&lt;/code&gt;이라는 이름이 왜 붙었는지도 자연스럽게 이해가 되는 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;구현&lt;/h1&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Solution {

    private int answer = 0;
    private int[] board;

    public int solution(int n) {
        board = new int[n];
        backtracking(0, n);
        return answer;
    }

    private void backtracking(int row, int n) {

        // 모든 행에 퀸을 배치했다면 하나의 경우 완성
        if (row == n) {
            answer++;
            return;
        }

        // 현재 행에서 퀸을 놓을 열을 하나씩 시도
        for (int col = 0; col &amp;lt; n; col++) {

            board[row] = col;

            // 현재까지의 배치가 유효한 경우에만 다음 행 탐색
            if (isPossible(row)) {
                backtracking(row + 1, n);
            }
        }
    }

    private boolean isPossible(int row) {

        for (int previousRow = 0;
             previousRow &amp;lt; row;
             previousRow++) {

            // 같은 열
            if (board[previousRow] == board[row]) {
                return false;
            }

            // 같은 대각선
            if (Math.abs(row - previousRow)
                    == Math.abs(board[row] - board[previousRow])) {
                return false;
            }
        }

        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드로 다시 보고 이해해보자&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;row&lt;/code&gt;는 행 번호이면서 탐색 깊이다&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;backtracking(row, n)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 &lt;code&gt;row&lt;/code&gt;는 단순히 현재 행을 뜻하기도 하지만, 재귀 관점에서는 지금까지 몇 개의 퀸을 배치했는지를 나타내는 탐색 깊이이기도 했다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;row = 0 &amp;rarr; 첫 번째 퀸 배치
row = 1 &amp;rarr; 두 번째 퀸 배치
row = 2 &amp;rarr; 세 번째 퀸 배치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;row == n&lt;/code&gt;이 되면 모든 행에 퀸을 하나씩 배치했다는 뜻이고, 하나의 정답이 완성된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;board[row] = col&lt;/code&gt;은 확정이 아니라 일단 시도해보는 선택이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 코드를 짜며 구성해볼 때는&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;board[row] = col;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 보면 정말 퀸을 놓았다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 백트래킹에서는 이 값을 &lt;b&gt;정답으로 확정한 것&lt;/b&gt;이 아니라, 현재 후보를 하나 선택해본 것으로 보는 게 더 맞았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 &lt;code&gt;isPossible(row)&lt;/code&gt;에서 현재까지의 상태가 유효한지 검사하고, 가능할 때만 다음 행으로 내려간다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;if (isPossible(row)) {
    backtracking(row + 1, n);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌이 발생하면 그 아래 경우는 전부 확인하지 않는다.&lt;br /&gt;이 부분이 이번 문제에서 실제 가지치기가 일어나는 지점이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그런데 왜 원상복구 코드가 없을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백트래킹을 공부할 때 자주 보는 코드는 보통 이런 형태다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;visited[i] = true;

backtracking(depth + 1);

visited[i] = false;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 N-Queen을 푸는 코드를 작성할 때도 선택을 취소하는 코드가 있어야 할 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서는&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;board[row] = col;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 있고, 다시 &lt;code&gt;board[row]&lt;/code&gt;를 지우는 코드는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 부분이 꽤 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유를 따라가 보니 같은 &lt;code&gt;row&lt;/code&gt;에서 다음 &lt;code&gt;col&lt;/code&gt;을 시도할 때 값이 바로 덮어써지고, &lt;code&gt;isPossible()&lt;/code&gt;도 현재 행보다 이전 행만 검사하기 때문에 이후 행에 남아 있는 예전 값은 현재 탐색에 영향을 주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;이 문제에서는 명시적으로 복구하지 않아도 다음 후보를 탐색하는 데 문제가 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이걸 보고 &quot;백트래킹은 복구를 안 해도 된다&quot;라고 기억하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;visited&lt;/code&gt; 배열을 사용한다면 현재 선택의 흔적이 다음 탐색에도 남기 때문에 반드시 원상복구해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;visited[i] = true;

backtracking(depth + 1);

visited[i] = false;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제를 풀면서 백트래킹의 복구 코드를 외우는 것보다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 변경한 상태가 다음 후보 탐색에 영향을 주는가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 판단하는 것 또한 중요하다고 느꼈다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;백트래킹 문제를 볼 때의 생각 순서&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제를 풀고 나니, 백트래킹 문제에서는 아래 순서로 먼저 정리하면 재귀 코드를 작성하기가 조금 수월할 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 무엇을 하나씩 선택할 것인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N-Queen에서는&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 행에서 퀸을 놓을 열&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 현재 상태를 무엇으로 표현할 것인가?&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;board[row] = col&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 지금까지 선택한 퀸의 위치를 저장했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 언제 정답이 완성되는가?&lt;/h3&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;if (row == n)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 행에 퀸을 하나씩 배치했을 때이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 언제 더 내려가면 안 되는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 퀸과&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;같은 열
또는
같은 대각선&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 있는 경우다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 어떤 상태를 복구해야 하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 선택이 다음 후보 탐색에 영향을 준다면 반드시 복구해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 재귀 코드를 바로 작성하기 전에 이 다섯 가지를 먼저 적어볼 생각이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;완전탐색과 비교하면서 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백트래킹도 여러 경우의 수를 탐색한다는 점에서는 완전탐색과 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이는 &lt;b&gt;실패를 언제 판단하느냐&lt;/b&gt;에 있었다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;완전탐색
&amp;rarr; 가능한 경우를 끝까지 만든 뒤 정답인지 확인

백트래킹
&amp;rarr; 만드는 도중 이미 실패한 경우라면 그 아래 탐색을 중단&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 백트래킹은 가지치기가 잘 될수록 실제 탐색량이 크게 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 백트래킹이라고 해서 경우의 수 문제가 갑자기 빠른 문제가 되는 것은 아니다.&lt;br /&gt;N-Queen도 최악의 경우 여전히 많은 경우를 확인할 수 있기 때문에 입력 크기와 가능한 경우의 수를 먼저 보는 습관이 필요하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;백트래킹과 DP의 차이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 불필요한 연산을 줄인다는 점 때문에 헷갈릴 수 있는데, 줄이는 대상이 다르다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;백트래킹
&amp;rarr; 정답이 될 수 없는 경우를 더 이상 탐색하지 않는다.

DP
&amp;rarr; 이미 계산한 상태의 결과를 다시 계산하지 않는다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백트래킹은 &lt;b&gt;경로를 잘라내는 것&lt;/b&gt;, DP는 &lt;b&gt;중복 계산을 재사용하는 것&lt;/b&gt;이라고 생각하니 구분하기 쉬웠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;오늘의 회고&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 크게 정리된 것은 백트래킹을 단순히 &quot;재귀로 푸는 경우의 수 문제&quot;로 보면 안 된다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 재귀 함수가 보이면 호출 흐름 자체를 따라가는 데 집중했는데, N-Queen을 풀어보니 오히려 더 중요한 것은 다음 세 가지였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 단계에서 무엇을 선택하는지&lt;/li&gt;
&lt;li&gt;어떤 조건에서 이 선택을 버릴지&lt;/li&gt;
&lt;li&gt;재귀에서 돌아왔을 때 어떤 상태를 복구해야 하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 N-Queen을 처음에는 2차원 체스판 그대로 표현하려고 했지만, 각 행에 퀸 하나만 놓는다는 조건을 이용해 1차원 배열로 줄일 수 있다는 점이 이 문제의 흥미로웠던 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 그대로 코드로 옮기기보다 &lt;b&gt;필요한 상태만 남기는 과정&lt;/b&gt;이 중요하다는 걸 다시 느꼈다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어려웠던 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 어려웠던 건 재귀 자체보다 가지치기 조건을 찾는 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;같은 열&lt;/code&gt;은 바로 이해됐지만, &lt;code&gt;행 차이 == 열 차이&lt;/code&gt;이면 같은 대각선이라는 조건은 처음에 바로 떠오르지 않았다.&lt;br /&gt;이 부분은 좌표를 직접 그려보고 나서야 확실히 이해됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 상태 복구도 조금 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백트래킹이면 항상 &lt;code&gt;선택 &amp;rarr; 재귀 &amp;rarr; 선택 취소&lt;/code&gt; 코드가 있어야 한다고 생각했는데, N-Queen에서는 현재 행의 값을 다음 반복에서 덮어쓰기 때문에 별도의 복구 코드가 없어도 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험 덕분에 앞으로는 패턴을 그대로 외우기보다 &lt;b&gt;현재 상태가 다음 탐색에 실제로 영향을 주는지&lt;/b&gt;를 먼저 확인해야겠다고 생각했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앞으로 해볼 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 백트래킹 기본 개념 정리&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; DFS와 백트래킹의 차이 이해&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 가지치기(Pruning) 개념 정리&lt;/li&gt;
&lt;li&gt;&lt;input checked=&quot;checked&quot; disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 프로그래머스 &lt;code&gt;N-Queen&lt;/code&gt; 풀이&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 순열을 백트래킹으로 직접 구현하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 조합을 백트래킹으로 직접 구현하기&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 같은 상태가 반복되는 문제에서 메모이제이션을 함께 사용할 수 있는 사례 찾아보기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 기억해둘 문장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백트래킹 문제를 만나면 코드를 바로 외워 쓰기보다 아래 네 가지를 먼저 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;무엇을 선택할 것인가?
언제 실패라고 판단할 것인가?
언제 정답이 완성되는가?
무엇을 다시 복구해야 하는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 흐름은 짧게&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;선택 &amp;rarr; 검사 &amp;rarr; 탐색 &amp;rarr; 복구&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 기억하면 될 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12952&quot;&gt;프로그래머스 코딩테스트 연습 - N-Queen&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 학습 예정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Union-Find&lt;/li&gt;
&lt;li&gt;위상 정렬(Topological Sort)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CS/자료구조 &amp;middot; 알고리즘</category>
      <category>backtracking</category>
      <category>til</category>
      <category>백트래킹</category>
      <category>알고리즘</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/58</guid>
      <comments>https://slowgrid.tistory.com/entry/til-Backtracking#entry58comment</comments>
      <pubDate>Sat, 22 Aug 2026 21:58:46 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] - 2026.08.21 | 누적합 (Prefix Sum)</title>
      <link>https://slowgrid.tistory.com/entry/til-Prefix-Sum</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;누적합을 두 번 쓰는 이유 — 프로그래머스 광고 삽입으로 이해하기&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;오늘 배운 내용&lt;/h2&gt;
&lt;h3&gt;누적합(Prefix Sum)이란?&lt;/h3&gt;
&lt;p&gt;누적합은 배열의 값을 앞에서부터 차례대로 더해서 미리 저장해두는 방법이다.&lt;/p&gt;
&lt;p&gt;처음에는 솔직히&lt;br&gt;“그냥 숫자를 계속 더하는 건데 이게 왜 따로 알고리즘이라고 불리지?”&lt;br&gt;라는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;그런데 문제를 풀어보니 핵심은 &lt;strong&gt;더하는 행위 자체가 아니라, 반복 계산을 줄이기 위한 전처리&lt;/strong&gt;에 있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;같은 구간을 여러 번 계산해야 한다면, 매번 처음부터 더하지 말고 한 번 계산한 값을 재사용하자.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;예를 들어 배열이 다음과 같다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;arr = [1, 2, 3, 4, 5]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2번째 숫자부터 4번째 숫자까지의 합은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;2 + 3 + 4 = 9&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;이걸 한 번만 구한다면 그냥 반복문으로 더해도 된다.&lt;/p&gt;
&lt;p&gt;하지만 배열이 크고,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 ~ 100
50 ~ 300
200 ~ 700
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 구간 합을 계속 물어본다면 이야기가 달라진다.&lt;/p&gt;
&lt;p&gt;매번 해당 범위를 처음부터 더하는 대신, 누적합 배열을 만들어두면 훨씬 빠르게 처리할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;누적합의 기본 원리&lt;/h2&gt;
&lt;p&gt;원본 배열이&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 4, 5]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라면 누적합 배열은 다음처럼 만들 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원본 배열
[1, 2, 3, 4, 5]

누적합 배열
[0, 1, 3, 6, 10, 15]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;맨 앞에 &lt;code&gt;0&lt;/code&gt;을 하나 두면 인덱스를 계산하기가 편하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;prefix[0] = 0
prefix[1] = 1
prefix[2] = 1 + 2 = 3
prefix[3] = 1 + 2 + 3 = 6
prefix[4] = 1 + 2 + 3 + 4 = 10
prefix[5] = 1 + 2 + 3 + 4 + 5 = 15&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;prefix[i]
= 처음부터 i번째 원소까지의 합&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 미리 저장해두는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;구간 합 구하기&lt;/h2&gt;
&lt;p&gt;다시&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;arr = [1, 2, 3, 4, 5]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;에서 2번째부터 4번째까지의 합을 구해보자.&lt;/p&gt;
&lt;p&gt;4번째까지의 누적합은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;prefix[4] = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이고, 첫 번째까지의 누적합은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;prefix[1] = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;10 - 1 = 9&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;공식으로 쓰면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;i ~ j 구간의 합
= prefix[j] - prefix[i - 1]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;핵심은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;큰 범위의 누적값에서, 내가 필요하지 않은 앞부분을 빼는 것&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;시간복잡도&lt;/h2&gt;
&lt;p&gt;누적합 배열을 만드는 데는 전체 배열을 한 번 순회하므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(N)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 필요하다.&lt;/p&gt;
&lt;p&gt;대신 누적합 배열을 만들어두고 나면 구간 합 하나는 두 값의 차이만 계산하면 되기 때문에&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(1)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;에 구할 수 있다.&lt;/p&gt;
&lt;p&gt;구간 합 요청이 &lt;code&gt;M&lt;/code&gt;번 있다고 하면,&lt;/p&gt;
&lt;h3&gt;매번 직접 계산&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(N × M)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;누적합 사용&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;누적합 생성 O(N)
+ M번의 구간 계산 O(M)

= O(N + M)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;입력 크기가 커질수록 이 차이가 꽤 크게 느껴진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;실습: 프로그래머스 광고 삽입&lt;/h1&gt;
&lt;p&gt;누적합 개념을 공부한 뒤 프로그래머스 &lt;strong&gt;광고 삽입&lt;/strong&gt; 문제를 풀어봤다.&lt;/p&gt;
&lt;p&gt;단순히 배열의 구간 합만 구하는 문제는 아니었지만, 오히려 그래서 누적합을 왜 쓰는지 더 잘 이해할 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;문제를 어떻게 봤는가&lt;/h2&gt;
&lt;p&gt;문제에서는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;전체 영상 길이&lt;/li&gt;
&lt;li&gt;광고 길이&lt;/li&gt;
&lt;li&gt;여러 시청자의 시청 구간&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 주어진다.&lt;/p&gt;
&lt;p&gt;예를 들어 두 시청자가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;00:00:01-00:00:05
00:00:03-00:00:07&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;동안 영상을 봤다고 해보자.&lt;/p&gt;
&lt;p&gt;초 단위로 보면 대략 이런 느낌이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;시간     1  2  3  4  5  6
시청자A  O  O  O  O
시청자B        O  O  O  O&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러면 각 초의 시청자 수는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1초 → 1명
2초 → 1명
3초 → 2명
4초 → 2명
5초 → 1명
6초 → 1명&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;광고 길이가 3초라면 가능한 시작 위치를 하나씩 옮겨가면서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 ~ 3초
1 ~ 4초
2 ~ 5초
3 ~ 6초
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 구간의 &lt;strong&gt;누적 시청 시간이 가장 큰 위치&lt;/strong&gt;를 찾아야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;처음 떠올린 방법&lt;/h2&gt;
&lt;p&gt;가장 먼저 떠오른 건 시청 기록마다 해당 구간을 직접 증가시키는 방식이었다.&lt;/p&gt;
&lt;p&gt;예를 들어 어떤 사람이 10초부터 100초까지 영상을 봤다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (int i = 10; i &amp;lt; 100; i++) {
    viewers[i]++;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;문제는 시청 기록이 많고 각 시청 시간이 길어질수록 같은 배열 위치를 계속 반복해서 건드리게 된다는 점이었다.&lt;/p&gt;
&lt;p&gt;여기서 차분 배열 아이디어가 나온다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;범위 전체를 직접 수정하지 말고, 시작과 끝만 표시해두면 어떨까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h1&gt;차분 배열 + 누적합&lt;/h1&gt;
&lt;p&gt;예를 들어 한 사람이 3초부터 7초까지 시청했다고 해보자.&lt;/p&gt;
&lt;p&gt;실제로 시청한 구간은 &lt;code&gt;[3, 7)&lt;/code&gt;이므로 3, 4, 5, 6초이다.&lt;/p&gt;
&lt;p&gt;이 네 칸을 전부 &lt;code&gt;+1&lt;/code&gt; 하지 않고,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;3초 위치 +1
7초 위치 -1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만 기록한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;시간
0 1 2 3 4 5 6 7 8

변화량
0 0 0 1 0 0 0 -1 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 이 배열에 누적합을 적용하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 0 0 1 1 1 1 0 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 된다.&lt;/p&gt;
&lt;p&gt;결국 3초부터 6초까지는 시청자 수가 1명이라는 원래 정보가 복원된다.&lt;/p&gt;
&lt;p&gt;이 부분이 처음에는 꽤 신기했다.&lt;/p&gt;
&lt;p&gt;범위 전체를 수정하는 게 아니라,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;시작점에서 +1
끝점에서 -1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 &lt;strong&gt;이벤트만 기록해두고 나중에 한 번에 복원&lt;/strong&gt;하는 방식이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;여러 명이 겹치면?&lt;/h2&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A : 1 ~ 5초
B : 3 ~ 7초&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 시청했다고 하자.&lt;/p&gt;
&lt;p&gt;변화량만 기록하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1초 +1
5초 -1

3초 +1
7초 -1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이고, 배열로 쓰면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;시간    0 1 2 3 4 5 6 7
변화량  0 1 0 1 0 -1 0 -1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;여기에 누적합을 적용하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;시간      0 1 2 3 4 5 6 7
시청자 수  0 1 1 2 2 1 1 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;한 번의 누적합으로 각 초에 몇 명이 보고 있었는지를 알 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;그런데 누적합을 한 번 더 한다&lt;/h1&gt;
&lt;p&gt;여기서 끝이 아니었다.&lt;/p&gt;
&lt;p&gt;문제에서 필요한 값은 단순히&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;각 초의 시청자 수&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 아니라,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;광고가 재생되는 구간 전체의 누적 시청 시간&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;예를 들어 초당 시청자 수가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[0, 1, 1, 2, 2, 1, 1]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라면, 여기에 다시 누적합을 만든다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[0, 0, 1, 2, 4, 6, 7, 8]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러면 특정 광고 구간의 전체 시청 시간을&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;prefix[end] - prefix[start]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 바로 계산할 수 있다.&lt;/p&gt;
&lt;p&gt;이 문제에서는 누적합이 두 번 쓰인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;시청 시작/종료 변화량 기록
        ↓
1차 누적합
        ↓
각 초의 실제 시청자 수
        ↓
2차 누적합
        ↓
특정 시간까지의 누적 시청 시간&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h1&gt;내가 헷갈렸던 부분&lt;/h1&gt;
&lt;p&gt;처음 문제를 읽었을 때는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;각 초의 시청자 수를 구한 뒤, 광고 길이만큼 계속 더하면 되지 않을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;라고 생각했다.&lt;/p&gt;
&lt;p&gt;그런데 광고 길이가 10,000초라고 하면 광고 시작 위치를 한 칸 옮길 때마다 다시 10,000개를 더해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 ~ 9999
1 ~ 10000
2 ~ 10001
3 ~ 10002
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서로 거의 다 겹치는 구간인데도 매번 처음부터 다시 계산하게 된다.&lt;/p&gt;
&lt;p&gt;누적합을 사용하면 광고 시작 위치마다&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;prefix[start + 광고길이] - prefix[start]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만 계산하면 된다.&lt;/p&gt;
&lt;p&gt;즉 광고 구간 하나를 확인하는 비용이&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(광고 길이)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(1)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 줄어든다.&lt;/p&gt;
&lt;p&gt;이 부분에서 “아, 누적합이 그래서 필요한 거구나”라는 느낌이 가장 확실하게 들었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;시간 문자열을 초로 바꾸기&lt;/h1&gt;
&lt;p&gt;이 문제에서는 알고리즘 외에도 시간 문자열 처리가 필요하다.&lt;/p&gt;
&lt;p&gt;입력은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;quot;01:30:59&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;형태인데, 배열 인덱스로 사용하려면 초 단위 정수로 바꾸는 게 편하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;시간 × 3600
+ 분 × 60
+ 초&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;01:30:59&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 × 3600
+ 30 × 60
+ 59

= 5459초&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;코드 구현&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {

    public String solution(String play_time, String adv_time, String[] logs) {

        int playTime = toSeconds(play_time);
        int advTime = toSeconds(adv_time);

        // 각 초마다 시청자 수의 변화량을 저장
        long[] viewers = new long[playTime + 1];

        for (String log : logs) {

            String[] times = log.split(&amp;quot;-&amp;quot;);

            int start = toSeconds(times[0]);
            int end = toSeconds(times[1]);

            // 차분 배열
            viewers[start]++;
            viewers[end]--;
        }

        // 1차 누적합
        // 변화량 → 각 초의 실제 시청자 수
        for (int i = 1; i &amp;lt; playTime; i++) {
            viewers[i] += viewers[i - 1];
        }

        // 2차 누적합
        // prefix[i] = 0초부터 i초 직전까지의 누적 시청 시간
        long[] prefix = new long[playTime + 1];

        for (int i = 0; i &amp;lt; playTime; i++) {
            prefix[i + 1] = prefix[i] + viewers[i];
        }

        // 광고를 0초부터 시작하는 경우
        long maxViewTime = prefix[advTime];
        int answerStart = 0;

        // 광고 시작 위치를 한 초씩 이동
        for (int start = 1;
             start + advTime &amp;lt;= playTime;
             start++) {

            int end = start + advTime;

            long currentViewTime =
                    prefix[end] - prefix[start];

            // 더 큰 경우에만 갱신
            // 같으면 기존의 더 빠른 시작 시간이 유지된다.
            if (currentViewTime &amp;gt; maxViewTime) {
                maxViewTime = currentViewTime;
                answerStart = start;
            }
        }

        return toTime(answerStart);
    }

    private int toSeconds(String time) {

        String[] split = time.split(&amp;quot;:&amp;quot;);

        int hour = Integer.parseInt(split[0]);
        int minute = Integer.parseInt(split[1]);
        int second = Integer.parseInt(split[2]);

        return hour * 3600
                + minute * 60
                + second;
    }

    private String toTime(int seconds) {

        int hour = seconds / 3600;
        seconds %= 3600;

        int minute = seconds / 60;
        int second = seconds % 60;

        return String.format(
                &amp;quot;%02d:%02d:%02d&amp;quot;,
                hour,
                minute,
                second
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h1&gt;코드에서 중요했던 부분&lt;/h1&gt;
&lt;h2&gt;1. 시작점과 끝점만 기록하기&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;viewers[start]++;
viewers[end]--;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 이 두 줄만 보고&lt;br&gt;“이게 어떻게 전체 구간에 적용되는 거지?”&lt;br&gt;싶었다.&lt;/p&gt;
&lt;p&gt;하지만 뒤에서 누적합을 계산하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;viewers[i] += viewers[i - 1];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;시작점의 &lt;code&gt;+1&lt;/code&gt;이 뒤쪽으로 계속 전달된다.&lt;/p&gt;
&lt;p&gt;그러다가 종료 시점의 &lt;code&gt;-1&lt;/code&gt;을 만나면 다시 원래 값으로 돌아간다.&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;start → 시청 시작
end   → 시청 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 이벤트만 저장하는 셈이다.&lt;/p&gt;
&lt;p&gt;작은 배열을 직접 그려보니 이 부분이 가장 잘 이해됐다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 첫 번째 누적합&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (int i = 1; i &amp;lt; playTime; i++) {
    viewers[i] += viewers[i - 1];
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 과정을 마치면 &lt;code&gt;viewers[i]&lt;/code&gt;는 더 이상 변화량이 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;i초에 영상을 보고 있는 사람의 수&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 두 번째 누적합&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (int i = 0; i &amp;lt; playTime; i++) {
    prefix[i + 1] = prefix[i] + viewers[i];
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이번에는 각 초의 시청자 수를 다시 누적한다.&lt;/p&gt;
&lt;p&gt;이제 &lt;code&gt;prefix&lt;/code&gt;를 이용하면 원하는 광고 구간의 전체 시청 시간을 바로 계산할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 광고 구간 계산&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;long currentViewTime =
        prefix[end] - prefix[start];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;광고가 &lt;code&gt;[start, end)&lt;/code&gt; 동안 재생된다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 ~ end 직전까지의 누적값
-
0 ~ start 직전까지의 누적값&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 계산하는 것이다.&lt;/p&gt;
&lt;p&gt;결국 누적합의 기본 원리였던&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;큰 범위의 누적값
-
필요 없는 앞부분&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 그대로 적용된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;왜 &lt;code&gt;long&lt;/code&gt;을 사용했을까?&lt;/h1&gt;
&lt;p&gt;각 초의 시청자 수 자체는 &lt;code&gt;int&lt;/code&gt;로 충분해 보일 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 이 값을 시간 전체에 걸쳐 계속 누적하면 결과가 꽤 커질 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;300,000명이
100,000초 동안
계속 시청&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;했다고 생각하면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;300,000 × 100,000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만 해도 &lt;code&gt;int&lt;/code&gt; 범위를 넘을 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 누적값은 &lt;code&gt;long&lt;/code&gt;으로 처리하는 게 안전하다.&lt;/p&gt;
&lt;p&gt;누적합 문제에서는 원소 하나의 최댓값만 볼 게 아니라,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;모든 값이 누적됐을 때 최댓값이 어디까지 갈 수 있는지&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;를 확인해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;시간복잡도&lt;/h1&gt;
&lt;p&gt;영상 전체 시간을 &lt;code&gt;T&lt;/code&gt;, 시청 기록 수를 &lt;code&gt;N&lt;/code&gt;이라고 하면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;로그의 시작/종료 표시: &lt;code&gt;O(N)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;1차 누적합: &lt;code&gt;O(T)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;2차 누적합: &lt;code&gt;O(T)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;가능한 광고 시작 위치 탐색: &lt;code&gt;O(T)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 전체 시간복잡도는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(N + T)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;광고 시작 위치마다 모든 시청 구간이나 광고 길이만큼 다시 계산하는 방식보다 훨씬 효율적이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;이번 문제를 풀면서 느낀 누적합의 핵심&lt;/h1&gt;
&lt;p&gt;처음에는 누적합을 그냥&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;구간 합을 빠르게 구하는 방법&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도로만 생각했다.&lt;/p&gt;
&lt;p&gt;그런데 광고 삽입 문제에서는 누적합이 조금 다르게 쓰였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;범위의 변화만 기록
        ↓
누적합으로 실제 값 복원
        ↓
다시 누적합
        ↓
원하는 구간 값을 O(1)에 계산&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 핵심은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;반복해서 계산할 값을 미리 정리해두고, 필요한 순간에 재사용하는 것&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이라고 느꼈다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;누적합은 언제 떠올리면 좋을까?&lt;/h1&gt;
&lt;p&gt;문제를 읽다가 다음과 같은 느낌이 들면 누적합을 한 번 의심해볼 것 같다.&lt;/p&gt;
&lt;h3&gt;1. 같은 배열에서 구간 계산을 계속 한다&lt;/h3&gt;
&lt;p&gt;예를 들면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;구간 합
구간의 등장 횟수
구간의 누적 시청 시간&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;같은 값을 여러 번 구해야 하는 경우다.&lt;/p&gt;
&lt;h3&gt;2. 배열이 자주 바뀌지 않는다&lt;/h3&gt;
&lt;p&gt;한 번 전처리해놓고 계속 재사용할 수 있다면 누적합과 잘 맞는다.&lt;/p&gt;
&lt;h3&gt;3. 매번 직접 계산하면 너무 느리다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(N²)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이나&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(N × M)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도가 나온다면 미리 계산해서 줄일 수 있는 부분이 없는지 확인해볼 만하다.&lt;/p&gt;
&lt;h3&gt;4. 범위 전체에 같은 연산을 반복한다&lt;/h3&gt;
&lt;p&gt;이번 문제처럼&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;start ~ end 전체를 +1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해야 한다면 차분 배열과 누적합을 같이 떠올릴 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;누적합에서 조심할 점&lt;/h1&gt;
&lt;h2&gt;1. 범위가 어디까지 포함되는지&lt;/h2&gt;
&lt;p&gt;누적합은 인덱스 하나만 잘못 잡아도 결과가 바로 틀린다.&lt;/p&gt;
&lt;p&gt;특히&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[start, end)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;인지&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[start, end]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;인지 꼭 먼저 확인해야 한다.&lt;/p&gt;
&lt;p&gt;시간 구간 문제에서는 종료 시간이 포함되는지도 중요하다.&lt;/p&gt;
&lt;p&gt;이번 문제에서는 &lt;code&gt;[start, end)&lt;/code&gt; 기준으로 처리했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 자료형&lt;/h2&gt;
&lt;p&gt;여러 값이 계속 더해지므로 최종 합이 &lt;code&gt;int&lt;/code&gt; 범위를 넘을 수 있다.&lt;/p&gt;
&lt;p&gt;코드부터 작성하기 전에 최댓값이 어느 정도까지 커질 수 있는지 계산해보는 습관이 필요하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 무엇을 누적할지 생각하기&lt;/h2&gt;
&lt;p&gt;누적합이라고 해서 무조건 원본 배열의 값만 더하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;이번 문제에서는 처음에&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;시청자 수의 변화량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 기록했고,&lt;/p&gt;
&lt;p&gt;첫 번째 누적합으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;초당 시청자 수&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 만든 다음,&lt;/p&gt;
&lt;p&gt;그 값을 다시 누적해&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;특정 시간까지의 전체 누적 시청 시간&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 만들었다.&lt;/p&gt;
&lt;p&gt;결국 중요한 질문은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;무엇을 누적해두면 다음 계산이 쉬워질까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;인 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;배운 점&lt;/h2&gt;
&lt;p&gt;오늘은 누적합과 차분 배열을 같이 사용하는 방법을 공부했다.&lt;/p&gt;
&lt;p&gt;누적합 배열을 미리 만들어두면 특정 구간의 합을 매번 처음부터 계산하지 않고 &lt;code&gt;O(1)&lt;/code&gt;에 구할 수 있다는 점이 가장 인상적이었다.&lt;br&gt;그리고 범위 전체에 같은 값을 반영해야 하는 경우에는 차분 배열로 변화가 생기는 지점만 기록해두고, 나중에 누적합으로 실제 값을 복원할 수 있다는 것도 배웠다.&lt;/p&gt;
&lt;p&gt;특히 이번 문제를 풀면서 누적합을 꼭 한 번만 사용하는 건 아니라는 걸 알게 됐다.&lt;br&gt;광고 삽입에서는 차분 배열을 누적합해서 시간대별 시청자 수를 구하고, 다시 한 번 누적합을 만들어 광고 구간의 누적 시청 시간을 빠르게 계산했다.&lt;/p&gt;
&lt;p&gt;처음에는 그냥 계산을 최대한 빠르게 하는 게 중요하다고 생각했는데, 이번에는 &lt;strong&gt;처음에 데이터를 잘 전처리해두면 이후 계산이 훨씬 단순해질 수 있다&lt;/strong&gt;는 점을 제대로 느꼈다.&lt;/p&gt;
&lt;h2&gt;어려웠던 점&lt;/h2&gt;
&lt;p&gt;가장 헷갈렸던 부분은 역시 이 코드였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;viewers[start]++;
viewers[end]--;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음 봤을 때는 시작점과 끝점만 바꾸는데 어떻게 그 사이 구간 전체에 &lt;code&gt;+1&lt;/code&gt;이 적용되는지 바로 이해되지 않았다.&lt;/p&gt;
&lt;p&gt;설명만 읽을 때는 계속 애매했는데, 작은 배열을 하나 직접 만들어서 누적합을 손으로 계산해보니 그제야 감이 왔다.&lt;/p&gt;
&lt;p&gt;시작점에서 &lt;code&gt;+1&lt;/code&gt;을 해두면 그 값이 누적합을 통해 뒤쪽까지 계속 이어지고, 종료 시점에서 &lt;code&gt;-1&lt;/code&gt;을 만나면서 그 효과가 사라지는 구조였다.&lt;/p&gt;
&lt;p&gt;알고 나면 간단한데, 처음 떠올리기는 꽤 어려운 아이디어라는 생각이 들었다.&lt;/p&gt;
&lt;p&gt;그리고 누적합 문제를 풀다 보면 알고리즘 자체보다 인덱스 때문에 더 헷갈릴 때가 많았다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;start&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt;, &lt;code&gt;start - 1&lt;/code&gt;, &lt;code&gt;end + 1&lt;/code&gt; 중에서 어떤 범위를 포함하고 어떤 범위를 제외하는지 생각하다 보면 알고 있던 것도 갑자기 헷갈렸다.&lt;/p&gt;
&lt;p&gt;이번에도 &lt;code&gt;[start, end)&lt;/code&gt; 형태의 구간을 제대로 잡는 게 중요했다.&lt;/p&gt;
&lt;p&gt;다음부터는 머릿속으로만 인덱스를 맞추려고 하지 말고, 바로 코드를 작성하기 전에 작은 예제를 하나 만들어서 실제로 어느 위치까지 값이 들어가는지 먼저 확인해봐야겠다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;다음에 해볼 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;✅ 누적합(Prefix Sum)의 기본 개념 정리&lt;/li&gt;
&lt;li&gt;✅ 구간 합을 &lt;code&gt;O(1)&lt;/code&gt;에 계산하는 원리 이해&lt;/li&gt;
&lt;li&gt;✅ 프로그래머스 광고 삽입 풀이&lt;/li&gt;
&lt;li&gt;✅ 차분 배열 + 누적합 방식 이해&lt;/li&gt;
&lt;li&gt;⬜ 기본적인 2차원 누적합 문제 풀어보기&lt;/li&gt;
&lt;li&gt;⬜ 차분 배열(Difference Array) 따로 정리하기&lt;/li&gt;
&lt;li&gt;⬜ 슬라이딩 윈도우와 누적합 비교하기&lt;/li&gt;
&lt;li&gt;⬜ 프로그래머스 파괴되지 않은 건물 도전하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;오늘은 문제를 완전히 혼자 떠올려서 풀었다기보다는, 풀이를 따라가면서 &lt;strong&gt;왜 이런 방식으로 푸는지 이해하는 데 집중했다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아직 차분 배열을 바로 떠올릴 정도로 익숙하지는 않지만, 적어도 다음에 비슷한 형태의 범위 업데이트 문제가 나오면&lt;br&gt;“혹시 차분 배열이나 누적합을 사용할 수 있지 않을까?” 정도는 생각해볼 수 있을 것 같다.&lt;/p&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/72414?utm_source=chatgpt.com&quot;&gt;프로그래머스 코딩테스트 연습 - 광고 삽입&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CS/자료구조 &amp;middot; 알고리즘</category>
      <category>prefix sum</category>
      <category>til</category>
      <category>누적합</category>
      <category>알고리즘</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/57</guid>
      <comments>https://slowgrid.tistory.com/entry/til-Prefix-Sum#entry57comment</comments>
      <pubDate>Fri, 21 Aug 2026 18:34:40 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] - 2026.08.20 | 투 포인터 (Two Pointers)</title>
      <link>https://slowgrid.tistory.com/entry/til-Two-Pointers</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;코딩테스트 준비 - 투 포인터(Two Pointers) 정리&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;오늘 공부한 내용&lt;/h2&gt;
&lt;h3&gt;투 포인터(Two Pointers)란?&lt;/h3&gt;
&lt;p&gt;투 포인터는 말 그대로 &lt;strong&gt;두 개의 포인터를 두고 배열이나 리스트를 탐색하는 방식&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;처음에는 “그냥 인덱스 변수 두 개 쓰는 거 아닌가?” 싶었다. 그런데 문제를 몇 개 보고 나니, 중요한 건 포인터가 두 개라는 사실보다 &lt;strong&gt;현재 상태를 보고 어느 쪽 포인터를 움직일지 결정해서 필요 없는 탐색을 건너뛰는 것&lt;/strong&gt;이었다.&lt;/p&gt;
&lt;p&gt;알고리즘 자체가 비슷한 것은 아니지만, 불필요한 탐색 범위를 줄여 나간다는 사고방식 측면에서는 이전에 공부한 양방향 다익스트라가 떠올랐다.&lt;/p&gt;
&lt;p&gt;예를 들어 어떤 조건을 만족하는 두 원소나 연속된 구간을 찾는다고 하면, 가장 단순하게는 모든 경우를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;for i = 0 ~ N
    for j = i ~ N
        i부터 j까지 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 경우에 따라 &lt;code&gt;O(N²)&lt;/code&gt;까지 갈 수 있다.&lt;/p&gt;
&lt;p&gt;반면 투 포인터는 이미 답이 될 수 없다고 판단한 범위를 다시 보지 않는 식으로 탐색량을 줄인다.&lt;/p&gt;
&lt;p&gt;내가 이해한 핵심은 이렇다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;전부 다 보려고 하지 말고, 현재 조건을 보고 한쪽 포인터를 움직여서 필요 없는 경우를 버리자.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;투 포인터는 언제 떠올리면 좋을까?&lt;/h3&gt;
&lt;p&gt;아무 배열 문제에나 쓸 수 있는 건 아니다. 문제를 보면서 아래 같은 조건이 보이면 한 번 의심해볼 만했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;특정 조건을 만족하는 &lt;strong&gt;연속된 구간&lt;/strong&gt;을 찾아야 할 때&lt;/li&gt;
&lt;li&gt;두 수의 합처럼 &lt;strong&gt;두 값의 관계&lt;/strong&gt;를 확인해야 할 때&lt;/li&gt;
&lt;li&gt;배열이 정렬되어 있어서 포인터를 옮겼을 때 값이 어떻게 변할지 예측할 수 있을 때&lt;/li&gt;
&lt;li&gt;구간을 늘리거나 줄이면서 현재 상태를 바로 갱신할 수 있을 때&lt;/li&gt;
&lt;li&gt;&lt;code&gt;O(N²)&lt;/code&gt;으로는 시간 제한을 통과하기 어려울 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;특히 문제에 &lt;code&gt;연속된 구간&lt;/code&gt;, &lt;code&gt;두 수의 합&lt;/code&gt;, &lt;code&gt;정렬된 배열&lt;/code&gt;, &lt;code&gt;특정 합&lt;/code&gt; 같은 표현이 나오면 투 포인터를 먼저 떠올려볼 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;투 포인터의 대표적인 두 가지 형태&lt;/h2&gt;
&lt;p&gt;투 포인터라고 해서 항상 같은 모양으로 움직이는 건 아니었다.&lt;/p&gt;
&lt;h3&gt;1. 양쪽 끝에서 서로 가까워지는 방식&lt;/h3&gt;
&lt;p&gt;정렬된 배열에서 특정 합을 만드는 두 수를 찾는 문제를 생각하면 이해하기 쉽다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 5, 7, 9]
 ↑              ↑
left          right&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;목표 합이 &lt;code&gt;10&lt;/code&gt;이라면 처음부터&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 + 9 = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라서 바로 답을 찾을 수 있다.&lt;/p&gt;
&lt;p&gt;만약 현재 합이 목표보다 작다면 더 큰 값이 필요하므로 &lt;code&gt;left&lt;/code&gt;를 오른쪽으로 옮긴다.&lt;/p&gt;
&lt;p&gt;반대로 합이 목표보다 크다면 더 작은 값이 필요하므로 &lt;code&gt;right&lt;/code&gt;를 왼쪽으로 옮긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;합 &amp;lt; target → left++
합 &amp;gt; target → right--
합 = target → 정답&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이게 가능한 이유는 &lt;strong&gt;배열이 정렬되어 있어서 포인터를 옮겼을 때 합이 어느 방향으로 변할지 알 수 있기 때문&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 같은 방향으로 움직이는 방식&lt;/h3&gt;
&lt;p&gt;두 번째는 &lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;가 둘 다 왼쪽에서 시작해서 오른쪽으로 이동하는 형태다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 4, 5]
 ↑
 L
 ↑
 R&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;right&lt;/code&gt;를 옮기면서 구간을 늘리고,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 4, 5]
 ↑     ↑
 L     R&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;조건을 넘으면 &lt;code&gt;left&lt;/code&gt;를 옮겨서 구간을 다시 줄인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 4, 5]
    ↑  ↑
    L  R&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정리하면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;right&lt;/code&gt;는 구간을 늘리고, &lt;code&gt;left&lt;/code&gt;는 구간을 줄인다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이렇게 기억하니 훨씬 덜 헷갈렸다.&lt;/p&gt;
&lt;p&gt;그리고 이 형태는 슬라이딩 윈도우(Sliding Window)를 설명할 때도 자주 같이 나오는 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 시간복잡도가 O(N)일까?&lt;/h2&gt;
&lt;p&gt;여기서 한 번 헷갈렸다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;가 있고 코드 안에 &lt;code&gt;for&lt;/code&gt;문이랑 &lt;code&gt;while&lt;/code&gt;문까지 들어가니까 처음에는 이중 반복문처럼 보여서 &lt;code&gt;O(N²)&lt;/code&gt; 아닌가 싶었다.&lt;/p&gt;
&lt;p&gt;그런데 실제로 중요한 건 반복문이 몇 겹인지보다 &lt;strong&gt;각 포인터가 전체 실행 동안 몇 번 움직이는지&lt;/strong&gt;였다.&lt;/p&gt;
&lt;p&gt;길이가 N인 배열이라면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;left  → 최대 N번 이동
right → 최대 N번 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;둘을 합쳐도 대략 &lt;code&gt;2N&lt;/code&gt;번 정도다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(2N) → O(N)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 두 포인터가 뒤로 돌아가지 않고 한 방향으로만 움직이는 구조라면 전체 시간복잡도는 &lt;code&gt;O(N)&lt;/code&gt;이 된다.&lt;/p&gt;
&lt;p&gt;완전탐색으로 모든 구간을 확인했을 때 &lt;code&gt;O(N²)&lt;/code&gt;이 되는 문제를 &lt;code&gt;O(N)&lt;/code&gt;으로 줄일 수 있다는 게 투 포인터의 가장 큰 장점이라고 느꼈다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;실습: 프로그래머스 「연속된 부분 수열의 합」&lt;/h2&gt;
&lt;p&gt;이번에는 프로그래머스의 &lt;strong&gt;연속된 부분 수열의 합&lt;/strong&gt; 문제를 투 포인터로 풀어봤다.&lt;/p&gt;
&lt;h3&gt;문제 요약&lt;/h3&gt;
&lt;p&gt;비내림차순으로 정렬된 수열 &lt;code&gt;sequence&lt;/code&gt;와 숫자 &lt;code&gt;k&lt;/code&gt;가 주어진다.&lt;/p&gt;
&lt;p&gt;조건은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;연속된 부분 수열의 합이 &lt;code&gt;k&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;여러 개라면 길이가 가장 짧은 구간&lt;/li&gt;
&lt;li&gt;길이도 같다면 시작 인덱스가 가장 작은 구간&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 구간의 시작 인덱스와 마지막 인덱스를 반환하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;처음 생각한 방식&lt;/h3&gt;
&lt;p&gt;가장 먼저 제약 조건을 봤다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sequence&lt;/code&gt;의 길이가 최대 &lt;strong&gt;1,000,000&lt;/strong&gt;까지 들어올 수 있어서, 모든 시작점과 끝점을 조합해서 확인하는 방식은 어렵겠다고 생각했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 ~ 0
0 ~ 1
0 ~ 2
...
1 ~ 1
1 ~ 2
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 전부 확인하면 경우의 수가 너무 많다.&lt;/p&gt;
&lt;p&gt;문제를 다시 보니 여기서 중요한 조건이 하나 있었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;모든 수가 양수&lt;/strong&gt;라는 점이다.&lt;/p&gt;
&lt;p&gt;이 조건 덕분에 구간의 합이 어떻게 변할지 예측할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;right를 오른쪽으로 이동
→ 양수를 하나 추가
→ 합이 증가

left를 오른쪽으로 이동
→ 양수를 하나 제거
→ 합이 감소&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 현재 합을 &lt;code&gt;sum&lt;/code&gt;이라고 했을 때 다음처럼 움직일 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;sum &amp;lt; k → right를 이동해서 구간을 늘린다.
sum &amp;gt; k → left를 이동해서 구간을 줄인다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한 번 지나간 범위로 다시 돌아갈 필요가 없다는 점에서 투 포인터를 적용할 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;포인터 이동을 직접 따라가 보기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;sequence = [1, 2, 3, 4, 5]
k = 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 &lt;code&gt;1&lt;/code&gt;부터 시작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 4, 5]
 ↑
 L
 ↑
 R

sum = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;합이 &lt;code&gt;7&lt;/code&gt;보다 작으니 &lt;code&gt;right&lt;/code&gt;를 옮긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 4, 5]
 ↑  ↑
 L  R

sum = 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아직 작다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 4, 5]
 ↑     ↑
 L     R

sum = 6&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한 칸 더 옮기면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 4, 5]
 ↑        ↑
 L        R

sum = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이번에는 &lt;code&gt;7&lt;/code&gt;보다 커졌다.&lt;/p&gt;
&lt;p&gt;왼쪽 값을 하나 빼면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;10 - 1 = 9

[1, 2, 3, 4, 5]
    ↑     ↑
    L     R&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래도 크니까 한 번 더 줄인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;9 - 2 = 7

[1, 2, 3, 4, 5]
       ↑  ↑
       L  R&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;드디어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;3 + 4 = 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 되고, 답은 &lt;code&gt;[2, 3]&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;코드만 봤을 때보다 이렇게 직접 그려보니까 &lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;가 왜 뒤로 갈 필요가 없는지 훨씬 잘 보였다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;코드 구현&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public int[] solution(int[] sequence, int k) {
        int left = 0;
        long sum = 0;

        int bestLeft = 0;
        int bestRight = sequence.length - 1;
        int bestLength = sequence.length + 1;

        // right를 이동시키면서 구간 확장
        for (int right = 0; right &amp;lt; sequence.length; right++) {
            sum += sequence[right];

            // 합이 k보다 크면 left를 이동시키면서 구간 축소
            while (sum &amp;gt; k &amp;amp;&amp;amp; left &amp;lt;= right) {
                sum -= sequence[left];
                left++;
            }

            // 합이 k라면 정답 후보 갱신
            if (sum == k) {
                int currentLength = right - left + 1;

                if (currentLength &amp;lt; bestLength
                        || (currentLength == bestLength &amp;amp;&amp;amp; left &amp;lt; bestLeft)) {
                    bestLength = currentLength;
                    bestLeft = left;
                    bestRight = right;
                }
            }
        }

        return new int[]{bestLeft, bestRight};
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4&gt;코드에서 중요하게 본 부분&lt;/h4&gt;
&lt;p&gt;먼저 &lt;code&gt;right&lt;/code&gt;가 이동할 때마다 새 값을 현재 합에 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;sum += sequence[right];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 합이 &lt;code&gt;k&lt;/code&gt;보다 커지면 &lt;code&gt;left&lt;/code&gt;를 옮기면서 앞쪽 값을 뺀다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;while (sum &amp;gt; k &amp;amp;&amp;amp; left &amp;lt;= right) {
    sum -= sequence[left];
    left++;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음에는 이 &lt;code&gt;while&lt;/code&gt;문 때문에 시간복잡도가 &lt;code&gt;O(N²)&lt;/code&gt;처럼 느껴졌다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;code&gt;left&lt;/code&gt;는 한 번 오른쪽으로 간 뒤 다시 왼쪽으로 돌아오지 않는다. 전체 실행 동안 최대 N번만 이동한다.&lt;/p&gt;
&lt;p&gt;그래서 전체 시간복잡도는 &lt;code&gt;O(N)&lt;/code&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;구간 합을 매번 다시 계산하지 않는 이유&lt;/h2&gt;
&lt;p&gt;이 부분도 이번에 확실히 짚고 넘어갔다.&lt;/p&gt;
&lt;p&gt;현재 구간이&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[2, 3, 4]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이고 합이 &lt;code&gt;9&lt;/code&gt;라고 하자.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;right&lt;/code&gt;가 다음 숫자 &lt;code&gt;5&lt;/code&gt;로 이동했다고 해서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;2 + 3 + 4 + 5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 처음부터 다시 더할 필요는 없다.&lt;/p&gt;
&lt;p&gt;기존 합에 &lt;code&gt;5&lt;/code&gt;만 추가하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;9 + 5 = 14&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반대로 &lt;code&gt;left&lt;/code&gt;가 이동하면 빠지는 값만 빼면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;14 - 2 = 12&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;right 이동 → sum += sequence[right]
left 이동  → sum -= sequence[left]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 이전 계산 결과를 그대로 이어서 쓰는 방식이다.&lt;/p&gt;
&lt;p&gt;DP를 공부할 때도 이전 결과를 재사용하는 게 중요했는데, 여기서도 “매번 처음부터 다시 계산하지 않는다”는 생각이 연결돼서 기억에 남았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;투 포인터와 슬라이딩 윈도우&lt;/h2&gt;
&lt;p&gt;처음에는 두 용어가 거의 같은 말처럼 보여서 헷갈렸다.&lt;/p&gt;
&lt;p&gt;공부하면서는 슬라이딩 윈도우를 &lt;strong&gt;투 포인터를 사용하는 응용 형태 중 하나&lt;/strong&gt;로 생각하니 이해하기 편했다.&lt;/p&gt;
&lt;p&gt;투 포인터는 넓게 보면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;두 개의 포인터를 움직이면서 탐색 범위를 조절하는 기법&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이고, 슬라이딩 윈도우는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;두 포인터 사이의 연속된 구간(Window)을 유지하면서 이동하는 방식&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 4, 5]
 ↑     ↑
left right&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 &lt;code&gt;left ~ right&lt;/code&gt; 구간 자체를 관리하는 문제라면 슬라이딩 윈도우 느낌이 강하다.&lt;/p&gt;
&lt;p&gt;반면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[1, 2, 3, 5, 7, 9]
 ↑              ↑
left          right&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 정렬된 배열 양쪽 끝에서 시작해서 서로 가까워지는 방식도 투 포인터지만, 보통 슬라이딩 윈도우라고 부르지는 않는다.&lt;/p&gt;
&lt;p&gt;지금은 이렇게 구분해서 기억하고 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;투 포인터는 “두 포인터를 어떻게 움직일까?”가 핵심이고, 슬라이딩 윈도우는 “두 포인터 사이 구간을 어떻게 관리할까?”가 핵심이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;투 포인터를 쓸 때 주의할 점&lt;/h2&gt;
&lt;h3&gt;1. 포인터를 왜 움직여도 되는지 설명할 수 있어야 한다&lt;/h3&gt;
&lt;p&gt;이게 제일 중요했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (sum &amp;gt; k) left++;
else right++;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드를 공식처럼 외우는 게 아니라,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;왜 합이 커졌을 때 &lt;code&gt;left&lt;/code&gt;를 옮겨도 답을 놓치지 않는가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;를 설명할 수 있어야 한다.&lt;/p&gt;
&lt;p&gt;이번 문제에서는 &lt;strong&gt;모든 원소가 양수&lt;/strong&gt;라서 가능했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 음수가 있으면 같은 방식이 깨질 수 있다&lt;/h3&gt;
&lt;p&gt;배열에 음수가 들어가면 &lt;code&gt;right&lt;/code&gt;를 옮겼다고 해서 합이 무조건 커진다는 보장이 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[3, -10, 20]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 새 값을 추가했는데 오히려 합이 작아질 수도 있다.&lt;/p&gt;
&lt;p&gt;그러면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;sum &amp;lt; k → right++
sum &amp;gt; k → left++&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라는 규칙을 그대로 적용할 수 없다.&lt;/p&gt;
&lt;p&gt;그래서 투 포인터 문제를 볼 때는 코드를 떠올리기 전에 &lt;strong&gt;입력값이 어떤 성질을 가지는지&lt;/strong&gt; 먼저 확인해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. ‘연속’인지 꼭 확인하기&lt;/h3&gt;
&lt;p&gt;투 포인터나 슬라이딩 윈도우는 주로 연속된 범위를 다룬다.&lt;/p&gt;
&lt;p&gt;문제에서 말하는 부분 수열이&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;연속된 부분 수열&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;인지,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원소를 임의로 골라도 되는 부분 수열&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;인지 구분해야 한다.&lt;/p&gt;
&lt;p&gt;이걸 놓치면 처음부터 다른 알고리즘을 골라야 할 수도 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;오늘의 회고&lt;/h2&gt;
&lt;h3&gt;배운 점&lt;/h3&gt;
&lt;p&gt;투 포인터 자체보다도 &lt;strong&gt;탐색할 필요가 없는 범위를 판단해서 버리는 사고방식&lt;/strong&gt;이 더 중요하다는 걸 알게 됐다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;for&lt;/code&gt;문 안에 &lt;code&gt;while&lt;/code&gt;문이 있다고 무조건 &lt;code&gt;O(N²)&lt;/code&gt;이라고 생각하면 안 된다는 것도 다시 느꼈다. 반복문 모양보다 각 포인터가 전체 과정에서 실제로 몇 번 움직이는지를 봐야 한다는 점!!&lt;/p&gt;
&lt;h3&gt;어려웠던 점&lt;/h3&gt;
&lt;p&gt;처음에는 &lt;code&gt;sum &amp;gt; k&lt;/code&gt;면 &lt;code&gt;left&lt;/code&gt;를 옮긴다는 걸 거의 공식처럼 받아들였다.&lt;/p&gt;
&lt;p&gt;그런데 왜 그래도 되는지 생각해보니 결국 &lt;strong&gt;모든 원소가 양수라서 합의 변화 방향이 보장되기 때문&lt;/strong&gt;이었다. 음수가 들어가면 같은 방식이 바로 깨질 수 있다는 점을 처음에는 놓쳤다.&lt;/p&gt;
&lt;p&gt;다음부터는 투 포인터 문제를 보면 코드를 먼저 쓰기보다 아래 세 가지를 먼저 적어볼 생각이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;left&lt;/code&gt;를 움직이면 현재 상태가 어떻게 변하는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;right&lt;/code&gt;를 움직이면 현재 상태가 어떻게 변하는가?&lt;/li&gt;
&lt;li&gt;그 변화가 항상 한 방향으로 보장되는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 세 가지가 설명되면 그때 구현으로 넘어가는 편이 덜 헷갈릴 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;액션 플랜&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;✅ 투 포인터 기본 개념과 시간복잡도 정리&lt;/li&gt;
&lt;li&gt;✅ 프로그래머스 &lt;code&gt;연속된 부분 수열의 합&lt;/code&gt; 풀이&lt;/li&gt;
&lt;li&gt;⬜ 정렬된 배열에서 두 수의 합을 찾는 양방향 투 포인터 문제 풀기&lt;/li&gt;
&lt;li&gt;⬜ 슬라이딩 윈도우를 따로 공부하고 투 포인터와 차이 정리하기&lt;/li&gt;
&lt;li&gt;⬜ 누적합(Prefix Sum)과 투 포인터를 어떤 상황에서 각각 쓰는지 비교하기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;같이 이야기해보고 싶은 점&lt;/h2&gt;
&lt;p&gt;이번 문제에서는 모든 원소가 양수라서 구간 합을 투 포인터로 깔끔하게 처리할 수 있었다.&lt;/p&gt;
&lt;p&gt;그렇다면 배열에 음수가 섞여서 구간 합이 한 방향으로 변하지 않는 경우에는 어떤 방법이 더 적절할까?&lt;/p&gt;
&lt;p&gt;누적합과 HashMap을 같이 쓰는 방식도 있다고 들었는데, 투 포인터와 문제를 바라보는 기준이 어떻게 다른지 다음에 더 공부해보고 싶다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;프로그래머스 코딩테스트 연습 - &lt;strong&gt;연속된 부분 수열의 합&lt;/strong&gt; &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/178870?utm_source=chatgpt.com&quot;&gt;문제 바로가기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;투 포인터(Two Pointers) 알고리즘&lt;/strong&gt; &lt;a href=&quot;https://www.geeksforgeeks.org/dsa/two-pointers-technique/?utm_source=chatgpt.com&quot;&gt;개념 설명 바로가기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;학습 예정&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;누적합(Prefix Sum)&lt;/li&gt;
&lt;li&gt;슬라이딩 윈도우(Sliding Window)&lt;/li&gt;
&lt;li&gt;백트래킹(Backtracking)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CS/자료구조 &amp;middot; 알고리즘</category>
      <category>til</category>
      <category>Two Pointers</category>
      <category>알고리즘</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/56</guid>
      <comments>https://slowgrid.tistory.com/entry/til-Two-Pointers#entry56comment</comments>
      <pubDate>Thu, 20 Aug 2026 20:06:12 +0900</pubDate>
    </item>
    <item>
      <title>AWS EC2 + ECR + GitHub Actions OIDC + DuckDNS + HTTPS 배포 가이드</title>
      <link>https://slowgrid.tistory.com/entry/AWS-EC2-ECR-GitHub-Actions-OIDC-DuckDNS-HTTPS-%EB%B0%B0%ED%8F%AC-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;div&gt;
&lt;style&gt;
/* slowgrid Tistory post-scoped styles. No global skin selectors are overridden. */
.sg-guide {
  --sg-text: #262626;
  --sg-muted: #6b7280;
  --sg-line: #e5e7eb;
  --sg-soft: #f7f7f8;
  --sg-soft-2: #fafafa;
  --sg-accent: #111827;
  color: inherit;
  font: inherit;
  line-height: 1.82;
  overflow-wrap: anywhere;
  word-break: keep-all;
}
.sg-guide, .sg-guide * { box-sizing: border-box; }
.sg-guide .sg-lead {
  margin: 0 0 2.2rem;
  padding: 0 0 1.35rem;
  border-bottom: 1px solid var(--sg-line);
}
.sg-guide .sg-lead-title {
  margin: 0 0 .6rem;
  color: var(--sg-accent);
  font-size: clamp(1.28rem, 2.3vw, 1.62rem);
  font-weight: 760;
  line-height: 1.42;
  letter-spacing: -.025em;
}
.sg-guide .sg-lead-note {
  margin: 0;
  color: var(--sg-muted);
  font-size: .92em;
}
.sg-guide .sg-toc {
  margin: 1.4rem 0 2.7rem;
  border: 1px solid var(--sg-line);
  border-radius: 12px;
  background: var(--sg-soft-2);
  overflow: hidden;
}
.sg-guide .sg-toc-head {
  padding: 1rem 1.05rem .9rem;
  border-bottom: 1px solid var(--sg-line);
}
.sg-guide .sg-toc-head &gt; div {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  flex-wrap: wrap;
}
.sg-guide .sg-toc-title {
  color: var(--sg-accent);
  font-size: 1.02rem;
  font-weight: 760;
}
.sg-guide .sg-toc-count {
  color: var(--sg-muted);
  font-size: .78em;
}
.sg-guide .sg-toc-help {
  margin: .28rem 0 0;
  color: var(--sg-muted);
  font-size: .82em;
  line-height: 1.55;
}
.sg-guide .sg-toc-quick {
  display: flex;
  gap: .42rem;
  padding: .78rem 1.05rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-bottom: 1px solid var(--sg-line);
  scrollbar-width: thin;
}
.sg-guide .sg-toc-quick a {
  flex: 0 0 auto;
  padding: .42rem .66rem;
  border: 1px solid #dedede;
  border-radius: 999px;
  background: #fff;
  color: #4b5563;
  font-size: .78em;
  line-height: 1.3;
  text-decoration: none;
  white-space: nowrap;
}
.sg-guide .sg-toc-quick a:hover {
  border-color: #b9bcc2;
  color: var(--sg-accent);
}
.sg-guide .sg-toc-groups { padding: .28rem .8rem .72rem; }
.sg-guide .sg-toc-group {
  margin: .42rem 0 0;
  border: 1px solid transparent;
  border-radius: 9px;
  background: transparent;
}
.sg-guide .sg-toc-group[open] {
  border-color: var(--sg-line);
  background: #fff;
}
.sg-guide .sg-toc-group &gt; summary {
  display: flex;
  align-items: center;
  gap: .7rem;
  padding: .68rem .72rem;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.sg-guide .sg-toc-group &gt; summary::-webkit-details-marker { display: none; }
.sg-guide .sg-toc-group &gt; summary::after {
  content: '+';
  margin-left: auto;
  color: var(--sg-muted);
  font-size: 1rem;
  line-height: 1;
}
.sg-guide .sg-toc-group[open] &gt; summary::after { content: '−'; }
.sg-guide .sg-toc-step {
  flex: 0 0 auto;
  min-width: 2rem;
  color: #9ca3af;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .03em;
}
.sg-guide .sg-toc-group-copy {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: .04rem;
}
.sg-guide .sg-toc-group-copy strong {
  color: var(--sg-accent);
  font-size: .88rem;
  font-weight: 710;
  line-height: 1.42;
}
.sg-guide .sg-toc-group-copy small {
  color: var(--sg-muted);
  font-size: .72rem;
  line-height: 1.4;
}
.sg-guide .sg-toc-links {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 .7rem;
  padding: .05rem .72rem .7rem 3.42rem;
}
.sg-guide .sg-toc-links a {
  display: flex;
  align-items: baseline;
  gap: .55rem;
  min-width: 0;
  padding: .34rem 0;
  color: inherit;
  font-size: .82em;
  line-height: 1.5;
  text-decoration: none;
}
.sg-guide .sg-toc-links a:hover { text-decoration: underline; }
.sg-guide .sg-toc-num {
  flex: 0 0 1.65rem;
  color: #9ca3af;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: .78em;
  text-align: right;
}
.sg-guide .sg-anchor {
  display: block;
  position: relative;
  top: -72px;
  height: 0;
  overflow: hidden;
  visibility: hidden;
}
.sg-guide h2.sg-major-heading,
.sg-guide h3.sg-major-heading {
  scroll-margin-top: 88px;
  margin: 3.1rem 0 1.15rem;
  padding-top: .15rem;
  color: var(--sg-accent);
  font-size: clamp(1.28rem, 2.4vw, 1.55rem);
  font-weight: 760;
  line-height: 1.45;
  letter-spacing: -.02em;
}
.sg-guide h3.sg-heading,
.sg-guide h4.sg-heading,
.sg-guide h5.sg-heading,
.sg-guide h6.sg-heading {
  margin: 2.05rem 0 .85rem;
  color: var(--sg-accent);
  font-weight: 720;
  line-height: 1.5;
  letter-spacing: -.015em;
}
.sg-guide h3.sg-heading { font-size: 1.22rem; }
.sg-guide h4.sg-heading { font-size: 1.08rem; }
.sg-guide h5.sg-heading, .sg-guide h6.sg-heading { font-size: 1rem; }
.sg-guide p { margin: .72rem 0; }
.sg-guide ul, .sg-guide ol {
  margin: .8rem 0 1rem;
  padding-left: 1.45rem;
}
.sg-guide li { margin: .28rem 0; }
.sg-guide li::marker { color: #8b8b8b; }
.sg-guide .sg-task-item { list-style: none; margin-left: -1.15rem; }
.sg-guide .sg-check {
  display: inline-block;
  width: .9rem;
  height: .9rem;
  margin: 0 .55rem 0 0;
  border: 1.5px solid #9ca3af;
  border-radius: 2px;
  vertical-align: -.08rem;
  background: #fff;
}
.sg-guide blockquote {
  margin: 1.25rem 0;
  padding: .9rem 1rem;
  border-left: 3px solid #c9ccd1;
  background: var(--sg-soft-2);
  color: #4b5563;
}
.sg-guide blockquote &gt; :first-child { margin-top: 0; }
.sg-guide blockquote &gt; :last-child { margin-bottom: 0; }
.sg-guide hr {
  margin: 2.55rem 0;
  border: 0;
  border-top: 1px solid var(--sg-line);
}
.sg-guide code {
  padding: .12em .36em;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  background: #f5f5f5;
  color: #333;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, &quot;Liberation Mono&quot;, monospace;
  font-size: .89em;
  word-break: break-word;
}
.sg-guide pre.sg-code-block {
  width: 100%;
  max-width: 100%;
  margin: 1rem 0 1.35rem;
  padding: 1rem 1.05rem;
  border: 1px solid #e3e3e3;
  border-radius: 8px;
  background: #f6f6f6;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  white-space: pre;
  line-height: 1.62;
  tab-size: 2;
}
.sg-guide pre.sg-code-block code {
  display: block;
  min-width: max-content;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: #252525;
  font-size: .86rem;
  word-break: normal;
  overflow-wrap: normal;
}
.sg-guide .sg-table-wrap {
  width: 100%;
  margin: 1.1rem 0 1.45rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--sg-line);
  border-radius: 8px;
}
.sg-guide table {
  width: 100%;
  min-width: 620px;
  margin: 0;
  border-collapse: collapse;
  border-spacing: 0;
  background: #fff;
  font-size: .92em;
  line-height: 1.58;
}
.sg-guide th,
.sg-guide td {
  padding: .72rem .8rem;
  border-right: 1px solid var(--sg-line);
  border-bottom: 1px solid var(--sg-line);
  text-align: left;
  vertical-align: top;
  word-break: keep-all;
}
.sg-guide th:last-child, .sg-guide td:last-child { border-right: 0; }
.sg-guide tr:last-child td { border-bottom: 0; }
.sg-guide th {
  background: var(--sg-soft);
  color: var(--sg-accent);
  font-weight: 700;
  white-space: nowrap;
}
.sg-guide a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
}
.sg-guide strong { color: var(--sg-accent); font-weight: 760; }
.sg-guide img { max-width: 100%; height: auto; }
@media (max-width: 680px) {
  .sg-guide { line-height: 1.78; word-break: normal; }
  .sg-guide .sg-toc-links { grid-template-columns: 1fr; padding-left: 2.72rem; }
  .sg-guide .sg-toc-quick { padding-left: .85rem; padding-right: .85rem; }
  .sg-guide h2.sg-major-heading,
  .sg-guide h3.sg-major-heading { margin-top: 2.55rem; }
  .sg-guide pre.sg-code-block { padding: .9rem; border-radius: 6px; }
  .sg-guide pre.sg-code-block code { font-size: .81rem; }
  .sg-guide table { min-width: 560px; font-size: .88em; }
  .sg-guide th, .sg-guide td { padding: .62rem .68rem; }
}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;sg-guide&quot;&gt;
&lt;div class=&quot;sg-lead&quot;&gt;
&lt;div class=&quot;sg-lead-title&quot;&gt;AWS EC2 + ECR + GitHub Actions OIDC + DuckDNS + HTTPS 배포 가이드&lt;/div&gt;
&lt;p class=&quot;sg-lead-note&quot; data-ke-size=&quot;size16&quot;&gt;Tistory 본문용으로 정리한 배포 가이드입니다. 긴 코드와 표는 가로 스크롤로 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;nav id=&quot;sg-toc&quot; class=&quot;sg-toc&quot; aria-label=&quot;이 글의 목차&quot;&gt;
&lt;div class=&quot;sg-toc-head&quot;&gt;
&lt;div&gt;&lt;b&gt;목차&lt;/b&gt; &lt;span class=&quot;sg-toc-count&quot;&gt;62개 섹션 &amp;middot; 9개 단계&lt;/span&gt;&lt;/div&gt;
&lt;p class=&quot;sg-toc-help&quot; data-ke-size=&quot;size16&quot;&gt;큰 단계로 먼저 찾고, 펼친 뒤 원하는 항목을 누르면 해당 본문으로 이동합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sg-toc-quick&quot; aria-label=&quot;단계 빠른 이동&quot;&gt;&lt;a href=&quot;#sg-section-0&quot;&gt;개요 &amp;middot; 로컬 준비&lt;/a&gt; &lt;a href=&quot;#sg-section-7&quot;&gt;AWS 인프라 구성&lt;/a&gt; &lt;a href=&quot;#sg-section-17&quot;&gt;EC2 배포 스크립트&lt;/a&gt; &lt;a href=&quot;#sg-section-19&quot;&gt;GitHub OIDC &amp;middot; CI/CD&lt;/a&gt; &lt;a href=&quot;#sg-section-30&quot;&gt;DuckDNS &amp;middot; Nginx &amp;middot; HTTPS&lt;/a&gt; &lt;a href=&quot;#sg-section-39&quot;&gt;운영 &amp;middot; 재배포 &amp;middot; 롤백&lt;/a&gt; &lt;a href=&quot;#sg-section-42&quot;&gt;트러블슈팅&lt;/a&gt; &lt;a href=&quot;#sg-section-55&quot;&gt;보안 &amp;middot; 비용 &amp;middot; 정리&lt;/a&gt; &lt;a href=&quot;#sg-section-58&quot;&gt;최종 확인 &amp;middot; 참고&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;sg-toc-groups&quot;&gt;&lt;details class=&quot;sg-toc-group&quot; open=&quot;&quot;&gt;
&lt;summary&gt;&lt;span class=&quot;sg-toc-step&quot;&gt;01&lt;/span&gt; &lt;span class=&quot;sg-toc-group-copy&quot;&gt; &lt;b&gt;개요 &amp;middot; 로컬 준비&lt;/b&gt; &lt;small&gt;전체 구조와 로컬 Docker 검증 &amp;middot; 0&amp;ndash;6&lt;/small&gt; &lt;/span&gt;&lt;/summary&gt;
&lt;div class=&quot;sg-toc-links&quot;&gt;&lt;a href=&quot;#sg-section-0&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;0&lt;/span&gt;&lt;span&gt;이 가이드에서 최종적으로 만드는 구조&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-1&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;1&lt;/span&gt;&lt;span&gt;시작 전에 정할 값&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-2&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;2&lt;/span&gt;&lt;span&gt;사전 준비사항&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-3&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;3&lt;/span&gt;&lt;span&gt;Repository에 필요한 파일&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-4&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;4&lt;/span&gt;&lt;span&gt;Dockerfile 작성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-5&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;5&lt;/span&gt;&lt;span&gt;.dockerignore 작성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-6&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;6&lt;/span&gt;&lt;span&gt;AWS로 넘어가기 전에 로컬 Docker 검증&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/details&gt;&lt;details class=&quot;sg-toc-group&quot;&gt;
&lt;summary&gt;&lt;span class=&quot;sg-toc-step&quot;&gt;02&lt;/span&gt; &lt;span class=&quot;sg-toc-group-copy&quot;&gt; &lt;b&gt;AWS 인프라 구성&lt;/b&gt; &lt;small&gt;ECR, EC2, IAM, 네트워크, SSM &amp;middot; 7&amp;ndash;16&lt;/small&gt; &lt;/span&gt;&lt;/summary&gt;
&lt;div class=&quot;sg-toc-links&quot;&gt;&lt;a href=&quot;#sg-section-7&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;7&lt;/span&gt;&lt;span&gt;AWS Region 고정&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-8&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;8&lt;/span&gt;&lt;span&gt;Amazon ECR Repository 생성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-9&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;9&lt;/span&gt;&lt;span&gt;EC2용 IAM Role 생성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-10&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;10&lt;/span&gt;&lt;span&gt;EC2 Instance 생성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-11&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;11&lt;/span&gt;&lt;span&gt;Security Group 설정&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-12&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;12&lt;/span&gt;&lt;span&gt;Public Subnet / Internet Gateway 확인&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-13&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;13&lt;/span&gt;&lt;span&gt;Elastic IP 생성 및 EC2에 연결&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-14&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;14&lt;/span&gt;&lt;span&gt;SSM Session Manager 접속 확인&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-15&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;15&lt;/span&gt;&lt;span&gt;EC2에 Docker 설치&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-16&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;16&lt;/span&gt;&lt;span&gt;EC2 AWS CLI가 없는 경우&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/details&gt;&lt;details class=&quot;sg-toc-group&quot;&gt;
&lt;summary&gt;&lt;span class=&quot;sg-toc-step&quot;&gt;03&lt;/span&gt; &lt;span class=&quot;sg-toc-group-copy&quot;&gt; &lt;b&gt;EC2 배포 스크립트&lt;/b&gt; &lt;small&gt;deploy.sh 작성과 롤백 동작 &amp;middot; 17&amp;ndash;18&lt;/small&gt; &lt;/span&gt;&lt;/summary&gt;
&lt;div class=&quot;sg-toc-links&quot;&gt;&lt;a href=&quot;#sg-section-17&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;17&lt;/span&gt;&lt;span&gt;최종형 deploy.sh 작성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-18&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;18&lt;/span&gt;&lt;span&gt;deploy.sh 동작 방식&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/details&gt;&lt;details class=&quot;sg-toc-group&quot;&gt;
&lt;summary&gt;&lt;span class=&quot;sg-toc-step&quot;&gt;04&lt;/span&gt; &lt;span class=&quot;sg-toc-group-copy&quot;&gt; &lt;b&gt;GitHub OIDC &amp;middot; CI/CD&lt;/b&gt; &lt;small&gt;OIDC, IAM Role, Actions, 최초 배포 &amp;middot; 19&amp;ndash;29&lt;/small&gt; &lt;/span&gt;&lt;/summary&gt;
&lt;div class=&quot;sg-toc-links&quot;&gt;&lt;a href=&quot;#sg-section-19&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;19&lt;/span&gt;&lt;span&gt;GitHub Actions용 AWS OIDC Provider 생성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-20&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;20&lt;/span&gt;&lt;span&gt;GitHub OIDC sub 형식 확인 &amp;mdash; 매우 중요&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-21&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;21&lt;/span&gt;&lt;span&gt;GitHub Actions용 IAM Role 생성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-22&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;22&lt;/span&gt;&lt;span&gt;GitHub Actions Role에 최소 권한 IAM Policy 추가&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-23&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;23&lt;/span&gt;&lt;span&gt;GitHub Repository Variables 등록&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-24&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;24&lt;/span&gt;&lt;span&gt;GitHub Actions Workflow 작성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-25&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;25&lt;/span&gt;&lt;span&gt;왜 Docker image tag를 latest가 아닌 commit SHA로 사용하는가&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-26&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;26&lt;/span&gt;&lt;span&gt;최초 CI 검증 &amp;mdash; 바로 main에 push하지 않기&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-27&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;27&lt;/span&gt;&lt;span&gt;PR Merge 후 최초 CD 실행&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-28&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;28&lt;/span&gt;&lt;span&gt;ECR Push 결과 확인&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-29&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;29&lt;/span&gt;&lt;span&gt;EC2에서 최초 배포 확인&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/details&gt;&lt;details class=&quot;sg-toc-group&quot;&gt;
&lt;summary&gt;&lt;span class=&quot;sg-toc-step&quot;&gt;05&lt;/span&gt; &lt;span class=&quot;sg-toc-group-copy&quot;&gt; &lt;b&gt;DuckDNS &amp;middot; Nginx &amp;middot; HTTPS&lt;/b&gt; &lt;small&gt;도메인부터 인증서 자동 갱신까지 &amp;middot; 30&amp;ndash;38&lt;/small&gt; &lt;/span&gt;&lt;/summary&gt;
&lt;div class=&quot;sg-toc-links&quot;&gt;&lt;a href=&quot;#sg-section-30&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;30&lt;/span&gt;&lt;span&gt;DuckDNS 도메인 생성&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-31&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;31&lt;/span&gt;&lt;span&gt;DuckDNS DNS 확인&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-32&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;32&lt;/span&gt;&lt;span&gt;Nginx + Certbot 설치&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-33&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;33&lt;/span&gt;&lt;span&gt;Nginx Reverse Proxy 설정&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-34&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;34&lt;/span&gt;&lt;span&gt;HTTPS 발급 전에 HTTP 경로 확인&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-35&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;35&lt;/span&gt;&lt;span&gt;Let's Encrypt HTTPS 인증서 발급&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-36&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;36&lt;/span&gt;&lt;span&gt;HTTPS 검증&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-37&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;37&lt;/span&gt;&lt;span&gt;인증서 자동 갱신 확인&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-38&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;38&lt;/span&gt;&lt;span&gt;최종 포트 구조 확인&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/details&gt;&lt;details class=&quot;sg-toc-group&quot;&gt;
&lt;summary&gt;&lt;span class=&quot;sg-toc-step&quot;&gt;06&lt;/span&gt; &lt;span class=&quot;sg-toc-group-copy&quot;&gt; &lt;b&gt;운영 &amp;middot; 재배포 &amp;middot; 롤백&lt;/b&gt; &lt;small&gt;운영 흐름과 변경 배포 검증 &amp;middot; 39&amp;ndash;41&lt;/small&gt; &lt;/span&gt;&lt;/summary&gt;
&lt;div class=&quot;sg-toc-links&quot;&gt;&lt;a href=&quot;#sg-section-39&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;39&lt;/span&gt;&lt;span&gt;최종 CI/CD 운영 흐름&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-40&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;40&lt;/span&gt;&lt;span&gt;새 코드 배포 테스트&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-41&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;41&lt;/span&gt;&lt;span&gt;배포 실패 시 Rollback 확인&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/details&gt;&lt;details class=&quot;sg-toc-group&quot;&gt;
&lt;summary&gt;&lt;span class=&quot;sg-toc-step&quot;&gt;07&lt;/span&gt; &lt;span class=&quot;sg-toc-group-copy&quot;&gt; &lt;b&gt;트러블슈팅&lt;/b&gt; &lt;small&gt;Docker, SSM, OIDC, DNS, Nginx, Certbot &amp;middot; 42&amp;ndash;54&lt;/small&gt; &lt;/span&gt;&lt;/summary&gt;
&lt;div class=&quot;sg-toc-links&quot;&gt;&lt;a href=&quot;#sg-section-42&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;42&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 1 &amp;mdash; Docker build 실패&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-43&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;43&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 2 &amp;mdash; Container가 즉시 종료됨&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-44&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;44&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 3 &amp;mdash; curl-minimal 충돌&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-45&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;45&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 4 &amp;mdash; SSM Session Manager 접속 불가&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-46&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;46&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 5 &amp;mdash; ECR Pull AccessDenied&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-47&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;47&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 6 &amp;mdash; GitHub OIDC AssumeRole 실패&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-48&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;48&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 7 &amp;mdash; GitHub Actions는 성공했는데 사이트가 안 열림&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-49&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;49&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 8 &amp;mdash; 브라우저가 HTTP를 HTTPS로 자동 변경&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-50&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;50&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 9 &amp;mdash; DuckDNS domain은 해석되지만 접속 불가&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-51&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;51&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 10 &amp;mdash; Nginx 502 Bad Gateway&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-52&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;52&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 11 &amp;mdash; Certbot 발급 실패&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-53&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;53&lt;/span&gt;&lt;span&gt;자주 발생하는 문제 12 &amp;mdash; Port 80 충돌&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-54&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;54&lt;/span&gt;&lt;span&gt;GitHub Actions 배포 실패 로그 확인&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/details&gt;&lt;details class=&quot;sg-toc-group&quot;&gt;
&lt;summary&gt;&lt;span class=&quot;sg-toc-step&quot;&gt;08&lt;/span&gt; &lt;span class=&quot;sg-toc-group-copy&quot;&gt; &lt;b&gt;보안 &amp;middot; 비용 &amp;middot; 정리&lt;/b&gt; &lt;small&gt;보안 권장사항과 Cleanup &amp;middot; 55&amp;ndash;57&lt;/small&gt; &lt;/span&gt;&lt;/summary&gt;
&lt;div class=&quot;sg-toc-links&quot;&gt;&lt;a href=&quot;#sg-section-55&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;55&lt;/span&gt;&lt;span&gt;보안 관련 권장사항&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-56&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;56&lt;/span&gt;&lt;span&gt;비용 관련 주의사항&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-57&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;57&lt;/span&gt;&lt;span&gt;프로젝트 종료 시 Cleanup Checklist&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/details&gt;&lt;details class=&quot;sg-toc-group&quot;&gt;
&lt;summary&gt;&lt;span class=&quot;sg-toc-step&quot;&gt;09&lt;/span&gt; &lt;span class=&quot;sg-toc-group-copy&quot;&gt; &lt;b&gt;최종 확인 &amp;middot; 참고&lt;/b&gt; &lt;small&gt;완료 체크리스트와 재구축 순서 &amp;middot; 58&amp;ndash;61&lt;/small&gt; &lt;/span&gt;&lt;/summary&gt;
&lt;div class=&quot;sg-toc-links&quot;&gt;&lt;a href=&quot;#sg-section-58&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;58&lt;/span&gt;&lt;span&gt;전체 구축 완료 체크리스트&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-59&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;59&lt;/span&gt;&lt;span&gt;최종 상태 요약&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-60&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;60&lt;/span&gt;&lt;span&gt;공식 문서 참고 링크&lt;/span&gt;&lt;/a&gt; &lt;a href=&quot;#sg-section-61&quot;&gt;&lt;span class=&quot;sg-toc-num&quot;&gt;61&lt;/span&gt;&lt;span&gt;다음번에 가장 빠르게 재구축하는 순서&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/details&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성 기준: 2026-08-18&lt;br /&gt;예시 프로젝트명: &lt;code&gt;testcicd&lt;/code&gt;&lt;br /&gt;예시 리전: &lt;code&gt;ap-northeast-2&lt;/code&gt; (서울)&lt;br /&gt;목적: Docker 기반 애플리케이션을 GitHub Actions에서 빌드/검증하고 Amazon ECR에 푸시한 뒤, AWS Systems Manager(SSM)로 EC2에 자동 배포하며, DuckDNS + Nginx + Let's Encrypt로 HTTPS까지 구성한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h3 id=&quot;sg-section-0&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size23&quot;&gt;0. 이 가이드에서 최종적으로 만드는 구조&lt;/h3&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;Developer
   │
   │ git push / Pull Request
   ▼
GitHub Repository
   │
   ▼
GitHub Actions
   │
   ├─ Docker build
   ├─ Smoke test
   ├─ GitHub OIDC &amp;rarr; AWS IAM Role
   └─ Docker image push
          │
          ▼
      Amazon ECR
          │
          │ SSM SendCommand
          ▼
        Amazon EC2
          │
          ├─ /opt/testcicd/deploy.sh
          ├─ ECR image pull
          ├─ Docker container
          │    └─ 127.0.0.1:8000
          │
          └─ Nginx
               ├─ :80  &amp;rarr; HTTPS redirect
               └─ :443 &amp;rarr; reverse proxy
                        │
                        ▼
             testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 외부 접속 주소 예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block avrasm&quot;&gt;&lt;code&gt;https://testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가이드는 애플리케이션 자체의 비즈니스 로직과 무관하게 사용할 수 있도록 중립적인 이름을 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-1&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;1. 시작 전에 정할 값&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 값은 작업 전에 한 번 정하고 끝까지 동일하게 사용한다.&lt;/p&gt;
&lt;div class=&quot;sg-table-wrap&quot; role=&quot;region&quot; aria-label=&quot;표 영역&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;예시 값&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS Region&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ap-northeast-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서울 리전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECR Repository&lt;/td&gt;
&lt;td&gt;&lt;code&gt;testcicd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Docker image 저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;testcicd-prod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;운영 EC2 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 IAM Role&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TestCicdEc2Role&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;EC2가 사용하는 IAM Role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions IAM Role&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TestCicdGitHubActionsRole&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GitHub Actions가 OIDC로 Assume하는 Role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions IAM Policy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TestCicdGitHubActionsPolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ECR Push + SSM 권한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Group&lt;/td&gt;
&lt;td&gt;&lt;code&gt;testcicd-prod-sg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;EC2 보안 그룹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;testcicd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서버에서 실행할 Docker container 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application Port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;컨테이너 내부 애플리케이션 포트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host Bind&lt;/td&gt;
&lt;td&gt;&lt;code&gt;127.0.0.1:8000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nginx만 애플리케이션에 접근하도록 localhost에 바인딩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health Endpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;애플리케이션 상태 확인 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DuckDNS Domain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;testcicd.duckdns.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실제 생성한 값으로 교체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment Branch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CD가 실행되는 브랜치&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요:&lt;/b&gt; &lt;code&gt;/health&lt;/code&gt;는 예시다. 기존 애플리케이션에 &lt;code&gt;/api/config&lt;/code&gt;, &lt;code&gt;/healthz&lt;/code&gt;, &lt;code&gt;/api/health&lt;/code&gt; 등 이미 안정적으로 &lt;code&gt;200 OK&lt;/code&gt;를 반환하는 endpoint가 있다면 그 경로를 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-2&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-2&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;2. 사전 준비사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 계정/도구:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS 계정&lt;/li&gt;
&lt;li&gt;GitHub repository&lt;/li&gt;
&lt;li&gt;DuckDNS 계정&lt;/li&gt;
&lt;li&gt;로컬 Git&lt;/li&gt;
&lt;li&gt;로컬 Docker Desktop 또는 Docker Engine&lt;/li&gt;
&lt;li&gt;배포할 애플리케이션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션은 최소한 다음 조건을 만족해야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Docker container 안에서 실행 가능해야 한다.&lt;/li&gt;
&lt;li&gt;외부에서 접근할 포트를 지정할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;컨테이너 내부에서 &lt;code&gt;0.0.0.0&lt;/code&gt;으로 bind해야 한다.&lt;/li&gt;
&lt;li&gt;health check에 사용할 HTTP endpoint가 있어야 한다.&lt;/li&gt;
&lt;li&gt;서버 시작에 시간이 필요한 경우 health check가 충분히 기다리도록 구성해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 애플리케이션이 컨테이너 내부에서 &lt;code&gt;127.0.0.1:8000&lt;/code&gt;만 listen하면 Docker port forwarding을 해도 외부에서 접근이 안 될 수 있다. 컨테이너 내부 서버는 일반적으로 다음과 같이 bind한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block accesslog&quot;&gt;&lt;code&gt;0.0.0.0:8000
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-3&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-3&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;3. Repository에 필요한 파일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 repository에는 최소 다음 파일이 존재한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block jboss-cli&quot;&gt;&lt;code&gt;project-root/
├── Dockerfile
├── .dockerignore
├── requirements.txt          # Python 프로젝트인 경우
├── ...                       # 애플리케이션 코드/리소스
└── .github/
    └── workflows/
        └── cicd.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에는 다음 파일을 둔다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block jboss-cli&quot;&gt;&lt;code&gt;/opt/testcicd/
└── deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx에는 다음 파일을 둔다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block awk&quot;&gt;&lt;code&gt;/etc/nginx/conf.d/testcicd.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-4&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-4&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;4. Dockerfile 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Python 서버의 중립적인 예시다. 프로젝트의 Python 버전과 실행 명령이 다르면 수정한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dockerfile&quot;&gt;&lt;code&gt;FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

RUN apt-get update \
    &amp;amp;&amp;amp; apt-get install -y --no-install-recommends libgomp1 \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

COPY requirements.txt .

RUN python -m pip install --upgrade pip \
    &amp;amp;&amp;amp; python -m pip install -r requirements.txt

COPY . .

EXPOSE 8000

HEALTHCHECK \
    --interval=30s \
    --timeout=10s \
    --start-period=90s \
    --retries=3 \
    CMD python -c &quot;import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=10)&quot;

CMD [&quot;python&quot;, &quot;server.py&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 맞게 반드시 확인할 부분:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;FROM python:3.14-slim
requirements.txt
/health
server.py
--host 0.0.0.0
--port 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 다른 프레임워크라면 마지막 &lt;code&gt;CMD&lt;/code&gt;만 해당 프로젝트 실행 명령으로 교체하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dockerfile&quot;&gt;&lt;code&gt;CMD [&quot;python&quot;, &quot;app.py&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dockerfile&quot;&gt;&lt;code&gt;CMD [&quot;uvicorn&quot;, &quot;app:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-5&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-5&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;5. &lt;code&gt;.dockerignore&lt;/code&gt; 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;.git
.github

__pycache__
*.pyc
*.pyo
*.pyd

.venv
venv

.vscode
.idea

.DS_Store

results
cache
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;주의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행에 필요한 파일은 &lt;code&gt;.dockerignore&lt;/code&gt;에 넣으면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 런타임에 다음 파일이 필요하다면 Docker image 안에 포함되어야 한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block arduino&quot;&gt;&lt;code&gt;models/
data/processed/
config/
static/
templates/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker build는 성공했는데 실제 container가 &lt;code&gt;FileNotFoundError&lt;/code&gt; 등으로 즉시 종료되는 경우 &lt;code&gt;.dockerignore&lt;/code&gt; 때문에 런타임 파일이 제외되지 않았는지 먼저 확인한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-6&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-6&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;6. AWS로 넘어가기 전에 로컬 Docker 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block mercury&quot;&gt;&lt;code&gt;docker build -t testcicd:local .
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공하면:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block applescript&quot;&gt;&lt;code&gt;docker run --rm \
  --name testcicd-local \
  -p 8000:8000 \
  testcicd:local
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 터미널에서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl http://127.0.0.1:8000/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 기준:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 200&lt;/li&gt;
&lt;li&gt;health endpoint가 정상 응답&lt;/li&gt;
&lt;li&gt;container가 종료되지 않음&lt;/li&gt;
&lt;li&gt;브라우저가 필요한 애플리케이션이면 &lt;code&gt;http://localhost:8000&lt;/code&gt; 접속 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계가 실패하면 AWS 설정으로 넘어가지 않는다. 먼저 Docker/application 문제를 해결한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-7&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-7&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;7. AWS Region 고정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가이드에서는 서울 리전을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;ap-northeast-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 다음 항목을 모두 같은 리전에 생성한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ECR&lt;/li&gt;
&lt;li&gt;EC2&lt;/li&gt;
&lt;li&gt;Elastic IP&lt;/li&gt;
&lt;li&gt;관련 SSM 대상&lt;/li&gt;
&lt;li&gt;ECR ARN&lt;/li&gt;
&lt;li&gt;IAM Policy 내부 리전 문자열&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM 자체는 글로벌 서비스처럼 보이지만 정책 안의 ECR/EC2/SSM ARN에는 리전이 포함될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-8&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-8&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;8. Amazon ECR Repository 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block gradle&quot;&gt;&lt;code&gt;Amazon ECR
&amp;rarr; Private registry
&amp;rarr; Repositories
&amp;rarr; Create repository
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block delphi&quot;&gt;&lt;code&gt;Repository type: Private
Repository name: testcicd
Region: ap-northeast-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Image tag mutability는 단순한 CI/CD 예제에서는 &lt;code&gt;Mutable&lt;/code&gt;로 시작해도 된다. commit SHA를 image tag로 사용하는 경우 동일 SHA를 다시 push할 수 있는지 운영 정책에 맞게 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Encryption은 특별한 요구가 없다면 기본 AES-256 설정을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 후 Repository URI를 기록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형식:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block stylus&quot;&gt;&lt;code&gt;&amp;lt;AWS_ACCOUNT_ID&amp;gt;.dkr.ecr.ap-northeast-2.amazonaws.com/testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block stylus&quot;&gt;&lt;code&gt;123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 URI는 이후 GitHub Actions와 EC2 배포에서 사용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-9&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-9&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;9. EC2용 IAM Role 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Role은 &lt;b&gt;EC2 자신이 사용하는 Role&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block pgsql&quot;&gt;&lt;code&gt;IAM
&amp;rarr; Roles
&amp;rarr; Create role
&amp;rarr; Trusted entity type: AWS service
&amp;rarr; Use case: EC2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Role 이름:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block nginx&quot;&gt;&lt;code&gt;TestCicdEc2Role
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 AWS Managed Policy 두 개를 연결한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block nginx&quot;&gt;&lt;code&gt;AmazonSSMManagedInstanceCore
AmazonEC2ContainerRegistryPullOnly
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block arduino&quot;&gt;&lt;code&gt;AmazonSSMManagedInstanceCore
&amp;rarr; SSH 없이 Session Manager/SSM 사용

AmazonEC2ContainerRegistryPullOnly
&amp;rarr; EC2가 ECR의 Docker image를 pull
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AdministratorAccess&lt;/code&gt;, ECR FullAccess 등 불필요하게 넓은 권한은 사용하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 상태:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block nginx&quot;&gt;&lt;code&gt;TestCicdEc2Role
├── AmazonSSMManagedInstanceCore
└── AmazonEC2ContainerRegistryPullOnly
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-10&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-10&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;10. EC2 Instance 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block mipsasm&quot;&gt;&lt;code&gt;EC2
&amp;rarr; Instances
&amp;rarr; Launch instances
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 설정:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;Name: testcicd-prod
AMI: Amazon Linux 2023
Architecture: 64-bit (x86_64)
Instance type: m7i-flex.large
Storage: 30 GiB gp3
IAM instance profile: TestCicdEc2Role
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Key pair&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSM Session Manager만 사용할 경우:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block livecodeserver&quot;&gt;&lt;code&gt;Proceed without a key pair
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가이드에서는 SSH를 사용하지 않는다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Instance type&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시에서는:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;m7i-flex.large
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계정의 무료 플랜, 크레딧, 프로모션, 리전별 제공 여부는 시점과 계정에 따라 달라질 수 있으므로 생성 화면의 과금 정보를 반드시 확인한다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Storage&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작값:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block basic&quot;&gt;&lt;code&gt;30 GiB gp3
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker image가 매우 크거나 여러 버전을 오래 보관해야 하면 더 크게 잡는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-11&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-11&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;11. Security Group 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group 이름 예시:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;testcicd-prod-sg
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 필요한 inbound:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;HTTP
TCP 80
Source: 0.0.0.0/0

HTTPS
TCP 443
Source: 0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 22는 열지 않는다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;SSH / TCP 22 &amp;rarr; 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IPv6를 실제로 사용한다면 별도로 &lt;code&gt;::/0&lt;/code&gt; 규칙과 VPC IPv6 구성을 검토한다. IPv4만 사용할 경우 굳이 추가하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbound는 초기에는 기본값을 유지한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block accesslog&quot;&gt;&lt;code&gt;All traffic
0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSM, ECR, package repository, 인증서 발급 등의 외부 통신에 outbound HTTPS가 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-12&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-12&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;12. Public Subnet / Internet Gateway 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 직접 접근할 EC2라면 subnet의 route table에 다음과 같은 경로가 있어야 한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block yaml&quot;&gt;&lt;code&gt;Destination: 0.0.0.0/0
Target: igw-xxxxxxxxxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block accesslog&quot;&gt;&lt;code&gt;0.0.0.0/0
   &amp;darr;
Internet Gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 접속이 안 될 때 Security Group만 확인하고 끝내지 말고 다음도 점검한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Subnet route table&lt;/li&gt;
&lt;li&gt;Internet Gateway 연결&lt;/li&gt;
&lt;li&gt;Network ACL&lt;/li&gt;
&lt;li&gt;Public IPv4 / Elastic IP&lt;/li&gt;
&lt;li&gt;실제 EC2에 연결된 Security Group&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-13&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-13&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;13. Elastic IP 생성 및 EC2에 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;EC2
&amp;rarr; Network &amp;amp; Security
&amp;rarr; Elastic IP addresses
&amp;rarr; Allocate Elastic IP address
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 후:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;Actions
&amp;rarr; Associate Elastic IP address
&amp;rarr; Resource type: Instance
&amp;rarr; Instance: testcicd-prod
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic IP 예시:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;54.xxx.xxx.xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 IP는 이후 DuckDNS에 등록한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Public IPv4/Elastic IP 과금 정책은 변경될 수 있다. 사용 전 현재 AWS 가격표를 확인하고, 프로젝트 종료 시 사용하지 않는 Elastic IP는 반드시 Release한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-14&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-14&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;14. SSM Session Manager 접속 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block pgsql&quot;&gt;&lt;code&gt;EC2
&amp;rarr; Instances
&amp;rarr; testcicd-prod
&amp;rarr; Connect
&amp;rarr; Session Manager
&amp;rarr; Connect
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접속되면:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;whoami
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 다음 계정으로 접속된다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block crmsh&quot;&gt;&lt;code&gt;ssm-user
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSM Agent 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block lua&quot;&gt;&lt;code&gt;sudo systemctl status amazon-ssm-agent --no-pager
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block applescript&quot;&gt;&lt;code&gt;Active: active (running)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS CLI 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ada&quot;&gt;&lt;code&gt;aws --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리전 확인이 필요하면 EC2 metadata를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dts&quot;&gt;&lt;code&gt;TOKEN=$(curl -sS -X PUT \
  -H &quot;X-aws-ec2-metadata-token-ttl-seconds: 60&quot; \
  http://169.254.169.254/latest/api/token)

curl -sS \
  -H &quot;X-aws-ec2-metadata-token: $TOKEN&quot; \
  http://169.254.169.254/latest/meta-data/placement/region
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대값:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;ap-northeast-2
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-15&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-15&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;15. EC2에 Docker 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 패키지를 업데이트한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;sudo dnf update -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Amazon Linux 2023의 curl 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon Linux 2023에서는 &lt;code&gt;curl-minimal&lt;/code&gt;이 이미 설치된 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ada&quot;&gt;&lt;code&gt;curl --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 출력이 나오면 &lt;b&gt;별도로 &lt;code&gt;curl&lt;/code&gt; 패키지를 설치하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 설치하면 환경에 따라 &lt;code&gt;curl-minimal&lt;/code&gt;과 &lt;code&gt;curl&lt;/code&gt; 충돌이 날 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block cmake&quot;&gt;&lt;code&gt;sudo dnf install -y docker curl
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 가이드에서는 Docker만 설치한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block cmake&quot;&gt;&lt;code&gt;sudo dnf install -y docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 활성화:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block pgsql&quot;&gt;&lt;code&gt;sudo systemctl enable docker
sudo systemctl start docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block pgsql&quot;&gt;&lt;code&gt;sudo systemctl enable --now docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block properties&quot;&gt;&lt;code&gt;sudo systemctl status docker --no-pager
sudo docker version
sudo docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker ps&lt;/code&gt;가 container 없이 header만 출력해도 정상이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-16&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-16&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;16. EC2 AWS CLI가 없는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 먼저 다음을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ada&quot;&gt;&lt;code&gt;aws --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상이라면 이 절은 건너뛴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 없는 경우 AWS CLI v2 공식 설치 절차를 사용한다. x86_64 예시:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block properties&quot;&gt;&lt;code&gt;sudo dnf install -y unzip

curl &quot;https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip&quot; \
  -o /tmp/awscliv2.zip

cd /tmp
unzip awscliv2.zip
sudo ./aws/install

aws --version
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-17&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-17&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;17. 최종형 &lt;code&gt;deploy.sh&lt;/code&gt; 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 Nginx를 앞에 둘 것을 고려하여 Docker container는 외부 &lt;code&gt;0.0.0.0:80&lt;/code&gt;에 직접 노출하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 구성:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;Nginx :80/:443
   &amp;darr;
127.0.0.1:8000
   &amp;darr;
Docker container :8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디렉터리 생성:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block vim&quot;&gt;&lt;code&gt;sudo mkdir -p /opt/testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 생성:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block bash&quot;&gt;&lt;code&gt;sudo tee /opt/testcicd/deploy.sh &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'
#!/usr/bin/env bash

set -Eeuo pipefail

IMAGE_URI=&quot;${1:?IMAGE_URI is required}&quot;
HEALTH_PATH=&quot;${2:-/health}&quot;

AWS_REGION=&quot;ap-northeast-2&quot;
CONTAINER_NAME=&quot;testcicd&quot;
HOST_IP=&quot;127.0.0.1&quot;
HOST_PORT=&quot;8000&quot;
CONTAINER_PORT=&quot;8000&quot;

REGISTRY=&quot;${IMAGE_URI%%/*}&quot;

health_check() {
    curl \
      --fail \
      --silent \
      --show-error \
      &quot;http://${HOST_IP}:${HOST_PORT}${HEALTH_PATH}&quot; \
      &amp;gt;/dev/null 2&amp;gt;&amp;amp;1
}

echo &quot;========================================&quot;
echo &quot;Application deployment&quot;
echo &quot;Image: ${IMAGE_URI}&quot;
echo &quot;Health path: ${HEALTH_PATH}&quot;
echo &quot;========================================&quot;

echo &quot;[1/6] Logging in to Amazon ECR...&quot;

aws ecr get-login-password \
  --region &quot;${AWS_REGION}&quot; \
  | docker login \
      --username AWS \
      --password-stdin &quot;${REGISTRY}&quot;

echo &quot;[2/6] Pulling new image...&quot;

docker pull &quot;${IMAGE_URI}&quot;

OLD_IMAGE=&quot;&quot;

if docker inspect &quot;${CONTAINER_NAME}&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
    OLD_IMAGE=&quot;$(docker inspect \
      --format='{{.Config.Image}}' \
      &quot;${CONTAINER_NAME}&quot;)&quot;

    echo &quot;Previous image: ${OLD_IMAGE}&quot;
fi

echo &quot;[3/6] Removing previous container...&quot;

docker rm -f &quot;${CONTAINER_NAME}&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true

echo &quot;[4/6] Starting new container...&quot;

docker run -d \
  --name &quot;${CONTAINER_NAME}&quot; \
  --restart unless-stopped \
  -p &quot;${HOST_IP}:${HOST_PORT}:${CONTAINER_PORT}&quot; \
  &quot;${IMAGE_URI}&quot;

echo &quot;[5/6] Waiting for health check...&quot;

HEALTHY=0

for i in $(seq 1 90); do
    if health_check; then
        HEALTHY=1
        break
    fi

    if ! docker ps \
        --format '{{.Names}}' \
        | grep -qx &quot;${CONTAINER_NAME}&quot;; then
        echo &quot;Container stopped unexpectedly.&quot;
        break
    fi

    echo &quot;Waiting... (${i}/90)&quot;
    sleep 2
done

if [ &quot;${HEALTHY}&quot; -ne 1 ]; then
    echo &quot;========================================&quot;
    echo &quot;Deployment health check FAILED&quot;
    echo &quot;========================================&quot;

    docker logs --tail 200 &quot;${CONTAINER_NAME}&quot; || true
    docker rm -f &quot;${CONTAINER_NAME}&quot; || true

    if [ -n &quot;${OLD_IMAGE}&quot; ]; then
        echo &quot;Rolling back to ${OLD_IMAGE}...&quot;

        docker run -d \
          --name &quot;${CONTAINER_NAME}&quot; \
          --restart unless-stopped \
          -p &quot;${HOST_IP}:${HOST_PORT}:${CONTAINER_PORT}&quot; \
          &quot;${OLD_IMAGE}&quot;

        ROLLBACK_HEALTHY=0

        for i in $(seq 1 30); do
            if health_check; then
                ROLLBACK_HEALTHY=1
                break
            fi

            sleep 2
        done

        if [ &quot;${ROLLBACK_HEALTHY}&quot; -eq 1 ]; then
            echo &quot;Rollback successful.&quot;
        else
            echo &quot;Rollback health check also failed.&quot;
        fi
    fi

    exit 1
fi

echo &quot;[6/6] Deployment successful.&quot;

docker image prune -f || true

echo &quot;========================================&quot;
echo &quot;Deployment completed.&quot;
echo &quot;========================================&quot;
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 권한:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block vim&quot;&gt;&lt;code&gt;sudo chmod 755 /opt/testcicd/deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block vim&quot;&gt;&lt;code&gt;ls -l /opt/testcicd/deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상 형태:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block jboss-cli&quot;&gt;&lt;code&gt;-rwxr-xr-x ... /opt/testcicd/deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bash 문법 검사:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block armasm&quot;&gt;&lt;code&gt;sudo bash -n /opt/testcicd/deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무 출력이 없으면 문법상 정상이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-18&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-18&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;18. &lt;code&gt;deploy.sh&lt;/code&gt; 동작 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block gcode&quot;&gt;&lt;code&gt;IMAGE_URI
HEALTH_PATH (선택, 기본 /health)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;sudo /opt/testcicd/deploy.sh \
  123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/testcicd:abc123 \
  /health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;ECR 로그인
   &amp;darr;
새 image pull
   &amp;darr;
현재 container의 old image URI 기록
   &amp;darr;
기존 container 제거
   &amp;darr;
새 container 실행
   &amp;darr;
127.0.0.1:8000/health 반복 확인
   &amp;darr;
성공 &amp;rarr; 완료
실패 &amp;rarr; 신규 container 제거 &amp;rarr; old image 재실행 &amp;rarr; rollback
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker publish는 다음 형태다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block accesslog&quot;&gt;&lt;code&gt;127.0.0.1:8000 -&amp;gt; container:8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 외부에서 EC2의 &lt;code&gt;:8000&lt;/code&gt;으로 직접 접근할 수 없고 Nginx만 접근한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-19&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-19&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;19. GitHub Actions용 AWS OIDC Provider 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 GitHub에 장기 AWS Access Key를 저장하지 않는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 secret을 만들지 않는다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block nginx&quot;&gt;&lt;code&gt;AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 GitHub OIDC &amp;rarr; AWS STS &amp;rarr; 임시 credential을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;IAM
&amp;rarr; Identity providers
&amp;rarr; Add provider
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;Provider type: OpenID Connect
Provider URL: https://token.actions.githubusercontent.com
Audience: sts.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 후 provider 목록에 다음이 보여야 한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;token.actions.githubusercontent.com
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 AWS 계정에 GitHub OIDC Provider가 이미 있다면 같은 provider를 중복 생성할 필요가 없다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-20&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-20&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;20. GitHub OIDC &lt;code&gt;sub&lt;/code&gt; 형식 확인 &amp;mdash; 매우 중요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub repository 생성 시점에 따라 OIDC subject(&lt;code&gt;sub&lt;/code&gt;) 형식이 다를 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;2026-07-15 이후 생성된 repository&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub의 immutable subject claim 기본 형식을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형식:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dts&quot;&gt;&lt;code&gt;repo:&amp;lt;OWNER&amp;gt;@&amp;lt;OWNER_ID&amp;gt;/&amp;lt;REPOSITORY&amp;gt;@&amp;lt;REPOSITORY_ID&amp;gt;:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block less&quot;&gt;&lt;code&gt;repo:my-org@123456789/testcicd@987654321:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;2026-07-15 이전 repository&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;immutable subject claims를 별도로 opt-in하지 않았다면 일반적으로 다음 legacy 형식을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dts&quot;&gt;&lt;code&gt;repo:&amp;lt;OWNER&amp;gt;/&amp;lt;REPOSITORY&amp;gt;:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block less&quot;&gt;&lt;code&gt;repo:my-org/testcicd:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Repository ID / Owner ID / created_at 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block vim&quot;&gt;&lt;code&gt;https://api.github.com/repos/&amp;lt;OWNER&amp;gt;/&amp;lt;REPOSITORY&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block crystal&quot;&gt;&lt;code&gt;https://api.github.com/repos/my-org/testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON에서 다음 값을 찾는다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block jboss-cli&quot;&gt;&lt;code&gt;&quot;id&quot;: ...            &amp;larr; repository ID

&quot;owner&quot;: {
  &quot;id&quot;: ...          &amp;larr; owner ID
}

&quot;created_at&quot;: &quot;...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trust Policy를 만들기 전에 반드시 실제 repository가 사용하는 subject 형식을 확인한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Environment를 workflow에 추가하면 OIDC &lt;code&gt;sub&lt;/code&gt; 형식이 달라질 수 있다. 이 가이드는 GitHub Environment를 사용하지 않고 &lt;code&gt;main&lt;/code&gt; branch ref를 직접 제한하는 구성을 기준으로 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-21&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-21&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;21. GitHub Actions용 IAM Role 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;IAM
&amp;rarr; Roles
&amp;rarr; Create role
&amp;rarr; Custom trust policy
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Role 이름:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;TestCicdGitHubActionsRole
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;2026-07-15 이후 생성 repository용 Trust Policy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 값을 교체한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block apache&quot;&gt;&lt;code&gt;&amp;lt;AWS_ACCOUNT_ID&amp;gt;
&amp;lt;OWNER&amp;gt;
&amp;lt;OWNER_ID&amp;gt;
&amp;lt;REPOSITORY&amp;gt;
&amp;lt;REPOSITORY_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block json&quot;&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;Federated&quot;: &quot;arn:aws:iam::&amp;lt;AWS_ACCOUNT_ID&amp;gt;:oidc-provider/token.actions.githubusercontent.com&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;token.actions.githubusercontent.com:aud&quot;: &quot;sts.amazonaws.com&quot;,
          &quot;token.actions.githubusercontent.com:sub&quot;: &quot;repo:&amp;lt;OWNER&amp;gt;@&amp;lt;OWNER_ID&amp;gt;/&amp;lt;REPOSITORY&amp;gt;@&amp;lt;REPOSITORY_ID&amp;gt;:ref:refs/heads/main&quot;
        }
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 형태:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block less&quot;&gt;&lt;code&gt;repo:my-org@123456789/testcicd@987654321:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Legacy subject를 사용하는 repository용 Trust Policy&lt;/h3&gt;
&lt;pre class=&quot;sg-code-block json&quot;&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;Federated&quot;: &quot;arn:aws:iam::&amp;lt;AWS_ACCOUNT_ID&amp;gt;:oidc-provider/token.actions.githubusercontent.com&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;token.actions.githubusercontent.com:aud&quot;: &quot;sts.amazonaws.com&quot;,
          &quot;token.actions.githubusercontent.com:sub&quot;: &quot;repo:&amp;lt;OWNER&amp;gt;/&amp;lt;REPOSITORY&amp;gt;:ref:refs/heads/main&quot;
        }
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 &lt;code&gt;AdministratorAccess&lt;/code&gt; 같은 policy를 붙이지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-22&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-22&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;22. GitHub Actions Role에 최소 권한 IAM Policy 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 준비할 값:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;AWS Account ID: 12자리 숫자
EC2 Instance ID: i-xxxxxxxxxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 Instance ID 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;EC2
&amp;rarr; Instances
&amp;rarr; testcicd-prod
&amp;rarr; Instance ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;IAM
&amp;rarr; Roles
&amp;rarr; TestCicdGitHubActionsRole
&amp;rarr; Permissions
&amp;rarr; Add permissions
&amp;rarr; Create inline policy
&amp;rarr; JSON
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에서 다음 값을 실제 값으로 바꾼다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block apache&quot;&gt;&lt;code&gt;&amp;lt;AWS_ACCOUNT_ID&amp;gt;
&amp;lt;EC2_INSTANCE_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Policy:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block json&quot;&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Sid&quot;: &quot;EcrAuthentication&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;ecr:GetAuthorizationToken&quot;
      ],
      &quot;Resource&quot;: &quot;*&quot;
    },
    {
      &quot;Sid&quot;: &quot;PushApplicationImage&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;ecr:BatchCheckLayerAvailability&quot;,
        &quot;ecr:CompleteLayerUpload&quot;,
        &quot;ecr:InitiateLayerUpload&quot;,
        &quot;ecr:PutImage&quot;,
        &quot;ecr:UploadLayerPart&quot;,
        &quot;ecr:BatchGetImage&quot;
      ],
      &quot;Resource&quot;: &quot;arn:aws:ecr:ap-northeast-2:&amp;lt;AWS_ACCOUNT_ID&amp;gt;:repository/testcicd&quot;
    },
    {
      &quot;Sid&quot;: &quot;DeployToApplicationInstance&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;ssm:SendCommand&quot;
      ],
      &quot;Resource&quot;: [
        &quot;arn:aws:ssm:ap-northeast-2::document/AWS-RunShellScript&quot;,
        &quot;arn:aws:ec2:ap-northeast-2:&amp;lt;AWS_ACCOUNT_ID&amp;gt;:instance/&amp;lt;EC2_INSTANCE_ID&amp;gt;&quot;
      ]
    },
    {
      &quot;Sid&quot;: &quot;ReadDeploymentResult&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;ssm:GetCommandInvocation&quot;
      ],
      &quot;Resource&quot;: &quot;*&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Policy 이름:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;TestCicdGitHubActionsPolicy
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 구조:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block armasm&quot;&gt;&lt;code&gt;TestCicdGitHubActionsRole
├── Trust relationship
│   └── GitHub OIDC + 특정 repository/main branch 제한
│
└── TestCicdGitHubActionsPolicy
    ├── ECR authorization
    ├── testcicd ECR push
    ├── 특정 EC2에 SSM SendCommand
    └── SSM command result 조회
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-23&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-23&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;23. GitHub Repository Variables 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub repository:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block gams&quot;&gt;&lt;code&gt;Settings
&amp;rarr; Secrets and variables
&amp;rarr; Actions
&amp;rarr; Variables
&amp;rarr; New repository variable
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개를 만든다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;AWS_ROLE_ARN&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block elixir&quot;&gt;&lt;code&gt;arn:aws:iam::&amp;lt;AWS_ACCOUNT_ID&amp;gt;:role/TestCicdGitHubActionsRole
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;EC2_INSTANCE_ID&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;i-xxxxxxxxxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block nginx&quot;&gt;&lt;code&gt;AWS_ROLE_ARN
EC2_INSTANCE_ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장기 AWS key는 등록하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block properties&quot;&gt;&lt;code&gt;AWS_ACCESS_KEY_ID        &amp;rarr; 만들지 않음
AWS_SECRET_ACCESS_KEY    &amp;rarr; 만들지 않음
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-24&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-24&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;24. GitHub Actions Workflow 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block asciidoc&quot;&gt;&lt;code&gt;.github/workflows/cicd.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block arduino&quot;&gt;&lt;code&gt;mkdir -p .github/workflows
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 workflow:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block http&quot;&gt;&lt;code&gt;name: Application CI/CD

on:
  pull_request:
    branches:
      - main

  push:
    branches:
      - main

permissions:
  contents: read
  id-token: write

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: testcicd
  HEALTH_PATH: /health

jobs:
  build-test-deploy:
    name: Build, Test and Deploy
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v7

      # ----------------------------------------------------
      # CI
      # ----------------------------------------------------

      - name: Build Docker image
        run: |
          docker build \
            -t testcicd:${GITHUB_SHA} \
            .

      - name: Start smoke-test container
        run: |
          docker run -d \
            --name testcicd-test \
            -p 18000:8000 \
            testcicd:${GITHUB_SHA}

      - name: Smoke test
        run: |
          SUCCESS=0

          for i in $(seq 1 90); do
            if curl \
              --fail \
              --silent \
              &quot;http://127.0.0.1:18000${HEALTH_PATH}&quot; \
              &amp;gt;/dev/null; then

              SUCCESS=1
              break
            fi

            echo &quot;Waiting for application... (${i}/90)&quot;
            sleep 2
          done

          if [ &quot;${SUCCESS}&quot; -ne 1 ]; then
            echo &quot;Smoke test failed.&quot;
            docker logs testcicd-test
            exit 1
          fi

          echo &quot;Smoke test passed.&quot;

      - name: Stop smoke-test container
        if: always()
        run: |
          docker rm -f testcicd-test || true

      # ----------------------------------------------------
      # CD
      # main branch push에서만 실행
      # ----------------------------------------------------

      - name: Configure AWS credentials
        if: github.event_name == 'push'
        uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        if: github.event_name == 'push'
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Push Docker image to ECR
        if: github.event_name == 'push'
        id: push-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          IMAGE_URI=&quot;${ECR_REGISTRY}/${ECR_REPOSITORY}:${GITHUB_SHA}&quot;

          docker tag \
            testcicd:${GITHUB_SHA} \
            &quot;${IMAGE_URI}&quot;

          docker push &quot;${IMAGE_URI}&quot;

          echo &quot;image_uri=${IMAGE_URI}&quot; &amp;gt;&amp;gt; &quot;${GITHUB_OUTPUT}&quot;

          echo &quot;Pushed image:&quot;
          echo &quot;${IMAGE_URI}&quot;

      - name: Deploy to EC2 with SSM
        if: github.event_name == 'push'
        env:
          EC2_INSTANCE_ID: ${{ vars.EC2_INSTANCE_ID }}
          IMAGE_URI: ${{ steps.push-image.outputs.image_uri }}
        run: |
          PARAMETERS=$(jq -nc \
            --arg image &quot;${IMAGE_URI}&quot; \
            --arg health &quot;${HEALTH_PATH}&quot; \
            '{commands:[(&quot;sudo /opt/testcicd/deploy.sh &quot; + $image + &quot; &quot; + $health)]}')

          COMMAND_ID=$(aws ssm send-command \
            --instance-ids &quot;${EC2_INSTANCE_ID}&quot; \
            --document-name &quot;AWS-RunShellScript&quot; \
            --parameters &quot;${PARAMETERS}&quot; \
            --comment &quot;Deploy application ${GITHUB_SHA}&quot; \
            --query 'Command.CommandId' \
            --output text)

          echo &quot;SSM command ID: ${COMMAND_ID}&quot;

          for i in $(seq 1 120); do
            STATUS=$(aws ssm get-command-invocation \
              --command-id &quot;${COMMAND_ID}&quot; \
              --instance-id &quot;${EC2_INSTANCE_ID}&quot; \
              --query 'Status' \
              --output text 2&amp;gt;/dev/null || true)

            echo &quot;Deployment status: ${STATUS}&quot;

            case &quot;${STATUS}&quot; in
              Success)
                aws ssm get-command-invocation \
                  --command-id &quot;${COMMAND_ID}&quot; \
                  --instance-id &quot;${EC2_INSTANCE_ID}&quot; \
                  --query 'StandardOutputContent' \
                  --output text

                echo &quot;Deployment successful.&quot;
                exit 0
                ;;

              Failed|Cancelled|TimedOut|Cancelling)
                echo &quot;Deployment failed.&quot;

                aws ssm get-command-invocation \
                  --command-id &quot;${COMMAND_ID}&quot; \
                  --instance-id &quot;${EC2_INSTANCE_ID}&quot; \
                  --query '{
                    Status:Status,
                    Stdout:StandardOutputContent,
                    Stderr:StandardErrorContent
                  }' \
                  --output json || true

                exit 1
                ;;
            esac

            sleep 3
          done

          echo &quot;Deployment polling timed out.&quot;

          aws ssm get-command-invocation \
            --command-id &quot;${COMMAND_ID}&quot; \
            --instance-id &quot;${EC2_INSTANCE_ID}&quot; \
            --output json || true

          exit 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Workflow에서 반드시 확인할 항목&lt;/h3&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;ECR_REPOSITORY: testcicd
HEALTH_PATH: /health
Docker container port: 8000
/opt/testcicd/deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 상황이 다르면 일관되게 수정한다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;GitHub Action 버전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성 시점 예시는 다음 major를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;actions/checkout@v7
aws-actions/configure-aws-credentials@v6
aws-actions/amazon-ecr-login@v2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions는 시간이 지나며 major version이 변경될 수 있으므로 새 구축 시 공식 repository release를 다시 확인한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-25&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-25&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;25. 왜 Docker image tag를 &lt;code&gt;latest&lt;/code&gt;가 아닌 commit SHA로 사용하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions에서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block awk&quot;&gt;&lt;code&gt;${GITHUB_SHA}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 image tag로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block lasso&quot;&gt;&lt;code&gt;...amazonaws.com/testcicd:4f819aa...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block arduino&quot;&gt;&lt;code&gt;Commit A &amp;rarr; image:A
Commit B &amp;rarr; image:B
Commit C &amp;rarr; image:C
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 commit이 어느 image인지 명확하게 추적할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 현재 &lt;code&gt;deploy.sh&lt;/code&gt;는 기존 container가 사용하던 image URI를 저장하므로 새 버전 health check 실패 시 이전 image로 rollback할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-26&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-26&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;26. 최초 CI 검증 &amp;mdash; 바로 &lt;code&gt;main&lt;/code&gt;에 push하지 않기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 branch를 만든다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block armasm&quot;&gt;&lt;code&gt;git checkout -b chore/cicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 파일:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block asciidoc&quot;&gt;&lt;code&gt;Dockerfile
.dockerignore
.github/workflows/cicd.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dockerfile&quot;&gt;&lt;code&gt;git add Dockerfile
git add .dockerignore
git add .github/workflows/cicd.yml

git commit -m &quot;chore: add EC2 CI/CD pipeline&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Push:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block maxima&quot;&gt;&lt;code&gt;git push -u origin chore/cicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub에서 Pull Request를 만든다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block&quot;&gt;&lt;code&gt;chore/cicd &amp;rarr; main
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR에서는 CI까지만 성공하는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block sql&quot;&gt;&lt;code&gt;Checkout repository           ✅
Build Docker image            ✅
Start smoke-test container    ✅
Smoke test                    ✅
Stop smoke-test container     ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR에서 다음 CD 단계가 실행되지 않는 것이 정상이다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block pgsql&quot;&gt;&lt;code&gt;Configure AWS credentials     skipped
Login to Amazon ECR           skipped
Push Docker image to ECR      skipped
Deploy to EC2 with SSM        skipped
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI가 실패한 상태에서 merge하지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-27&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-27&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;27. PR Merge 후 최초 CD 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI가 성공하면 PR을 &lt;code&gt;main&lt;/code&gt;에 merge한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main&lt;/code&gt; push가 발생하면 workflow가 다시 실행되고 이번에는 CD 단계까지 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 순서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;Checkout
   &amp;darr;
Docker build
   &amp;darr;
Smoke test
   &amp;darr;
GitHub OIDC
   &amp;darr;
AWS IAM Role Assume
   &amp;darr;
ECR Login
   &amp;darr;
Image Push
   &amp;darr;
SSM SendCommand
   &amp;darr;
EC2 deploy.sh
   &amp;darr;
ECR Pull
   &amp;darr;
Docker Run
   &amp;darr;
Health Check
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions 화면에서 전 단계가 초록색인지 확인한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-28&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-28&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;28. ECR Push 결과 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Console:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block gradle&quot;&gt;&lt;code&gt;ECR
&amp;rarr; Repositories
&amp;rarr; testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;image가 생성되어 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tag는 &lt;code&gt;latest&lt;/code&gt;가 아니라 GitHub commit SHA 형태여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;4f819aa3...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-29&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-29&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;29. EC2에서 최초 배포 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session Manager로 접속:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;sudo docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;CONTAINER ID   IMAGE                                  STATUS   PORTS
xxxx           .../testcicd:&amp;lt;commit-sha&amp;gt;              Up       127.0.0.1:8000-&amp;gt;8000/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ada&quot;&gt;&lt;code&gt;sudo docker logs --tail 200 testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 로그:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;sudo docker logs -f testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 health check:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl http://127.0.0.1:8000/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 응답이 나오면 container와 application은 정상이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-30&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-30&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;30. DuckDNS 도메인 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DuckDNS에 로그인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 예시:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 결과:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DuckDNS의 IPv4 입력란에 EC2에 연결한 &lt;b&gt;Elastic IP&lt;/b&gt;를 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;54.xxx.xxx.xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Update IP를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic IP를 사용하므로 IP가 고정되어 있는 동안 일반적인 DDNS cron updater는 필요하지 않다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-31&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-31&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;31. DuckDNS DNS 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;nslookup testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과의 Address가 Elastic IP와 같아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;Name:    testcicd.duckdns.org
Address: 54.xxx.xxx.xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;권한 없는 응답&lt;/code&gt; 또는 &lt;code&gt;Non-authoritative answer&lt;/code&gt;는 그 자체로 오류가 아니다. 현재 사용 중인 resolver가 authoritative DNS가 아니라 캐시/재귀 조회를 통해 응답했다는 뜻일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IPv6를 사용하지 않는다면 AAAA 레코드도 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block elm&quot;&gt;&lt;code&gt;nslookup -type=AAAA testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 IPv6를 구성하지 않았는데 불필요한 AAAA record가 있다면 제거한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-32&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-32&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;32. Nginx + Certbot 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 Session Manager에서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block cmake&quot;&gt;&lt;code&gt;sudo dnf install -y nginx certbot python3-certbot-nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon Linux 2023의 현재 package repository에는 &lt;code&gt;certbot&lt;/code&gt; 및 &lt;code&gt;python3-certbot-nginx&lt;/code&gt; 패키지가 제공되는 릴리스가 있다. 새 환경에서는 &lt;code&gt;dnf&lt;/code&gt;의 실제 결과를 기준으로 확인한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-33&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-33&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;33. Nginx Reverse Proxy 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 생성:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block nginx&quot;&gt;&lt;code&gt;sudo tee /etc/nginx/conf.d/testcicd.conf &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'
server {
    listen 80;
    server_name testcicd.duckdns.org;

    location / {
        proxy_pass http://127.0.0.1:8000;

        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_read_timeout 120s;
    }
}
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 DuckDNS domain으로 &lt;code&gt;server_name&lt;/code&gt;을 교체한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;sudo nginx -t
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 예:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block subunit&quot;&gt;&lt;code&gt;syntax is ok
test is successful
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 실행:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block pgsql&quot;&gt;&lt;code&gt;sudo systemctl enable nginx
sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block pgsql&quot;&gt;&lt;code&gt;sudo systemctl enable --now nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block lua&quot;&gt;&lt;code&gt;sudo systemctl status nginx --no-pager
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-34&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-34&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;34. HTTPS 발급 전에 HTTP 경로 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 환경에 따라 HTTP를 HTTPS로 자동 업그레이드할 수 있다. 인증서가 아직 없을 때는 브라우저보다 &lt;code&gt;curl&lt;/code&gt;로 확인하는 것이 명확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows/macOS/Linux:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl -v http://testcicd.duckdns.org/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 기준:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;DNS &amp;rarr; Elastic IP
TCP 80 연결 성공
HTTP 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP로도 테스트할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl -v http://&amp;lt;ELASTIC_IP&amp;gt;/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인으로 200이 나온다면 다음이 정상이라는 뜻이다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;DuckDNS
&amp;rarr; Elastic IP
&amp;rarr; EC2 Security Group :80
&amp;rarr; Nginx
&amp;rarr; 127.0.0.1:8000
&amp;rarr; Docker application
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-35&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-35&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;35. Let's Encrypt HTTPS 인증서 발급&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 조건이 먼저 충족되어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DuckDNS가 올바른 Elastic IP를 가리킴&lt;/li&gt;
&lt;li&gt;Security Group 80 허용&lt;/li&gt;
&lt;li&gt;Security Group 443 허용&lt;/li&gt;
&lt;li&gt;Nginx 실행 중&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server_name&lt;/code&gt;이 실제 DuckDNS domain과 일치&lt;/li&gt;
&lt;li&gt;외부에서 HTTP 80 접근 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Certbot 실행:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;sudo certbot --nginx \
  -d testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 domain으로 바꾼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Certbot 질문에서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일 입력&lt;/li&gt;
&lt;li&gt;이용약관 동의&lt;/li&gt;
&lt;li&gt;HTTP &amp;rarr; HTTPS redirect 선택 가능 시 redirect 사용 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공 후 Nginx 설정에 인증서 관련 설정과 HTTPS server block이 반영된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-36&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-36&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;36. HTTPS 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;sudo nginx -t
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 PC:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl -I https://testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 health endpoint:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl -v https://testcicd.duckdns.org/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block avrasm&quot;&gt;&lt;code&gt;https://testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 HTTPS 주소로 정상 접속되면 완료다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-37&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-37&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;37. 인증서 자동 갱신 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Certbot 갱신 시뮬레이션:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dockerfile&quot;&gt;&lt;code&gt;sudo certbot renew --dry-run
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 갱신 스케줄이 설치되어 있는지도 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dsconfig&quot;&gt;&lt;code&gt;systemctl list-timers --all | grep -i certbot
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block gradle&quot;&gt;&lt;code&gt;grep -R &quot;certbot renew&quot; /etc/crontab /etc/cron.* 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Certbot 패키지 설치 방식에 따라 systemd timer 또는 cron이 사용될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-38&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-38&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;38. 최종 포트 구조 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;sudo docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker는 다음과 같이 localhost에만 bind되어야 한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block accesslog&quot;&gt;&lt;code&gt;127.0.0.1:8000-&amp;gt;8000/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx listen 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block coq&quot;&gt;&lt;code&gt;sudo ss -lntp | grep -E ':80|:443|:8000'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도한 구조:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block accesslog&quot;&gt;&lt;code&gt;0.0.0.0:80      &amp;rarr; nginx
0.0.0.0:443     &amp;rarr; nginx
127.0.0.1:8000  &amp;rarr; docker-proxy / container
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에 8000 포트를 Security Group으로 열 필요가 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-39&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-39&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;39. 최종 CI/CD 운영 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 개발자가 하는 일은 일반적인 Git workflow뿐이다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block properties&quot;&gt;&lt;code&gt;feature branch
   &amp;darr;
Pull Request
   &amp;darr;
Docker Build
   &amp;darr;
Smoke Test
   &amp;darr;
Merge to main
   &amp;darr;
Docker Build/Test
   &amp;darr;
OIDC AWS Login
   &amp;darr;
ECR Push : GITHUB_SHA
   &amp;darr;
SSM SendCommand
   &amp;darr;
EC2 deploy.sh
   &amp;darr;
Docker Pull/Run
   &amp;darr;
Health Check
   &amp;darr;
Nginx / HTTPS는 그대로 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 새로 배포할 때마다 다시 할 필요가 없는 것:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DuckDNS 재설정&lt;/li&gt;
&lt;li&gt;Elastic IP 재설정&lt;/li&gt;
&lt;li&gt;Nginx 재설치&lt;/li&gt;
&lt;li&gt;HTTPS 인증서 재발급&lt;/li&gt;
&lt;li&gt;IAM Role 재생성&lt;/li&gt;
&lt;li&gt;OIDC Provider 재생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들은 인프라가 유지되는 동안 그대로 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-40&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-40&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;40. 새 코드 배포 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 변경을 feature branch에서 만든다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block armasm&quot;&gt;&lt;code&gt;git checkout -b test/deployment-check
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정/커밋:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block pgsql&quot;&gt;&lt;code&gt;git add .
git commit -m &quot;test: verify deployment pipeline&quot;
git push -u origin test/deployment-check
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR 생성 후:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block stata&quot;&gt;&lt;code&gt;CI 성공 확인
&amp;rarr; main merge
&amp;rarr; CD 성공 확인
&amp;rarr; HTTPS 사이트에 변경사항 반영 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테스트까지 성공하면 CI/CD가 실제 운영 가능한 상태라고 판단할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-41&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-41&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;41. 배포 실패 시 Rollback 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;deploy.sh&lt;/code&gt;는 새 image를 실행하기 전에 기존 container의 image URI를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 version이 health check에 실패하면:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;새 container 제거
   &amp;darr;
이전 image로 container 재실행
   &amp;darr;
rollback health check
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions에서는 배포가 실패로 표시되지만 서비스는 이전 정상 image로 되돌아갈 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block properties&quot;&gt;&lt;code&gt;sudo docker ps
sudo docker logs --tail 200 testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 container image 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block livescript&quot;&gt;&lt;code&gt;sudo docker inspect \
  --format='{{.Config.Image}}' \
  testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-42&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-42&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;42. 자주 발생하는 문제 1 &amp;mdash; Docker build 실패&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block arduino&quot;&gt;&lt;code&gt;Actions
&amp;rarr; workflow run
&amp;rarr; Build Docker image
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 원인:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Python package 버전 충돌&lt;/li&gt;
&lt;li&gt;native system library 부족&lt;/li&gt;
&lt;li&gt;Docker build context에 필요한 파일 없음&lt;/li&gt;
&lt;li&gt;requirements lock 문제&lt;/li&gt;
&lt;li&gt;base image와 architecture 불일치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 로컬에서 동일하게 재현한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block mipsasm&quot;&gt;&lt;code&gt;docker build -t testcicd:debug .
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-43&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-43&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;43. 자주 발생하는 문제 2 &amp;mdash; Container가 즉시 종료됨&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;sudo docker ps -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;sudo docker logs testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 항목:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;runtime file 누락&lt;/li&gt;
&lt;li&gt;환경 변수 누락&lt;/li&gt;
&lt;li&gt;config 파일 누락&lt;/li&gt;
&lt;li&gt;model/data 파일 누락&lt;/li&gt;
&lt;li&gt;잘못된 working directory&lt;/li&gt;
&lt;li&gt;entrypoint 오류&lt;/li&gt;
&lt;li&gt;애플리케이션이 &lt;code&gt;0.0.0.0&lt;/code&gt;이 아니라 &lt;code&gt;127.0.0.1&lt;/code&gt;에만 bind&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-44&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-44&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;44. 자주 발생하는 문제 3 &amp;mdash; &lt;code&gt;curl-minimal&lt;/code&gt; 충돌&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon Linux 2023에서 다음 명령이 실패할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block cmake&quot;&gt;&lt;code&gt;sudo dnf install -y docker curl
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;curl-minimal&lt;/code&gt;이 이미 설치되어 있으면 &lt;code&gt;curl&lt;/code&gt; package와 conflict가 날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ada&quot;&gt;&lt;code&gt;curl --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정상이라면 &lt;code&gt;curl&lt;/code&gt;을 추가 설치하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker만:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block cmake&quot;&gt;&lt;code&gt;sudo dnf install -y docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구성의 HTTP health check에는 기본 curl 기능으로 충분하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-45&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-45&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;45. 자주 발생하는 문제 4 &amp;mdash; SSM Session Manager 접속 불가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 IAM Role 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block ebnf&quot;&gt;&lt;code&gt;AmazonSSMManagedInstanceCore
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSM Agent:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block lua&quot;&gt;&lt;code&gt;sudo systemctl status amazon-ssm-agent --no-pager
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2 outbound HTTPS 443 가능 여부&lt;/li&gt;
&lt;li&gt;Public subnet/Internet Gateway 또는 필요한 VPC endpoint 구성 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Role을 나중에 EC2에 붙였다면 IAM instance profile이 실제 instance에 연결되었는지 확인한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-46&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-46&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;46. 자주 발생하는 문제 5 &amp;mdash; ECR Pull &lt;code&gt;AccessDenied&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 IAM Role에 다음이 있는지 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block nginx&quot;&gt;&lt;code&gt;AmazonEC2ContainerRegistryPullOnly
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에서 ECR auth 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dsconfig&quot;&gt;&lt;code&gt;aws ecr get-login-password \
  --region ap-northeast-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 script가 사용하는 registry/region이 실제 ECR repository와 동일한지도 확인한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-47&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-47&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;47. 자주 발생하는 문제 6 &amp;mdash; GitHub OIDC &lt;code&gt;AssumeRole&lt;/code&gt; 실패&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 Trust Policy의 &lt;code&gt;sub&lt;/code&gt;를 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 repository가 2026-07-15 이후 생성되었다면 다음 immutable 형식 여부를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dts&quot;&gt;&lt;code&gt;repo:&amp;lt;OWNER&amp;gt;@&amp;lt;OWNER_ID&amp;gt;/&amp;lt;REPOSITORY&amp;gt;@&amp;lt;REPOSITORY_ID&amp;gt;:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 항목:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AWS Account ID가 정확한가&lt;/li&gt;
&lt;li&gt;OIDC Provider ARN이 정확한가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aud&lt;/code&gt;가 &lt;code&gt;sts.amazonaws.com&lt;/code&gt;인가&lt;/li&gt;
&lt;li&gt;owner ID가 정확한가&lt;/li&gt;
&lt;li&gt;repository ID가 정확한가&lt;/li&gt;
&lt;li&gt;branch가 정확히 &lt;code&gt;main&lt;/code&gt;인가&lt;/li&gt;
&lt;li&gt;GitHub workflow에 다음 권한이 있는가&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;sg-code-block applescript&quot;&gt;&lt;code&gt;permissions:
  contents: read
  id-token: write
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;8&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;workflow에 GitHub Environment를 추가해 subject 형식이 바뀌지 않았는가&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-48&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-48&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;48. 자주 발생하는 문제 7 &amp;mdash; GitHub Actions는 성공했는데 사이트가 안 열림&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 애플리케이션 문제와 네트워크 문제를 분리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 내부에서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block coq&quot;&gt;&lt;code&gt;curl -v http://127.0.0.1:8000/health
sudo docker ps
sudo ss -lntp | grep -E ':80|:443|:8000'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 health가 200이면 application/container는 정상일 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 외부 PC에서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl -v http://&amp;lt;ELASTIC_IP&amp;gt;/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 설치 후에는:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl -v http://testcicd.duckdns.org/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점검 순서:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;1. Docker container 상태
2. 127.0.0.1:8000 health
3. Nginx 상태
4. Nginx config
5. Security Group 80/443
6. Elastic IP 연결
7. Public subnet route
8. Internet Gateway
9. Network ACL
10. DuckDNS A record
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-49&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-49&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;49. 자주 발생하는 문제 8 &amp;mdash; 브라우저가 HTTP를 HTTPS로 자동 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI에서는:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl -v http://testcicd.duckdns.org/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 정상인데 브라우저에서는 자동으로:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block avrasm&quot;&gt;&lt;code&gt;https://testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 바뀔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서를 아직 설정하지 않은 단계라면 브라우저보다 &lt;code&gt;curl&lt;/code&gt;로 HTTP 연결을 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로는 Nginx + Let's Encrypt HTTPS를 정상 설정하면 이 문제는 자연스럽게 해결된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-50&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-50&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;50. 자주 발생하는 문제 9 &amp;mdash; DuckDNS domain은 해석되지만 접속 불가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;nslookup testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A record가 Elastic IP와 같은지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 domain으로 직접 HTTP 확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl -v http://testcicd.duckdns.org/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host header 문제를 분리하려면:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block dts&quot;&gt;&lt;code&gt;curl -v \
  -H &quot;Host: testcicd.duckdns.org&quot; \
  http://&amp;lt;ELASTIC_IP&amp;gt;/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 200이면 DNS와 HTTP server는 정상이며 브라우저 cache/HTTPS 자동 전환 등을 확인한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-51&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-51&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;51. 자주 발생하는 문제 10 &amp;mdash; Nginx 502 Bad Gateway&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx는 살아 있지만 backend container에 연결하지 못하는 경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;curl -v http://127.0.0.1:8000/health
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패하면 container/application 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공하면 Nginx 설정을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block stata&quot;&gt;&lt;code&gt;sudo nginx -t
sudo cat /etc/nginx/conf.d/testcicd.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;proxy_pass&lt;/code&gt;가 다음과 같은지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block groovy&quot;&gt;&lt;code&gt;proxy_pass http://127.0.0.1:8000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 로그:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block maxima&quot;&gt;&lt;code&gt;sudo tail -n 200 /var/log/nginx/error.log
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-52&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-52&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;52. 자주 발생하는 문제 11 &amp;mdash; Certbot 발급 실패&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 순서:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DuckDNS A record가 정확한 Elastic IP인가&lt;/li&gt;
&lt;li&gt;Port 80이 외부에 열려 있는가&lt;/li&gt;
&lt;li&gt;Nginx가 port 80에서 실행 중인가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server_name&lt;/code&gt;이 실제 domain과 정확히 일치하는가&lt;/li&gt;
&lt;li&gt;외부 PC에서 &lt;code&gt;curl http://domain/health&lt;/code&gt;가 성공하는가&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 검사:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block lua&quot;&gt;&lt;code&gt;sudo nginx -t
sudo systemctl status nginx --no-pager
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 검사:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block css&quot;&gt;&lt;code&gt;nslookup testcicd.duckdns.org
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-53&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-53&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;53. 자주 발생하는 문제 12 &amp;mdash; Port 80 충돌&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 다음처럼 실행했다면:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;-p 80:8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker가 host port 80을 점유하므로 Nginx가 시작하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 구성에서는 반드시:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block angelscript&quot;&gt;&lt;code&gt;-p 127.0.0.1:8000:8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block vim&quot;&gt;&lt;code&gt;sudo docker ps
sudo ss -lntp | grep ':80'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 구조:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;Port 80  &amp;rarr; Nginx
Port 443 &amp;rarr; Nginx
Port 8000 localhost &amp;rarr; Docker
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-54&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-54&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;54. GitHub Actions 배포 실패 로그 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions의 마지막 SSM 단계에서 실패하면 &lt;code&gt;GetCommandInvocation&lt;/code&gt;을 통해 stdout/stderr를 출력하도록 workflow를 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block applescript&quot;&gt;&lt;code&gt;Actions
&amp;rarr; 해당 workflow
&amp;rarr; Deploy to EC2 with SSM
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에서도:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block livecodeserver&quot;&gt;&lt;code&gt;sudo docker ps -a
sudo docker logs --tail 200 testcicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 확인한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-55&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-55&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;55. 보안 관련 권장사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조의 핵심 보안 선택:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;GitHub long-lived AWS key 저장 안 함
&amp;rarr; OIDC 사용

EC2 SSH 22 공개 안 함
&amp;rarr; SSM Session Manager 사용

Docker application port 외부 공개 안 함
&amp;rarr; 127.0.0.1:8000 only

외부 트래픽
&amp;rarr; Nginx :80/:443

GitHub AWS Role
&amp;rarr; 특정 repository + main branch로 제한

ECR Push Role
&amp;rarr; 특정 repository ARN으로 제한

SSM SendCommand
&amp;rarr; 특정 EC2 Instance ID로 제한
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 고려할 수 있는 것:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub branch protection&lt;/li&gt;
&lt;li&gt;required status checks&lt;/li&gt;
&lt;li&gt;GitHub Environment + approval gate&lt;/li&gt;
&lt;li&gt;AWS CloudTrail&lt;/li&gt;
&lt;li&gt;ECR image scanning&lt;/li&gt;
&lt;li&gt;OS patching&lt;/li&gt;
&lt;li&gt;fail2ban/WAF가 필요한 서비스인지 검토&lt;/li&gt;
&lt;li&gt;Nginx security headers&lt;/li&gt;
&lt;li&gt;Docker resource limit&lt;/li&gt;
&lt;li&gt;application secret을 image에 bake하지 않고 Secrets Manager/Parameter Store 등으로 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-56&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-56&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;56. 비용 관련 주의사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 리소스는 비용이 발생할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2 instance&lt;/li&gt;
&lt;li&gt;EBS gp3 volume&lt;/li&gt;
&lt;li&gt;Public IPv4 / Elastic IP&lt;/li&gt;
&lt;li&gt;ECR storage&lt;/li&gt;
&lt;li&gt;Data transfer&lt;/li&gt;
&lt;li&gt;CloudWatch logs를 별도로 구성한 경우 로그 저장량&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료 플랜/크레딧 적용 여부는 계정과 시점에 따라 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 종료 후 불필요한 리소스가 남아 있으면 계속 과금될 수 있으므로 Cleanup checklist를 수행한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-57&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-57&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;57. 프로젝트 종료 시 Cleanup Checklist&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 서비스를 종료할 경우 다음을 검토한다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;EC2&lt;/h3&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;EC2 instance terminate
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 stop만 하면 EBS 등 일부 비용은 계속 발생할 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Elastic IP&lt;/h3&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;Disassociate
&amp;rarr; Release Elastic IP address
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;ECR&lt;/h3&gt;
&lt;pre class=&quot;sg-code-block properties&quot;&gt;&lt;code&gt;Repository image 삭제
또는
Repository 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;EBS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 terminate 시 root volume 삭제 설정이 원하는 대로 되어 있는지 확인한다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;IAM&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 이상 사용할 필요가 없다면:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block nginx&quot;&gt;&lt;code&gt;TestCicdEc2Role
TestCicdGitHubActionsRole
TestCicdGitHubActionsPolicy
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub OIDC Provider는 동일 AWS account의 다른 GitHub CI/CD에서도 재사용할 수 있으므로 무조건 삭제할 필요는 없다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;GitHub Variables&lt;/h3&gt;
&lt;pre class=&quot;sg-code-block nginx&quot;&gt;&lt;code&gt;AWS_ROLE_ARN
EC2_INSTANCE_ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 이상 사용하지 않으면 제거한다.&lt;/p&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;DuckDNS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 이상 사용할 domain이면 DuckDNS에서 제거한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-58&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-58&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;58. 전체 구축 완료 체크리스트&lt;/h2&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Application / Docker&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Application이 container 내부에서 &lt;code&gt;0.0.0.0:8000&lt;/code&gt;으로 실행된다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Health endpoint가 존재한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;로컬 &lt;code&gt;docker build&lt;/code&gt;가 성공한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;로컬 container health check가 성공한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Runtime에 필요한 파일이 &lt;code&gt;.dockerignore&lt;/code&gt;로 빠지지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;AWS ECR&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;testcicd&lt;/code&gt; private repository가 생성되어 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;ECR region이 &lt;code&gt;ap-northeast-2&lt;/code&gt;다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;EC2 IAM Role&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;TestCicdEc2Role&lt;/code&gt;이 존재한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt;가 연결되어 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;AmazonEC2ContainerRegistryPullOnly&lt;/code&gt;가 연결되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;EC2&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Amazon Linux 2023을 사용한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;x86_64 architecture다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;원하는 instance type을 사용한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;TestCicdEc2Role&lt;/code&gt;이 instance profile로 연결되어 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Storage가 충분하다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Session Manager 접속이 된다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Docker가 active 상태다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;AWS CLI가 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Network&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Public subnet이다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;0.0.0.0/0 &amp;rarr; Internet Gateway&lt;/code&gt; route가 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Elastic IP가 EC2에 연결되어 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Security Group TCP 80이 열려 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Security Group TCP 443이 열려 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;SSH 22는 공개되어 있지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;EC2 Deployment Script&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;/opt/testcicd/deploy.sh&lt;/code&gt;가 존재한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;실행 권한이 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;bash -n&lt;/code&gt; 검사가 성공한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Region이 맞다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Container name이 맞다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Docker bind가 &lt;code&gt;127.0.0.1:8000:8000&lt;/code&gt;이다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Health path가 application과 일치한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Rollback logic이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;GitHub OIDC&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;token.actions.githubusercontent.com&lt;/code&gt; provider가 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Audience가 &lt;code&gt;sts.amazonaws.com&lt;/code&gt;이다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Repository 생성일을 확인했다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;필요한 경우 OWNER_ID / REPOSITORY_ID를 확인했다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Trust Policy의 &lt;code&gt;sub&lt;/code&gt; 형식이 실제 repository와 일치한다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;main&lt;/code&gt; branch로 제한되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;GitHub IAM Role&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;TestCicdGitHubActionsRole&lt;/code&gt;이 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;ECR authorization 권한이 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;특정 &lt;code&gt;testcicd&lt;/code&gt; repository에만 ECR push 가능하다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;특정 EC2에만 SSM SendCommand 가능하다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;ssm:GetCommandInvocation&lt;/code&gt; 권한이 있다.&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;AdministratorAccess를 붙이지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;GitHub Variables&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;AWS_ROLE_ARN&lt;/code&gt; 등록&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;EC2_INSTANCE_ID&lt;/code&gt; 등록&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;장기 AWS Access Key는 등록하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;GitHub Actions&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;.github/workflows/cicd.yml&lt;/code&gt; 존재&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;contents: read&lt;/code&gt;&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;id-token: write&lt;/code&gt;&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;PR에서 CI 성공&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;PR에서는 AWS 배포 단계 skip&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;main merge 후 ECR push 성공&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;main merge 후 SSM deploy 성공&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;ECR image tag가 commit SHA&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Docker on EC2&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;sudo docker ps&lt;/code&gt;에서 container가 Up&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;127.0.0.1:8000-&amp;gt;8000/tcp&lt;/code&gt; 형태&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;내부 health check 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;DuckDNS&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;DuckDNS domain 생성&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;IPv4가 Elastic IP로 설정됨&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;nslookup&lt;/code&gt; 결과가 Elastic IP와 일치&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;불필요한 AAAA record 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sg-heading&quot; data-ke-size=&quot;size23&quot;&gt;Nginx / HTTPS&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Nginx 설치&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;server_name&lt;/code&gt;이 DuckDNS domain과 일치&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;proxy_pass http://127.0.0.1:8000&lt;/code&gt;&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;nginx -t&lt;/code&gt; 성공&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;HTTP로 backend 응답 확인&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;Certbot 인증서 발급 성공&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;HTTPS 접속 성공&lt;/li&gt;
&lt;li class=&quot;sg-task-item&quot;&gt;&lt;span class=&quot;sg-check&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;code&gt;certbot renew --dry-run&lt;/code&gt; 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-59&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-59&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;59. 최종 상태 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;AWS
├── EC2
│   ├── Amazon Linux 2023
│   ├── m7i-flex.large (예시)
│   ├── 30 GiB gp3
│   ├── Docker
│   ├── Nginx
│   ├── Certbot
│   └── SSM Agent
│
├── ECR
│   └── testcicd
│
├── IAM
│   ├── TestCicdEc2Role
│   │   ├── AmazonSSMManagedInstanceCore
│   │   └── AmazonEC2ContainerRegistryPullOnly
│   │
│   └── TestCicdGitHubActionsRole
│       ├── GitHub OIDC Trust
│       ├── ECR Push
│       └── SSM SendCommand
│
├── Elastic IP
│
└── Security Group
    ├── TCP 80  public
    ├── TCP 443 public
    └── TCP 22  closed
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;Pull Request
   &amp;darr;
Docker Build
   &amp;darr;
Smoke Test
   &amp;darr;
Merge to main
   &amp;darr;
OIDC
   &amp;darr;
ECR : commit SHA
   &amp;darr;
SSM
   &amp;darr;
EC2 deploy.sh
   &amp;darr;
Docker health check
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 접속:&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;Client
   &amp;darr; HTTPS
DuckDNS
   &amp;darr;
Elastic IP
   &amp;darr;
Nginx :443
   &amp;darr;
127.0.0.1:8000
   &amp;darr;
Docker application
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-60&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-60&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;60. 공식 문서 참고 링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 환경에서 다시 구축할 때 서비스 사양과 Action major version은 바뀔 수 있으므로 아래 공식 문서를 함께 확인한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub Actions OIDC &amp;rarr; AWS&lt;br /&gt;&lt;a href=&quot;https://docs.github.com/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://docs.github.com/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AWS ECR image push IAM permissions&lt;br /&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-push-iam.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-push-iam.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AWS Systems Manager Session Manager&lt;br /&gt;&lt;a href=&quot;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Amazon Linux 2023 packages&lt;br /&gt;&lt;a href=&quot;https://docs.aws.amazon.com/linux/al2023/release-notes/all-packages-AL2023.12.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://docs.aws.amazon.com/linux/al2023/release-notes/all-packages-AL2023.12.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub &lt;code&gt;actions/checkout&lt;/code&gt; releases&lt;br /&gt;&lt;a href=&quot;https://github.com/actions/checkout/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/actions/checkout/releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AWS &lt;code&gt;configure-aws-credentials&lt;/code&gt; releases&lt;br /&gt;&lt;a href=&quot;https://github.com/aws-actions/configure-aws-credentials/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/aws-actions/configure-aws-credentials/releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AWS &lt;code&gt;amazon-ecr-login&lt;/code&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/aws-actions/amazon-ecr-login&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/aws-actions/amazon-ecr-login&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Certbot documentation&lt;br /&gt;&lt;a href=&quot;https://eff-certbot.readthedocs.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://eff-certbot.readthedocs.io/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DuckDNS&lt;br /&gt;&lt;a href=&quot;https://www.duckdns.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://www.duckdns.org/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;a class=&quot;sg-anchor&quot; name=&quot;sg-section-61&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;
&lt;h2 id=&quot;sg-section-61&quot; class=&quot;sg-major-heading&quot; data-ke-size=&quot;size26&quot;&gt;61. 다음번에 가장 빠르게 재구축하는 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 다시 만든다면 아래 순서로 진행하면 된다.&lt;/p&gt;
&lt;pre class=&quot;sg-code-block routeros&quot;&gt;&lt;code&gt;1. Application health endpoint 확인
2. Dockerfile 작성
3. .dockerignore 작성
4. Local Docker build/run 검증
5. ECR private repository 생성
6. EC2 IAM Role 생성
7. EC2 생성
8. Security Group 80/443 설정
9. Elastic IP 연결
10. Session Manager 확인
11. Docker 설치
12. deploy.sh 생성
13. GitHub OIDC Provider 생성/기존 provider 확인
14. Repository OIDC subject 형식 확인
15. GitHub Actions IAM Role 생성
16. ECR Push + SSM 최소 권한 Policy 연결
17. GitHub Variables 등록
18. cicd.yml 작성
19. Feature branch &amp;rarr; PR로 CI 검증
20. main merge &amp;rarr; 최초 CD 검증
21. DuckDNS &amp;rarr; Elastic IP 연결
22. Nginx 설치
23. HTTP reverse proxy 검증
24. Certbot HTTPS 발급
25. certbot renew --dry-run
26. 실제 코드 변경 &amp;rarr; PR &amp;rarr; main &amp;rarr; 자동 배포 재검증
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서대로 진행하면 중간에 문제가 발생했을 때 원인을 application, Docker, AWS IAM, AWS network, CI/CD, DNS, HTTPS 계층별로 분리해서 확인하기 쉽다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Guides</category>
      <category>AWS</category>
      <category>be</category>
      <category>DDNS</category>
      <category>EC2</category>
      <category>ecr</category>
      <category>https</category>
      <category>oidc</category>
      <category>가이드라인</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/55</guid>
      <comments>https://slowgrid.tistory.com/entry/AWS-EC2-ECR-GitHub-Actions-OIDC-DuckDNS-HTTPS-%EB%B0%B0%ED%8F%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#entry55comment</comments>
      <pubDate>Wed, 19 Aug 2026 19:56:22 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] - 2026.08.19 | 양방향 다익스트라 알고리즘</title>
      <link>https://slowgrid.tistory.com/entry/til-Bidirectional-Dijkstra</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;h2&gt;양방향 다익스트라(Bidirectional Dijkstra) 알고리즘 학습&lt;/h2&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;오늘 배운 내용&lt;/h2&gt;
&lt;p&gt;다익스트라는 한 출발점에서 다른 정점까지의 최단거리를 구하는 알고리즘이다.&lt;/p&gt;
&lt;p&gt;그런데 문제에서 출발점 &lt;code&gt;S&lt;/code&gt;와 도착점 &lt;code&gt;T&lt;/code&gt;가 이미 정해져 있다면 조금 아까운 느낌이 든다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;S&lt;/code&gt;에서 출발한 탐색이 &lt;code&gt;T&lt;/code&gt;까지 먼 길을 혼자 가야 할 이유가 있을까?&lt;/p&gt;
&lt;p&gt;반대로 &lt;code&gt;T&lt;/code&gt;에서도 출발해서 서로 가운데를 향해 움직이면 탐색해야 하는 영역을 줄일 수 있지 않을까?&lt;/p&gt;
&lt;p&gt;여기서 나온 아이디어가 &lt;strong&gt;양방향 다익스트라(Bidirectional Dijkstra)&lt;/strong&gt; 이다.&lt;/p&gt;
&lt;p&gt;말 그대로&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;S → T&lt;/code&gt; 방향으로 다익스트라 하나&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T → S&lt;/code&gt; 방향으로 다익스트라 하나&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;를 동시에 진행한다.&lt;/p&gt;
&lt;p&gt;처음에는 그냥 &lt;strong&gt;두 탐색이 처음 만나는 순간 끝내면 되는 알고리즘&lt;/strong&gt;이라고 생각했다.&lt;/p&gt;
&lt;p&gt;그런데 공부해보니 이 알고리즘에서 가장 중요한 부분은 탐색 방향이 두 개라는 사실보다 &lt;strong&gt;언제 탐색을 끝내도 되는가&lt;/strong&gt;를 판단하는 종료 조건이라고 볼 수 있었다.&lt;/p&gt;
&lt;p&gt;실제로 단순히 두 탐색에서 발견한 정점 집합이 처음 겹치는 순간 종료하는 방법은 최단경로를 보장하지 않는다. 양방향 최단경로 탐색에서는 이 종료 조건을 별도로 관리해야 한다.&lt;/p&gt;
&lt;h3&gt;왜 양방향으로 탐색하는가&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;일반 다익스트라&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;출발점 &lt;code&gt;S&lt;/code&gt;에서 시작해서 거리가 가까운 정점부터 계속 확장한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → → → → → → → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;도착점이 멀리 있다면 탐색 범위가 계속 커진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;양방향 다익스트라&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이번에는 양쪽에서 시작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → → →     ← ← ← T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;두 탐색이 각각 절반 정도의 거리만 진행하고 가운데에서 연결할 수 있다면 훨씬 적은 정점을 확인할 수도 있다.&lt;/p&gt;
&lt;p&gt;트리처럼 가지가 계속 늘어나는 구조를 생각하면 차이가 더 직관적이다.&lt;/p&gt;
&lt;p&gt;여기서 간선 가중치가 비슷하고 트리처럼 균일하게 가지가 퍼진다고 단순화해서 직관적으로 생각해보자. &lt;/p&gt;
&lt;p&gt;목표까지의 탐색 깊이가 &lt;code&gt;d&lt;/code&gt;, 한 정점에서 평균적으로 &lt;code&gt;b&lt;/code&gt;개의 다음 정점으로 퍼진다면 한쪽 탐색은 대략 b^d 수준의 영역까지 커질 수 있다.&lt;/p&gt;
&lt;p&gt;반면 양쪽에서 절반씩 탐색한다면 직관적으로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;b^(d/2) + b^(d/2)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도의 영역을 탐색하게 된다.&lt;/p&gt;
&lt;p&gt;물론 이것은 &lt;strong&gt;양방향 탐색이 왜 빨라질 수 있는지 보여주는 직관일 뿐 일반적인 시간복잡도 보장은 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;최악의 경우에는 양방향 탐색도 그래프의 대부분을 확인해야 한다. 실제 연구에서도 양방향 탐색은 큰 그래프에서 실용적으로 효과적인 경우가 많지만 최악의 경우 모든 정점과 간선을 살펴볼 수 있다고 설명한다.&lt;/p&gt;
&lt;h3&gt;기본 구조&lt;/h3&gt;
&lt;p&gt;필요한 것은 다익스트라 두 세트이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Forward
S에서 시작

dist_f[v] = 현재까지 발견한 S → v 경로 중 가장 짧은 거리


Backward
T에서 시작

dist_b[v] = 현재까지 발견한 v → T 경로 중 가장 짧은 거리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 우선순위 큐도 두 개가 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pq_f = [(0, start)]
pq_b = [(0, target)]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞쪽 탐색과 뒤쪽 탐색은 각각 평범한 다익스트라와 거의 같다.&lt;/p&gt;
&lt;p&gt;차이는 두 탐색이 서로의 정보를 확인한다는 것이다.&lt;/p&gt;
&lt;p&gt;예를 들어 어떤 정점 &lt;code&gt;v&lt;/code&gt;가 양쪽에서 모두 발견되었다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → v → T

dist_f[v] + dist_b[v]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 하나의 완성된 경로를 얻은 것이다.&lt;/p&gt;
&lt;p&gt;현재까지 발견한 가장 짧은 완성 경로를 &lt;code&gt;μ(mu)&lt;/code&gt;라고 두면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;μ = min(μ, dist_f[v] + dist_b[v])&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 계속 갱신할 수 있다.&lt;/p&gt;
&lt;h3&gt;핵심은 μ&lt;/h3&gt;
&lt;p&gt;처음에는 아직 &lt;code&gt;S → T&lt;/code&gt; 경로를 하나도 발견하지 못했으므로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;mu = infinity&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;탐색을 진행하다 양쪽 탐색이 연결될 수 있는 지점을 발견하면 실제 경로 하나가 만들어진다.&lt;/p&gt;
&lt;p&gt;예를 들어&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S --2-- A --3-- C
                 \
                  4
                   \
                    D --2-- T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 하고&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dist_f[C] = 5
dist_b[D] = 2
C → D = 4&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;를 알고 있다면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → ... → C → D → ... → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 경로의 길이는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;5 + 4 + 2 = 11&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;μ = 11&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로 둘 수 있다.&lt;/p&gt;
&lt;p&gt;중요한 것은 &lt;code&gt;μ&lt;/code&gt;가 &lt;strong&gt;현재까지 실제로 발견한 S-T 경로 중 가장 짧은 것&lt;/strong&gt;이라는 점이다.&lt;/p&gt;
&lt;p&gt;아직 진짜 최단거리라고 확정된 것은 아니다.&lt;/p&gt;
&lt;h3&gt;처음 만났다고 끝내면 안 된다&lt;/h3&gt;
&lt;p&gt;처음에는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if node in backward_visited:
    return&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도로 생각했다.&lt;/p&gt;
&lt;p&gt;그런데 이 방법은 틀릴 수 있다.&lt;/p&gt;
&lt;p&gt;두 탐색이 가장 먼저 만난 위치가 반드시 전체 최단경로 위라는 보장이 없기 때문이다. 실제 양방향 탐색 연구에서도 두 탐색의 reached set이 처음 겹치는 순간 종료하는 조건은 잘못된 종료 조건으로 지적되어 있다.&lt;/p&gt;
&lt;p&gt;양방향 다익스트라에서 확인해야 하는 것은 &lt;strong&gt;만났는가&lt;/strong&gt;가 아니라&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;아직 발견하지 않은 경로가 현재 최선인 &lt;code&gt;μ&lt;/code&gt;보다 더 짧아질 가능성이 남아 있는가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;Forward 우선순위 큐에서 아직 처리하지 않은 가장 작은 거리 값을&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min_f&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backward 쪽을&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min_b&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 하자.&lt;/p&gt;
&lt;p&gt;그러면 아직 탐색하지 않은 경로가 양쪽을 연결하려면 적어도&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min_f + min_b&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만큼의 비용이 필요하다.&lt;/p&gt;
&lt;p&gt;그런데 이미 발견한 경로가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;μ&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이고&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min_f + min_b &amp;gt;= μ&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 되었다면?&lt;/p&gt;
&lt;p&gt;앞으로 발견할 경로는 현재 가지고 있는 &lt;code&gt;μ&lt;/code&gt;보다 짧아질 수 없다.&lt;/p&gt;
&lt;p&gt;따라서 그 순간 탐색을 종료할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if min_f + min_b &amp;gt;= mu:
    break&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 조건이 양방향 다익스트라에서 가장 중요한 부분이다.&lt;/p&gt;
&lt;p&gt;Pohl의 양방향 탐색 연구에서도 현재 발견한 최선 경로의 길이를 유지하고, 양쪽 탐색에서 아직 남은 최소 거리의 합이 그 값 이상일 때 탐색을 종료하는 방식이 사용된다.&lt;/p&gt;
&lt;h3&gt;방향 그래프에서는 역방향 그래프가 필요하다&lt;/h3&gt;
&lt;p&gt;무방향 그래프에서는 크게 신경 쓰지 않아도 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A --3-- B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 있으면&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B
B → A&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;둘 다 갈 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;하지만 방향 그래프에서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 간선이 있다고 해서 &lt;code&gt;B → A&lt;/code&gt;가 존재하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;Backward 탐색의 의미는 단순히 &lt;code&gt;T&lt;/code&gt;에서 원래 그래프를 다익스트라 하는 것이 아니다.&lt;/p&gt;
&lt;p&gt;우리가 알고 싶은 것은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;v → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;의 최단거리다.&lt;/p&gt;
&lt;p&gt;따라서 모든 간선 방향을 뒤집은 &lt;strong&gt;reverse graph&lt;/strong&gt;에서 &lt;code&gt;T&lt;/code&gt;를 출발점으로 다익스트라를 해야 한다.&lt;/p&gt;
&lt;p&gt;원래 그래프가&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B
B → C
C → T&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라면 역방향 그래프는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;T → C
C → B
B → A&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;그러면 역방향 그래프에서 계산한&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;T → v 거리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 원래 그래프에서는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;v → T 거리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;와 같아진다.&lt;/p&gt;
&lt;p&gt;결국, 양방향 다익스트라의 정식 구조도 Forward는 원래 그래프를, Backward는 간선을 뒤집은 그래프를 탐색하는 형태라고 볼 수 있다.&lt;/p&gt;
&lt;h3&gt;어느 방향을 먼저 탐색할까&lt;/h3&gt;
&lt;p&gt;두 탐색을 운영하는 방식은 여러 가지가 있다.&lt;/p&gt;
&lt;p&gt;가장 단순하게는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Forward
Backward
Forward
Backward
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 번갈아 실행할 수도 있다.&lt;/p&gt;
&lt;p&gt;이번 구현에서는 두 우선순위 큐의 최소값을 비교해서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if pq_f[0][0] &amp;lt;= pq_b[0][0]:
    # Forward
else:
    # Backward&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처럼 &lt;strong&gt;현재 더 가까운 쪽을 먼저 확장&lt;/strong&gt;하도록 했다.&lt;/p&gt;
&lt;p&gt;핵심은 어느 쪽을 선택하느냐보다 각 방향이 다익스트라의 거리 순서를 지키고, 올바른 &lt;code&gt;μ&lt;/code&gt;와 종료 조건을 유지하는 것이다.&lt;/p&gt;
&lt;h3&gt;정리하면&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구성&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;놓치면&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Forward Dijkstra&lt;/td&gt;
&lt;td&gt;&lt;code&gt;S → v&lt;/code&gt; 최단거리 탐색&lt;/td&gt;
&lt;td&gt;출발점 쪽 탐색 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backward Dijkstra&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v → T&lt;/code&gt; 최단거리 탐색&lt;/td&gt;
&lt;td&gt;도착점 쪽 탐색 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse Graph&lt;/td&gt;
&lt;td&gt;방향 그래프의 역방향 탐색&lt;/td&gt;
&lt;td&gt;잘못된 방향으로 탐색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;μ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 발견한 최선의 완성 경로&lt;/td&gt;
&lt;td&gt;종료 시점을 판단할 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min_f + min_b &amp;gt;= μ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;더 짧은 경로가 없음을 확인&lt;/td&gt;
&lt;td&gt;너무 일찍 종료할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;실습 코드&lt;/h2&gt;
&lt;p&gt;챗지피티와 함께 일반 다익스트라와 양방향 다익스트라를 구현하고, 실제로 어떤 차이가 생기는지 비교해보았다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import heapq
from math import inf


def bidirectional_dijkstra(graph, start, target):
    if start == target:
        return 0, [start]

    # [1] 역방향 그래프 생성
    reverse_graph = {u: [] for u in graph}

    for u in graph:
        for v, weight in graph[u]:
            reverse_graph.setdefault(v, [])
            reverse_graph[v].append((u, weight))

    # [2] 양쪽 거리 배열
    dist_f = {start: 0}
    dist_b = {target: 0}

    # 경로 복원용
    parent_f = {start: None}
    parent_b = {target: None}

    pq_f = [(0, start)]
    pq_b = [(0, target)]

    visited_f = set()
    visited_b = set()

    # 현재까지 발견한 최선의 S -&amp;gt; T 경로
    mu = inf

    # 최선 경로에서 Forward와 Backward가 연결되는 부분
    bridge = None

    while pq_f and pq_b:

        # 오래된 heap 원소 제거
        while pq_f and (
            pq_f[0][1] in visited_f
            or pq_f[0][0] != dist_f.get(pq_f[0][1], inf)
        ):
            heapq.heappop(pq_f)

        while pq_b and (
            pq_b[0][1] in visited_b
            or pq_b[0][0] != dist_b.get(pq_b[0][1], inf)
        ):
            heapq.heappop(pq_b)

        if not pq_f or not pq_b:
            break

        min_f = pq_f[0][0]
        min_b = pq_b[0][0]

        # [3] 가장 중요한 종료 조건
        if min_f + min_b &amp;gt;= mu:
            break

        # [4] 현재 거리가 더 작은 방향을 확장
        if min_f &amp;lt;= min_b:

            d, u = heapq.heappop(pq_f)

            if u in visited_f:
                continue

            visited_f.add(u)

            # 같은 정점에서 두 탐색이 연결되는 경우
            if u in dist_b:
                candidate = d + dist_b[u]

                if candidate &amp;lt; mu:
                    mu = candidate
                    bridge = (u, u)

            # Forward relaxation
            for v, weight in graph.get(u, []):

                new_dist = d + weight

                if new_dist &amp;lt; dist_f.get(v, inf):
                    dist_f[v] = new_dist
                    parent_f[v] = u
                    heapq.heappush(pq_f, (new_dist, v))

                # u -&amp;gt; v 간선을 통해 Backward 탐색과 연결
                if v in dist_b:
                    candidate = d + weight + dist_b[v]

                    if candidate &amp;lt; mu:
                        mu = candidate
                        bridge = (u, v)

        else:

            d, u = heapq.heappop(pq_b)

            if u in visited_b:
                continue

            visited_b.add(u)

            if u in dist_f:
                candidate = dist_f[u] + d

                if candidate &amp;lt; mu:
                    mu = candidate
                    bridge = (u, u)

            # reverse_graph의 v -&amp;gt; u는
            # 원래 그래프에서는 u &amp;lt;- v
            for v, weight in reverse_graph.get(u, []):

                new_dist = d + weight

                if new_dist &amp;lt; dist_b.get(v, inf):
                    dist_b[v] = new_dist

                    # 원래 그래프에서 v 다음에 u로 이동
                    parent_b[v] = u

                    heapq.heappush(pq_b, (new_dist, v))

                if v in dist_f:
                    candidate = dist_f[v] + weight + d

                    if candidate &amp;lt; mu:
                        mu = candidate
                        bridge = (v, u)

    if mu == inf:
        return inf, []

    # [5] 경로 복원
    left, right = [], []

    u, v = bridge

    cur = u
    while cur is not None:
        left.append(cur)
        cur = parent_f[cur]

    left.reverse()

    cur = v
    while cur is not None:
        right.append(cur)
        cur = parent_b[cur]

    if u == v:
        path = left + right[1:]
    else:
        path = left + right

    return mu, path&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;간단한 예제로 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;graph = {
    &amp;quot;S&amp;quot;: [(&amp;quot;A&amp;quot;, 2), (&amp;quot;B&amp;quot;, 5)],
    &amp;quot;A&amp;quot;: [(&amp;quot;C&amp;quot;, 2)],
    &amp;quot;B&amp;quot;: [(&amp;quot;C&amp;quot;, 1), (&amp;quot;T&amp;quot;, 8)],
    &amp;quot;C&amp;quot;: [(&amp;quot;D&amp;quot;, 2)],
    &amp;quot;D&amp;quot;: [(&amp;quot;T&amp;quot;, 1)],
    &amp;quot;T&amp;quot;: []
}

distance, path = bidirectional_dijkstra(
    graph,
    &amp;quot;S&amp;quot;,
    &amp;quot;T&amp;quot;
)

print(&amp;quot;최단거리:&amp;quot;, distance)
print(&amp;quot;경로:&amp;quot;, path)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;출력은&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;최단거리: 7
경로: [&amp;#39;S&amp;#39;, &amp;#39;A&amp;#39;, &amp;#39;C&amp;#39;, &amp;#39;D&amp;#39;, &amp;#39;T&amp;#39;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 된다.&lt;/p&gt;
&lt;p&gt;실제로&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S → A → C → D → T
2 + 2 + 2 + 1 = 7&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;h3&gt;일반 다익스트라와 탐색량 비교&lt;/h3&gt;
&lt;p&gt;양방향 탐색에 유리한 그래프 구조에서 탐색량이 어떻게 달라지는지 관찰하기 위해 완전 이진 트리를 만들었다.&lt;/p&gt;
&lt;p&gt;깊이 16이면 전체 정점 수는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;131,071&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;개다.&lt;/p&gt;
&lt;p&gt;트리의 가장 왼쪽 리프에서 가장 오른쪽 리프까지 최단경로를 찾도록 했다.&lt;/p&gt;
&lt;p&gt;같은 우선순위 큐 기반 구현에서 확정한 정점 수를 세어보니 예시는 다음과 같이 나왔다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;정점 수: 131071

일반 다익스트라
거리: 32
확정한 정점: 131071

양방향 다익스트라
거리: 32
확정한 정점: 1020&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;최단거리는 둘 다 &lt;code&gt;32&lt;/code&gt;로 동일하지만 탐색한 영역에는 큰 차이가 있었다.&lt;/p&gt;
&lt;p&gt;다만 여기서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;양방향 다익스트라는 항상 100배 빠르다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라고 해석하면 안 된다.&lt;/p&gt;
&lt;p&gt;이번 트리는 양방향 탐색에 굉장히 유리하도록 가지가 넓게 퍼지는 구조다.&lt;/p&gt;
&lt;p&gt;그래프 구조, 가중치 분포, 출발점과 도착점의 위치에 따라 효과는 크게 달라질 수 있고 최악 시간복잡도 자체가 더 좋아지는 것은 아니다.&lt;/p&gt;
&lt;h3&gt;시간복잡도&lt;/h3&gt;
&lt;p&gt;이진 힙을 사용하는 일반적인 다익스트라는&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O((V + E) log V)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정도로 볼 수 있다.&lt;/p&gt;
&lt;p&gt;양방향 다익스트라도 최악의 경우 두 방향에서 많은 정점과 간선을 확인해야 하므로 점근적인 최악 시간복잡도는 여전히&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O((V + E) log V)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이다.&lt;/p&gt;
&lt;p&gt;즉 이 알고리즘의 장점은 Big-O를 바꾸는 것보다는 &lt;strong&gt;특정 출발점과 도착점 사이의 탐색 영역을 실질적으로 줄이는 것&lt;/strong&gt;에 있다.&lt;/p&gt;
&lt;p&gt;최근 연구에서도 적절하게 구현한 양방향 다익스트라가 큰 그래프의 &lt;code&gt;s-t&lt;/code&gt; 최단경로 문제에서 탐색해야 하는 간선 수 측면에서 매우 강한 성질을 가진다는 결과가 제시되어 있다.&lt;/p&gt;
&lt;h3&gt;공간복잡도&lt;/h3&gt;
&lt;p&gt;Forward와 Backward 각각의 거리 배열, 부모 배열,&lt;br&gt;우선순위 큐를 유지해야 하며 방향 그래프에서는 reverse graph도 저장한다.&lt;/p&gt;
&lt;p&gt;따라서 전체 공간복잡도는 점근적으로&lt;/p&gt;
&lt;p&gt;O(V + E)&lt;/p&gt;
&lt;p&gt;정도로 볼 수 있다.&lt;/p&gt;
&lt;p&gt;양방향 탐색을 위해 자료구조가 두 세트 필요하므로&lt;br&gt;일반 다익스트라보다 상수 수준의 추가 메모리가 필요하지만,&lt;br&gt;Big-O 자체는 동일하다.&lt;/p&gt;
&lt;h3&gt;다익스트라와 마찬가지로 음수 간선은 안 된다&lt;/h3&gt;
&lt;p&gt;양방향이라는 부분만 새로 생겼을 뿐 각 방향에서 실행되는 것은 결국 다익스트라다.&lt;/p&gt;
&lt;p&gt;따라서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;음수 가중치 간선이 없는 그래프&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;라는 기본 조건 역시 그대로 따라온다.&lt;/p&gt;
&lt;p&gt;다익스트라의 핵심은 현재 가장 가까운 정점을 꺼냈다면 그 거리가 확정되었다고 판단하는 것인데, 이후 음수 간선을 만나 거리가 더 작아질 수 있다면 이 전제가 무너지기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;오늘의 회고&lt;/h2&gt;
&lt;h3&gt;1. 배운 점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;양방향 탐색의 핵심은 방향이 두 개라는 것이 아니었다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;처음에는 다익스트라를 시작점과 끝점에서 하나씩 돌리면 끝이라고 생각했다. 그런데 실제 핵심은 &lt;code&gt;μ&lt;/code&gt;였다.&lt;/p&gt;
&lt;p&gt;현재 알고 있는 최선의 완성 경로는 &lt;code&gt;μ&lt;/code&gt;, 아직 탐색하지 않은 경로가 가질 수 있는 최소 비용은 &lt;code&gt;min_f + min_b&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;그래서&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;min_f + min_b &amp;gt;= μ&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가 되는 순간 더 탐색할 필요가 없어진다.&lt;/p&gt;
&lt;p&gt;그렇기에 종료 조건이 알고리즘의 핵심이었다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;발견(discovered)과 확정(settled)을 구분해야 했다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;우선순위 큐에 들어왔다고 해서 그 정점의 거리가 확정된 것은 아니다.&lt;/p&gt;
&lt;p&gt;다익스트라에서 큐의 최솟값으로 실제 꺼내졌을 때 비로소 거리가 확정되는 것처럼, 양방향 다익스트라도 이 차이를 놓치면 종료 조건을 구현하면서 계속 헷갈리게 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 어려운 점 / 개선할 점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;처음 만나는 지점이 왜 답이 아닌지 직관적으로 이해하는 데 시간이 걸렸다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BFS의 양방향 탐색 이미지를 가지고 있어서 &lt;code&gt;두 탐색이 만남 = 끝&lt;/code&gt;이라고 생각하기 쉬웠다.&lt;/p&gt;
&lt;p&gt;그런데 가중치가 존재하면 탐색의 모양이 단순한 원처럼 퍼지지 않는다.&lt;/p&gt;
&lt;p&gt;가중치 &lt;code&gt;1&lt;/code&gt;짜리 간선을 여러 개 지난 경로가 가중치 &lt;code&gt;100&lt;/code&gt;짜리 간선 하나보다 짧을 수도 있다.&lt;/p&gt;
&lt;p&gt;결국 만났다는 사실보다 &lt;strong&gt;현재 경로보다 더 좋은 후보가 남아 있는지&lt;/strong&gt;를 판단해야 했다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 함께 나누고 싶은 점&lt;/h3&gt;
&lt;p&gt;양방향 다익스트라를 공부하면서 가장 궁금해진 것은 실제 지도 서비스에서는 두 탐색의 균형을 어떻게 잡는가였다.&lt;/p&gt;
&lt;p&gt;이번 구현에서는 단순히 두 우선순위 큐의 최소 거리 중 작은 쪽을 확장했는데, 도로망처럼 방향별 밀도가 크게 다른 그래프에서는 한쪽 탐색이 훨씬 크게 퍼질 수도 있을 것 같다.&lt;/p&gt;
&lt;p&gt;또 현실의 길찾기에서는 정점 좌표라는 추가 정보가 있으니 순수한 양방향 다익스트라보다 A* 같은 휴리스틱 탐색이나 전처리를 결합하는 편이 훨씬 유리할 것이다.&lt;/p&gt;
&lt;p&gt;결국 양방향 다익스트라는 단순히 다익스트라를 두 개 붙인 알고리즘이라기보다,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미 찾은 경로의 상한과
아직 찾지 않은 경로의 하한을 비교해서
필요 없는 탐색을 멈추는 방법&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이라고 기억하면 될 것 같다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;E. W. Dijkstra — &lt;a href=&quot;https://doi.org/10.1007/BF01386390&quot;&gt;&lt;em&gt;A Note on Two Problems in Connexion with Graphs&lt;/em&gt; (1959)&lt;/a&gt; (&lt;a href=&quot;https://doi.org/10.1007/BF01386390?utm_source=chatgpt.com&quot; title=&quot;A note on two problems in connexion with graphs | Numerische Mathematik | Springer Nature Link&quot;&gt;DOI&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Ira Pohl — &lt;a href=&quot;https://www.osti.gov/biblio/1453875&quot;&gt;&lt;em&gt;Bi-Directional and Heuristic Search in Path Problems&lt;/em&gt; (1969)&lt;/a&gt; (&lt;a href=&quot;https://www.osti.gov/biblio/1453875?utm_source=chatgpt.com&quot; title=&quot;Bi-Directional and Heuristic Search in Path Problems [Thesis] (Technical Report) | OSTI.GOV&quot;&gt;OSTI&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Bernhard Haeupler, Richard Hladík, Václav Rozhoň, Robert E. Tarjan, Jakub Tětek — &lt;a href=&quot;https://arxiv.org/abs/2410.14638&quot;&gt;&lt;em&gt;Bidirectional Dijkstra&amp;#39;s Algorithm is Instance-Optimal&lt;/em&gt;&lt;/a&gt; (&lt;a href=&quot;https://arxiv.org/abs/2410.14638?utm_source=chatgpt.com&quot; title=&quot;Bidirectional Dijkstra&amp;#39;s Algorithm is Instance-Optimal&quot;&gt;arxiv.org&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CS/자료구조 &amp;middot; 알고리즘</category>
      <category>til</category>
      <category>다익스트라</category>
      <category>알고리즘</category>
      <category>양방향</category>
      <author>slowgrid</author>
      <guid isPermaLink="true">https://slowgrid.tistory.com/54</guid>
      <comments>https://slowgrid.tistory.com/entry/til-Bidirectional-Dijkstra#entry54comment</comments>
      <pubDate>Wed, 19 Aug 2026 09:12:40 +0900</pubDate>
    </item>
  </channel>
</rss>