<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>devdiary-sj 님의 블로그</title>
    <link>https://devdiary-sj.tistory.com/</link>
    <description>매일매일 일기쓰기</description>
    <language>ko</language>
    <pubDate>Mon, 28 Sep 2026 18:17:48 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>devdiary-sj</managingEditor>
    <image>
      <title>devdiary-sj 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/7379576/attach/8b65a894344f4f2dbe64f652b0074092</url>
      <link>https://devdiary-sj.tistory.com</link>
    </image>
    <item>
      <title>[TIL] Unreal 리플렉션과 GC, CDO의 동작 흐름</title>
      <link>https://devdiary-sj.tistory.com/43</link>
      <description>&lt;h2 id=&quot;reflection-title&quot; data-ke-size=&quot;size26&quot;&gt;표준 C++에 리플렉션을 더하는 이유&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;리플렉션은 실행 중인 프로그램이 자신의 타입과 멤버 구조를 조회하고 다룰 수 있는 기능입니다. 표준 C++의 타입 정보만으로는 에디터가 임의의 클래스를 읽어 디테일 패널을 만들거나, Blueprint 노드와 C++ 함수를 연결하고, 저장할 프로퍼티와 객체 참조를 일관되게 찾기 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Unreal은&lt;span&gt;&amp;nbsp;&lt;/span&gt;UCLASS,&lt;span&gt;&amp;nbsp;&lt;/span&gt;UFUNCTION,&lt;span&gt;&amp;nbsp;&lt;/span&gt;UPROPERTY로 엔진에 공개할 대상을 표시하고 빌드 과정에서 필요한 코드를 생성합니다. 이렇게 등록된 정보는 에디터&amp;middot;Blueprint뿐 아니라 직렬화, 네트워크 복제, 기본값 관리, GC의 참조 추적에도 쓰입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;매크로가 기능을 직접 수행하는 것이 아니라, UHT가 매크로를 단서로 엔진이 사용할 타입 정보를 생성한다는 점이 핵심입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;specifiers-title&quot; data-ke-size=&quot;size26&quot;&gt;매크로와 프로퍼티 지정자&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;Stats&quot;)
    int32 Health = 100;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;UCLASS는 이 타입을 UObject 시스템에 등록하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;GENERATED_BODY는 UHT가 생성한 선언이 클래스에 들어갈 위치를 제공합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;UPROPERTY&lt;span&gt;&amp;nbsp;&lt;/span&gt;뒤의 지정자는 해당 필드를 어디에서 편집하고 어떤 시스템에 노출할지 설명합니다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;&lt;b&gt;지정자&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.7519%;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1007%;&quot;&gt;&lt;b&gt;주의점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;EditAnywhere&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.7519%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;클래스 기본값과 인스턴스에서 편집 허용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1007%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;Blueprint 읽기&amp;middot;쓰기를 자동 허용하지는 않음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;VisibleAnywhere&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.7519%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;디테일 패널에 표시하되 직접 편집은 금지&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1007%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;포인터 자체의 교체를 막는 용도로 자주 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;BlueprintReadOnly&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.7519%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;Blueprint에서 값을 읽도록 허용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1007%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;에디터의 디테일 패널 편집 여부와는 별개&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;BlueprintReadWrite&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.7519%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;Blueprint에서 읽고 쓰도록 허용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1007%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;외부 변경을 허용해도 되는 상태인지 검토 필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.1473%;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;Transient&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.7519%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;저장하지 않을 런타임 임시 값으로 표시&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1007%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;로드 후 다시 계산하거나 초기화해야 함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
Category,&lt;span&gt;&amp;nbsp;&lt;/span&gt;DisplayName,&lt;span&gt;&amp;nbsp;&lt;/span&gt;ClampMin&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 메타데이터는 주로 편집 경험을 보조합니다. 반면 편집 가능 여부나 직렬화처럼 동작 자체를 결정하는 지정자도 있으므로 둘을 같은 단순 주석으로 보면 안 됩니다.&lt;/div&gt;
&lt;h2 id=&quot;build-flow-title&quot; data-ke-size=&quot;size26&quot;&gt;UBT와 UHT의 빌드 흐름&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Unreal Build Tool(UBT)은 타깃, 모듈, 플랫폼,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Build.cs, 플러그인 의존성을 읽어 전체 빌드 환경을 구성합니다. 리플렉션 대상 헤더가 있으면 UBT가 Unreal Header Tool(UHT)을 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;UBT가 타깃과 모듈 구성 분석
&amp;rarr; UHT가 Unreal 매크로가 있는 헤더 파싱
&amp;rarr; 리플렉션 등록용 코드와 *.generated.h 생성
&amp;rarr; C++ 컴파일러가 원본 코드와 생성 코드를 함께 컴파일&amp;middot;링크
&amp;rarr; 엔진이 모듈을 로드하고 UClass 등록
&amp;rarr; 각 UClass의 CDO 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;UHT는 완전한 범용 C++ 컴파일러가 아니라 UObject 시스템을 위한 파서이자 코드 생성 도구입니다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;*.generated.h는 헤더의 마지막 include로 두고, 매크로 문법과 지원되는 타입 규칙을 지켜야 합니다. UHT가 성공한 뒤에야 일반 C++ 컴파일 단계가 이어집니다.&lt;/p&gt;
&lt;h2 id=&quot;gc-title&quot; data-ke-size=&quot;size26&quot;&gt;리플렉션과 GC의 연결&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;표준 C++ 객체는 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;new/delete&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는 스마트 포인터의 소유권으로 수명을 관리합니다. 반면&lt;span&gt;&amp;nbsp;&lt;/span&gt;UObject는 엔진의 가비지 컬렉터가 관리합니다. GC는 루트 집합에서 출발해 알려진 참조를 따라가며 도달 가능한 객체를 표시하고, 도달하지 못한 객체를 이후 정리하는 Mark &amp;amp; Sweep 계열 방식입니다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;UPROPERTY()
TObjectPtr&amp;lt;UMyItem&amp;gt; EquippedItem;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;엔진이 참조를 추적하려면 그 참조를 알아야 합니다. 멤버로 보관하는 강한 UObject 참조는 일반적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;UPROPERTY가 붙은&lt;span&gt;&amp;nbsp;&lt;/span&gt;TObjectPtr로 선언합니다. 이렇게 해야 대상이 사용 중인 동안 GC에서 보호되고, 직렬화와 참조 갱신 같은 UObject 시스템의 지원도 받을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;TWeakObjectPtr는 객체를 살려 두지 않으며 사용 전에 유효성을 확인할 때 적합합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;TSoftObjectPtr는 에셋 경로를 보관해 필요할 때 로드하는 참조에 적합합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;리플렉션에 등록되지 않은 일반 raw pointer는 대상을 살려 두지 않고 자동으로 안전한 참조가 되지도 않습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;표준 스마트 포인터는 일반 C++ 객체에 사용하며, GC가 관리하는 UObject에 그대로 적용하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;즉&lt;span&gt;&amp;nbsp;&lt;/span&gt;UPROPERTY는 &amp;ldquo;GC 매크로&amp;rdquo; 하나로만 이해하기보다, 엔진이 필드의 존재와 성격을 알게 하는 관문으로 보는 편이 정확합니다. 정수 같은 값 타입에 붙은&lt;span&gt;&amp;nbsp;&lt;/span&gt;UPROPERTY가 GC 대상을 만드는 것은 아니며, UObject 참조 필드일 때 참조 그래프 구성에 관여합니다.&lt;/p&gt;
&lt;h2 id=&quot;cdo-title&quot; data-ke-size=&quot;size26&quot;&gt;CDO는 무엇인가&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Class Default Object(CDO)는 각&lt;span&gt;&amp;nbsp;&lt;/span&gt;UClass가 하나씩 보유하는 기본 템플릿 객체입니다. C++ 클래스 관점의 타입 설명이&lt;span&gt;&amp;nbsp;&lt;/span&gt;UClass라면, 그 클래스 인스턴스가 처음 가져야 할 프로퍼티 값의 기준은 CDO에 담깁니다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.7286%;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.4961%;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.7752%;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.7286%;&quot;&gt;&lt;b&gt;UClass&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.4961%;&quot;&gt;타입, 프로퍼티, 함수 등의 리플렉션 정보&lt;/td&gt;
&lt;td style=&quot;width: 35.7752%;&quot;&gt;설계도와 항목정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.7286%;&quot;&gt;&lt;b&gt;CDO&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.4961%;&quot;&gt;클래스당 하나의 기본 프로퍼티 값의 기준 객체&lt;/td&gt;
&lt;td style=&quot;width: 35.7752%;&quot;&gt;기본 레시피&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.7286%;&quot;&gt;&lt;b&gt;인스턴스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.4961%;&quot;&gt;월드나 게임에서 실제로 사용하는 개별 객체&lt;/td&gt;
&lt;td style=&quot;width: 35.7752%;&quot;&gt;레시피로 만든 각각의 결과물&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;CDO도 실제 UObject이지만 일반 게임플레이 인스턴스는 아닙니다. 클래스 생성자가 CDO를 구성하고, 이후 생성되는 객체는 CDO 또는 지정된 archetype의 프로퍼티 값을 바탕으로 초기화됩니다. 그래서 생성자에 쓴 기본값이 에디터의 Class Defaults에 나타나고 새 인스턴스의 출발점이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;const AMyCharacter* Defaults = AMyCharacter::StaticClass()
    -&amp;gt;GetDefaultObject&amp;lt;AMyCharacter&amp;gt;();

const int32 DefaultHealth = Defaults-&amp;gt;Health;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;클래스의 기본 설정을 읽기만 하면 될 때 CDO를 조회하면 임시 인스턴스를 생성할 필요가 없습니다. 다만 CDO는 여러 생성의 기준이므로 일반적으로 읽기 전용 템플릿처럼 취급해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;cdo-lifecycle-title&quot; data-ke-size=&quot;size26&quot;&gt;CDO 생성과 인스턴스 초기화&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;클래스 등록:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;모듈이 로드되면 생성 코드가 네이티브 타입의 리플렉션 정보를 등록해&lt;span&gt;&amp;nbsp;&lt;/span&gt;UClass를 구성합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;CDO 생성:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클래스의 CDO가 필요해지면 엔진은 해당 클래스 생성자를 실행해 기본 객체를 만듭니다. 부모 클래스의 기본 상태도 초기화 기준에 포함됩니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;기본값 보관:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;C++ 생성자와 프로퍼티 초기값, Blueprint 클래스라면 부모 기본값 위에 저장된 Blueprint 기본값이 반영됩니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;인스턴스 생성:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;NewObject나 Actor 스폰 과정에서 CDO/archetype의 프로퍼티가 새 객체에 복사되어 출발 상태가 만들어집니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;개별 상태 변경:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이후 생성 훅과 게임플레이가 실행되면서 각 인스턴스의 값이 서로 달라집니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;생성자가 실행된 뒤 CDO를 복사한다&amp;rdquo;처럼 단순화하면 혼동하기 쉽습니다. UObject 생성 파이프라인에서는 생성자가 객체의 네이티브 초기화와 기본 서브오브젝트 구성을 담당하고, 엔진의 초기화 과정이 archetype/CDO 기반 프로퍼티 값을 적용합니다. 중요한 결론은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;생성자와 CDO가 기본 상태를 정의하고, BeginPlay 같은 단계가 개별 런타임 동작을 담당한다&lt;/b&gt;는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Blueprint 클래스에도 별도의&lt;span&gt;&amp;nbsp;&lt;/span&gt;UBlueprintGeneratedClass와 CDO가 있습니다. Blueprint에서 바꾼 Class Defaults는 C++ 부모 CDO를 그대로 수정하는 것이 아니라, 파생 Blueprint 클래스 CDO의 기본값을 구성합니다. 따라서 같은 C++ 클래스를 부모로 둔 여러 Blueprint가 서로 다른 기본 체력과 컴포넌트 설정을 가질 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;cdo-cautions-title&quot; data-ke-size=&quot;size26&quot;&gt;CDO와 생성자 주의점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;월드 상태에 의존하지 않기&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;CDO는 게임 월드가 준비되기 전, 에디터의 클래스 로드 과정에서도 만들어질 수 있습니다. 생성자에서 월드의 Actor 검색이나 게임 상태 접근을 피합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;기본 서브오브젝트는 생성자에서 만들기&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;CreateDefaultSubobject로 만드는 컴포넌트는 클래스의 기본 구조에 속합니다. 상황에 따라 생기는 런타임 객체는 별도의 초기화 단계에서 생성합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;게임플레이 부작용 만들지 않기&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;생성자는 CDO와 모든 인스턴스에 관여할 수 있습니다. 이벤트 발송, 저장 데이터 변경, 타이머 시작은&lt;span&gt;&amp;nbsp;&lt;/span&gt;BeginPlay&lt;span&gt;&amp;nbsp;&lt;/span&gt;등 적절한 생명주기 함수로 옮깁니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;CDO를 런타임 저장소로 쓰지 않기&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;CDO 값을 변경하면 이후 인스턴스의 기준과 에디터 동작에 영향을 줄 수 있습니다. 전역 게임 상태는 GameInstance나 전용 시스템이 소유해야 합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;기존 인스턴스의 오버라이드 구분하기&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;기본값이 바뀌어도 에디터나 Blueprint에서 명시적으로 덮어쓴 값은 유지될 수 있습니다. 새 CDO 값과 저장된 인스턴스 값을 따로 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;생성자에서는 &amp;ldquo;이 클래스의 기본 형태&amp;rdquo;를 만들고, 런타임 초기화에서는 &amp;ldquo;이 인스턴스가 지금 무엇을 할지&amp;rdquo;를 결정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;summary-title&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;UBT는 프로젝트의 빌드 구성을 만들고 UHT를 호출하며, UHT는 Unreal 매크로를 해석해 UObject 시스템용 코드를 생성합니다. 이 리플렉션 정보 덕분에 에디터와 Blueprint가 타입을 다루고, 직렬화와 GC가 프로퍼티와 참조를 추적할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;클래스가 등록되면&lt;span&gt;&amp;nbsp;&lt;/span&gt;UClass마다 CDO가 만들어집니다. CDO는 새 객체가 출발할 기본 상태이며, Blueprint 파생 클래스도 자기 CDO를 가집니다. 따라서 생성자는 월드 로직을 실행하는 장소가 아니라 기본값과 기본 서브오브젝트를 정의하는 장소로 사용해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;references-title&quot; 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 style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-header-tool-for-unreal-engine&quot;&gt;Epic Games, Unreal Header Tool&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://dev.epicgames.com/documentation/unreal-engine/reflection-system-in-unreal-engine&quot;&gt;Epic Games, Reflection System&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://dev.epicgames.com/documentation/unreal-engine/objects-in-unreal-engine&quot;&gt;Epic Games, Objects&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-object-handling-in-unreal-engine&quot;&gt;Epic Games, Unreal Object Handling&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://dev.epicgames.com/documentation/en-us/unreal-engine/object-pointers-in-unreal-engine&quot;&gt;Epic Games, Object Pointers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내배캠_Unreal10기</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/43</guid>
      <comments>https://devdiary-sj.tistory.com/43#entry43comment</comments>
      <pubDate>Fri, 14 Aug 2026 20:44:40 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 언리얼 라이브 코딩</title>
      <link>https://devdiary-sj.tistory.com/42</link>
      <description>&lt;h2 id=&quot;same-goal-title&quot; data-ke-size=&quot;size26&quot;&gt;같은 목적, 다른 방식&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;코드 한 줄을 확인할 때마다 큰 프로젝트를 다시 빌드하고 에디터를 재시작하면 반복 시간이 길어집니다. Unreal Engine과 Unity는 이 시간을 줄이기 위해 실행 중인 에디터에 변경 코드를 반영하지만, 각각 네이티브 C++와 관리형 C# 환경을 사용하므로 갱신 단위와 상태 복원 방식이 다릅니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 &amp;ldquo;적용되었는가&amp;rdquo;뿐 아니라 &amp;ldquo;기존 객체와 런타임 상태가 새 코드와 일치하는가&amp;rdquo;까지 확인하는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;unreal-title&quot; data-ke-size=&quot;size26&quot;&gt;Unreal Live Coding: 네이티브 코드 패치&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Live Coding은 에디터 프로세스를 유지한 채 변경된 C++ 소스를 컴파일하고, 생성된 패치 모듈을 불러와 기존 함수가 새 구현을 실행하도록 메모리의 코드를 갱신합니다. 기본 단축키는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ctrl + Alt + F11입니다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;C++ 코드 수정
&amp;rarr; 변경된 소스와 모듈 컴파일
&amp;rarr; 패치 바이너리 로드
&amp;rarr; 기존 코드가 새 함수 구현을 사용하도록 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;함수 본문의 계산식, 조건문, 로그처럼 객체의 메모리 배치를 바꾸지 않는 수정은 Live Coding으로 확인하기 좋습니다. 반면 멤버 변수를 추가하거나 제거하는 변경, 상속 관계 변경,&lt;span&gt;&amp;nbsp;&lt;/span&gt;UCLASS&amp;middot;UPROPERTY&amp;middot;UFUNCTION&lt;span&gt;&amp;nbsp;&lt;/span&gt;선언 변경은 클래스 메타데이터나 객체 레이아웃에도 영향을 줍니다.&lt;/p&gt;
&lt;h2 id=&quot;reinstancing-title&quot; data-ke-size=&quot;size26&quot;&gt;Object Reinstancing의 역할과 한계&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Unreal은 구조가 바뀐 리플렉션 대상 타입을 처리하기 위해 Object Reinstancing을 사용할 수 있습니다. 새 클래스 정보로 객체를 만들고 기존 객체의 속성과 참조를 가능한 범위에서 옮겨, 실행 중인 인스턴스를 새 구조에 맞춥니다. 하지만 모든 네이티브 상태와 외부 포인터를 완벽히 이전한다는 뜻은 아닙니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;이미 생성된 객체는 생성자를 다시 실행하지 않으므로 생성자 로직의 변경을 그대로 확인하기 어렵습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;C++ 생성자에서 바꾼 기본값은 기존 인스턴스나 이미 저장된 Blueprint 자산에 즉시 반영되지 않을 수 있습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;리인스턴싱 전 객체의 주소를 일반 포인터나 엔진 밖의 자료구조가 보관하면 오래된 참조가 남을 수 있습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;모듈 의존성,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Build.cs, 플러그인 구성처럼 빌드 구조를 바꾸는 수정은 Live Coding의 범위를 벗어날 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 &amp;ldquo;컴파일 성공&amp;rdquo;은 변경이 안전하게 적용됐다는 보장이 아닙니다. 헤더와 타입 구조를 크게 바꿨거나 결과가 이상하면 에디터를 종료한 뒤 IDE 또는 빌드 도구에서 다시 빌드하고 재실행하는 편이 안전합니다.&lt;/p&gt;
&lt;h2 id=&quot;unity-title&quot; data-ke-size=&quot;size26&quot;&gt;Unity의 스크립트 리로드: 어셈블리 교체&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Unity는 C# 스크립트가 바뀌면 영향을 받는 스크립트 어셈블리를 다시 컴파일합니다. 그동안 직렬화 가능한 상태를 보존하고 기존 관리 코드 환경을 내린 뒤 새 어셈블리를 로드하며, 새 타입 정보에 맞춰 상태를 복원합니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;C# 스크립트 수정
&amp;rarr; 영향받는 스크립트 어셈블리 컴파일
&amp;rarr; 직렬화 가능한 상태 보존
&amp;rarr; 관리 코드 환경과 어셈블리 재로드
&amp;rarr; 새 타입 정보로 상태 복원&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;네이티브 함수 주소를 패치하는 Unreal과 달리 관리 런타임에서 타입과 어셈블리를 다시 구성하므로, 필드 추가 같은 변경도 비교적 자연스럽게 처리됩니다. 다만 직렬화할 수 없는 런타임 데이터, 이벤트 구독, 스레드처럼 재로드 경계를 넘기 어려운 상태까지 자동 보존되는 것은 아닙니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;재로드 전후 정리가 필요하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;OnDisable,&lt;span&gt;&amp;nbsp;&lt;/span&gt;OnEnable과 관련 에디터 콜백의 호출 시점을 확인해야 합니다. 특히 에디터 도구나 전역 상태에 의존하는 코드는 재로드 후 중복 등록과 초기화 누락을 점검해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;domain-reload-title&quot; data-ke-size=&quot;size26&quot;&gt;Domain Reload는 언제 일어나는가&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;코드 변경으로 일어나는 스크립트 어셈블리 재로드와 Play Mode 진입 시 초기화는 관련되어 있지만 같은 사건은 아닙니다. Unity는 기본 설정에서 Play Mode에 들어갈 때 Domain Reload를 수행해 정적 필드와 등록된 정적 이벤트 핸들러를 초기 상태로 되돌립니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;public static int Score = 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이전 실행에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Score가 100이 되었더라도 Domain Reload 후에는 필드 초기화 코드가 다시 실행되어 0에서 시작합니다. Enter Play Mode Options에서 Domain Reload를 끄면 진입 시간은 줄어들지만 정적 상태가 이전 실행에서 남습니다. 이 경우 개발자가 정적 필드와 이벤트 구독을 명시적으로 초기화하지 않으면 테스트 결과가 실행 순서에 따라 달라질 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;comparison-title&quot; data-ke-size=&quot;size26&quot;&gt;두 엔진 비교&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5659%;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.403%;&quot;&gt;&lt;b&gt;Unreal Engine&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.031%;&quot;&gt;&lt;b&gt;Unity&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5659%;&quot;&gt;&lt;b&gt;주요 코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.403%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;네이티브 C++&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.031%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;관리형 C#&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5659%;&quot;&gt;&lt;b&gt;갱신 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.403%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;컴파일된 코드를 실행 중 프로세스에 패치&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.031%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;스크립트 어셈블리를 컴파일하고 재로드&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5659%;&quot;&gt;&lt;b&gt;기존 객체 대응&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.403%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;필요 시 Object Reinstancing&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.031%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;직렬화 가능한 상태 보존과 복원&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5659%;&quot;&gt;&lt;b&gt;구조 변경&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.403%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;객체 레이아웃과 메타데이터 변경에 주의&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.031%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;상대적으로 유연하지만 모든 런타임 상태를 보존하지는 않음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5659%;&quot;&gt;&lt;b&gt;별도 초기화 경계&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.403%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;에디터 재시작과 전체 빌드&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.031%;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;Play Mode의 Domain Reload 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 id=&quot;workflow-title&quot; 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 style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;함수 내부만 수정&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;Unreal은 Live Coding, Unity는 일반 스크립트 재컴파일로 빠르게 검증합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;필드와 리플렉션 선언 변경&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;새 인스턴스뿐 아니라 기존 레벨 객체, Prefab 또는 Blueprint의 저장값과 참조까지 확인합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;생성&amp;middot;초기화 코드 변경&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;이미 살아 있는 객체만 보지 말고 Play Mode나 에디터를 다시 시작해 처음부터 실행되는 경로를 검사합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;상속&amp;middot;모듈&amp;middot;빌드 구성 변경&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;Unreal은 에디터 종료 후 전체 빌드를 우선하고, Unity도 어셈블리 정의와 패키지 의존성까지 다시 검증합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;결과가 의심스러움&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;핫 리로드 상태를 디버깅하기보다 깨끗한 재시작에서 같은 현상이 재현되는지 먼저 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;summary-title&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Unreal Live Coding은 새 네이티브 코드를 실행 중인 프로세스에 패치하고, Unity는 C# 어셈블리와 관리 코드 환경을 다시 구성합니다. 그래서 Unreal에서는 객체의 메모리 구조가 바뀌는 수정에 특히 주의해야 하고, Unity에서는 직렬화되지 않는 런타임 상태와 Domain Reload 설정을 의식해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로는 작은 함수 구현 변경에는 빠른 갱신 기능을 사용하되, 타입 구조&amp;middot;생성 과정&amp;middot;빌드 구성이 바뀌면 깨끗한 재시작과 전체 빌드로 검증하겠습니다. 빠른 반복 도구는 재시작을 없애는 기능이 아니라, 안전하게 생략할 수 있는 재시작을 구분해 주는 도구에 가깝습니다.&lt;/p&gt;
&lt;h2 id=&quot;references-title&quot; 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 style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #111111;&quot; href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/using-live-coding-to-recompile-unreal-engine-applications-at-runtime&quot;&gt;Epic Games &amp;middot; 라이브 코딩을 사용하여 런타임 중에 언리얼 엔진 애플리케이션 리컴파일하기&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #111111;&quot; href=&quot;https://docs.unity3d.com/Manual/DomainReloading.html&quot;&gt;Unity Manual &amp;middot; Domain Reloading&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #111111;&quot; href=&quot;https://docs.unity3d.com/Manual/script-serialization-how-unity-uses.html&quot;&gt;Unity Manual &amp;middot; How Unity uses serialization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내배캠_Unreal10기</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/42</guid>
      <comments>https://devdiary-sj.tistory.com/42#entry42comment</comments>
      <pubDate>Tue, 11 Aug 2026 17:30:24 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 첫 번째 팀 프로젝트 회고</title>
      <link>https://devdiary-sj.tistory.com/41</link>
      <description>&lt;h2 id=&quot;project-title&quot; data-ke-size=&quot;size26&quot;&gt;프로젝트와 문제&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CH2 Text RPG&lt;/b&gt;는 여섯 명이 C++17로 만든 콘솔 게임입니다. 플레이어는 재료 몬스터를 처치해 골드와 식재료를 얻고, 요리를 완성해 능력치를 높입니다. 일반 요리 열 개를 완성하면 궁극의 햄버거와 보스전이 열리고, 최종 요리를 만들면 엔딩에 도달합니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;개발을 시작할 때 저는&lt;span&gt;&amp;nbsp;&lt;/span&gt;BattleManager,&lt;span&gt;&amp;nbsp;&lt;/span&gt;RecipeManager,&lt;span&gt;&amp;nbsp;&lt;/span&gt;ShopManager,&lt;span&gt;&amp;nbsp;&lt;/span&gt;UIManager&lt;span&gt;&amp;nbsp;&lt;/span&gt;등 기능별 매니저 파일을 빈 골격으로 먼저 생성했습니다. 시스템의 경계를 파일과 클래스 단위로 제시한 뒤 팀원들이 역할에 따라 각 영역의 내부 구현을 채우는 방식으로 작업을 나눴습니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;팀원들의 구현이 모인 초기 통합본에는 각 클래스와 함수가 존재했지만, 메뉴 선택은 실제 동작으로 이어지지 않았고 전투 보상은 인벤토리에 들어가지 않았습니다. 레시피와 상점 역시 화면과 로직이 분리된 채였기 때문에, 처음에 나눈 시스템 경계를 실제 게임의 실행 흐름으로 다시 연결해야 했습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;핵심 문제는 클래스의 부재가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;객체 사이의 계약과 상태 전이의 부재&lt;/b&gt;였습니다. &amp;ldquo;전투 기능이 있다&amp;rdquo;와 &amp;ldquo;전투를 마친 뒤 보상을 받고 메뉴로 돌아갈 수 있다&amp;rdquo;는 서로 다른 완성 조건입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;responsibility-title&quot; data-ke-size=&quot;size26&quot;&gt;내 역할과 설계 기준&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;저는 개발 초기에 기능별 매니저 파일과 클래스 골격을 만들고 팀의 작업 영역을 나눴습니다. 이후 프로그램의 진입점과&lt;span&gt;&amp;nbsp;&lt;/span&gt;GameManager, 전체 진행 흐름, 팀 코드 통합을 맡았습니다. 플레이어 초기화부터 튜토리얼, 메인 루프, 스킬 선택, 전투 보상, 요리, 상점, 보스전과 엔딩까지 하나의 유스케이스로 연결했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;즉 제 역할은 완성된 시스템을 마지막에 이어 붙이는 데서 시작하지 않았습니다. 먼저 시스템별 책임이 놓일 자리를 만들고, 팀원들이 구현한 코드를 그 구조 안에서 조율한 다음, 시스템 사이의 인터페이스와 상태 전이를 완성하는 작업까지 포함했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;통합 기준은 &amp;ldquo;매니저끼리 직접 참조하지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;GameManager가 조정한다&amp;rdquo;였습니다. UI가 상점을 직접 호출하거나 전투가 화면에 직접 출력하면 한 시스템의 인터페이스 변경이 다른 시스템으로 번지고 순환 의존성이 생기기 쉽기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;objectivec&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;UIManager ── 선택값 ──▶ GameManager ── 명령 ──▶ 각 도메인 시스템
UIManager ◀── 출력 데이터 ─ GameManager ◀── 실행 결과 ─ 각 도메인 시스템&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;각 시스템은 자신의 규칙을 처리하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;GameManager는 호출 순서와 데이터 전달을 담당하도록 경계를 잡았습니다.&lt;/p&gt;
&lt;h2 id=&quot;orchestration-title&quot; data-ke-size=&quot;size26&quot;&gt;GameManager로 흐름 연결하기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;막연한 &amp;ldquo;전투&amp;rdquo;를 실행 가능한 단계로 풀었습니다. 몬스터 생성 &amp;rarr; 스킬 선택 &amp;rarr; 자동 전투 &amp;rarr; 로그 출력 &amp;rarr; 승패 확인 &amp;rarr; 보상 지급 &amp;rarr; 체력&amp;middot;마나 복구 &amp;rarr; 메뉴 복귀 순서입니다. 메인 루프는 UI가 반환한 선택값만 해석합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;while (isRunning &amp;amp;&amp;amp; !recipe.IsFinalRecipeCompleted())
{
    switch (ui.PrintMenu())
    {
    case 1: RunNextIngredientBattle(); break;
    case 2: ui.PrintInventory(player.GetInventory()); break;
    case 3: HandleRecipeMenu(); break;
    case 4: HandleShopMenu(); break;
    case 0: isRunning = false; break;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;상점도 같은 방향으로 연결했습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;ShopManager가 상품 목록을 제공하면 UI는 이를 표시하고 선택한 ID만 반환합니다. 구매 가능 여부, 아이템 지급, 골드 차감은&lt;span&gt;&amp;nbsp;&lt;/span&gt;PurchaseItem()이 처리하며, 성공 여부를 다시 UI에 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;const int itemId = ui.PrintShop(shop.GetShopItems());
if (itemId &amp;lt;= 0) return;

const bool purchased = shop.PurchaseItem(player, itemId);
ui.PrintShopPurchaseResult(purchased, player);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;UI가 입력과 상태 변경을 함께 처리하지 않기 때문에 취소, 구매 실패, 메뉴 복귀 같은 전이를 한곳에서 추적할 수 있게 됐습니다.&lt;/p&gt;
&lt;h2 id=&quot;battle-data-title&quot; data-ke-size=&quot;size26&quot;&gt;전투 로직과 출력 분리&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;전투는 자동 진행되지만 시작 전에 스킬 하나를 선택합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;GameManager가 선택 ID를&lt;span&gt;&amp;nbsp;&lt;/span&gt;Skill&lt;span&gt;&amp;nbsp;&lt;/span&gt;포인터로 변환해 전달하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;BattleManager는 턴 주기나 HP 비율 같은 발동 조건과 MP를 검사합니다. 조건이 맞지 않으면 기본 공격으로 대체합니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;중요한 결정은 전투 중 콘솔을 직접 출력하지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;BattleInfo&lt;span&gt;&amp;nbsp;&lt;/span&gt;목록을 반환한 것입니다. 각 항목에는 턴, 사용 스킬, 몬스터 ID와 이름, 양측 피해량, 남은 HP가 담깁니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;const auto result = battle.StartBattle(player, monster, selectedSkill);
ui.PrintBattleLog(result);

if (result.first == BattleResult::Win)
{
    ApplyBattleReward(monster);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조에서 전투는 규칙만 알고 UI는 표현만 압니다. 콘솔 출력 형식을 바꾸더라도 피해 계산을 수정할 필요가 없고, 같은 로그 데이터로 리플레이나 다른 화면을 구현할 여지도 생깁니다.&lt;/p&gt;
&lt;h2 id=&quot;ownership-title&quot; data-ke-size=&quot;size26&quot;&gt;상태와 소유권 정리&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Player와&lt;span&gt;&amp;nbsp;&lt;/span&gt;Monster의 공통 능력치는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Character에 두고, 아이템은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Item에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Potion과&lt;span&gt;&amp;nbsp;&lt;/span&gt;Ingredient로 나눴습니다. 반면 플레이어와 인벤토리는 &amp;ldquo;is-a&amp;rdquo;가 아니므로 상속 대신 합성을 사용했습니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;struct InventorySlot
{
    std::unique_ptr&amp;lt;Item&amp;gt; ItemPtr;
    int Count;
};

class Player : public Character
{
    Inventory PlayerInventory;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;unique_ptr는 슬롯이 아이템을 단독 소유한다는 사실을 타입으로 표현합니다. 슬롯이 사라질 때 아이템도 함께 정리되어 수동 해제와 이중 해제 위험을 줄입니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;상태 변경 역시 데이터를 가진 객체에 두었습니다. 골드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Player::AddGold()와&lt;span&gt;&amp;nbsp;&lt;/span&gt;SpendGold(), 아이템 수량은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Inventory::AddItem()과&lt;span&gt;&amp;nbsp;&lt;/span&gt;ReduceItem(), 레시피 완료 상태는&lt;span&gt;&amp;nbsp;&lt;/span&gt;RecipeManager::CompleteRecipe()가 담당합니다. 외부에서 멤버를 직접 수정하는 것보다 &amp;ldquo;부족하면 차감하지 않는다&amp;rdquo;, &amp;ldquo;수량은 음수가 되지 않는다&amp;rdquo; 같은 불변식을 지키기 쉽습니다.&lt;/p&gt;
&lt;h2 id=&quot;integration-title&quot; data-ke-size=&quot;size26&quot;&gt;통합 과정에서 해결한 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레시피와 실제 인벤토리 연결&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;RecipeManager는 제작 가능 여부를 판단할 수 있었지만 플레이어 인벤토리를 직접 알지 못했습니다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;GameManager가 슬롯을&lt;span&gt;&amp;nbsp;&lt;/span&gt;RecipeIngredient&lt;span&gt;&amp;nbsp;&lt;/span&gt;목록으로 변환해 전달하고, 성공하면 레시피 상태 변경과 음식 능력치 적용을 차례로 호출했습니다. 독립된 두 모델 사이에 명시적인 변환 경계를 둔 셈입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;취소값을 상태 전이로 취급&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;레시피 상세 화면에서 취소를 눌러도 제작되는 버그는 반환값을 무시한 것이 원인이었습니다. 모든 UI 입력에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;0을 취소 또는 이전 화면으로 통일하고, 유효 범위를 검사한 뒤에만 도메인 동작을 호출하도록 수정했습니다. 콘솔 게임에서 입력값은 단순 숫자가 아니라 다음 상태를 결정하는 이벤트입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인코딩과 저장소 설정&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;UTF-8과 CP949가 섞인 파일에서는 한글이 깨지고 문자열 리터럴이 잘못 해석되었습니다. 파일별 인코딩을 확인해 필요한 파일을 변환했고, Git에 이미 추적된 빌드 산출물은&lt;span&gt;&amp;nbsp;&lt;/span&gt;.gitignore만 추가해도 사라지지 않는다는 점도 확인했습니다. 인코딩&amp;middot;줄바꿈&amp;middot;무시 규칙은 통합 단계의 잡일이 아니라 사전에 합의할 빌드 계약이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;verification-title&quot; data-ke-size=&quot;size26&quot;&gt;검증과 남은 한계&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;기능을 연결할 때마다 Visual Studio의 x64 Debug 빌드로 컴파일과 링크를 확인하고, 콘솔 입력을 전달해 튜토리얼 전투, 보상, 메뉴 복귀, 스킬 선택, ASCII 아트 출력까지 실행했습니다. 마지막에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;git diff --check로 공백과 줄바꿈 문제를 점검했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 빌드 성공은 실행 흐름의 정확성을 보장하지 않습니다. 자동화된 테스트가 없어 메뉴 경로를 수동으로 검증했고, 후반 콘텐츠를 빠르게 확인하기 위해 재료 차감 코드가 임시로 비활성화된 상태도 소스에 남았습니다. 이는 제품 규칙이 아니라 테스트 편의를 위한 기술 부채이며, 발행 시점의 코드 한계로 명확히 구분해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;또한 모든 조정 책임을 모은&lt;span&gt;&amp;nbsp;&lt;/span&gt;GameManager는 약 300줄 규모로 성장했습니다. 작은 프로젝트에서는 의존 흐름을 한눈에 볼 수 있었지만, 콘텐츠가 늘면 전투 진행, 메뉴 상태, 보상 처리 같은 흐름별 컨트롤러로 분리해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;lessons-title&quot; 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 style=&quot;color: #514c45;&quot;&gt;클래스를 나누는 것보다 객체가 주고받을 입력, 결과, 실패 조건을 먼저 합의해야 합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;UI는 선택을 반환하고 상태 변경은 해당 데이터를 소유한 객체가 맡아야 흐름을 추적하기 쉽습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;DTO 역할의&lt;span&gt;&amp;nbsp;&lt;/span&gt;BattleInfo처럼 로직과 표현 사이의 데이터 계약을 두면 변경 범위가 줄어듭니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;중앙 조정자는 직접 참조를 줄이지만, 커지기 시작하면 유스케이스 단위로 다시 분리해야 합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;컴파일뿐 아니라 실제 사용자 입력 경로를 실행해야 통합이 끝납니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;다음 팀 프로젝트에서는 구현 전에 공개 인터페이스와 공통 ID, 반환값과 실패 조건을 문서화하고, 인벤토리 수량 변경&amp;middot;구매 실패&amp;middot;레시피 제작 조건부터 단위 테스트를 추가할 계획입니다. PR에는 빌드 결과와 실행한 시나리오를 기록하고 UTF-8과 줄바꿈 규칙도 저장소 생성 시점에 고정해야 합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 얻은 결론은 단순합니다. 객체지향은 클래스를 많이 만드는 기술이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;변경의 책임을 어디에 두고 의존성이 어느 방향으로 흐르게 할지 결정하는 과정&lt;/b&gt;입니다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>내배캠_Unreal10기</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/41</guid>
      <comments>https://devdiary-sj.tistory.com/41#entry41comment</comments>
      <pubDate>Wed, 5 Aug 2026 16:56:22 +0900</pubDate>
    </item>
    <item>
      <title>[운영체제] 프로세스와 CPU 실행 구조</title>
      <link>https://devdiary-sj.tistory.com/40</link>
      <description>&lt;h2 id=&quot;overview-title&quot; data-ke-size=&quot;size26&quot;&gt;실행 흐름 한눈에 보기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;프로그램은 디스크에 저장된 명령과 데이터의 묶음입니다. 사용자가 프로그램을 실행하면 운영체제는 주소 공간과 자원을 준비해 프로세스를 만들고, 프로세스 안의 스레드를 실행 가능한 상태로 둡니다. 스케줄러가 그중 하나를 고르면 CPU가 해당 스레드의 레지스터와 프로그램 카운터를 복원하고 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;실행 파일
  &amp;rarr; 프로세스 생성: 가상 주소 공간과 OS 자원 준비
  &amp;rarr; 스레드 준비: PC, 레지스터, 스택과 실행 상태 생성
  &amp;rarr; Ready Queue에서 대기
  &amp;rarr; Scheduler가 실행 대상 선택
  &amp;rarr; CPU가 명령어 실행
  &amp;rarr; I/O 대기&amp;middot;시간 할당량 만료&amp;middot;종료
  &amp;rarr; 필요하면 Context Switch&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;CPU가 직접 실행하는 흐름은 스레드입니다. 프로세스는 그 실행에 필요한 주소 공간과 자원을 담는 경계라고 이해하면 뒤의 개념이 자연스럽게 연결됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;process-title&quot; data-ke-size=&quot;size26&quot;&gt;프로그램과 프로세스&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로그램&lt;/b&gt;은 실행 전의 정적인 파일이고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프로세스&lt;/b&gt;는 운영체제가 실행을 위해 관리하는 프로그램의 인스턴스입니다. 같은 실행 파일을 두 번 실행하면 코드 내용은 같더라도 서로 다른 프로세스와 주소 공간을 가질 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;가상 주소 공간
높은 주소  ┌─────────────┐
           │ Stack       │  함수 호출, 지역 변수
           │      &amp;darr;      │
           │             │
           │      &amp;uarr;      │
           │ Heap        │  동적 할당
           ├─────────────┤
           │ Data / BSS  │  전역&amp;middot;정적 데이터
낮은 주소  │ Code        │  실행 명령
           └─────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;운영체제는 프로세스에 가상 주소 공간뿐 아니라 열린 파일, 보안 자격 증명, 신호 처리 정보 같은 자원을 연결합니다. 프로세스마다 주소 공간이 격리되므로 한 프로세스의 일반 포인터로 다른 프로세스의 메모리를 직접 읽을 수 없습니다. 이 경계는 오류와 권한을 격리하지만 데이터를 주고받을 때는 IPC가 필요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;그림의 영역은 개념적인 구분입니다. 실제 배치, 공유 라이브러리, 메모리 매핑과 주소 증가 방향은 운영체제&amp;middot;ABI&amp;middot;실행 형식에 따라 달라질 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;thread-title&quot; data-ke-size=&quot;size26&quot;&gt;실행 단위인 스레드&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스레드&lt;/b&gt;는 프로세스 안의 실행 흐름입니다. 같은 프로세스의 스레드들은 코드, 전역 데이터, 힙과 열린 파일 같은 자원을 공유합니다. 반면 각 스레드는 자신의 스택, 프로그램 카운터, 레지스터 값과 스케줄링 상태를 가집니다.&lt;/p&gt;
&lt;div&gt;구분프로세스 안에서 공유스레드마다 독립메모리실행 상태자원
&lt;table style=&quot;background-color: #fffaf1; text-align: left; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;코드, 데이터, 힙, 메모리 매핑&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;스택과 스레드 지역 저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;프로세스 수준 설정&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;PC, 레지스터, 상태와 우선순위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;열린 파일 등 프로세스 자원&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;스레드 ID와 스케줄링 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;공유 메모리는 스레드 사이의 데이터 전달을 빠르게 만들지만, 동시에 같은 데이터를 수정하면 데이터 레이스가 생길 수 있습니다. 따라서 빠른 통신만 보고 스레드를 선택할 수는 없습니다. 상태 소유권, 동기화와 실패가 미치는 범위를 함께 설계해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;state-title&quot; data-ke-size=&quot;size26&quot;&gt;프로세스 상태와 대기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;실행 중인 모든 스레드가 항상 CPU를 사용하는 것은 아닙니다. 설명을 단순화하면 실행 주체는 다음 상태 사이를 이동합니다. 운영체제마다 상태 이름과 세부 단계는 다르지만 핵심 흐름은 같습니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;           admit
New ─────────────&amp;rarr; Ready ──dispatch──&amp;rarr; Running ──exit──&amp;rarr; Terminated
                     &amp;uarr;                    │
                     │ preempt            │ I/O 요청&amp;middot;이벤트 대기
                     └────────────────────┤
                                          &amp;darr;
                                      Waiting
                                          │ I/O 완료&amp;middot;이벤트 발생
                                          └────────────&amp;rarr; Ready&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;Ready&lt;/b&gt;: 실행할 준비가 되었지만 CPU를 기다립니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;Running&lt;/b&gt;: 현재 CPU 코어에서 명령어를 실행합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;Waiting 또는 Blocked&lt;/b&gt;: 파일 읽기, 네트워크 수신, 락이나 이벤트처럼 어떤 조건이 충족되기를 기다립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;I/O를 기다리는 스레드를 계속 CPU에서 실행하는 것은 낭비입니다. 운영체제는 이 스레드를 대기 상태로 옮기고 다른 준비된 스레드에 CPU를 줍니다. I/O 완료 인터럽트나 동기화 사건이 발생하면 다시 Ready 상태로 돌아갑니다.&lt;/p&gt;
&lt;h2 id=&quot;control-block-title&quot; data-ke-size=&quot;size26&quot;&gt;PCB와 TCB: 중단된 실행을 기억하는 정보&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PCB(Process Control Block)&lt;/b&gt;는 운영체제가 프로세스를 관리하기 위해 유지하는 커널 자료구조를 가리키는 일반적인 이름입니다. 프로세스 ID, 상태, 주소 공간과 메모리 관리 정보, 열린 파일, 보안 정보와 스케줄링 관련 정보 등이 연결됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;스레드별 실행 문맥은 흔히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;TCB(Thread Control Block)&lt;/b&gt;라고 설명합니다. 여기에는 스레드 ID, PC와 레지스터 같은 CPU 문맥, 스택 정보, 상태와 스케줄링 속성이 포함될 수 있습니다. 실제 커널 구현은 PCB와 TCB를 반드시 별도 구조체로 나누지는 않습니다. 예를 들어 Linux는 프로세스와 스레드를 모두 task로 다루며 여러 커널 구조를 연결해 관리합니다.&lt;/p&gt;
&lt;div&gt;관리 범위대표 정보목적프로세스 수준스레드 수준
&lt;table style=&quot;background-color: #fffaf1; text-align: left; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;PID, 주소 공간, 파일, 권한, 프로세스 상태&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;자원과 격리 경계 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;PC, 레지스터, 스택, 우선순위, 실행 상태&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;중단된 실행 흐름 재개&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;PCB에 모든 레지스터가 항상 저장되어 있다&amp;rdquo;기보다, 실행을 멈추는 경로에서 필요한 문맥을 커널 스택과 관련 구조에 저장하고 다시 스케줄될 때 복원한다고 이해하는 편이 정확합니다. 저장 범위와 방식은 CPU 아키텍처와 운영체제 구현에 따라 달라집니다.&lt;/p&gt;
&lt;h2 id=&quot;scheduling-title&quot; data-ke-size=&quot;size26&quot;&gt;CPU 스케줄링: 다음에는 누가 실행될까&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;실행 가능한 스레드가 CPU 코어보다 많으면 모두가 물리적으로 동시에 실행될 수 없습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;스케줄러&lt;/b&gt;는 Ready 상태의 작업 가운데 다음 실행 대상을 고릅니다. 목표는 환경에 따라 처리량, 평균 대기 시간, 응답성, 공정성, 마감 시간 보장처럼 달라집니다.&lt;/p&gt;
&lt;div&gt;방식핵심 아이디어주요 특성FCFSSJFRound RobinPriority
&lt;table style=&quot;background-color: #fffaf1; text-align: left; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;먼저 도착한 작업부터 실행&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;단순하지만 긴 작업 뒤에 짧은 작업이 기다릴 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;예상 실행 시간이 짧은 작업부터 실행&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;평균 대기 시간을 줄일 수 있지만 실행 시간 예측이 필요합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;각 작업에 시간 할당량을 주고 순환&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;응답성이 좋지만 할당량이 너무 짧으면 전환 비용이 커집니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;우선순위가 높은 작업을 먼저 실행&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;낮은 우선순위의 기아를 막는 정책이 필요할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이 알고리즘들은 원리를 이해하기 위한 모델입니다. 범용 운영체제의 실제 스케줄러는 우선순위, 공정성, CPU 친화도, 실시간 정책, 멀티코어 부하 분산 등을 함께 고려합니다. 또한 프로세스보다 스레드가 실질적인 스케줄링 단위인 시스템이 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;선점은 왜 필요한가&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선점형 스케줄링&lt;/b&gt;에서는 시간 할당량 만료나 더 높은 우선순위 작업의 준비 같은 이유로 운영체제가 현재 실행을 멈출 수 있습니다. 한 작업이 CPU를 오래 독점하지 못하게 해 상호작용 프로그램의 응답성을 지킵니다. 반대로 협력형 방식은 실행 중인 작업이 자발적으로 CPU를 양보해야 하므로 잘못된 작업 하나가 전체 응답성을 해칠 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;context-switch-title&quot; data-ke-size=&quot;size26&quot;&gt;문맥 교환과 보이지 않는 비용&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문맥 교환(Context Switch)&lt;/b&gt;은 CPU가 실행할 스레드나 프로세스를 바꾸는 과정입니다. 현재 실행 상태를 저장하고 다음 대상의 상태를 복원한 뒤, 그 대상이 중단되었던 지점부터 실행을 계속합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;인터럽트, 시스템 호출, 대기 또는 선점으로 커널이 제어권을 얻습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;현재 실행의 PC, 스택 포인터와 필요한 레지스터 상태를 저장합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;스케줄러가 다음 실행 대상을 선택합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;주소 공간과 커널의 현재 작업 정보를 필요에 따라 전환합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;다음 대상의 문맥을 복원하고 사용자 코드로 돌아갑니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;문맥 교환은 애플리케이션의 실제 문제를 직접 처리하지 않으므로 오버헤드입니다. 직접 비용에는 커널 진입, 레지스터 저장&amp;middot;복원과 스케줄러 실행이 있습니다. 간접 비용으로는 새 작업의 코드와 데이터가 캐시에 없어 생기는 cache miss, 분기 예측기 상태 변화, 주소 공간 전환에 따른 TLB 영향이 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;프로세스 전환이 항상 스레드 전환보다 일정한 값만큼 비싼 것은 아닙니다. 다른 주소 공간으로 바뀌면 메모리 관리 문맥의 영향이 추가될 수 있지만, PCID/ASID, TLB 관리 방식, 캐시 공유와 스케줄링 위치에 따라 실제 비용은 크게 달라집니다. 성능 판단에는 측정이 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;인터럽트와 문맥 교환도 같은 말이 아닙니다. 인터럽트 처리 후 원래 스레드로 복귀할 수 있고, 시스템 호출도 다른 스레드를 선택하지 않으면 스케줄링 문맥 교환은 일어나지 않습니다.&lt;/p&gt;
&lt;h2 id=&quot;ipc-title&quot; data-ke-size=&quot;size26&quot;&gt;IPC: 격리된 프로세스가 협력하는 방법&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;서로 다른 프로세스는 기본적으로 주소 공간이 분리되어 있으므로 운영체제가 제공하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;IPC(Inter-Process Communication)&lt;/b&gt;를 사용합니다. 선택 기준은 같은 컴퓨터인지, 데이터 양과 지연 요구가 어떤지, 메시지 경계가 필요한지, 실패를 얼마나 격리해야 하는지입니다.&lt;/p&gt;
&lt;div&gt;방식특징주의점Pipe / Named PipeMessage QueueShared MemorySocketSignal / Event
&lt;table style=&quot;background-color: #fffaf1; text-align: left; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;바이트 스트림으로 단순하게 연결&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;방향, 수명과 메시지 경계를 설계해야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;메시지 단위 교환과 비동기 처리&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;크기 제한, 복사와 큐 포화 정책이 필요합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;큰 데이터를 낮은 복사 비용으로 공유&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;동기화, 수명과 메모리 가시성을 직접 관리해야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;로컬 및 네트워크 통신에 사용 가능&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;직렬화, 부분 송수신, 연결 실패를 처리해야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;사건이나 상태 변화를 알림&lt;/td&gt;
&lt;td style=&quot;color: #514c45;&quot;&gt;복잡한 데이터 전달보다 알림에 적합합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Shared Memory는 데이터를 매번 커널 버퍼로 복사하지 않을 수 있어 빠르지만, 그 자체로 안전한 통신을 보장하지는 않습니다. 어느 프로세스가 언제 쓰는지 정하기 위한 mutex, semaphore, atomic 연산이나 명시적인 소유권 프로토콜이 필요합니다.&lt;/p&gt;
&lt;h2 id=&quot;game-title&quot; data-ke-size=&quot;size26&quot;&gt;게임 클라이언트에서의 적용&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;게임 클라이언트에는 게임 로직, 렌더링 준비, 오디오, 네트워크, 파일 I/O와 작업 시스템이 함께 존재할 수 있습니다. 이 이름들이 반드시 각각 하나의 전용 스레드를 뜻하는 것은 아닙니다. 엔진과 플랫폼에 따라 전용 스레드, 워커 풀, 비동기 I/O와 GPU 큐의 조합으로 구현됩니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;Main / Game Thread
  ├─ 입력과 게임 상태 갱신
  ├─ 작은 작업들을 Worker Pool에 제출
  ├─ Network&amp;middot;I/O 완료 결과 소비
  └─ 렌더링에 필요한 상태 전달

Worker Threads
  └─ 서로 독립적인 애니메이션&amp;middot;물리&amp;middot;가시성 계산 등&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스레드를 늘리기 전에 확인할 것&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;작업이 병렬 가능한가?&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;앞 작업의 결과를 기다린다면 스레드 수보다 의존성 구조를 먼저 개선해야 합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;작업 크기가 충분한가?&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;매우 작은 작업은 큐잉, 깨우기와 문맥 교환 비용이 계산 시간보다 커질 수 있습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;공유 쓰기가 많은가?&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;락 경합과 코어 사이의 캐시 라인 이동이 병렬 이득을 상쇄할 수 있습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;코어보다 실행 가능한 스레드가 과도하게 많은가?&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;oversubscription은 문맥 교환과 캐시 손실을 늘립니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;평균이 아닌 프레임 시간이 안정적인가?&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;평균 FPS뿐 아니라 긴 프레임과 작업 타임라인을 프로파일링해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 파일이나 네트워크 입력을 기다리는 작업은 CPU를 계속 점유하지 않고 대기 상태가 되는 편이 낫습니다. 반면 프레임마다 수행하는 순수 계산은 코어 수와 작업 크기에 맞춰 워커 풀에 분배할 수 있습니다. 핵심은 &amp;ldquo;스레드를 많이 만든다&amp;rdquo;가 아니라 CPU를 쓸 수 있는 작업과 기다려야 하는 작업을 분리하고, 공유 상태를 최소화하는 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;summary-title&quot; 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 style=&quot;color: #514c45;&quot;&gt;프로그램은 저장된 파일이고, 프로세스는 주소 공간과 운영체제 자원을 가진 실행 인스턴스입니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;CPU가 실행하는 흐름은 스레드이며, 각 스레드는 PC, 레지스터와 스택을 따로 가집니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;PCB와 TCB는 프로세스 자원과 스레드 실행 상태를 관리한다는 개념적 구분입니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;스케줄러는 Ready 상태의 작업 가운데 CPU를 받을 대상을 선택합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;문맥 교환에는 직접적인 저장&amp;middot;복원 비용과 캐시&amp;middot;TLB에 미치는 간접 비용이 있습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;프로세스 격리는 안정성을 높이지만 데이터 교환에는 IPC와 명시적인 통신 규약이 필요합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;게임에서는 스레드 수보다 작업 의존성, 공유 상태, 락 경합과 최악 프레임 시간을 먼저 봐야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;한 문장으로 정리하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프로세스는 실행 자원의 경계이고, 스레드는 CPU가 스케줄링하는 실행 흐름이며, 운영체제는 저장된 문맥을 교체해 제한된 코어 위에서 여러 작업을 진행시킵니다.&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;references-title&quot; 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 style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://docs.kernel.org/scheduler/sched-design-CFS.html&quot;&gt;Linux Kernel Documentation: CFS Scheduler&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://man7.org/linux/man-pages/man7/sched.7.html&quot;&gt;Linux manual page: sched(7)&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://man7.org/linux/man-pages/man7/signal.7.html&quot;&gt;Linux manual page: signal(7)&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://learn.microsoft.com/en-us/windows/win32/procthread/processes-and-threads&quot;&gt;Microsoft Learn: Processes and Threads&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>운영체제</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/40</guid>
      <comments>https://devdiary-sj.tistory.com/40#entry40comment</comments>
      <pubDate>Mon, 27 Jul 2026 20:41:29 +0900</pubDate>
    </item>
    <item>
      <title>[컴퓨터 구조] 파이프라인 Hazard, Interrupt, DMA</title>
      <link>https://devdiary-sj.tistory.com/39</link>
      <description>&lt;h2 id=&quot;overview-title&quot; data-ke-size=&quot;size26&quot;&gt;서로 다른 두 종류의 병목&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hazard&lt;/b&gt;는 한 코어 안에서 여러 명령어를 겹쳐 실행할 때 다음 단계로 진행하지 못하는 문제입니다. 반면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DMA&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Interrupt&lt;/b&gt;는 CPU와 입출력 장치가 일을 나누는 방법입니다. 발생 위치는 다르지만 목표는 같습니다. CPU가 불필요하게 기다리는 시간을 줄이는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;CPU 내부: 명령어 A &amp;middot; B &amp;middot; C를 겹쳐 실행
              └─ 의존성이나 자원 충돌 &amp;rarr; Hazard

CPU 외부: CPU가 전송 설정 &amp;rarr; DMA가 데이터 이동
                              └─ 완료 &amp;rarr; Interrupt&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 파이프라인 안에서 무엇이 흐름을 막는지 살펴보고, 이후 장치 I/O가 CPU와 어떻게 비동기로 협력하는지 연결해 보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;hazard-title&quot; data-ke-size=&quot;size26&quot;&gt;파이프라인 Hazard&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;설명용 5단계 파이프라인은 IF(인출), ID(해석), EX(실행), MEM(메모리 접근), WB(결과 기록)로 나눌 수 있습니다. 파이프라이닝은 명령어 하나의 지연 시간을 줄이기보다 여러 명령어의 단계를 겹쳐&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;처리량&lt;/b&gt;을 높입니다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;Cycle 1: A IF
Cycle 2: A ID  | B IF
Cycle 3: A EX  | B ID  | C IF
Cycle 4: A MEM | B EX  | C ID
Cycle 5: A WB  | B MEM | C EX&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 다음 명령어가 필요한 데이터&amp;middot;실행 경로&amp;middot;하드웨어 자원을 확보하지 못하면 Hazard가 발생합니다. CPU는 진행을 잠시 멈추는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;stall&lt;/b&gt;을 만들 수 있고, 그 결과 파이프라인에 유효한 작업이 없는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;bubble&lt;/b&gt;이 생깁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Data Hazard: 필요한 값이 아직 준비되지 않았다&lt;/h3&gt;
&lt;pre class=&quot;tp&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;ADD R1, R2, R3   ; R1 = R2 + R3
SUB R4, R1, R5   ; R1이 필요하다&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;두 번째 명령어는 첫 번째 명령어가 만든 R1을 읽어야 합니다. 이런&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RAW(Read After Write)&lt;/b&gt;는 실제 데이터 흐름 때문에 생기는 진짜 의존성입니다. 결과가 레지스터 파일에 기록될 때까지 기다리지 않고 EX나 MEM 단계의 결과를 다음 실행 유닛 입력으로 보내는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;forwarding&lt;/b&gt;으로 대기 시간을 줄일 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;ADD의 ALU 결과 ─────────&amp;rarr; SUB의 ALU 입력
              forwarding&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;다만 forwarding이 모든 대기를 없애지는 못합니다. 예를 들어 load 직후 그 값을 사용하는 명령어는 데이터가 늦게 준비되어 한 사이클 이상 멈출 수 있습니다. 정확한 비용은 파이프라인 구조에 따라 달라집니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WAR(Write After Read)&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;WAW(Write After Write)&lt;/b&gt;는 레지스터 이름을 재사용해 생기는 거짓 의존성입니다. 단순한 순차 파이프라인에서는 명령어가 정해진 순서로 읽고 쓰므로 보통 나타나지 않지만, 비순차 실행이나 쓰기 단계가 여러 개인 구조에서는 순서가 뒤바뀔 수 있습니다. 현대 CPU는 논리 레지스터를 서로 다른 물리 레지스터에 대응시키는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;레지스터 리네이밍&lt;/b&gt;으로 이를 제거합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Control Hazard: 다음 명령어 주소를 모른다&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;조건 분기의 결과가 확정되기 전에는 어느 경로에서 명령어를 가져와야 할지 모릅니다. 결과를 기다리면 프런트엔드가 쉬게 되므로 CPU는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;분기 예측&lt;/b&gt;을 사용해 한 경로를 먼저 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;분기 예측 성공 &amp;rarr; 준비한 명령어를 계속 실행
분기 예측 실패 &amp;rarr; 잘못된 경로를 flush &amp;rarr; 올바른 주소에서 다시 fetch&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;예측 실패 비용은 잘못 들어온 작업을 버리고 파이프라인을 다시 채우는 시간입니다. 파이프라인 깊이와 CPU 구조에 따라 비용이 다르므로, 자세한 원리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;branch-prediction.html&quot;&gt;분기 예측 글&lt;/a&gt;에서 따로 설명했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Structural Hazard: 같은 자원을 동시에 요구한다&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;두 작업이 동시에 같은 하드웨어 자원을 요구하지만 수용량이 부족하면 구조적 Hazard가 발생합니다. 명령어 인출과 데이터 접근이 하나의 메모리 포트를 두고 경쟁하거나, 같은 종류의 실행 유닛으로 요청이 몰리는 상황이 예입니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;명령어 캐시와 데이터 캐시를 분리하고, 실행 유닛&amp;middot;load/store 포트&amp;middot;메모리 포트를 늘리면 충돌을 줄일 수 있습니다. 하지만 면적과 전력 비용이 있으므로 현실의 CPU는 자원을 무한히 복제하지 않고 스케줄링과 대기를 함께 사용합니다.&lt;/p&gt;
&lt;h2 id=&quot;interrupt-title&quot; data-ke-size=&quot;size26&quot;&gt;Interrupt와 Exception&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Interrupt&lt;/b&gt;는 타이머 만료, 키 입력, 네트워크 패킷 도착, DMA 완료처럼 현재 실행 중인 명령어 흐름과 독립적으로 발생할 수 있는 비동기 사건입니다. CPU가 장치 상태를 계속 확인하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;polling&lt;/b&gt;과 달리, 사건이 생겼을 때 장치가 처리를 요청합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;장치가 인터럽트 요청을 보냅니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;CPU는 아키텍처가 정한 안전한 경계에서 요청을 받아들입니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;복귀에 필요한 PC와 상태를 하드웨어와 운영체제가 나누어 저장합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;인터럽트 벡터를 통해 ISR(Interrupt Service Routine)로 이동합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;필요한 처리를 마치고 저장한 실행 상태로 복귀합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;현재 명령어를 항상 끝낸 뒤 인터럽트를 처리한다&amp;rdquo;는 설명은 직관적인 모델입니다. 실제로 어느 상태를 저장하고 어느 지점으로 복귀하는지는 ISA와 예외 종류에 따라 다르며, 프로그래머에게 일관된 상태를 보이도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;precise interrupt/exception&lt;/b&gt;을 제공하는지가 중요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Exception은 명령어 실행과 동기적으로 발생한다&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;0으로 나누기, 잘못된 명령어, 페이지 폴트처럼 현재 명령어 실행 때문에 생기는 사건은 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Exception&lt;/b&gt;이라고 구분합니다. x86 문맥에서는 복귀 위치와 복구 가능성에 따라 다음 용어를 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;Fault&lt;/b&gt;: 원인을 처리한 뒤 문제가 된 명령어를 다시 실행할 수 있습니다. 페이지 폴트가 대표적입니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;Trap&lt;/b&gt;: 명령어 실행 뒤 보고되어 다음 명령어로 복귀할 수 있습니다. 디버깅과 시스템 호출에서 볼 수 있습니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;b&gt;Abort&lt;/b&gt;: 정확한 재시작 위치를 보장하기 어려운 심각한 오류입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이 분류는 모든 ISA가 같은 이름과 의미로 사용하지는 않습니다. 예를 들어 어떤 아키텍처는 인터럽트와 예외를 더 큰 범주의 trap으로 묶습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Interrupt는 Context Switch와 다르다&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;인터럽트를 처리하기 위해 실행 문맥 일부를 저장해도, 반드시 다른 프로세스나 스레드로 바뀌는 것은 아닙니다. ISR을 마친 뒤 원래 코드로 돌아갈 수 있습니다. 운영체제가 타이머 인터럽트를 계기로 스케줄러를 실행하고 다른 스레드를 선택했을 때 비로소 스케줄링 문맥의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;context switch&lt;/b&gt;가 일어납니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;Timer Interrupt
  &amp;rarr; ISR과 Scheduler 실행
  &amp;rarr; 같은 Thread 계속 실행 또는 다른 Thread로 Context Switch&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;dma-title&quot; data-ke-size=&quot;size26&quot;&gt;DMA: CPU가 모든 바이트를 옮기지 않게 한다&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DMA(Direct Memory Access)&lt;/b&gt;는 장치 또는 DMA 엔진이 CPU의 개별 load/store 명령을 거치지 않고 메모리와 데이터를 전송하는 방식입니다. &amp;ldquo;CPU를 거치지 않는다&amp;rdquo;는 말은 CPU가 완전히 관여하지 않는다는 뜻이 아닙니다. CPU와 드라이버가 버퍼를 준비하고 전송 방향&amp;middot;크기&amp;middot;장치가 사용할 DMA 주소를 설정하며, 실제 데이터 이동을 하드웨어가 담당한다는 뜻입니다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;CPU 복사: 장치 &amp;rarr; CPU load/store 반복 &amp;rarr; RAM

DMA 전송: CPU가 descriptor와 buffer 설정
          장치/DMA 엔진 ─────────────&amp;rarr; RAM
          CPU는 다른 작업 수행&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;운영체제와 드라이버가 DMA에 사용할 메모리를 할당하거나 매핑합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;CPU가 장치에 DMA 주소, 크기와 전송 방향 등의 descriptor를 전달합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;장치 또는 DMA 엔진이 버스를 통해 메모리를 읽거나 씁니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;완료되면 상태를 기록하고 보통 인터럽트로 CPU에 알립니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;드라이버가 완료를 확인하고 대기 중인 작업을 깨우거나 다음 전송을 제출합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;DMA도 메모리 버스와 캐시 인터커넥트 같은 공유 자원을 사용하므로 &amp;ldquo;공짜 복사&amp;rdquo;는 아닙니다. CPU 연산을 다른 일에 쓸 수 있게 하지만 전송 설정, 주소 변환, 동기화, 버스 대역폭 비용은 남습니다. 작은 복사는 DMA 준비 비용이 더 클 수 있어 전송 크기와 플랫폼에 따라 선택해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DMA 주소는 CPU의 포인터와 같지 않을 수 있다&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;CPU 가상 주소, 물리 주소, 장치가 버스에서 사용하는 DMA 주소는 서로 다를 수 있습니다. 운영체제의 DMA API는 장치가 접근 가능한 주소를 만들고, 필요하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;IOMMU&lt;/b&gt;가 장치 주소를 물리 메모리로 변환합니다. 따라서 드라이버는 일반 포인터를 임의로 장치에 넘기지 않고 플랫폼의 매핑 API를 사용해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;coherence-title&quot; data-ke-size=&quot;size26&quot;&gt;DMA와 메모리 일관성&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;DMA 장치와 CPU가 같은 버퍼를 사용하면 두 종류의 문제가 생깁니다. 첫째는 CPU 캐시와 메모리에 서로 다른 값이 남는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;cache coherence&lt;/b&gt;, 둘째는 데이터와 &amp;ldquo;준비 완료&amp;rdquo; 플래그가 장치에 보이는 순서가 뒤바뀌는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;memory ordering&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;CPU Cache: old data
RAM:       DMA가 기록한 new data

CPU가 캐시를 그대로 읽으면 old data를 볼 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;일관성을 자동으로 유지하는 하드웨어도 있지만 모든 시스템이 그렇지는 않습니다. Linux DMA API는 용도에 따라 장기간 함께 사용하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;coherent mapping&lt;/b&gt;과 전송 단위로 소유권을 넘기는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;streaming mapping&lt;/b&gt;을 구분합니다. 후자는 플랫폼에 따라 map/unmap 또는 sync 과정에서 캐시 정리와 무효화가 필요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;coherent mapping도 메모리 접근 순서까지 자동 보장하지는 않습니다. 예를 들어 descriptor 내용을 먼저 쓰고 마지막에 valid 플래그를 세워야 한다면, 장치가 그 순서로 관찰하도록 적절한 메모리 배리어가 필요합니다. 캐시 일관성, 작업 완료 동기화와 접근 순서는 서로 다른 문제입니다.&lt;/p&gt;
&lt;h2 id=&quot;game-title&quot; data-ke-size=&quot;size26&quot;&gt;게임 리소스 스트리밍으로 연결하기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;오픈 월드에서 다음 지역의 텍스처를 준비하는 상황을 생각해 보겠습니다. 실제 경로는 저장 장치, 운영체제, 그래픽 API와 하드웨어에 따라 달라지지만 개념적인 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;1. 게임이 비동기 파일 읽기를 요청
2. OS&amp;middot;드라이버가 저장 장치의 DMA 전송을 준비
3. SSD &amp;rarr; 시스템 메모리로 데이터 이동
4. 완료 Interrupt &amp;rarr; OS가 요청 완료 처리
5. CPU가 압축 해제&amp;middot;형식 변환 작업을 스케줄
6. 그래픽 API에 copy command와 동기화 조건 기록
7. GPU copy engine이 시스템 메모리 &amp;rarr; VRAM 전송
8. fence/semaphore 완료 뒤 렌더링에서 리소스 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 DMA와 Interrupt는 CPU를 데이터 복사와 상태 확인 반복에서 해방합니다. 그렇다고 CPU 비용이 사라지는 것은 아닙니다. 요청 제출, 압축 해제, 드라이버 처리와 동기화가 남고, 저장 장치&amp;middot;PCIe&amp;middot;메모리&amp;middot;GPU 대역폭 사이에 병목이 생길 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;Hazard는 이 흐름의 다른 층에서 발생합니다. CPU가 완료 처리와 게임 로직을 실행하는 동안에도 각 코어 내부에서는 데이터 의존성, 분기와 실행 포트 경쟁이 처리량을 제한합니다. 즉, DMA가 시스템 수준의 대기를 숨기고 파이프라인 최적화가 코어 내부의 대기를 줄입니다.&lt;/p&gt;
&lt;h2 id=&quot;summary-title&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 126px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.0542%; height: 18px;&quot;&gt;&lt;b&gt;개념&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8915%; height: 18px;&quot;&gt;&lt;b&gt;발생 위치&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%; height: 18px;&quot;&gt;&lt;b&gt;핵심 역할 또는 문제&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.0542%; height: 18px;&quot;&gt;&lt;b&gt;Hazard&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8915%; height: 18px;&quot;&gt;CPU &lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;파이프라인 내부&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;의존성, 분기, 자원 충돌이 다음 단계 진행을 막습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;width: 22.0542%; height: 36px;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;Interrupt&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8915%; height: 36px;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;CPU와 장치&amp;middot;시스템의 경계&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%; height: 36px;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;비동기 사건을 알려 CPU가 처리 루틴으로 이동하게 합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.0542%; height: 18px;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;Exception&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8915%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;현재 명령어 실행&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;명령어 때문에 동기적으로 발생한 사건을 처리합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;width: 22.0542%; height: 36px;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;DMA&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8915%; height: 36px;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;장치와 메모리 사이&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%; height: 36px;&quot;&gt;&lt;span style=&quot;color: #514c45; text-align: left;&quot;&gt;CPU의 바이트별 복사 없이 하드웨어가 데이터를 전송합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;RAW는 실제 데이터 의존성이고, WAR&amp;middot;WAW는 주로 비순차 실행에서 레지스터 이름 재사용 때문에 생깁니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;forwarding, stall, 분기 예측, 레지스터 리네이밍과 자원 복제가 각 Hazard를 줄입니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;DMA는 CPU가 설정하고 장치가 전송하며, 완료는 흔히 Interrupt로 통보합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;DMA 버퍼는 주소 매핑뿐 아니라 캐시 일관성, 메모리 순서와 작업 완료 동기화도 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;한 문장으로 압축하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Hazard는 코어 안의 겹친 실행을 막는 문제이고, DMA는 데이터 이동을 하드웨어에 위임하는 방식이며, Interrupt는 비동기 사건과 전송 완료를 CPU에 알리는 메커니즘&lt;/b&gt;입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;references-title&quot; 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 style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html&quot;&gt;Intel&amp;reg; 64 and IA-32 Architectures Software Developer Manuals&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://docs.kernel.org/core-api/dma-api-howto.html&quot;&gt;Linux Kernel Documentation: Dynamic DMA mapping Guide&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://docs.kernel.org/core-api/dma-api.html&quot;&gt;Linux Kernel Documentation: Dynamic DMA mapping using the generic device&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>컴퓨터 구조</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/39</guid>
      <comments>https://devdiary-sj.tistory.com/39#entry39comment</comments>
      <pubDate>Thu, 23 Jul 2026 19:00:26 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 이분 탐색과 탐욕법 문제 풀이</title>
      <link>https://devdiary-sj.tistory.com/38</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로그래머스 단속카메라&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 탐욕법을 활용하는 프로그래머스 &lt;a href=&quot;https://devdiary-sj.tistory.com/36&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;단속카메라&lt;/b&gt;&lt;/a&gt; 문제를 풀었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 차량은 고속도로에 진입한 지점과 진출한 지점을 가지고 있으며, 모든 차량이 최소 한 번은 단속카메라를 만나도록 카메라를 설치해야 합니다. 목표는 필요한 카메라의 최소 개수를 구하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 차량의 진입 지점을 기준으로 정렬하는 방법을 생각했습니다. 하지만 진입 지점을 기준으로 보면 현재 차량과 이후 차량이 어느 지점까지 겹치는지 계속 관리해야 해서 판단이 복잡해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진출 지점은 해당 차량을 단속할 수 있는 가장 오른쪽 위치입니다. 따라서 현재 차량을 놓치지 않으면서 뒤에 나오는 다른 차량까지 함께 단속할 가능성을 최대한 높일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카메라를 설치한 이후에는 다음 차량의 진입 지점이 현재 카메라 위치보다 오른쪽에 있는지만 확인하면 됩니다. 진입 지점이 카메라 위치보다 작거나 같다면 기존 카메라로 함께 단속할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제를 통해 탐욕법에서는 무엇을 기준으로 정렬하는지가 중요하다는 점을 다시 확인했습니다. 단순히 가장 빠른 값이나 가장 작은 값을 선택하는 것이 아니라, 현재 선택이 이후 선택의 가능성을 최대한 남겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;프로그래머스 입국심사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분 탐색을 활용하는 프로그래머스 &lt;a href=&quot;https://devdiary-sj.tistory.com/37&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;입국심사&lt;/b&gt;&lt;/a&gt; 문제도 풀었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 심사관이 서로 다른 속도로 입국심사를 진행할 때, 모든 사람이 심사를 마치는 데 필요한 최소 시간을 구하는 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 각 심사대가 언제 비는지를 관리하면서 사람을 한 명씩 배치하는 방법을 생각할 수 있습니다. 하지만 대기 인원이 최대 10억 명이기 때문에 사람을 기준으로 직접 시뮬레이션하면 시간 안에 해결할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서는 사람을 어떻게 배치할지를 직접 결정하는 대신, 특정 시간이 주어졌을 때 그 시간 안에 몇 명을 심사할 수 있는지를 계산했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 심사관의 심사 시간이 time이고 주어진 시간이 T라면, 해당 심사관은 T / time명을 처리할 수 있습니다. 모든 심사관이 처리할 수 있는 인원을 합한 값이 대기 인원 n 이상이라면 T분 안에 모든 심사를 끝낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 단조성을 이용해 모든 인원을 처리할 수 있는 최초의 시간을 이분 탐색으로 찾았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탐색 범위의 최솟값은 1로 두고, 최댓값은 가장 느린 심사관이 모든 사람을 혼자 처리하는 시간으로 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;long long right =
    static_cast&amp;lt;long long&amp;gt;(*max_element(times.begin(), times.end())) * n;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력값과 정답의 범위가 크기 때문에 자료형 관리도 중요했습니다. n과 심사 시간은 각각 int 범위에 들어가더라도 두 값을 곱한 결과는 int 범위를 넘을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 결과를 long long 변수에 저장하는 것만으로는 충분하지 않습니다. 두 피연산자가 모두 int라면 곱셈이 먼저 int로 수행된 뒤 변환되므로, 연산 전에 하나를 long long으로 변환해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;판정 과정에서도 모든 심사관의 처리 인원을 계속 더하면 합계가 매우 커질 수 있습니다. 따라서 합계가 n 이상이 되는 순간 반복을 종료하면 오버플로 위험과 불필요한 계산을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제를 통해 최솟값을 구하는 문제라도 정답을 직접 계산할 필요는 없다는 점을 학습했습니다. 정답 후보가 가능한지를 빠르게 판정할 수 있고, 그 판정에 단조성이 존재한다면 파라메트릭 서치를 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;특정 값이 가능한지 빠르게 판정할 수 있는지&lt;/li&gt;
&lt;li&gt;후보 값이 변할 때 판정 결과가 한 방향으로 변하는지&lt;/li&gt;
&lt;li&gt;입력값뿐 아니라 중간 계산 결과에도 적절한 자료형을 사용했는지&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>내배캠_Unreal10기</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/38</guid>
      <comments>https://devdiary-sj.tistory.com/38#entry38comment</comments>
      <pubDate>Wed, 22 Jul 2026 17:13:02 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 문제] 이분 탐색 - 프로그래머스 입국심사(level 3)</title>
      <link>https://devdiary-sj.tistory.com/37</link>
      <description>&lt;h2 id=&quot;problem-title&quot; data-ke-size=&quot;size26&quot;&gt;문제 이해&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;심사관마다 한 사람을 처리하는 시간이 다르고 모든 심사대는 처음에 비어 있습니다. 기다리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;n명이 어떤 심사대를 선택하든 상관없이, 전원이 심사를 마치는 데 필요한 최소 시간을 구해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;n = 6,&lt;span&gt;&amp;nbsp;&lt;/span&gt;times = [7, 10]이면 7분 심사대는 28분 동안 4명, 10분 심사대는 2명을 처리할 수 있으므로 답은 28분입니다.&lt;/p&gt;
&lt;h2 id=&quot;simulation-title&quot; data-ke-size=&quot;size26&quot;&gt;시뮬레이션이 어려운 이유&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;가장 직관적인 방법은 각 심사대가 다음에 비는 시간을 관리하며 사람을 한 명씩 배치하는 것입니다. 하지만 사람 수가 최대 10억 명이므로 한 사람마다 작업하는 방식은 입력 상한에서 사용할 수 없습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;답을 만드는 과정을 직접 재현하기보다, 어떤 시간&lt;span&gt;&amp;nbsp;&lt;/span&gt;T가 답으로 충분한지 빠르게 판정할 수 있는지를 먼저 생각해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;decision-title&quot; data-ke-size=&quot;size26&quot;&gt;시간을 결정 문제로 바꾸기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;한 사람을 심사하는 데&lt;span&gt;&amp;nbsp;&lt;/span&gt;time분이 걸리는 심사관은&lt;span&gt;&amp;nbsp;&lt;/span&gt;T분 동안&lt;span&gt;&amp;nbsp;&lt;/span&gt;T / time명을 처리할 수 있습니다. 정수 나눗셈을 사용하는 이유는 해당 시간 안에 끝난 심사만 세어야 하기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;processed(T) = T / times[0]
             + T / times[1]
             + ...

processed(T) &amp;gt;= n 이면 T분 안에 모두 심사 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;시간이 늘면 처리 가능한 사람 수는 줄지 않습니다. 따라서 어떤 시점 전까지는 불가능하고, 그 시점부터는 계속 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;시간:  0 ... 27 | 28 ...
판정: 불가능     | 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 판정 결과가 한 번만 바뀌므로 이분 탐색으로 최초의 가능한 시간을 찾을 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;range-title&quot; data-ke-size=&quot;size26&quot;&gt;탐색 범위 정하기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;최소 시간은 아무도 심사하지 않은&lt;span&gt;&amp;nbsp;&lt;/span&gt;0분을 하한으로 둘 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;left = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;상한은 가장 빠른 심사관이&lt;span&gt;&amp;nbsp;&lt;/span&gt;n명을 혼자 처리하는 시간입니다. 실제로는 다른 심사관도 함께 일하므로 정답은 반드시 이 값 이하입니다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;right = min(times) * n&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;times를 오름차순 정렬하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;times[0]으로 가장 빠른 심사 시간을 얻을 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;boundary-title&quot; data-ke-size=&quot;size26&quot;&gt;가능한 첫 시간을 찾기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;left와&lt;span&gt;&amp;nbsp;&lt;/span&gt;right를 모두 정답 후보가 될 수 있는 닫힌 범위로 관리합니다. 중간 시간에&lt;span&gt;&amp;nbsp;&lt;/span&gt;n명 이상 처리할 수 있다면 그 시간도 답 후보이므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;right = mid로 줄입니다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;while (left &amp;lt; right) {
    long long mid = left + (right - left) / 2;
    long long count = 처리 가능한 사람 수;

    if (count &amp;gt;= n) {
        right = mid;
    } else {
        left = mid + 1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;처리할 수 없다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;mid는 정답이 아니므로 제외하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;left = mid + 1로 이동합니다. 반복이 끝나&lt;span&gt;&amp;nbsp;&lt;/span&gt;left == right가 되면 두 값이 가리키는 위치가 최초의 가능한 시간입니다.&lt;/p&gt;
&lt;h2 id=&quot;example-title&quot; data-ke-size=&quot;size26&quot;&gt;예제로 따라가기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;n = 6,&lt;span&gt;&amp;nbsp;&lt;/span&gt;times = [7, 10]이면 탐색 범위는&lt;span&gt;&amp;nbsp;&lt;/span&gt;0부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;7 &amp;times; 6 = 42까지입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.3721%;&quot;&gt;&lt;b&gt;mid&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.7442%;&quot;&gt;&lt;b&gt;처리 인원&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.3023%;&quot;&gt;&lt;b&gt;판정&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.5814%;&quot;&gt;&lt;b&gt;다음 범위&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.3721%;&quot;&gt;&lt;b&gt;21&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.7442%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;21 / 7 + 21 / 10 = 5&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.3023%;&quot;&gt;불가능&lt;/td&gt;
&lt;td style=&quot;width: 20.5814%;&quot;&gt;[22, 42]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.3721%;&quot;&gt;&lt;b&gt;32&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.7442%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;32 / 7 + 32 / 10 = 7&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.3023%;&quot;&gt;가능&lt;/td&gt;
&lt;td style=&quot;width: 20.5814%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;[22, 32]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.3721%;&quot;&gt;&lt;b&gt;27&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.7442%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;27 / 7 + 27 / 10 = 5&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.3023%;&quot;&gt;불가능&lt;/td&gt;
&lt;td style=&quot;width: 20.5814%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;[28, 32]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.3721%;&quot;&gt;&lt;b&gt;30&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.7442%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;30 / 7 + 30 / 10 = 7&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.3023%;&quot;&gt;가능&lt;/td&gt;
&lt;td style=&quot;width: 20.5814%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;[28, 30]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.3721%;&quot;&gt;&lt;b&gt;29&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.7442%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;29 / 7 + 29 / 10 = 6&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.3023%;&quot;&gt;가능&lt;/td&gt;
&lt;td style=&quot;width: 20.5814%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;[28, 29]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.3721%;&quot;&gt;&lt;b&gt;28&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.7442%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;28 / 7 + 28 / 10 = 6&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.3023%;&quot;&gt;가능&lt;/td&gt;
&lt;td style=&quot;width: 20.5814%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;[28, 28]&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;27분에는 5명만 처리할 수 있지만 28분에는 6명을 처리할 수 있으므로 최소 시간은 28분입니다.&lt;/p&gt;
&lt;h2 id=&quot;overflow-title&quot; data-ke-size=&quot;size26&quot;&gt;절반만 통과한 이유&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;처음 작성한 코드에서는 반환형만&lt;span&gt;&amp;nbsp;&lt;/span&gt;long long으로 두고 다음처럼 상한을 계산했습니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;long long right = times[0] * n;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;대입받는 변수가&lt;span&gt;&amp;nbsp;&lt;/span&gt;long long이어도 곱셈의 두 피연산자가 모두&lt;span&gt;&amp;nbsp;&lt;/span&gt;int이면 곱셈부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;int로 수행됩니다. 제한값에서는 최대&lt;span&gt;&amp;nbsp;&lt;/span&gt;1018까지 필요하므로 중간 결과가 먼저 오버플로한 뒤 잘못된 값이 저장될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;long long right = static_cast&amp;lt;long long&amp;gt;(times[0])
                * static_cast&amp;lt;long long&amp;gt;(n);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;곱셈 전에 피연산자를&lt;span&gt;&amp;nbsp;&lt;/span&gt;long long으로 변환해야 실제 연산도 64비트 정수로 수행됩니다.&lt;/p&gt;
&lt;h2 id=&quot;code-title&quot; data-ke-size=&quot;size26&quot;&gt;최종 코드&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;

using namespace std;

long long solution(int n, vector&amp;lt;int&amp;gt; times) {
    sort(times.begin(), times.end());

    long long left = 0;
    long long right = static_cast&amp;lt;long long&amp;gt;(times[0])
                    * static_cast&amp;lt;long long&amp;gt;(n);

    while (left &amp;lt; right) {
        long long mid = left + (right - left) / 2;
        long long count = 0;

        for (int time : times) {
            count += mid / time;
        }

        if (count &amp;gt;= n) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }

    return left;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;mid = left + (right - left) / 2&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태를 사용하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;left + right를 먼저 더할 때 생길 수 있는 오버플로도 피할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;complexity-title&quot; 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 style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;시간복잡도는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(M log(N &amp;times; T))&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;심사관 수를&lt;span&gt;&amp;nbsp;&lt;/span&gt;M, 가장 빠른 심사 시간을&lt;span&gt;&amp;nbsp;&lt;/span&gt;T라 하면 각 이분 탐색 단계에서 심사관을 한 번씩 순회합니다. 정렬의&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(M log M)도 필요합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;공간복잡도는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(1)&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;정렬 구현의 내부 공간을 제외하면 탐색 경계와 처리 인원만 저장합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;값의 범위를 먼저 계산한다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;정답과 상한이 최대&lt;span&gt;&amp;nbsp;&lt;/span&gt;1018이므로 시간 관련 변수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;long long이어야 합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;가능한 경우에도&lt;span&gt;&amp;nbsp;&lt;/span&gt;mid를 보존한다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;최솟값을 찾으므로 가능한&lt;span&gt;&amp;nbsp;&lt;/span&gt;mid&lt;span&gt;&amp;nbsp;&lt;/span&gt;자체가 정답일 수 있습니다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;right = mid - 1이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;right = mid를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;summary-title&quot; 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 style=&quot;color: #514c45;&quot;&gt;최대 10억 명을 한 명씩 배치하는 시뮬레이션은 피한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;T분 동안 처리 가능한 인원은 각 심사대의&lt;span&gt;&amp;nbsp;&lt;/span&gt;T / time을 더해 구한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;시간이 늘수록 가능 여부가 불가능에서 가능으로 한 번만 바뀌므로 이분 탐색을 적용한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;가장 빠른 심사관이 모두 처리하는 시간을 안전한 상한으로 사용한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;큰 수의 곱셈은 연산 전에&lt;span&gt;&amp;nbsp;&lt;/span&gt;long long으로 형변환해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이번 문제에서는 대상을 직접 배치하는 대신 답의 후보인 시간을 탐색하는 관점 전환이 핵심이었습니다. 알고리즘이 맞아도 자료형 변환 시점 때문에 실패할 수 있다는 점까지 함께 확인했습니다.&lt;/p&gt;</description>
      <category>프로그래머스 문제 풀이</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/37</guid>
      <comments>https://devdiary-sj.tistory.com/37#entry37comment</comments>
      <pubDate>Wed, 22 Jul 2026 17:04:25 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 문제] 탐욕법 - 프로그래머스 단속 카메라(level 3)</title>
      <link>https://devdiary-sj.tistory.com/36</link>
      <description>&lt;h2 id=&quot;problem-title&quot; data-ke-size=&quot;size26&quot;&gt;문제 이해&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;각 차량의 경로는 진입 지점과 진출 지점으로 이루어진 닫힌 구간&lt;span&gt;&amp;nbsp;&lt;/span&gt;[start, end]입니다. 카메라가 구간 안에 있으면 해당 차량을 단속할 수 있으며, 진입점이나 진출점에 설치해도 포함됩니다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;차량 경로: [start, end]
단속 조건: start &amp;lt;= camera &amp;lt;= end&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;결국 모든 구간을 적어도 한 번 찌르는 점을 최소 개수로 고르는 문제입니다. 어떤 구간을 먼저 처리하고 그 안의 어느 위치를 고를지가 핵심입니다.&lt;/p&gt;
&lt;h2 id=&quot;choice-title&quot; data-ke-size=&quot;size26&quot;&gt;탐욕적 선택 찾기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; 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 style=&quot;color: #514c45;&quot;&gt;이 차량은 진출 지점을 지나면 다시 만날 수 없으므로, 카메라 하나가 반드시 현재 구간 안에 있어야 합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;구간 안에서 가장 오른쪽인 진출 지점에 설치하면 현재 차량을 단속하면서 뒤에 오는 구간과 겹칠 가능성을 가장 크게 남깁니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;따라서 차량을 진출 지점 기준 오름차순으로 정렬하고, 아직 단속되지 않은 첫 차량의 진출 지점에 카메라를 설치합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;1. routes를 end 기준 오름차순 정렬
2. 현재 카메라를 포함하지 않는 첫 구간을 찾음
3. 그 구간의 end에 새 카메라 설치
4. 모든 구간을 확인할 때까지 반복&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;proof-title&quot; data-ke-size=&quot;size26&quot;&gt;선택이 최적인 이유&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;진출 지점이 가장 빠른 구간을&lt;span&gt;&amp;nbsp;&lt;/span&gt;A = [start, end]라고 하겠습니다. 어떤 최적해도&lt;span&gt;&amp;nbsp;&lt;/span&gt;A를 단속하려면 그 안의 위치&lt;span&gt;&amp;nbsp;&lt;/span&gt;p에 카메라 하나를 두어야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이 카메라를&lt;span&gt;&amp;nbsp;&lt;/span&gt;p에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;A.end로 옮겨도 손해가 없습니다. 정렬상 아직 처리할 구간&lt;span&gt;&amp;nbsp;&lt;/span&gt;B는&lt;span&gt;&amp;nbsp;&lt;/span&gt;A.end &amp;lt;= B.end를 만족합니다. 기존 카메라&lt;span&gt;&amp;nbsp;&lt;/span&gt;p가&lt;span&gt;&amp;nbsp;&lt;/span&gt;B에도 포함되었다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;B.start &amp;lt;= p &amp;lt;= A.end이고, 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;A.end&lt;span&gt;&amp;nbsp;&lt;/span&gt;역시&lt;span&gt;&amp;nbsp;&lt;/span&gt;B&lt;span&gt;&amp;nbsp;&lt;/span&gt;안에 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 첫 카메라를 가장 빠른 진출 지점에 둔 최적해가 항상 하나 이상 존재합니다. 이 선택 뒤에 남은 구간에도 같은 논리를 반복할 수 있으므로 탐욕법이 전체 최적해를 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;first-code-title&quot; data-ke-size=&quot;size26&quot;&gt;첫 구현과 병목&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 설치한 카메라를 벡터에 모두 저장하고, 각 차량이 기존 카메라 중 하나를 포함하는지 전부 검사했습니다. 탐욕적 선택 자체는 맞지만 차량마다 카메라 목록을 다시 순회하므로 최악에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N&amp;sup2;)이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; cameras;

for (const vector&amp;lt;int&amp;gt;&amp;amp; route : routes) {
    bool covered = false;

    for (int camera : cameras) {
        if (route[0] &amp;lt;= camera &amp;amp;&amp;amp; camera &amp;lt;= route[1]) {
            covered = true;
            break;
        }
    }

    if (!covered) {
        cameras.push_back(route[1]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;차량 수가 최대 10,000대이므로 정렬로 만든 규칙을 이용해 이 중첩 검사를 없애는 편이 좋습니다.&lt;/p&gt;
&lt;h2 id=&quot;improvement-title&quot; data-ke-size=&quot;size26&quot;&gt;마지막 카메라만 확인하기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;카메라는 처리 순서에 따라 항상 오른쪽으로 이동합니다. 새 카메라는 현재 미처리 차량의 진출 지점에 설치되고, 이후 차량의 진출 지점은 이보다 작지 않기 때문입니다. 따라서 과거의 모든 카메라를 저장할 필요 없이 가장 최근에 설치한 카메라 위치만 기억하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;if (camera &amp;lt; route[0]) {
    camera = route[1];
    ++answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;camera &amp;lt; route[0]이면 마지막 카메라가 현재 차량의 진입점보다 왼쪽에 있어 이 차량을 단속하지 못합니다. 반대로 이 조건이 거짓이면 정렬 특성상&lt;span&gt;&amp;nbsp;&lt;/span&gt;camera &amp;lt;= route[1]도 보장되므로 카메라는 현재 구간 안에 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;example-title&quot; data-ke-size=&quot;size26&quot;&gt;예제로 따라가기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;입력 구간을 진출 지점 기준으로 정렬하면 다음 순서가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;[-20, -15], [-18, -13], [-14, -5], [-5, -3]&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;[-20, -15]는 아직 단속되지 않았으므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;-15에 첫 카메라를 설치합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;[-18, -13]은&lt;span&gt;&amp;nbsp;&lt;/span&gt;-15를 포함하므로 같은 카메라로 처리됩니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;[-14, -5]의 진입점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;-15보다 오른쪽이므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;-5에 두 번째 카메라를 설치합니다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;[-5, -3]은 경계값&lt;span&gt;&amp;nbsp;&lt;/span&gt;-5를 포함하므로 추가 설치가 필요 없습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;모든 차량을 단속하는 최소 카메라 수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;2입니다.&lt;/p&gt;
&lt;h2 id=&quot;code-title&quot; data-ke-size=&quot;size26&quot;&gt;최종 코드&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;climits&amp;gt;

using namespace std;

int solution(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; routes) {
    sort(routes.begin(), routes.end(),
         [](const vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b) {
             return a[1] &amp;lt; b[1];
         });

    int answer = 0;
    int camera = INT_MIN;

    for (const vector&amp;lt;int&amp;gt;&amp;amp; route : routes) {
        if (camera &amp;lt; route[0]) {
            camera = route[1];
            ++answer;
        }
    }

    return answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;camera를 입력 범위보다 작은&lt;span&gt;&amp;nbsp;&lt;/span&gt;INT_MIN으로 초기화해 첫 차량에서는 반드시 카메라가 설치되도록 했습니다. 차량의 진입&amp;middot;진출 지점은 모두&lt;span&gt;&amp;nbsp;&lt;/span&gt;int&lt;span&gt;&amp;nbsp;&lt;/span&gt;범위 안이므로 안전합니다.&lt;/p&gt;
&lt;h2 id=&quot;complexity-title&quot; 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 style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;시간복잡도는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N log N)&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;진출 지점 기준 정렬이&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N log N), 정렬 뒤 한 번의 순회가&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N)입니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;추가 공간은&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(1)&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;정렬 구현의 내부 공간을 제외하면 카메라 위치와 개수만 저장합니다. 별도의 카메라 목록은 필요하지 않습니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;진출 지점을 기준으로 정렬한다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;진입 지점 기준 정렬만으로는 가장 먼저 놓칠 차량과 카메라의 최적 위치를 바로 결정할 수 없습니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;경계값도 단속에 포함한다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;진입점에 카메라가 있어도 단속되므로 새 카메라 조건은&lt;span&gt;&amp;nbsp;&lt;/span&gt;camera &amp;lt; route[0]입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;=를 사용하면 불필요한 카메라가 추가됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;summary-title&quot; 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 style=&quot;color: #514c45;&quot;&gt;차량 경로를 닫힌 구간으로 보고 모든 구간을 찌르는 최소 개수의 점을 찾는다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;진출 지점이 가장 빠른 차량은 더 늦기 전에 반드시 처리해야 한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;현재 차량의 진출 지점에 카메라를 두면 이후 구간을 포함할 가능성을 가장 크게 남긴다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;교환 논증으로 임의의 최적해의 첫 카메라를 이 위치로 옮겨도 손해가 없음을 확인할 수 있다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;정렬 뒤에는 마지막 카메라 하나만 확인해 전체 풀이를&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N log N)에 끝낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이번 풀이에서는 탐욕적 선택을 찾는 것만큼, 정렬이 만들어 낸 순서를 끝까지 활용하는 것이 중요했습니다. 선택은 맞았지만 모든 카메라를 재검사하던 초안을 개선하면서 탐욕법의 간결함과 효율을 코드에도 그대로 반영할 수 있었습니다.&lt;/p&gt;</description>
      <category>프로그래머스 문제 풀이</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/36</guid>
      <comments>https://devdiary-sj.tistory.com/36#entry36comment</comments>
      <pubDate>Wed, 22 Jul 2026 15:29:08 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 방의 개수 / 이분 탐색과 탐욕법</title>
      <link>https://devdiary-sj.tistory.com/35</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로그래머스 방의 개수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 그래프를 활용하는 프로그래머스 &lt;a href=&quot;https://devdiary-sj.tistory.com/33&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;방의 개수&lt;/b&gt;&lt;/a&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;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;또한 최댓값이나 최솟값을 직접 구하기 어려운 문제를 &amp;ldquo;이 값이 가능한가?&amp;rdquo;라는 결정 문제로 바꾸고, 판정 결과의 단조성을 이용해 답을 찾는 파라메트릭 서치 방식도 정리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분 탐색에서는 left, right, mid의 의미와 탐색 구간을 일관되게 유지하는 것이 중요하다는 점을 학습했습니다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탐욕법 문제에서는 구현보다 어떤 기준으로 선택할 것인지, 그리고 그 선택이 왜 안전한지를 설명하는 과정이 중요하다는 점을 알게 되었습니다.&lt;/p&gt;</description>
      <category>내배캠_Unreal10기</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/35</guid>
      <comments>https://devdiary-sj.tistory.com/35#entry35comment</comments>
      <pubDate>Tue, 21 Jul 2026 17:00:14 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 이분 탐색과 탐욕법</title>
      <link>https://devdiary-sj.tistory.com/34</link>
      <description>&lt;h2 id=&quot;overview-title&quot; data-ke-size=&quot;size26&quot;&gt;두 기법은 무엇을 버리는가&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이분 탐색(Binary Search)은 정렬된 배열이나 단조로운 판정 결과에서 가운데 후보를 검사하고, 정답이 존재할 수 없는 절반을 버리는 탐색 기법입니다. 탐색 공간의 크기가&lt;span&gt;&amp;nbsp;&lt;/span&gt;N &amp;rarr; N/2 &amp;rarr; N/4 &amp;rarr; ...로 줄기 때문에 판정 한 번이&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(1)이라면 전체 탐색은&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(log N)입니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;탐욕법(Greedy Algorithm)은 현재 상태에서 가장 유리하다고 정한 선택 하나를 확정하고 나머지 문제로 넘어가는 설계 기법입니다. 모든 조합을 비교하지 않으므로 빠르지만, 현재의 선택이 전체 최적해를 해치지 않는다는 근거가 반드시 필요합니다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.4961%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 41.0077%;&quot;&gt;이분 탐색&lt;/td&gt;
&lt;td style=&quot;width: 44.4961%;&quot;&gt;탐욕법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.4961%;&quot;&gt;줄이는 것&lt;/td&gt;
&lt;td style=&quot;width: 41.0077%;&quot;&gt;후보 값 또는 인덱스 구간&lt;/td&gt;
&lt;td style=&quot;width: 44.4961%;&quot;&gt;남은 선택&amp;nbsp; 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.4961%;&quot;&gt;핵심 전제&lt;/td&gt;
&lt;td style=&quot;width: 41.0077%;&quot;&gt;정렬 또는 판정 결과의 단조성&lt;/td&gt;
&lt;td style=&quot;width: 44.4961%;&quot;&gt;탐욕적 선택 속성과 최적 부분 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.4961%;&quot;&gt;핵심 질문&lt;/td&gt;
&lt;td style=&quot;width: 41.0077%;&quot;&gt;정답이 어느 쪽에 있는가?&lt;/td&gt;
&lt;td style=&quot;width: 44.4961%;&quot;&gt;이 선택을 지금 확정해도 되는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.4961%;&quot;&gt;대표 위험&lt;/td&gt;
&lt;td style=&quot;width: 41.0077%;&quot;&gt;경계/반복 조건 오류&lt;/td&gt;
&lt;td style=&quot;width: 44.4961%;&quot;&gt;증명되지 않은 기준을 직감으로 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 id=&quot;binary-search-title&quot; data-ke-size=&quot;size26&quot;&gt;이분 탐색: 정렬된 값에서 절반을 버린다&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;정렬된 배열&lt;span&gt;&amp;nbsp;&lt;/span&gt;[1, 3, 5, 7, 9, 11, 13]에서 9를 찾는다고 해보겠습니다. 가운데 값 7보다 목표가 크므로 7을 포함한 왼쪽 구간은 모두 버릴 수 있습니다. 남은 구간의 가운데 값 11보다 목표가 작으므로 오른쪽을 버리면 9만 남습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;left와&lt;span&gt;&amp;nbsp;&lt;/span&gt;right로 아직 확인할 구간을 표현한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;가운데 인덱스&lt;span&gt;&amp;nbsp;&lt;/span&gt;mid의 값을 목표와 비교한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;같으면 반환하고, 목표가 크면 왼쪽 절반을, 작으면 오른쪽 절반을 제외한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;구간이 빌 때까지 반복하고 찾지 못하면 실패를 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;#include &amp;lt;cstddef&amp;gt;
#include &amp;lt;vector&amp;gt;

int binarySearch(const std::vector&amp;lt;int&amp;gt;&amp;amp; values, int target)
{
    int left = 0;
    int right = static_cast&amp;lt;int&amp;gt;(values.size()) - 1;

    while (left &amp;lt;= right)
    {
        const int mid = left + (right - left) / 2;

        if (values[mid] == target)
            return mid;

        if (values[mid] &amp;lt; target)
            left = mid + 1;
        else
            right = mid - 1;
    }

    return -1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;(left + right) / 2도 수학적으로는 같지만 두 인덱스의 합이 정수 범위를 넘을 수 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;left + (right - left) / 2는 같은 가운데 값을 구하면서 덧셈 오버플로 위험을 줄입니다. 빈 배열에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;right가&lt;span&gt;&amp;nbsp;&lt;/span&gt;-1이 되어 반복문에 들어가지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;배열을 먼저 정렬해야 한다면 총비용은&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N log N)&lt;span&gt;&amp;nbsp;&lt;/span&gt;정렬과&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(log N)&lt;span&gt;&amp;nbsp;&lt;/span&gt;탐색입니다. 한 번만 찾는 상황에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N)&lt;span&gt;&amp;nbsp;&lt;/span&gt;순차 탐색이 더 단순할 수 있고, 같은 데이터에서 여러 번 찾을 때 정렬 비용이 상쇄됩니다.&lt;/p&gt;
&lt;h2 id=&quot;invariant-title&quot; data-ke-size=&quot;size26&quot;&gt;코드를 외우기보다 구간 불변식을 정한다&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이분 탐색의 오류 대부분은&lt;span&gt;&amp;nbsp;&lt;/span&gt;left,&lt;span&gt;&amp;nbsp;&lt;/span&gt;right가 무엇을 포함하는지 모호할 때 발생합니다. 반복 내내 참으로 유지할 문장, 즉&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구간 불변식&lt;/b&gt;을 먼저 정하면 종료 조건과 갱신식이 자연스럽게 결정됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;표현&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;초기 구간&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;반복 조건&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;오른쪽 제거&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;왼쪽 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;폐구간&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;[0, N-1]&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;left &amp;lt;= right&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;right = mid - 1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;left = mid + 1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;반개구간&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;[0, N)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;left &amp;lt; right&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;right = mid&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;&lt;span style=&quot;color: #171717; text-align: left;&quot;&gt;left = mid + 1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;폐구간 구현에서 반복 시작 시 정답 후보가 있다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;[left, right]&lt;span&gt;&amp;nbsp;&lt;/span&gt;안에 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;values[mid] &amp;lt; target이면&lt;span&gt;&amp;nbsp;&lt;/span&gt;mid까지는 정답이 아니므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;left = mid + 1로 갱신합니다. 이처럼 한 번의 반복마다 적어도 한 원소를 확실히 제거해야 종료할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;폐구간과 반개구간 중 어느 쪽도 더 절대적으로 좋지는 않습니다. 한 구현 안에서 초기값, 반복 조건, 갱신식을 같은 규칙으로 유지하는 것이 중요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;boundaries-title&quot; data-ke-size=&quot;size26&quot;&gt;이분 탐색의 진짜 활용: 값이 아니라 경계 찾기&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;실전에서는 특정 값 하나보다 조건이 바뀌는 첫 위치나 마지막 위치를 찾는 경우가 더 많습니다. 정렬된 배열에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;target&lt;span&gt;&amp;nbsp;&lt;/span&gt;이상인 첫 위치를 찾는&lt;span&gt;&amp;nbsp;&lt;/span&gt;lowerBound가 대표적입니다. 탐색 중&lt;span&gt;&amp;nbsp;&lt;/span&gt;values[mid] &amp;gt;= target이면&lt;span&gt;&amp;nbsp;&lt;/span&gt;mid도 답일 수 있으므로 버리지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;right = mid로 좁힙니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;int lowerBound(const std::vector&amp;lt;int&amp;gt;&amp;amp; values, int target)
{
    int left = 0;
    int right = static_cast&amp;lt;int&amp;gt;(values.size()); // [left, right)

    while (left &amp;lt; right)
    {
        const int mid = left + (right - left) / 2;

        if (values[mid] &amp;lt; target)
            left = mid + 1;
        else
            right = mid;
    }

    return left; // 없다면 values.size()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;upperBound는&lt;span&gt;&amp;nbsp;&lt;/span&gt;target보다 큰 첫 위치를 찾습니다. 위 코드의 비교를&lt;span&gt;&amp;nbsp;&lt;/span&gt;values[mid] &amp;lt;= target으로 바꾸면 됩니다. 따라서 정렬된 배열에서 특정 값의 개수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;upperBound(target) - lowerBound(target)로 구할 수 있습니다. C++ 표준 라이브러리에는 같은 역할의&lt;span&gt;&amp;nbsp;&lt;/span&gt;std::lower_bound와&lt;span&gt;&amp;nbsp;&lt;/span&gt;std::upper_bound가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;값:     1  2  2  2  4  7
인덱스: 0  1  2  3  4  5
            &amp;uarr;        &amp;uarr;
       lower(2)=1  upper(2)=4&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;parametric-title&quot; data-ke-size=&quot;size26&quot;&gt;파라메트릭 서치: 최적화 문제를 결정 문제로 바꾼다&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;정답 후보&lt;span&gt;&amp;nbsp;&lt;/span&gt;x에 대해&lt;span&gt;&amp;nbsp;&lt;/span&gt;possible(x)를 계산했을 때 결과가 한 번만 바뀐다면 값의 범위에도 이분 탐색을 적용할 수 있습니다. 이런 형태를 흔히 파라메트릭 서치라고 부릅니다. &amp;ldquo;최댓값을 직접 구하라&amp;rdquo;는 최적화 문제를 &amp;ldquo;x가 가능한가?&amp;rdquo;라는 결정 문제로 바꾸는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;x:           1  2  3  4  5  6  7
possible(x): T  T  T  T  F  F  F
                         &amp;uarr;
                    가능한 최댓값&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 길이가 긴 랜선일수록 만들 수 있는 조각 수는 같거나 줄어듭니다. 길이&lt;span&gt;&amp;nbsp;&lt;/span&gt;x로 필요한 개수 이상을 만들 수 있는지를&lt;span&gt;&amp;nbsp;&lt;/span&gt;possible(x)로 두면 결과는&lt;span&gt;&amp;nbsp;&lt;/span&gt;true &amp;rarr; false로 한 번만 변합니다. 이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;단조성&lt;/b&gt;이 절반을 안전하게 버릴 수 있는 근거입니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;long long maxFeasible(long long low, long long high)
{
    long long answer = low - 1;

    while (low &amp;lt;= high)
    {
        const long long mid = low + (high - low) / 2;

        if (possible(mid))
        {
            answer = mid;
            low = mid + 1;      // 더 큰 가능한 값 탐색
        }
        else
        {
            high = mid - 1;
        }
    }

    return answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 최소 가능한 값을 찾는다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;possible(mid)일 때 답을 저장하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;high = mid - 1로 더 작은 후보를 탐색합니다. 총 시간복잡도는 후보 범위의 크기를&lt;span&gt;&amp;nbsp;&lt;/span&gt;R, 판정 비용을&lt;span&gt;&amp;nbsp;&lt;/span&gt;C라 할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(C log R)입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;R이 10억이어도 판정은 약 30회, 64비트 양의 범위라도 약 63회면 충분합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제에서 발견할 수 있는 신호&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;가능한 최대 길이, 최대 거리, 최소 시간처럼 최솟값이나 최댓값을 요구한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;&amp;ldquo;N개 이상 만들 수 있는가?&amp;rdquo;, &amp;ldquo;K번 이하로 가능한가?&amp;rdquo;라는 판정 함수를 만들 수 있다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;정답의 값 범위는 매우 크지만 후보 하나를 검사하는 일은 비교적 쉽다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;후보를 키우거나 줄일수록 가능 여부가 한 방향으로만 변한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;신호만 보고 적용해서는 안 됩니다. 예를 들어 가능한 후보가&lt;span&gt;&amp;nbsp;&lt;/span&gt;T, F, T처럼 다시 가능해질 수 있다면 가운데 결과만으로 어느 절반을 버릴지 결정할 수 없습니다. 구현 전에&lt;span&gt;&amp;nbsp;&lt;/span&gt;x가 커질 때 판정 결과가 어느 방향으로 변하는지 문장으로 설명해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;binary-mistakes-title&quot; 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 style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;범위가 실제로 줄지 않는다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;폐구간에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;left = mid나&lt;span&gt;&amp;nbsp;&lt;/span&gt;right = mid를 무심코 사용하면 두 원소가 남았을 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;mid가 반복될 수 있습니다. 후보에서 제외한다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;mid &amp;plusmn; 1로 이동합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;가능한 답을 함께 버린다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;첫 경계를 찾을 때 조건을 만족한&lt;span&gt;&amp;nbsp;&lt;/span&gt;mid는 답일 수 있습니다. 반개구간의&lt;span&gt;&amp;nbsp;&lt;/span&gt;right = mid처럼 후보를 보존하는 갱신이 필요합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;최적값을 저장하지 않는다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;가능한 최댓값을 찾다가 더 큰 후보가 실패해도 직전에 성공한 값이 정답입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;answer에 저장하거나 종료 후 경계가 정답이 되도록 불변식을 설계합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;검색 범위와 자료형이 잘못됐다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;거리 합, 시간, 개수가&lt;span&gt;&amp;nbsp;&lt;/span&gt;int를 넘는지 확인하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;long long을 사용합니다. 가능한 최소&amp;middot;최대 후보도 문제 조건에서 정확히 도출해야 합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;판정 함수의 단조성을 확인하지 않는다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;빠른 판정 함수가 있어도 결과가 한 번만 바뀌지 않으면 이분 탐색으로 절반을 버릴 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;greedy-title&quot; data-ke-size=&quot;size26&quot;&gt;탐욕법: 현재 선택 하나를 확정한다&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;탐욕법은 각 단계에서 지금 가장 좋아 보이는 선택을 하고 이를 되돌리지 않습니다. 구현은 보통 정렬한 뒤 앞에서부터 선택하거나, 우선순위 큐에서 최솟값&amp;middot;최댓값을 반복해서 꺼내는 형태입니다. 하지만 &amp;ldquo;가장 큰 것부터&amp;rdquo;, &amp;ldquo;가장 짧은 것부터&amp;rdquo;는 탐욕법의 정의가 아니라 문제마다 검증해야 할 후보 규칙일 뿐입니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;500원, 100원, 50원, 10원 동전으로 1,260원을 만들 때 큰 동전부터 사용하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;500&amp;times;2 + 100&amp;times;2 + 50&amp;times;1 + 10&amp;times;1, 총 6개가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;int money = 1260;
const std::vector&amp;lt;int&amp;gt; coins = {500, 100, 50, 10};
int count = 0;

for (const int coin : coins)
{
    count += money / coin;
    money %= coin;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이 규칙은 위 동전 체계에서는 맞지만 모든 동전 집합에서 성립하지 않습니다. 동전이 1원, 3원, 4원이고 6원을 만든다면 큰 동전부터 고를 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;4+1+1로 3개가 필요하지만 최적해는&lt;span&gt;&amp;nbsp;&lt;/span&gt;3+3의 2개입니다. 같은 코드가 입력 체계에 따라 맞기도 하고 틀리기도 한다는 점이 탐욕법의 핵심 위험입니다.&lt;/p&gt;
&lt;h2 id=&quot;greedy-proof-title&quot; data-ke-size=&quot;size26&quot;&gt;탐욕적 선택이 성립하는 조건과 증명&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;탐욕적 선택 속성&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;현재 단계의 탐욕적 선택을 포함하는 최적해가 적어도 하나 존재해야 합니다. 즉 지금의 선택 때문에 앞으로 얻을 수 있는 최적 결과를 잃지 않아야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최적 부분 구조&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;탐욕적 선택을 확정하고 남은 부분도 같은 형태의 더 작은 최적화 문제여야 합니다. 이 성질은 DP에도 등장하지만 두 기법의 대응은 다릅니다. DP는 여러 상태의 결과를 저장하고 비교하는 반면, 탐욕법은 하나의 선택이 안전하다는 증명을 바탕으로 다른 후보를 버립니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;교환 논증&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; 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 style=&quot;color: #514c45;&quot;&gt;최적해&lt;span&gt;&amp;nbsp;&lt;/span&gt;O가 존재한다고 가정한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;O의 첫 선택이 탐욕적 선택&lt;span&gt;&amp;nbsp;&lt;/span&gt;G와 다르면 둘을 교환한다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;교환해도 유효하고 목적값이 나빠지지 않음을 보인다.&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;그러면&lt;span&gt;&amp;nbsp;&lt;/span&gt;G를 포함한 최적해도 존재하므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;G를 안전하게 확정할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이 밖에도 탐욕 선택이 항상 다른 선택보다 뒤의 선택 공간을 더 넓게 남긴다는 지배 관계를 보이거나, 각 단계 뒤에 유지되는 불변식을 증명할 수 있습니다. 중요한 것은 예제 몇 개에서 맞았다는 관찰이 아니라 모든 유효 입력에 적용되는 논리입니다.&lt;/p&gt;
&lt;h2 id=&quot;meeting-title&quot; data-ke-size=&quot;size26&quot;&gt;대표 예제: 회의실 배정&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;한 회의실에서 서로 겹치지 않게 최대한 많은 회의를 진행하려고 합니다. 가장 일찍 시작하는 회의, 가장 짧은 회의, 가장 빨리 끝나는 회의 중 무엇을 골라야 할까요? 정답은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;현재 선택 가능한 회의 중 종료 시간이 가장 빠른 회의&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;최적 스케줄의 첫 회의를&lt;span&gt;&amp;nbsp;&lt;/span&gt;O, 가장 빨리 끝나는 회의를&lt;span&gt;&amp;nbsp;&lt;/span&gt;G라고 하겠습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;G는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O보다 늦게 끝나지 않으므로 최적 스케줄에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;O를&lt;span&gt;&amp;nbsp;&lt;/span&gt;G로 바꿔도 이후 회의들은 그대로 배치할 수 있습니다. 회의 수는 줄지 않습니다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;G를 포함하는 최적해가 존재하며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;G&lt;span&gt;&amp;nbsp;&lt;/span&gt;뒤의 회의에도 같은 논리를 반복할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;utility&amp;gt;
#include &amp;lt;vector&amp;gt;

int maxMeetingCount(std::vector&amp;lt;std::pair&amp;lt;int, int&amp;gt;&amp;gt; meetings)
{
    std::sort(meetings.begin(), meetings.end(),
        [](const auto&amp;amp; a, const auto&amp;amp; b)
        {
            if (a.second != b.second)
                return a.second &amp;lt; b.second; // 종료 시간
            return a.first &amp;lt; b.first;       // 같은 종료 시간
        });

    int count = 0;
    int lastEnd = 0; // 시간이 음수일 수 있다면 조건에 맞게 초기화

    for (const auto&amp;amp; [start, end] : meetings)
    {
        if (start &amp;gt;= lastEnd)
        {
            ++count;
            lastEnd = end;
        }
    }

    return count;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;정렬에&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N log N), 한 번의 순회에&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N)이므로 전체 시간복잡도는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N log N)입니다. &amp;ldquo;가장 짧은 회의&amp;rdquo;는 시작 시간이 늦어 앞의 공간을 낭비할 수 있고, &amp;ldquo;가장 일찍 시작하는 회의&amp;rdquo;는 매우 늦게 끝나 뒤의 선택을 막을 수 있습니다. 목적함수와 직접 연결되는 기준은 종료 후 남는 공간을 최대화하는 가장 빠른 종료입니다.&lt;/p&gt;
&lt;h2 id=&quot;router-title&quot; data-ke-size=&quot;size26&quot;&gt;이분 탐색과 탐욕법 함께 쓰기: 공유기 설치&lt;/h2&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;집의 좌표가&lt;span&gt;&amp;nbsp;&lt;/span&gt;1, 2, 4, 8, 9이고 공유기 3개를 설치한다고 해보겠습니다. 가장 인접한 두 공유기 사이 거리의 최솟값을 최대화해야 합니다. 어떤 집 조합이 최적인지 직접 고르기는 어렵지만, 거리 후보&lt;span&gt;&amp;nbsp;&lt;/span&gt;d가 주어졌을 때 &amp;ldquo;모든 공유기 간격을 적어도&lt;span&gt;&amp;nbsp;&lt;/span&gt;d로 설치할 수 있는가?&amp;rdquo;는 쉽게 판정할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 이분 탐색할 값과 범위&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;탐색 대상은 집 인덱스가 아니라 최소 거리입니다. 서로 다른 좌표라면 최소 후보는 1, 최대 후보는&lt;span&gt;&amp;nbsp;&lt;/span&gt;마지막 집 - 첫 집입니다. 공유기가 둘 이상이라는 일반적인 문제 조건을 전제로 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 판정 함수는 탐욕적으로 설치한다&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;가장 왼쪽 집에 첫 공유기를 놓고, 직전 공유기에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;d&lt;span&gt;&amp;nbsp;&lt;/span&gt;이상 떨어진&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;첫 번째 집&lt;/b&gt;에 다음 공유기를 놓습니다. 더 오른쪽 집을 고르는 것보다 이른 집을 고르는 편이 이후 설치 공간을 좁히지 않습니다. 임의의 유효한 배치가 고른 위치보다 탐욕 배치의 각 공유기 위치가 항상 같거나 왼쪽이라는 불변식을 귀납적으로 보일 수 있으므로, 이 방식은 거리&lt;span&gt;&amp;nbsp;&lt;/span&gt;d에서 설치 가능한 공유기 수를 최대화합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;bool canInstall(const std::vector&amp;lt;long long&amp;gt;&amp;amp; houses,
                int requiredRouters,
                long long minimumDistance)
{
    int installed = 1;
    long long lastPosition = houses.front();

    for (std::size_t i = 1; i &amp;lt; houses.size(); ++i)
    {
        if (houses[i] - lastPosition &amp;gt;= minimumDistance)
        {
            ++installed;
            lastPosition = houses[i];

            if (installed &amp;gt;= requiredRouters)
                return true;
        }
    }

    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 판정 결과는 단조롭다&lt;/h3&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;거리&lt;span&gt;&amp;nbsp;&lt;/span&gt;d로 설치할 수 있다면 그보다 작은 거리로도 같은 배치를 사용할 수 있습니다. 반대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;d로 설치할 수 없다면 더 큰 거리에서도 설치할 수 없습니다. 따라서 판정 결과는 거리가 커질수록&lt;span&gt;&amp;nbsp;&lt;/span&gt;true &amp;rarr; false로 한 번만 바뀝니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 가능한 거리의 최댓값을 찾는다&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fffaf1;&quot;&gt;&lt;code&gt;long long maxMinimumDistance(std::vector&amp;lt;long long&amp;gt; houses,
                             int requiredRouters)
{
    std::sort(houses.begin(), houses.end());

    long long left = 1;
    long long right = houses.back() - houses.front();
    long long answer = 0;

    while (left &amp;lt;= right)
    {
        const long long mid = left + (right - left) / 2;

        if (canInstall(houses, requiredRouters, mid))
        {
            answer = mid;
            left = mid + 1;
        }
        else
        {
            right = mid - 1;
        }
    }

    return answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;집이&lt;span&gt;&amp;nbsp;&lt;/span&gt;N개이고 좌표 범위를&lt;span&gt;&amp;nbsp;&lt;/span&gt;D라 하면 정렬은&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N log N), 한 번의 판정은&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N), 판정 횟수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(log D)입니다. 전체 시간복잡도는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(N log N + N log D), 정렬을 제외한 추가 공간은&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(1)입니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fffaf1;&quot;&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;역할을 분리해 보면 명확합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이분 탐색&lt;/b&gt;은 최소 거리 후보를 빠르게 찾고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;탐욕법&lt;/b&gt;은 주어진 거리에서 공유기를 최대한 많이 설치해 가능 여부를 정확히 판정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;comparison-title&quot; data-ke-size=&quot;size26&quot;&gt;어떤 기법을 선택할지 판단하는 체크리스트&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 68.4884%;&quot;&gt;질문&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%;&quot;&gt;그렇다면 검토할 기법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 68.4884%;&quot;&gt;정렬된 데이터에서 값이나 첫/마지막 위치를 찾는가?&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%;&quot;&gt;인덱스 이분 탐색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 68.4884%;&quot;&gt;최적값 후보 x의 가능 여부가 단조로운가?&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%;&quot;&gt;값 이분 탐색/ 파라메트릭 서치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 68.4884%;&quot;&gt;국소&amp;nbsp; 선택을 포함하는 최적해가 항상 존재함을 증명할 수 있는가?&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%;&quot;&gt;탐욕법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 68.4884%;&quot;&gt;여러 선택을 비교해야 하고 부분 문제가 반복되는가?&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%;&quot;&gt;동적 계획법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 68.4884%;&quot;&gt;후보 판정 자체를 가장 이른 위치부터 선택해 최대로 구성할 수 있는가?&lt;/td&gt;
&lt;td style=&quot;width: 31.5116%;&quot;&gt;이분 탐색 + 탐욕 판정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;무엇을 탐색하는지 한 문장으로 정의한다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;배열 인덱스인지, 답의 값인지, 첫 가능한 경계인지 명확히 합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;단조성을 방향까지 적는다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;false &amp;rarr; true인지&lt;span&gt;&amp;nbsp;&lt;/span&gt;true &amp;rarr; false인지 적으면 어느 경계를 찾아야 할지 드러납니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;구간 표기 하나를 끝까지 지킨다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;폐구간과 반개구간의 초기값&amp;middot;종료 조건&amp;middot;갱신식을 섞지 않습니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;탐욕 기준에 반례를 시도한다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;가장 큰 것, 가장 짧은 것 같은 직관이 작은 반례에서도 깨지지 않는지 먼저 확인합니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;교환해도 나빠지지 않음을 증명한다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;임의의 최적해를 탐욕 선택을 포함하는 해로 바꿀 수 있어야 선택을 확정할 수 있습니다.&lt;/li&gt;
&lt;li style=&quot;background-color: #fffaf1; color: #514c45;&quot;&gt;복잡도를 판정 함수까지 포함해 계산한다&lt;/li&gt;
&lt;li style=&quot;color: #514c45;&quot;&gt;파라메트릭 서치는&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(log R)만이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;O(C log R)입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #514c45;&quot; data-ke-size=&quot;size16&quot;&gt;이분 탐색과 탐욕법은 모두 많은 후보를 보지 않고 버린다는 점에서 강력합니다. 그만큼 &amp;ldquo;왜 버려도 되는가&amp;rdquo;가 알고리즘의 본체입니다. 이분 탐색에서는 단조성과 구간 불변식이, 탐욕법에서는 교환 논증과 최적 부분 구조가 그 근거가 됩니다. 템플릿을 암기하기 전에 이 근거를 먼저 세우면 경계 조건이 달라져도 안정적으로 구현할 수 있습니다.&lt;/p&gt;</description>
      <category>알고리즘과 자료구조</category>
      <author>devdiary-sj</author>
      <guid isPermaLink="true">https://devdiary-sj.tistory.com/34</guid>
      <comments>https://devdiary-sj.tistory.com/34#entry34comment</comments>
      <pubDate>Tue, 21 Jul 2026 14:25:29 +0900</pubDate>
    </item>
  </channel>
</rss>