<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>red-sprout.log</title>
        <link>https://velog.io/</link>
        <description>https://sprout6626.tistory.com</description>
        <lastBuildDate>Mon, 23 Feb 2026 15:01:55 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>red-sprout.log</title>
            <url>https://velog.velcdn.com/images/red-sprout/profile/9b17d32b-d7c5-42b4-8c77-c48c5f96544c/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. red-sprout.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/red-sprout" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[DB] Oracle 대용량 일괄 UPDATE 프로시저]]></title>
            <link>https://velog.io/@red-sprout/Oracle-%EB%8C%80%EC%9A%A9%EB%9F%89-%EC%9D%BC%EA%B4%84-UPDATE-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80</link>
            <guid>https://velog.io/@red-sprout/Oracle-%EB%8C%80%EC%9A%A9%EB%9F%89-%EC%9D%BC%EA%B4%84-UPDATE-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80</guid>
            <pubDate>Mon, 23 Feb 2026 15:01:55 GMT</pubDate>
            <description><![CDATA[<h2 id="1-개요"><strong>1. 개요</strong></h2>
<p>작업 요건은 단순했습니다. 시스템 전체에 걸쳐 <code>TEN_ID</code> 컬럼의 값을 일괄 변경해야 하는 상황이었습니다. <code>TEN_ID</code> 는 테넌트를 식별하는 공통 키 컬럼으로, 수십 개의 테이블에 걸쳐 존재하는 컬럼이었습니다.</p>
<p>처음에는 단순하게 접근했습니다. <code>USER_TAB_COLUMNS</code>에서 <code>TEN_ID</code> 컬럼을 가진 테이블 목록을 뽑아 루프를 돌면서 테이블마다 <code>UPDATE</code> 를 치는 방식이었습니다.</p>
<p>그런데 곧 문제가 생겼습니다. FK 제약조건 때문이었습니다. </p>
<p><code>TEN_ID</code>를 참조하는 자식 테이블이 존재하는 상황에서 부모 테이블을 먼저 UPDATE하면 Oracle은 즉시 오류를 발생시킵니다. 그렇다고 자식 테이블을 먼저 UPDATE하자니, 이번엔 또 그 자식 테이블을 참조하는 다른 테이블이 있습니다. 320개 테이블의 FK 참조 관계를 모두 파악해서 UPDATE 순서를 수작업으로 정하는 것은 사실상 불가능한 일이었습니다.</p>
<p>결국 방향을 바꿨습니다. UPDATE 전에 제약조건과 인덱스를 비활성화하고, 병렬 처리를 적용한 뒤, 작업이 끝난 후 다시 복원하는 방식입니다. 결과적으로 <strong>320개 테이블, 2,800만 건의 데이터를 15분 안에 처리</strong>할 수 있었습니다.</p>
<h2 id="2-단순-update가-실패하는-이유">2. 단순 UPDATE가 실패하는 이유</h2>
<h3 id="2-1-fk-제약조건의-벽">2-1. FK 제약조건의 벽</h3>
<p>관계형 데이터베이스에서 외래 키(Foreign Key, FK)는 두 테이블 사이의 참조 무결성을 보장하는 제약조건입니다. 예를 들어 <code>ORDERS</code> 테이블의 <code>TEN_ID</code> 컬럼이 <code>TEN</code> 테이블의 <code>TEN_ID</code>를 참조하고 있다면, <code>TEN</code> 테이블의 <code>TEN_ID</code> 값을 변경하는 순간 <code>ORDERS</code> 테이블의 참조가 깨지기 때문에 Oracle은 즉시 오류를 발생시킵니다.</p>
<pre><code>ORA-02292: integrity constraint violated - child record found</code></pre><p>이 문제를 단순하게 해결하려면 참조하는 자식 테이블을 먼저 UPDATE하고, 그다음에 부모 테이블을 UPDATE해야 합니다. 그런데 이 의존 관계가 수십 개의 테이블에 걸쳐 있고, 테이블 간 참조 관계가 복잡하게 얽혀 있다면 순서를 파악하는 것 자체가 쉽지 않습니다. 테이블이 수백 개라면 사실상 수작업으로는 불가능합니다.</p>
<p>가장 근본적인 해결책은 UPDATE를 수행하는 동안 FK 제약조건 자체를 잠시 비활성화하는 것입니다.</p>
<h3 id="2-2-인덱스가-성능을-잡아먹는-구조">2-2. 인덱스가 성능을 잡아먹는 구조</h3>
<p>인덱스는 SELECT 성능을 높여주지만, DML(INSERT/UPDATE/DELETE) 성능에는 반대로 작용합니다. 특히 UPDATE의 경우, Oracle은 값이 변경될 때마다 해당 컬럼에 걸린 인덱스 엔트리를 삭제하고 새로운 엔트리를 삽입하는 작업을 내부적으로 수행합니다.</p>
<p>대상 테이블이 수십 개이고, 각 테이블마다 수십만 건의 레코드가 있으며, 해당 컬럼에 인덱스가 걸려 있다면 UPDATE 한 건당 인덱스 유지 비용이 실제 데이터 변경 비용보다 훨씬 커지는 상황이 발생합니다. 전체 작업 시간의 대부분이 인덱스 유지에 소비되는 것입니다.</p>
<p>이 문제를 해결하는 전략은 UPDATE 전에 인덱스를 <code>UNUSABLE</code> 상태로 만들고, UPDATE가 완료된 후에 인덱스를 일괄 재생성하는 것입니다. 인덱스를 처음부터 다시 쌓는 비용이 건건이 유지하는 비용보다 훨씬 저렴하기 때문입니다.</p>
<h2 id="3-설계-전략-개요">3. 설계 전략 개요</h2>
<p>위의 문제들을 해결하기 위한 프로시저의 전체 흐름은 다음과 같습니다.</p>
<pre><code>STEP 1. 제약조건 비활성화 (FK → UNIQUE → PK 순서)
STEP 2. 비고유 인덱스 UNUSABLE 처리
STEP 3. USER_TAB_COLUMNS 기반 동적 병렬 UPDATE
STEP 4. UNUSABLE 인덱스 PARALLEL REBUILD
STEP 5. PK 제약조건 재활성화
STEP 6. UNIQUE 제약조건 재활성화
STEP 7. FK 제약조건 재활성화</code></pre><p>각 단계는 독립적으로 보이지만, 순서가 바뀌면 오류가 발생하거나 성능이 크게 저하됩니다. 이 흐름 자체가 이 작업의 핵심 설계입니다.</p>
<h2 id="4-step-1--제약조건-비활성화">4. STEP 1 — 제약조건 비활성화</h2>
<h3 id="4-1-제약조건-종류별-처리-순서가-중요한-이유">4-1. 제약조건 종류별 처리 순서가 중요한 이유</h3>
<p>Oracle의 제약조건 중 DML 성능에 영향을 미치는 제약 조건은 세 가지가 있습니다.</p>
<ul>
<li><strong>PK (Primary Key)</strong>: 테이블의 기본 키입니다. 다른 테이블의 FK가 참조하는 대상이 됩니다.</li>
<li><strong>UNIQUE</strong>: 특정 컬럼의 유일성을 보장합니다. FK 참조 대상이 될 수도 있습니다.</li>
<li><strong>FK (Foreign Key)</strong>: 다른 테이블의 PK 또는 UNIQUE를 참조하는 제약조건입니다.</li>
</ul>
<p>비활성화할 때는 FK를 먼저 끄고, 그다음에 PK와 UNIQUE를 꺼야 합니다. PK를 먼저 비활성화하려고 하면, 해당 PK를 참조하는 FK가 아직 살아있기 때문에 Oracle이 오류를 발생시킵니다.</p>
<pre><code class="language-sql">-- 비활성화 순서: FK → UNIQUE → PK
-- 활성화 순서는 반대:  PK → UNIQUE → FK</code></pre>
<p>코드 상에서는 <code>CONSTRAINT_TYPE DESC</code> 정렬을 활용합니다. Oracle이 제약조건 타입에 사용하는 코드값은 <code>P</code>(Primary Key), <code>R</code>(Referential, 즉 FK), <code>U</code>(Unique)입니다. 이를 알파벳 역순으로 정렬하면 <code>U → R → P</code> 순서가 되며, 덕분에 FK(<code>R</code>)가 PK(<code>P</code>)보다 먼저 비활성화됩니다.</p>
<pre><code class="language-sql">FOR C IN (
    SELECT CONSTRAINT_NAME, TABLE_NAME, CONSTRAINT_TYPE
    FROM USER_CONSTRAINTS
    WHERE CONSTRAINT_TYPE IN (&#39;R&#39;, &#39;P&#39;, &#39;U&#39;)
    ORDER BY CONSTRAINT_TYPE DESC  -- U → R → P 순서
) LOOP
    ...
END LOOP;</code></pre>
<h3 id="4-2-cascade-옵션">4-2. CASCADE 옵션</h3>
<p>PK를 비활성화할 때는 <code>CASCADE</code> 옵션이 필요할 수 있습니다. <code>CASCADE</code>는 해당 PK를 참조하는 FK들도 함께 비활성화하라는 의미입니다. FK를 먼저 모두 끈 후에 PK를 끄는 구조라면 이미 FK가 비활성화되어 있어 CASCADE가 실질적으로 동작하지 않을 수 있지만, 예외 상황에 대비해 PK에는 CASCADE를 붙여두는 것이 안전합니다.</p>
<pre><code class="language-sql">IF C.CONSTRAINT_TYPE = &#39;P&#39; THEN
    EXECUTE IMMEDIATE &#39;ALTER TABLE &#39; || C.TABLE_NAME ||
                    &#39; DISABLE CONSTRAINT &#39; || C.CONSTRAINT_NAME || &#39; CASCADE&#39;;
ELSE
    EXECUTE IMMEDIATE &#39;ALTER TABLE &#39; || C.TABLE_NAME ||
                    &#39; DISABLE CONSTRAINT &#39; || C.CONSTRAINT_NAME;
END IF;</code></pre>
<p>모든 작업은 <code>EXCEPTION WHEN OTHERS THEN NULL</code>로 감싸서, 특정 테이블의 제약조건 비활성화가 실패하더라도 전체 루프가 중단되지 않도록 합니다. 이미 비활성화된 제약조건이나 권한 문제 등으로 일부 실패가 발생할 수 있기 때문입니다.</p>
<h2 id="5-step-2--인덱스-unusable-처리">5. STEP 2 — 인덱스 UNUSABLE 처리</h2>
<h3 id="5-1-nonunique-인덱스만-대상으로-하는-이유">5-1. NONUNIQUE 인덱스만 대상으로 하는 이유</h3>
<p>인덱스를 비활성화할 때 주의할 점이 있습니다. UNIQUE 인덱스는 건드리지 않습니다.</p>
<p>UNIQUE 인덱스는 UNIQUE 제약조건 또는 PK 제약조건과 연결되어 있습니다. 이 인덱스를 UNUSABLE로 만들면 제약조건 자체가 무력화되거나, 이후 활성화 과정에서 예기치 않은 오류가 발생할 수 있습니다. 또한 PK 인덱스가 UNUSABLE 상태에서는 해당 테이블에 대한 DML 자체가 거부되는 경우도 있습니다.</p>
<p>따라서 성능 최적화를 위해 비활성화하는 대상은 <code>NONUNIQUE</code> 인덱스로 한정합니다.</p>
<pre><code class="language-sql">FOR IDX IN (
    SELECT INDEX_NAME
    FROM USER_INDEXES
    WHERE INDEX_TYPE NOT IN (&#39;LOB&#39;)
      AND UNIQUENESS = &#39;NONUNIQUE&#39;
) LOOP
    EXECUTE IMMEDIATE &#39;ALTER INDEX &#39; || IDX.INDEX_NAME || &#39; UNUSABLE&#39;;
END LOOP;</code></pre>
<p><code>INDEX_TYPE NOT IN (&#39;LOB&#39;)</code>을 추가하는 이유는 LOB 컬럼에 연결된 인덱스는 일반적인 방법으로 UNUSABLE 처리가 되지 않기 때문입니다. 이 조건 없이 LOB 인덱스에 UNUSABLE을 시도하면 오류가 발생합니다.</p>
<p>인덱스가 UNUSABLE 상태가 되면 Oracle은 해당 인덱스를 DML 과정에서 완전히 무시합니다. UPDATE 시 인덱스 유지 비용이 사라지므로 대용량 변경 작업에서 극적인 성능 향상을 기대할 수 있습니다.</p>
<h2 id="6-step-3--병렬-update-실행">6. STEP 3 — 병렬 UPDATE 실행</h2>
<h3 id="6-1-user_tab_columns로-대상-테이블-동적-수집">6-1. USER_TAB_COLUMNS로 대상 테이블 동적 수집</h3>
<p>이 작업의 핵심은 변경 대상 컬럼이 존재하는 테이블을 자동으로 찾아내는 것입니다. 하드코딩으로 테이블 목록을 관리하면, 테이블이 추가되거나 삭제될 때마다 프로시저를 수정해야 합니다. 대신 Oracle의 데이터 딕셔너리 뷰인 <code>USER_TAB_COLUMNS</code>를 활용하면 현재 스키마에서 해당 컬럼을 가진 모든 테이블을 실행 시점에 동적으로 수집할 수 있습니다.</p>
<pre><code class="language-sql">FOR COL IN (
    SELECT TABLE_NAME
    FROM USER_TAB_COLUMNS
    WHERE COLUMN_NAME = &#39;TEN_ID&#39;  -- 변경 대상 컬럼명
    ORDER BY TABLE_NAME
) LOOP
    ...
END LOOP;</code></pre>
<p><code>USER_TAB_COLUMNS</code>는 현재 사용자 소유의 테이블 컬럼 정보를 모두 담고 있습니다. <code>ALL_TAB_COLUMNS</code>나 <code>DBA_TAB_COLUMNS</code>를 사용하면 다른 스키마의 테이블까지 포함할 수 있습니다.</p>
<h3 id="6-2-parallel-힌트-동작-원리">6-2. PARALLEL 힌트 동작 원리</h3>
<p>각 테이블에 대한 UPDATE는 <code>PARALLEL</code> 힌트를 사용해 병렬로 실행합니다.</p>
<pre><code class="language-sql">EXECUTE IMMEDIATE
    &#39;UPDATE /*+ PARALLEL(t, 4) */ &#39; || COL.TABLE_NAME || &#39; t &#39; ||
    &#39;SET TEN_ID = :1 &#39; ||
    &#39;WHERE TEN_ID = :2&#39;
    USING AFTR_VALUE, PREV_VALUE;</code></pre>
<p><code>PARALLEL(t, 4)</code>는 <code>t</code>라는 별칭을 가진 테이블에 대해 4개의 병렬 프로세스를 사용하라는 지시입니다. Oracle은 이 힌트를 받으면 테이블의 데이터를 4개의 구간으로 나누어 각 병렬 프로세스가 동시에 UPDATE를 수행하도록 실행 계획을 수립합니다.</p>
<p>병렬 처리가 유효하게 동작하려면 몇 가지 전제 조건이 필요합니다.</p>
<ul>
<li>데이터베이스 레벨에서 병렬 처리가 활성화되어 있어야 합니다(<code>PARALLEL_MAX_SERVERS</code> 파라미터).</li>
<li>해당 세션에 병렬 DML이 허용되어 있어야 합니다. 필요하다면 <code>ALTER SESSION ENABLE PARALLEL DML</code>을 사전에 실행해야 합니다.</li>
<li>너무 많은 병렬 프로세스는 오히려 리소스 경합을 유발할 수 있으므로, CPU 코어 수와 서버 부하를 고려해 병렬도를 결정해야 합니다.</li>
</ul>
<h2 id="7-step-4--인덱스-rebuild-parallel--nologging">7. STEP 4 — 인덱스 REBUILD (PARALLEL + NOLOGGING)</h2>
<p>UPDATE가 완료된 후에는 UNUSABLE 상태의 인덱스를 재생성해야 합니다. STEP 2에서 UNUSABLE로 만들었기 때문에, <code>STATUS = &#39;UNUSABLE&#39;</code> 조건으로 대상을 찾으면 됩니다.</p>
<pre><code class="language-sql">FOR IDX IN (
    SELECT INDEX_NAME
    FROM USER_INDEXES
    WHERE STATUS = &#39;UNUSABLE&#39;
) LOOP
    EXECUTE IMMEDIATE &#39;ALTER INDEX &#39; || IDX.INDEX_NAME ||
                    &#39; REBUILD PARALLEL 4 NOLOGGING&#39;;
    EXECUTE IMMEDIATE &#39;ALTER INDEX &#39; || IDX.INDEX_NAME || &#39; NOPARALLEL&#39;;
END LOOP;</code></pre>
<p><strong>PARALLEL 4</strong>는 인덱스 재생성 자체도 4개의 병렬 프로세스로 수행하라는 의미입니다. 대용량 테이블의 인덱스를 순차적으로 재생성하면 시간이 매우 오래 걸리기 때문에, 병렬로 처리해 재생성 시간을 크게 단축합니다.</p>
<p><strong>NOLOGGING</strong>은 인덱스 재생성 과정에서 Redo 로그 생성을 최소화하는 옵션입니다. 일반적인 DML은 장애 발생 시 복구를 위해 모든 변경 내역을 Redo 로그에 기록합니다. 그런데 인덱스 REBUILD는 데이터 자체가 아닌 파생 구조물을 만드는 작업이므로, 혹시 장애가 발생하더라도 인덱스를 다시 재생성하면 그만입니다. 따라서 Redo 로그 기록을 생략해 I/O 부하를 줄이는 NOLOGGING 옵션이 매우 효과적입니다.</p>
<p>REBUILD가 끝난 후 <code>NOPARALLEL</code>을 명시적으로 설정하는 이유는, REBUILD 시 사용한 병렬 설정이 인덱스 속성으로 남아 이후 일반 DML에도 불필요하게 병렬 처리를 시도하는 부작용을 방지하기 위함입니다.</p>
<h2 id="8-step-57--제약조건-enable-novalidate-전략">8. STEP 5~7 — 제약조건 ENABLE NOVALIDATE 전략</h2>
<h3 id="8-1-validate-vs-novalidate-차이">8-1. VALIDATE vs NOVALIDATE 차이</h3>
<p>제약조건을 다시 활성화할 때 두 가지 옵션 중 하나를 선택할 수 있습니다.</p>
<ul>
<li><strong>ENABLE VALIDATE</strong> (기본값): 제약조건을 활성화하면서 기존 데이터 전체를 검사합니다. 모든 데이터가 제약조건을 만족하는지 확인하므로 데이터가 많을수록 시간이 오래 걸립니다.</li>
<li><strong>ENABLE NOVALIDATE</strong>: 제약조건을 활성화하되, 기존 데이터는 검사하지 않습니다. 이후 신규로 입력되는 데이터에 대해서만 제약조건을 적용합니다.</li>
</ul>
<p>이번 작업에서는 <code>ENABLE NOVALIDATE</code>를 사용합니다. 이유는 명확합니다. UPDATE 작업을 통해 모든 데이터를 일관성 있게 변경했기 때문에 기존 데이터를 다시 검증할 필요가 없습니다. 불필요한 전체 데이터 스캔을 건너뜀으로써 제약조건 활성화 시간을 크게 단축할 수 있습니다.</p>
<pre><code class="language-sql">EXECUTE IMMEDIATE &#39;ALTER TABLE &#39; || C.TABLE_NAME ||
                &#39; ENABLE NOVALIDATE CONSTRAINT &#39; || C.CONSTRAINT_NAME;</code></pre>
<h3 id="8-2-pk-→-unique-→-fk-순서의-이유">8-2. PK → UNIQUE → FK 순서의 이유</h3>
<p>제약조건 활성화 순서는 비활성화 순서의 정반대입니다. FK는 PK나 UNIQUE를 참조하기 때문에, 참조 대상인 PK와 UNIQUE가 먼저 활성화되어 있어야 FK를 활성화할 수 있습니다. 만약 FK를 먼저 활성화하려고 하면 참조 대상이 아직 비활성화 상태이므로 오류가 발생합니다.</p>
<pre><code>STEP 5. PK     활성화 — 참조 대상 먼저 준비
STEP 6. UNIQUE 활성화 — 참조 대상 준비 완료
STEP 7. FK     활성화 — 참조 대상이 모두 준비된 후 마지막으로 활성화</code></pre><h2 id="9-트랜잭션-설계--중간-commit-전략">9. 트랜잭션 설계 — 중간 COMMIT 전략</h2>
<p>대용량 일괄 UPDATE에서 트랜잭션 관리는 매우 중요합니다. 모든 테이블의 UPDATE를 하나의 트랜잭션으로 묶으면 두 가지 문제가 발생합니다.</p>
<p>첫째, <strong>Undo 세그먼트 부족</strong> 문제입니다. Oracle은 트랜잭션 롤백을 위해 변경 전 데이터를 Undo 세그먼트에 저장합니다. 수백만 건의 UPDATE를 하나의 트랜잭션으로 처리하면 Undo 세그먼트가 고갈되어 <code>ORA-01555: snapshot too old</code> 오류가 발생할 수 있습니다.</p>
<p>둘째, <strong>락(Lock) 경합</strong> 문제입니다. UPDATE된 레코드는 COMMIT이 이루어지기 전까지 행 수준 잠금이 유지됩니다. 트랜잭션이 길어질수록 잠금 보유 시간이 늘어나고, 다른 세션과의 경합 가능성이 높아집니다.</p>
<p>이를 해결하기 위해 각 테이블 단위로 <code>COMMIT</code>을 수행합니다.</p>
<pre><code class="language-sql">EXECUTE IMMEDIATE &#39;UPDATE ... SET COL = :1 WHERE COL = :2&#39; ...;
V_COUNT := SQL%ROWCOUNT;
COMMIT;  -- 테이블 단위 중간 COMMIT</code></pre>
<p>테이블 단위로 COMMIT을 하면 각 테이블의 UPDATE가 독립적인 트랜잭션으로 처리됩니다. 중간에 특정 테이블에서 오류가 발생하더라도 이미 COMMIT된 테이블은 롤백되지 않으므로, 오류 발생 테이블만 재처리할 수 있습니다.</p>
<p>단, 이 전략은 중간 실패 시 일부 테이블만 변경된 불일치 상태가 남을 수 있다는 점을 인지해야 합니다. 이 프로시저는 운영 중단 상태에서 수행하는 일회성 작업을 전제로 설계된 것이므로, 서비스 중 실행보다는 점검 시간에 수행하는 것이 안전합니다.</p>
<h2 id="10-전체-프로시저-코드">10. 전체 프로시저 코드</h2>
<p>지금까지 설명한 모든 내용을 담은 전체 프로시저 코드입니다.</p>
<pre><code class="language-sql">CREATE OR REPLACE PROCEDURE UPDATE_COLUMN_VALUE_FAST (
    P_COLUMN_NAME IN VARCHAR2,  -- 변경 대상 컬럼명
    PREV_VALUE    IN VARCHAR2,  -- 변경 전 값
    AFTR_VALUE    IN VARCHAR2   -- 변경 후 값
) IS
    V_TOTAL_UPDATED NUMBER := 0;
    V_TABLE_COUNT   NUMBER := 0;
    V_COUNT         NUMBER;
BEGIN
    DBMS_OUTPUT.ENABLE(1000000);

    -- STEP 1: 제약조건 비활성화 (U → R → P 순서)
    FOR C IN (
        SELECT CONSTRAINT_NAME, TABLE_NAME, CONSTRAINT_TYPE
        FROM USER_CONSTRAINTS
        WHERE CONSTRAINT_TYPE IN (&#39;R&#39;, &#39;P&#39;, &#39;U&#39;)
        ORDER BY CONSTRAINT_TYPE DESC
    ) LOOP
        BEGIN
            IF C.CONSTRAINT_TYPE = &#39;P&#39; THEN
                EXECUTE IMMEDIATE &#39;ALTER TABLE &#39; || C.TABLE_NAME ||
                    &#39; DISABLE CONSTRAINT &#39; || C.CONSTRAINT_NAME || &#39; CASCADE&#39;;
            ELSE
                EXECUTE IMMEDIATE &#39;ALTER TABLE &#39; || C.TABLE_NAME ||
                    &#39; DISABLE CONSTRAINT &#39; || C.CONSTRAINT_NAME;
            END IF;
        EXCEPTION
            WHEN OTHERS THEN NULL;
        END;
    END LOOP;

    -- STEP 2: NONUNIQUE 인덱스 UNUSABLE 처리
    FOR IDX IN (
        SELECT INDEX_NAME
        FROM USER_INDEXES
        WHERE INDEX_TYPE NOT IN (&#39;LOB&#39;)
          AND UNIQUENESS = &#39;NONUNIQUE&#39;
    ) LOOP
        BEGIN
            EXECUTE IMMEDIATE &#39;ALTER INDEX &#39; || IDX.INDEX_NAME || &#39; UNUSABLE&#39;;
        EXCEPTION
            WHEN OTHERS THEN NULL;
        END;
    END LOOP;

    -- STEP 3: 동적 병렬 UPDATE
    FOR COL IN (
        SELECT TABLE_NAME
        FROM USER_TAB_COLUMNS
        WHERE COLUMN_NAME = P_COLUMN_NAME
        ORDER BY TABLE_NAME
    ) LOOP
        BEGIN
            EXECUTE IMMEDIATE
                &#39;UPDATE /*+ PARALLEL(t, 4) */ &#39; || COL.TABLE_NAME || &#39; t &#39; ||
                &#39;SET &#39; || P_COLUMN_NAME || &#39; = :1 &#39; ||
                &#39;WHERE &#39; || P_COLUMN_NAME || &#39; = :2&#39;
                USING AFTR_VALUE, PREV_VALUE;

            V_COUNT := SQL%ROWCOUNT;

            IF V_COUNT &gt; 0 THEN
                V_TABLE_COUNT   := V_TABLE_COUNT + 1;
                V_TOTAL_UPDATED := V_TOTAL_UPDATED + V_COUNT;
            END IF;

            COMMIT;
        EXCEPTION
            WHEN OTHERS THEN
                ROLLBACK;
        END;
    END LOOP;

    -- STEP 4: 인덱스 REBUILD (PARALLEL + NOLOGGING)
    FOR IDX IN (
        SELECT INDEX_NAME
        FROM USER_INDEXES
        WHERE STATUS = &#39;UNUSABLE&#39;
    ) LOOP
        BEGIN
            EXECUTE IMMEDIATE &#39;ALTER INDEX &#39; || IDX.INDEX_NAME ||
                &#39; REBUILD PARALLEL 4 NOLOGGING&#39;;
            EXECUTE IMMEDIATE &#39;ALTER INDEX &#39; || IDX.INDEX_NAME || &#39; NOPARALLEL&#39;;
        EXCEPTION
            WHEN OTHERS THEN NULL;
        END;
    END LOOP;

    -- STEP 5: PK 활성화
    FOR C IN (
        SELECT CONSTRAINT_NAME, TABLE_NAME
        FROM USER_CONSTRAINTS
        WHERE CONSTRAINT_TYPE = &#39;P&#39; AND STATUS = &#39;DISABLED&#39;
    ) LOOP
        BEGIN
            EXECUTE IMMEDIATE &#39;ALTER TABLE &#39; || C.TABLE_NAME ||
                &#39; ENABLE NOVALIDATE CONSTRAINT &#39; || C.CONSTRAINT_NAME;
        EXCEPTION
            WHEN OTHERS THEN NULL;
        END;
    END LOOP;

    -- STEP 6: UNIQUE 활성화
    FOR C IN (
        SELECT CONSTRAINT_NAME, TABLE_NAME
        FROM USER_CONSTRAINTS
        WHERE CONSTRAINT_TYPE = &#39;U&#39; AND STATUS = &#39;DISABLED&#39;
    ) LOOP
        BEGIN
            EXECUTE IMMEDIATE &#39;ALTER TABLE &#39; || C.TABLE_NAME ||
                &#39; ENABLE NOVALIDATE CONSTRAINT &#39; || C.CONSTRAINT_NAME;
        EXCEPTION
            WHEN OTHERS THEN NULL;
        END;
    END LOOP;

    -- STEP 7: FK 활성화
    FOR C IN (
        SELECT CONSTRAINT_NAME, TABLE_NAME
        FROM USER_CONSTRAINTS
        WHERE CONSTRAINT_TYPE = &#39;R&#39; AND STATUS = &#39;DISABLED&#39;
    ) LOOP
        BEGIN
            EXECUTE IMMEDIATE &#39;ALTER TABLE &#39; || C.TABLE_NAME ||
                &#39; ENABLE NOVALIDATE CONSTRAINT &#39; || C.CONSTRAINT_NAME;
        EXCEPTION
            WHEN OTHERS THEN NULL;
        END;
    END LOOP;

    COMMIT;

    DBMS_OUTPUT.PUT_LINE(&#39;완료: &#39; || V_TABLE_COUNT || &#39;개 테이블, &#39; ||
        TO_CHAR(V_TOTAL_UPDATED, &#39;999,999,999&#39;) || &#39;건 업데이트&#39;);

EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE(&#39;오류 발생: &#39; || SQLERRM);
        RAISE;
END;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB] Oracle SQL*Plus AUTOTRACE 설정]]></title>
            <link>https://velog.io/@red-sprout/DB-Oracle-SQLPlus-AUTOTRACE-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@red-sprout/DB-Oracle-SQLPlus-AUTOTRACE-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Thu, 13 Nov 2025 02:58:56 GMT</pubDate>
            <description><![CDATA[<p>Oracle SQL*Plus에서 <code>SET AUTOTRACE</code>를 사용하는 과정에서 여러 가지 문제가 발생했습니다.</p>
<p><code>AUTOTRACE</code>는 쿼리 실행 결과뿐 아니라 <strong>실행 계획(Execution Plan)</strong>과 <strong>통계(Statistics)</strong> 정보를 함께 보여주는 기능입니다. 그러나 통계 정보를 출력하려면 내부적으로 <code>V$SESSTAT</code>, <code>V$STATNAME</code>, <code>V$MYSTAT</code>, <code>V$SESSION</code> 등의 성능 뷰를 참조해야 하며, 일반 사용자에게는 접근 권한이 없습니다. Oracle에서는 이를 위해 <strong>PLUSTRACE 롤</strong>을 사용하도록 설계되어 있습니다.</p>
<h2 id="문제-상황">문제 상황</h2>
<h3 id="plustrace-롤이-없어-발생한-오류">PLUSTRACE 롤이 없어 발생한 오류</h3>
<pre><code>SP2-0618: 세션 식별자를 찾을 수 없습니다. PLUSTRACE 롤이 사용으로 설정되었는지 점검하십시오
SP2-0611: STATISTICS 레포트를 사용 가능시 오류가 생겼습니다</code></pre><h3 id="oracle-12c-이상에서-cdb-루트에-접속해-발생한-오류">Oracle 12c 이상에서 CDB 루트에 접속해 발생한 오류</h3>
<pre><code>ORA-65096: 공통 사용자 또는 롤 이름은 C## 접두어로 시작해야 합니다</code></pre><h2 id="해결-방법">해결 방법</h2>
<h3 id="1-pdb로-전환-후-롤-생성">1. PDB로 전환 후 롤 생성</h3>
<p>우선 <code>SYSDBA</code> 로 접근 후 현재 컨테이너 확인합니다.</p>
<pre><code class="language-sql">SHOW CON_NAME;</code></pre>
<p>사용 가능한 PDB 확인합니다.</p>
<pre><code class="language-sql">SHOW PDBS;</code></pre>
<p>PDB로 전환합니다.</p>
<pre><code class="language-sql">ALTER SESSION SET CONTAINER = ORCLPDB1;</code></pre>
<p>롤 생성 및 권한을 부여합니다.</p>
<pre><code class="language-sql">CREATE ROLE PLUSTRACE;

GRANT SELECT ON V_$SESSTAT  TO PLUSTRACE;
GRANT SELECT ON V_$STATNAME TO PLUSTRACE;
GRANT SELECT ON V_$MYSTAT   TO PLUSTRACE;
GRANT SELECT ON V_$SESSION  TO PLUSTRACE;

GRANT PLUSTRACE TO SCOTT;</code></pre>
<p>또는 해당 롤은 <code>plustrce.sql</code>이라는 파일로 존재하기에, 위 대신 다음과 같이 간단하게 수행도 가능합니다.</p>
<pre><code class="language-sql">@?/sqlplus/admin/plustrce.sql</code></pre>
<h3 id="2-cdb-루트에서-공통-롤로-생성">2. CDB 루트에서 공통 롤로 생성</h3>
<p>이 방식은 CDB 에서 직접 권한을 주는 방법으로 비추천하는 방법입니다.</p>
<pre><code class="language-sql">CREATE ROLE C##PLUSTRACE CONTAINER=ALL;

GRANT SELECT ON V_$SESSTAT  TO C##PLUSTRACE CONTAINER=ALL;
GRANT SELECT ON V_$STATNAME TO C##PLUSTRACE CONTAINER=ALL;
GRANT SELECT ON V_$MYSTAT   TO C##PLUSTRACE CONTAINER=ALL;
GRANT SELECT ON V_$SESSION  TO C##PLUSTRACE CONTAINER=ALL;

GRANT C##PLUSTRACE TO SCOTT CONTAINER=ALL;</code></pre>
<h2 id="autotrace를-이쁘게-보는-방법">AUTOTRACE를 이쁘게 보는 방법</h2>
<h3 id="1-sqlplus-화면-포맷-조정">1. SQL*Plus 화면 포맷 조정</h3>
<pre><code class="language-sql">SET AUTOTRACE ON
SET LINESIZE 200
SET PAGESIZE 1000
SET TRIMOUT ON
SET TRIMSPOOL ON
SET TAB OFF
COLUMN &quot;Operation&quot; FORMAT A30
COLUMN &quot;Name&quot; FORMAT A15
COLUMN &quot;Cost (%CPU)&quot; FORMAT A15</code></pre>
<ul>
<li><p><code>SET AUTOTRACE ON</code> : 쿼리를 실행할 때 자동으로 실행 계획과 통계를 함께 출력합니다. 실행 결과만 보는 것이 아니라 성능 분석까지 바로 확인할 수 있는 핵심 옵션입니다.</p>
<ul>
<li><p>결과만 보고 싶으면: <code>SET AUTOTRACE OFF</code></p>
</li>
<li><p>실행 계획만 보고 싶으면: <code>SET AUTOTRACE TRACEONLY EXPLAIN</code></p>
</li>
<li><p>통계만 보고 싶으면: <code>SET AUTOTRACE TRACEONLY STATISTICS</code></p>
</li>
</ul>
</li>
<li><p><code>SET LINESIZE 200</code> : 한 줄에 출력할 최대 문자 수를 지정합니다. 기본값이 작으면 실행 계획 표가 줄 바꿈되어 읽기 불편하므로, 200 정도로 설정하면 대부분의 계획이 한 줄에 깔끔하게 표시됩니다.</p>
</li>
<li><p><code>SET PAGESIZE 1000</code> : 한 페이지에 표시할 최대 줄 수를 지정합니다. 작게 설정하면 실행 계획과 통계가 페이지 단위로 나뉘어 보기가 불편합니다. 큰 값으로 설정하면 전체 계획이 한 번에 표시됩니다.</p>
</li>
<li><p><code>SET TRIMOUT ON</code> / <code>SET TRIMSPOOL ON</code> : 출력과 SPOOL 파일에서 각 줄 끝의 공백을 제거합니다. 정렬이 깨지지 않고 깔끔하게 표시됩니다.</p>
</li>
<li><p><code>SET TAB OFF</code> : 컬럼 구분 시 탭 문자가 아닌 공백을 사용합니다. 탭 때문에 표가 들쭉날쭉해지는 문제를 방지합니다.</p>
</li>
<li><p><code>COLUMN &quot;Operation&quot; FORMAT A30</code>, <code>&quot;Name&quot; FORMAT A15</code>, <code>&quot;Cost (%CPU)&quot; FORMAT A15</code> : 실행 계획의 주요 컬럼 폭을 지정합니다. 기본 폭보다 좁으면 글자가 잘리거나 정렬이 깨지기 때문에, 폭을 지정해 가독성을 높입니다.</p>
</li>
</ul>
<h3 id="2-dbms_xplan-사용">2. DBMS_XPLAN 사용</h3>
<pre><code class="language-sql">EXPLAIN PLAN FOR
SELECT * FROM EMP WHERE DEPTNO = 10;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);</code></pre>
<p>실제 실행 통계까지 확인:</p>
<pre><code class="language-sql">SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, &#39;ALLSTATS LAST&#39;));</code></pre>
<h2 id="실제-실행하기">실제 실행하기</h2>
<p><strong>오라클 성능 고도화 원리와 해법2</strong> 에 나오는 스크립트를 실행해봅니다.</p>
<pre><code class="language-sql">CREATE TABLE T_EMP
AS
SELECT *
FROM  EMP, (SELECT ROWNUM NO FROM DUAL CONNECT BY LEVEL &lt;= 100000);

ALTER SESSION SET WORKAREA_SIZE_POLICY = MANUAL;

ALTER SESSION SET SORT_AREA_SIZE = 1048576;</code></pre>
<p>우선, 필요한 테이블을 만들고, 디스크 소트가 유발되는지 확인하기 위해 <code>WORKAREA_SIZE_POLICY</code>를 manual로, <code>SORT_AREA_SIZE</code> 를 1 MB 로 낮게 설정하였습니다.</p>
<pre><code class="language-sql">SET AUTOTRACE ON
SET LINESIZE 200
SET PAGESIZE 1000
SET TRIMOUT ON
SET TRIMSPOOL ON
SET TAB OFF
COLUMN &quot;Operation&quot; FORMAT A30
COLUMN &quot;Name&quot; FORMAT A15
COLUMN &quot;Cost (%CPU)&quot; FORMAT A15</code></pre>
<pre><code class="language-sql">SELECT *
FROM (
         SELECT NO, EMPNO, ENAME, JOB, MGR, SAL
              , AVG(SAL) OVER (PARTITION BY TO_CHAR(NO), DEPTNO) AVG_SAL
         FROM   T_EMP
     )
WHERE  NO = 1
ORDER BY SAL DESC;</code></pre>
<p>실행 결과는 아래와 같습니다.</p>
<pre><code>        NO      EMPNO ENAME                JOB                         MGR        SAL    AVG_SAL
---------- ---------- -------------------- -------------------- ---------- ---------- ----------
         1       1009 KING                 PRESIDENT                             5000       2250
         1       1019 KIM                  HR MANAGER                 1009       3500 2816.66667
...

50 행이 선택되었습니다.


Execution Plan
----------------------------------------------------------
Plan hash value: 4263631893

--------------------------------------------------------------------------------------
| Id  | Operation            | Name  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |       |  5000K|   424M|       |   156K  (1)| 00:00:07 |
|   1 |  SORT ORDER BY       |       |  5000K|   424M|   520M|   156K  (1)| 00:00:07 |
|*  2 |   VIEW               |       |  5000K|   424M|       | 54296   (1)| 00:00:03 |
|   3 |    WINDOW SORT       |       |  5000K|   162M|   248M| 54296   (1)| 00:00:03 |
|   4 |     TABLE ACCESS FULL| T_EMP |  5000K|   162M|       |  8875   (1)| 00:00:01 |
--------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter(&quot;NO&quot;=1)


Statistics
----------------------------------------------------------
        253  recursive calls
         13  db block gets
      32393  consistent gets
      96097  physical reads
        668  redo size
       3524  bytes sent via SQL*Net to client
        183  bytes received via SQL*Net from client
          5  SQL*Net roundtrips to/from client
          1  sorts (memory)
          1  sorts (disk)
         50  rows processed</code></pre><ul>
<li><p><code>recursive calls</code> : Oracle 내부에서 발생한 재귀 SQL문 실행 횟수입니다.</p>
</li>
<li><p><code>db block gets</code> : 쓰기 가능한 데이터 블록을 읽은 논리적 읽기 횟수로, 인덱스 검색 시 주로 증가합니다.</p>
</li>
<li><p><code>consistent gets</code> : 일관성 있는 데이터 블록을 읽은 횟수로, 대부분 메모리에서 처리됩니다.</p>
</li>
<li><p><code>physical reads</code> : 디스크에서 직접 읽은 블록 수입니다. 값이 높으면 I/O 부하가 크다는 신호입니다.</p>
</li>
<li><p><code>redo size</code> : 쿼리 수행 중 생성된 redo 로그 크기(bytes)입니다.</p>
</li>
<li><p><code>bytes sent via SQL*Net to client</code> : 서버에서 클라이언트로 전송된 데이터 크기입니다. 결과가 많으면 값이 커집니다.</p>
</li>
<li><p><code>bytes received via SQL*Net from client</code> : 클라이언트에서 서버로 전송된 SQL문이나 파라미터 크기입니다.</p>
</li>
<li><p><code>SQL*Net roundtrips to/from client</code> : 서버와 클라이언트 간의 왕복 통신 횟수입니다. <strong>네트워크 비용</strong>을 확인할 때 참고합니다.</p>
</li>
<li><p><code>sorts (memory)</code> : 메모리에서 수행된 정렬 연산 횟수입니다.</p>
</li>
<li><p><code>sorts (disk)</code> : 메모리 부족 시 디스크에 임시 파일을 만들어 수행한 정렬 횟수입니다.</p>
</li>
<li><p><code>rows processed</code> : 쿼리에서 <strong>실제로 처리된 행(row) 수</strong>입니다. SELECT는 반환 행, DML은 영향을 받은 행 수를 나타냅니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[후기] AWS CCP / SAA 취득 후기]]></title>
            <link>https://velog.io/@red-sprout/%ED%9B%84%EA%B8%B0-AWS-CCP-SAA-%EC%B7%A8%EB%93%9D-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@red-sprout/%ED%9B%84%EA%B8%B0-AWS-CCP-SAA-%EC%B7%A8%EB%93%9D-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 24 Sep 2025 22:46:53 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<h2 id="필요성">필요성</h2>
<p>SSAFY 다니던 시절 저는 인프라 구성에 대해서는 아예 모르고 있기 때문에 다른 세계의 이야기라고 생각하고 있었습니다. </p>
<p>하지만 프로젝트를 혼자서 해보고 싶다는 생각을 한 저는 인프라를 모르고서는 프로젝트가 의미가 있는지 생각해보게 되었습니다.</p>
<p>마침 주변에 인프라 하시는 분이 있어 CCP &amp; SAA 를 추천을 받아 공부하게 되었습니다.</p>
<h2 id="어떤-시험을-봐야할까">어떤 시험을 봐야할까?</h2>
<p>아래 사진은 현재 기준 모든 AWS 자격증입니다.
<strong>(SysOps의 경우 CloudOps로 변경되었습니다)</strong>
<img src="https://velog.velcdn.com/images/red-sprout/post/85d1cbab-fff9-4ffe-a11f-335c5f20346d/image.png" alt="AWS Certifications"></p>
<p>여러 자격증들이 있는데, 도메인 특정되지 않고 입문으로 가장 많이 치르는 가격증이 이제 AWS CCP 또는 AWS SAA 가 있습니다. </p>
<p>우선 <strong>AWS CCP(Certified Cloud Prationer, 시험 코드 CLF-C02)</strong>의 경우 Foundational 레벨로 AWS의 전반적인 서비스를 물어보는 시험입니다. AWS 서비스 어떤게 있는지 알고 있니?를 묻는 시험입니다.</p>
<p><strong>AWS SAA(Certified Solutions Architect Associate, 시험 코드 SAA-C03)</strong>는 Associate 레벨로 CCP 단계에서 더 나아가 실무적인 관점에서의 서비스가 결합되는 것들을 물어봅니다. 문제도 실무에서 있을 법한 상황이 주어지고, 이를 어떻게 AWS 서비스를 사용해서 개선하는지를 묻는 시험입니다.</p>
<p>AWS SAA 부터 시도해도 된다고 인터넷 상에서 글이 많이 보이는데, AWS에 대한 기초 지식이 없으시면 <strong>AWS CCP부터 시작하는 것이 좋습니다.</strong> 일부 겹치는 내용이 있고, CCP에서 공부한 내용들이나 용어들이 SAA를 도전하는게 아무래도 조금 더 탄탄하기 때문입니다.</p>
<p>만일 본인이 AWS 서비스를 어느정도 사용해보셨다(실무나 아니면 실제 프로젝트 운영 경험)면 SAA부터 시작하셔도 무방합니다.</p>
<h1 id="공통-팁">공통 팁</h1>
<h2 id="응시-언어">응시 언어</h2>
<p>시험 접수시 응시 언어를 선택할 수 있는데 <strong>반드시 한국어</strong>로 하시기 바랍니다. 번역이 이상할 수 있지만, 시험 중에 영어 원문도 볼 수 있기 때문에 한국어로 하지 않을 이유가 없습니다.</p>
<p>또한 비영어권 사용자들을 위해 30분이 추가 제공되는데,
<a href="https://cp.certmetrics.com/amazon/en/schedule/accommodations">https://cp.certmetrics.com/amazon/en/schedule/accommodations</a> 에서 가능합니다.</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/c15e699a-dbb1-46af-bf91-89f8e3a49f49/image.png" alt="30분 추가"></p>
<p>이렇게 30분 추가하면 추후 볼 시험에도 무제한 적용되니 꼭 적용하시기 바랍니다.</p>
<h2 id="시험-선택">시험 선택</h2>
<p>시험장을 선택하는 방법과 원격으로 하는 방법이 있는데, CCP의 경우 원격으로, SAA의 경우에는 시험장 방문을 선택했습니다. 장단점이 있는데, 개인적으로는 시험장이 근처에 있다면 <strong>시험장 방문을 추천드립니다.</strong></p>
<p>물론 사정상 원격밖에 안되면 원격으로 보셔도 무방합니다만, 개인적으로는 불편한 점들이 있었습니다.</p>
<ul>
<li>모니터가 있으면 절대 안됩니다. 스터디룸 빌려서 시험 봤었는데, 거기 모니터가 있었어서 큰 모니터 어떻게든 치우고 했던 기억이 납니다.</li>
<li>감독관 언어로 한국어는 없으니 영어를 선택하실껀데, 높은 확률로 <strong>인도식 영어 억양</strong>을 듣게 되실껍니다. 익숙하시면 괜찮지만, 전 전혀 익숙하지 않아서 그냥 채팅으로 지시사항 알려달라고 했습니다.</li>
<li>주변에서 좀 제한이 빡빡해서 시험 응시 못한 분도 있다고 듣긴 했습니다. 이건 그냥 들은거긴 해서...</li>
</ul>
<p>마음 편하게 시험장 가는게 낫긴 합니다. 단, 시험장 가실 때는 본인 명의 카드 하나 있어야 된다는 것만 주의하면 됩니다.</p>
<h1 id="aws-ccp">AWS CCP</h1>
<h2 id="공부-방법">공부 방법</h2>
<p>수습 기간 동안 개인 공부가 가능해서 그 기간(대략 1달) 정도만에 준비해서 취득한 시험입니다. 나름 시간이 있어서 압박감은 없이 치를 수 있었습니다.</p>
<p>강의와 유데미 덤프로 학습했고, 유데미 덤프는 2회독 했습니다.</p>
<h3 id="강의">강의</h3>
<p><a href="https://www.udemy.com/course/best-aws-certified-cloud/?couponCode=KEEPLEARNING">https://www.udemy.com/course/best-aws-certified-cloud/?couponCode=KEEPLEARNING</a></p>
<p>CCP를 도전하시는 분들은 AWS 서비스가 처음인 경우가 많아서 강의로 어느정도 감을 잡아가면서 공부하시는 것 추천드립니다. CCP의 경우 해당 강의를 완강했고, 강의에 있는 모의고사 하나만 남겨두고 시험이 얼마 남지 않은 시점에 풀었습니다.</p>
<p>또 유데미가 할인을 종종하는데, 회사에서 지원하는거 아니면 할인기간 중에 사는 것 추천드립니다.</p>
<h3 id="덤프">덤프</h3>
<p>보통 <a href="https://www.examtopics.com/exams/amazon/aws-certified-cloud-practitioner-clf-c02/view/">Examtopics 덤프</a>를 많이 보고, 추가적으로 <a href="https://www.udemy.com/course/aws-certified-cloud-practitioner-korean-practice-exams/?couponCode=KEEPLEARNING">유데미 덤프</a>를 많이 봅니다.</p>
<p>저의 경우에는 유데미만 2회독하고 응시하였습니다. examtopics 덤프는 기출 문제고, 유데미 덤프도 일부 겹치는데 전반적인 난이도는 examtopics &lt; udemy 라는 평이 많습니다.</p>
<h3 id="기타">기타</h3>
<p>강의와 덤프를 노션에 정리하면서 푸는데, 잘 이해가 안되는 부분이 있을수 있습니다. 그 경우에는 <strong>LLM 을 적극 활용</strong>하시는 것을 매우 추천드립니다. 공부 시간을 많이 줄일 수 있습니다. 검증이 필요하다면 역시 공식 문서를 틈틈히 읽어두는 것도 좋은 방법입니다.</p>
<h2 id="시험-후기">시험 후기</h2>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/98303b4d-41e1-48c0-8382-d518803b90c6/image.png" alt="CCP"></p>
<p>CCP는 AWS에 어떤 서비스가 있는지 아는지를 묻는 시험입니다. 그렇다보니 <strong>넓고 얇게 알아야 됩니다.</strong> 그렇다 보니 기간은 짧았지만, 공부하면서 이걸 다 외워야되나...? 라는 생각이 들었습니다(당시 SAA를 맛보지 않은 시점에서는...ㅎㅎ).</p>
<p>CCP만으로는 스펙이 되는 것도 아니고, AWS 서비스를 잘 만진다는 소리를 듣는 것도 절대 아닙니다. 다만, 가장 많이 응시하는 SAA를 대비하는데 있어 도움이 많이 됩니다. CCP에서 익혔던 개념들은 빠르게 스킵하고 넘어갈 수 있습니다.</p>
<h1 id="aws-saa">AWS SAA</h1>
<h2 id="공부-방법-1">공부 방법</h2>
<p>회사에서 업무를 하고 퇴근 후 공부를 하였는데, 퇴근 후 공부가 정말 쉽지 않았음을 느꼈습니다. 그래서 한번에 못땄다면 많이 힘들었을 것 같은데... 다행히 한번에 땄습니다.</p>
<p>CCP를 취득하고, SAA 따야되나 고민하고 있을 때, 주변에 취득하신 분이 SAA 덤프 문제를 보여준 적이 있었습니다. 처음 딱 보고 느낀 점은 쉽지 않겠다고 생각했습니다. 지문 길이나 선지 길이부터 CCP 보다 1.5 ~ 2배는 긴 것 같았습니다. </p>
<p>무엇보다 선지 중에 <strong>답 자체는 맞지만, 다소 효율적이지 않아서 오답</strong>인 선지들이 많아서 찍는 상황도 존재했습니다.</p>
<h3 id="강의-1">강의</h3>
<p><a href="https://www.udemy.com/course/best-aws-certified-solutions-architect-associate/">https://www.udemy.com/course/best-aws-certified-solutions-architect-associate/</a></p>
<p>회사 다니면서 완강하기가 너무 벅차긴 했습니다. 70퍼 정도까지 듣고 덤프 풀이에 들어갔습니다. 다만 강의 중에 <strong>솔루션 아키텍쳐 토론</strong> 강의가 있는데, AWS 아키텍쳐의 기본 구조를 잡는데 도움이 되기 때문에 강의가 있으시면 이 부분은 듣는 것을 추천드립니다.</p>
<h3 id="덤프-1">덤프</h3>
<p>examtopics 와 유데미 덤프를 모두 풀었습니다. 유데미를 먼저 풀었고, examtopics 는 200 문제정도 푼 것 같습니다.</p>
<h3 id="공부시간">공부시간</h3>
<p>강의는 지하철 출퇴근 시간을 사용해서 들어도 봤는데, 집중이 잘 안되긴 합니다. 따로 시간을 잡아서 듣는 편이 좋습니다.</p>
<p>문제 풀이의 경우 유데미는 모바일로 출퇴근 시간에 풀고, 집에서는 필요한 부분 정리하는 방식으로 진행했습니다.</p>
<h2 id="시험-후기-1">시험 후기</h2>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/c3ada5cb-379c-49be-b5c3-5df1d086bc5b/image.png" alt="SAA"></p>
<p>다행히 한번에 합격했습니다. 직장 다니면서 공부하는건 정말 쉬운 일이 아닙니다. 게다가 현재 SQLP 와 병행하고 있어 온전히 퇴근 후 AWS에 쏟기는 힘든 상황이라 며칠은 못할 때도 있었습니다.</p>
<p>시험 치는 주에는 계속 틀렸던거 틀리고 해서 좀 불안불안 했습니다. 그리고 시험 직전에 마지막 모의고사를 풀었는데 딱 커트라인 72%가 나와서 더욱 그랬습니다.</p>
<p>시험 때는 둘 중 하나 중에 찍는 상황이 많았고, 평소에 잘 봤던 S3는 쉽게 출제 되고, 잘 보지도 않던 <strong>EKS</strong>가 꽤 나왔습니다(제 생각에는 채점되지 않는 문제들에 속할지도 모르겠습니다.).</p>
<p>다행히 저녁에 뱃지가 와서 안도의 한숨을 내쉬었던 기억이 납니다.</p>
<h1 id="마무리">마무리</h1>
<p>SAA까지 땄는데 그러면 인프라 구성 잘하겠네? 하면 절대 아닙니다. 다만 SAA를 취득함으로써 이제야 인프라 아키텍쳐가 좀 보이고, 시작이구나를 느낍니다. 가야할 길은 멀지만, SAA가 이 길의 초석이 될 것 같습니다.</p>
<p><strong>그리고 퇴근 후 공부하는 모든 직장인 분들, 화이팅입니다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB] Oracle Docker 환경 구성]]></title>
            <link>https://velog.io/@red-sprout/DB-Oracle-Docker-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1</link>
            <guid>https://velog.io/@red-sprout/DB-Oracle-Docker-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1</guid>
            <pubDate>Fri, 15 Aug 2025 06:20:23 GMT</pubDate>
            <description><![CDATA[<p>윈도우, 맥을 왔다갔다 많이 하다 보니 운영체제에 독립적으로 DB 셋업을 할 일이 있었습니다. Oracle 베이스인 SQLP를 공부하는 만큼 Oracle 실습을 진행하고자 하는데, 셋업 및 사용법을 정리해보았습니다.</p>
<h2 id="docker-환경-구성">Docker 환경 구성</h2>
<p>도커 이미지는 여러가지가 있지만 <a href="https://www.oracle.com/kr/database/free/get-started/#quick-start">오라클 공식 사이트</a>에서 제공하는 것을 기준으로 작성하였습니다.</p>
<h3 id="도커-이미지만-다운로드">도커 이미지만 다운로드</h3>
<pre><code class="language-bash">docker pull container-registry.oracle.com/database/free:latest</code></pre>
<h3 id="도커로-이미지-다운받아-실행">도커로 이미지 다운받아 실행</h3>
<pre><code class="language-bash">docker run -d -p 1521:1521 --name oracle -e ORACLE_PASSWORD=oracle container-registry.oracle.com/database/free:latest</code></pre>
<ul>
<li><code>-d</code> : detach, 컨테이너가 백그라운드에서 실행될 수 있게 합니다(데몬 모드).</li>
<li><code>-p</code> : port, 각 포트를 설정합니다(오라클의 기본포트는 1521).</li>
<li><code>-e</code> : 컨테이너 내에서 사용될 환경변수를 설정합니다.</li>
</ul>
<h3 id="도커-프로세스-눈으로-확인">도커 프로세스 눈으로 확인</h3>
<pre><code class="language-bash">docker ps -a</code></pre>
<h3 id="도커-프로세스-실행상태-로그-출력">도커 프로세스 실행상태 로그 출력</h3>
<pre><code class="language-bash">docker logs -f oracle</code></pre>
<p>아래 메세지가 나올 때 까지 기다립니다.</p>
<pre><code>#########################
# DATABASE IS READY TO USE!
#########################</code></pre><h3 id="만약-sys나-system의-암호을-잊었다면">만약 sys나 system의 암호을 잊었다면</h3>
<pre><code class="language-bash">docker exec oracle resetPassword 원하는암호</code></pre>
<h3 id="중단">중단</h3>
<pre><code class="language-bash">docker stop oracle</code></pre>
<h3 id="시작">시작</h3>
<pre><code class="language-bash">docker start oracle</code></pre>
<h3 id="해당-컨테이너의-system-계정으로-접속">해당 컨테이너의 system 계정으로 접속</h3>
<pre><code class="language-bash">docker exec -it oracle /bin/bash

sqlplus / as sysdba</code></pre>
<ul>
<li><code>-i</code>, <code>--interactive=false</code> : 표준 입력(stdin)을 활성화하며 컨테이너와 연결(attach)되어 있지 않더라도 표준 입력을 유지한다.</li>
<li><code>-t</code>, <code>--tty=false</code> : TTY 모드(pseudo-TTY)를 사용한다. Bash를 사용하려면 이 옵션을 설정해야 한다. 이 옵션을 설정하지 않으면 명령을 입력할 수는 있지만 셸이 표시되지 않는다.</li>
<li><code>sqlplus / as sysdba</code> : 오라클 데이터베이스로 system 계정을 가지고 접속</li>
</ul>
<h2 id="접속-후-user-만들기">접속 후 User 만들기</h2>
<h3 id="pdb에-접속">PDB에 접속</h3>
<p>유저를 생성할 PDB에 접속합니다.</p>
<pre><code class="language-bash">SQL&gt; show pdbs

    CON_ID CON_NAME                       OPEN MODE  RESTRICTED
---------- ------------------------------ ---------- ----------
         2 PDB$SEED                       READ ONLY  NO
         3 FREEPDB1                       READ WRITE NO

## PDB에 접속
SQL&gt; alter session set container=FREEPDB1; 

Session altered.</code></pre>
<h3 id="user-생성">User 생성</h3>
<p>다음과 같은 예시 유저를 만들고 실습을 진행합니다.</p>
<blockquote>
<ul>
<li>유저명 : <code>docker</code></li>
<li>PW : <code>docker123</code></li>
</ul>
</blockquote>
<pre><code class="language-bash">## user 생성
SQL&gt; create user docker identified by &quot;docker123&quot;;

User created.

## 접속, resource 권한 부여
SQL&gt; grant connect, resource to docker;

Grant succeeded.

## users 테이블스페이스 사용 권한 설정
SQL&gt; alter user docker quota unlimited on users;

User altered.</code></pre>
<h3 id="client-tool-dbeaver-datagrip--접속">Client Tool (DBeaver, Datagrip, …) 접속</h3>
<ul>
<li>driver : thin</li>
<li>SID 대신 서비스명 으로 접속할 것</li>
<li>서비스명 : 처음 session 설정한 PDB(<code>FREEPDB1</code>)</li>
<li><code>jdbc:oracle:thin:@localhost:1521/FREEPDB1</code></li>
</ul>
<h3 id="sqlplus-접근-방법">SQL*Plus 접근 방법</h3>
<ul>
<li>실습 하다보면 SQL*Plus로 터미널로 직접 접근 할 일이 생김</li>
<li>방법 3가지 중 선택</li>
</ul>
<pre><code class="language-bash">## 1. Docker Oracle 컨테이너에서의 일반적인 접속
docker exec -it oracle sqlplus docker/docker123@FREEPDB1

## 2. exec 환경에서 PDB 지정하여 접속
## bash 는 /bin/bash 과 동일
docker exec -it oracle bash
sqlplus docker/docker123@FREEPDB1

## 3. exec 환경에서 관리자 접근 후 세션에서 PDB 변경 후 접속
docker exec -it oracle bash
sqlplus / as sysdba
ALTER SESSION SET CONTAINER = FREEPDB1;
CONNECT docker/docker123;</code></pre>
<h2 id="pdb">PDB?</h2>
<p>중간에 PDB가 나와서 뭐지...? 생각하셨을 수도 있어서 첨부합니다.</p>
<p>아래는 오라클 컨테이너 데이터베이스의 기본 구조입니다. CDB(Container Database) 가 중앙으로 관리하고, PDB(Pluggable Database)를 여러 개 붙이는 형태입니다. 이를 오라클 멀티테넌트(<code>Multitenant</code>)라 합니다.</p>
<pre><code>CDB (Container Database) - 컨테이너 데이터베이스
├── CDB$ROOT - 루트 컨테이너
├── PDB$SEED - 시드 PDB (템플릿)
├── PDB1 - 첫 번째 Pluggable Database
├── PDB2 - 두 번째 Pluggable Database
└── PDB3 - 세 번째 Pluggable Database</code></pre><h3 id="특징">특징</h3>
<ul>
<li>각 PDB는 논리적으로 완전히 분리</li>
<li>PDB를 다른 CDB로 쉽게 이동 가능, 마치 USB 처럼</li>
<li>리소스 효율성 - 메모리, CPU 등 시스템 리소스 공유</li>
</ul>
<h3 id="주요-명령어">주요 명령어</h3>
<ul>
<li>현재 환경 확인
```sql</li>
<li><ul>
<li>현재 컨테이너 확인
SHOW CON_NAME;</li>
</ul>
</li>
</ul>
<p>-- 모든 PDB 목록 조회
SELECT name, open_mode FROM v$pdbs;</p>
<p>-- CDB인지 확인
SELECT cdb FROM v$database;</p>
<pre><code>- PDB 간 이동
```sql
-- 특정 PDB로 세션 변경
ALTER SESSION SET CONTAINER = XEPDB1;

-- 루트 컨테이너로 이동
ALTER SESSION SET CONTAINER = CDB$ROOT;</code></pre><ul>
<li>PDB 관리
```sql</li>
<li><ul>
<li>PDB 열기
ALTER PLUGGABLE DATABASE pdb1 OPEN;</li>
</ul>
</li>
</ul>
<p>-- PDB 닫기
ALTER PLUGGABLE DATABASE pdb1 CLOSE;</p>
<p>-- PDB 생성
CREATE PLUGGABLE DATABASE pdb_new FROM pdb$seed;</p>
<p>-- PDB 삭제
DROP PLUGGABLE DATABASE pdb_old INCLUDING DATAFILES;</p>
<pre><code>
### 접속 방법

```bash
## 1. Docker Oracle 컨테이너에서의 일반적인 접속
docker exec -it oracle sqlplus docker/docker123@FREEPDB1

## 2. DataGrip 연결
jdbc:oracle:thin:@localhost:1521/FREEPDB1

## 3. exec 환경에서 PDB 지정하여 접속
docker exec -it oracle bash
sqlplus docker/docker123@FREEPDB1

## 4. exec 환경에서 관리자 접근 후 세션에서 PDB 변경 후 접속
docker exec -it oracle bash
sqlplus / as sysdba
ALTER SESSION SET CONTAINER = FREEPDB1;
CONNECT docker/docker123;</code></pre><h3 id="pdb-환경-확인">PDB 환경 확인</h3>
<pre><code class="language-sql">-- 1. CDB 환경인지 확인
SELECT cdb FROM v$database;

-- 2. 현재 위치 확인
SHOW CON_NAME;

-- 3. 모든 PDB 조회
SELECT name, open_mode, restricted FROM v$pdbs;

-- 4. 서비스 이름 확인
SELECT name FROM v$services;</code></pre>
<pre><code class="language-bash">SQL&gt; show pdbs

    CON_ID CON_NAME                       OPEN MODE  RESTRICTED
---------- ------------------------------ ---------- ----------
         2 PDB$SEED                       READ ONLY  NO
         3 FREEPDB1                       READ WRITE NO</code></pre>
<ul>
<li>PDB에 접속<pre><code>SQL&gt; alter session set container=FREEPDB1; 
</code></pre></li>
</ul>
<p>Session altered.
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB] 친절한 SQL 1장. SQL 처리과정과 I/O]]></title>
            <link>https://velog.io/@red-sprout/DB-%EC%B9%9C%EC%A0%88%ED%95%9C-SQL-1%EC%9E%A5</link>
            <guid>https://velog.io/@red-sprout/DB-%EC%B9%9C%EC%A0%88%ED%95%9C-SQL-1%EC%9E%A5</guid>
            <pubDate>Wed, 13 Aug 2025 13:46:40 GMT</pubDate>
            <description><![CDATA[<h2 id="sql-파싱과-최적화">SQL 파싱과 최적화</h2>
<h3 id="sql-최적화">SQL 최적화</h3>
<ul>
<li>SQL 파싱 과정<ul>
<li>파싱 트리 생성 : SQL문 이루는 개별 구성요소를 분석, 파싱 트리 생성</li>
<li>Syntax 체크 : 문법적 오류 확인</li>
<li>Semantic 체크 : 의미상 오류 확인</li>
</ul>
</li>
<li>SQL 최적화 - Optimizer</li>
<li>로우 소스 생성</li>
</ul>
<h3 id="sql-옵티마이저">SQL 옵티마이저</h3>
<ul>
<li>옵티마이저의 최적화 단계<ul>
<li>사용자로부터 전달받은 쿼리를 수행하는데 후보군이 될만한 실행계획들을 찾아냄</li>
<li>데이터 딕셔너리에 미리 수집해 둔 오브젝트 통계 및 시스템 통계정보를 이용해 각 실행계획의 예상비용을 산정</li>
<li><strong>최저 비용</strong>을 나타내는 실행계획을 세움</li>
</ul>
</li>
</ul>
<h3 id="실행계획과-비용">실행계획과 비용</h3>
<ul>
<li><p>옵티마이저는 비용에 따라 인덱스를 선택 및 실행 계획 수립</p>
<pre><code class="language-sql">  -- 테스트용 테이블 생성
  CREATE TABLE T AS
  SELECT D.NO, E.*
  FROM (
       SELECT * FROM EMPLOYEES
           JOIN DEPT_EMP USING (EMP_NO)
           JOIN DEPARTMENTS USING (DEPT_NO)
  ) E, (SELECT ROWNUM NO FROM DUAL CONNECT BY LEVEL &lt;= 100) D;

  -- 3316만 건
  SELECT COUNT(*) FROM T;

  -- 인덱스 생성
  CREATE INDEX T_X01 ON T(DEPT_NO, NO);
  CREATE INDEX T_X02 ON T(DEPT_NO, FIRST_NAME, LAST_NAME, NO);</code></pre>
<p>  별다른 힌트 없이 쿼리 실행 : 옵티마이저의 판단에 따라 T_X01 선택</p>
<pre><code class="language-sql">  SELECT * FROM T
  WHERE DEPT_NO = &#39;d005&#39;
  AND NO = 1;</code></pre>
<p>  T_X02 로 옵티마이저 힌트를 줄 때 : cost 증가</p>
<pre><code>  ---------------------------------------------------------------------------------------------
  | Id  | Operation                           | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
  ---------------------------------------------------------------------------------------------
  |   0 | SELECT STATEMENT                    |       | 36845 |  2266K|  3260   (1)| 00:00:01 |
  |   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T     | 36845 |  2266K|  3260   (1)| 00:00:01 |
  |*  2 |   INDEX RANGE SCAN                  | T_X01 | 36845 |       |   100   (0)| 00:00:01 |
  ---------------------------------------------------------------------------------------------</code></pre><p>  Full scan : cost 증가</p>
<pre><code class="language-sql">  SELECT /*+ INDEX(T T_X02) */ * FROM T
  WHERE DEPT_NO = &#39;d005&#39;
  AND NO = 1;</code></pre>
<pre><code>  ---------------------------------------------------------------------------------------------
  | Id  | Operation                           | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
  ---------------------------------------------------------------------------------------------
  |   0 | SELECT STATEMENT                    |       | 36845 |  2266K| 54548   (1)| 00:00:03 |
  |   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T     | 36845 |  2266K| 54548   (1)| 00:00:03 |
  |*  2 |   INDEX RANGE SCAN                  | T_X02 | 36845 |       | 17699   (1)| 00:00:01 |
  ---------------------------------------------------------------------------------------------</code></pre><pre><code class="language-sql">  SELECT /*+ FULL(T) */ * FROM T
  WHERE DEPT_NO = &#39;d005&#39;
  AND NO = 1;</code></pre>
<pre><code>  --------------------------------------------------------------------------
  | Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
  --------------------------------------------------------------------------
  |   0 | SELECT STATEMENT  |      | 36845 |  2266K| 86009   (1)| 00:00:04 |
  |*  1 |  TABLE ACCESS FULL| T    | 36845 |  2266K| 86009   (1)| 00:00:04 |
  --------------------------------------------------------------------------</code></pre></li>
<li><p>개발자가 직접 데이터 엑세스 경로를 변경 가능 → <strong>옵티마이저 힌트</strong></p>
</li>
<li><p>cost는 예상치이기에 실제 시간과는 차이 존재</p>
</li>
</ul>
<h3 id="옵티마이저-힌트">옵티마이저 힌트</h3>
<ul>
<li><p>사용법</p>
<pre><code class="language-sql">  SELECT /*+ INDEX(A 고객_PK) */
              고객명, 연락처, 주소, 가입일시
  FROM 고객 A
  WHERE 고객ID = &#39;1111&#39;</code></pre>
</li>
<li><p>주의 사항</p>
<pre><code class="language-sql">  -- 콤마 사용은 인자에서만 가능
  /*+ INDEX(A A_X01 INDEX(B,B_X03) */ -- 모두 유효
  /*+ INDEX(C), FULL(D) */ -- 첫 번째 힌트만 유효

  -- 테이블을 지정시 스키마 명 명시 금지
  SELECT /*+ FULL(PUBLIC.EMP) */ -- 무효
  FROM EMP

  -- FROM 에서 ALIAS 지정시 힌트에도 ALIAS 사용
  SELECT /*+ FULL(EMP) */ -- 무효
  FROM EMP E</code></pre>
</li>
</ul>
<h3 id="자주-사용하는-힌트-목록">자주 사용하는 힌트 목록</h3>
<table>
<thead>
<tr>
<th>분류</th>
<th>힌트</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>최적화 목표</td>
<td>ALL_ROWS</td>
<td>전체 처리 속도 최적화</td>
</tr>
<tr>
<td></td>
<td>FIRST_ROWS</td>
<td>최초 N건 응답 속도 최적화</td>
</tr>
<tr>
<td>액세스 방식</td>
<td>FULL</td>
<td>Table Full Scan으로 유도</td>
</tr>
<tr>
<td></td>
<td>INDEX</td>
<td>Index San으로 유도</td>
</tr>
<tr>
<td></td>
<td>INDEX_DESC</td>
<td>Index를 역순으로 스캔하도록 유도</td>
</tr>
<tr>
<td></td>
<td>INDEX_FFS</td>
<td>Index Fast Full Scan으로 유도</td>
</tr>
<tr>
<td></td>
<td>INDEX_SS</td>
<td>Index Skip Scan으로 유도</td>
</tr>
<tr>
<td>조인 순서</td>
<td>ORDERED</td>
<td>FROM 절에 나열된 순서대로 조인</td>
</tr>
<tr>
<td></td>
<td>LEADING</td>
<td>LEADING 힌트 괄호에 기술한 순서대로 조인</td>
</tr>
<tr>
<td></td>
<td>SWAP_JOIN_INPUTS</td>
<td>해시 조인 시, BUILD INPUT을 명시적으로 선택</td>
</tr>
<tr>
<td>조인 방식</td>
<td>USE_NL</td>
<td>NL 조인으로 유도</td>
</tr>
<tr>
<td></td>
<td>USE_MERGE</td>
<td>소트 머지 조인으로 유도</td>
</tr>
<tr>
<td></td>
<td>USE_HASH</td>
<td>해시 조인으로 유도</td>
</tr>
<tr>
<td></td>
<td>NL_SJ</td>
<td>NL 세미조인으로 유도</td>
</tr>
<tr>
<td></td>
<td>MERGE_SJ</td>
<td>소트 머지 세미조인으로 유도</td>
</tr>
<tr>
<td></td>
<td>HASH_SJ</td>
<td>해시 세미조인으로 유도</td>
</tr>
<tr>
<td>서브쿼리 팩토링</td>
<td>MATERIALIZE</td>
<td>WITH 문으로 정의한 집합을 물리적으로 생성하도록 유도</td>
</tr>
<tr>
<td></td>
<td>INLINE</td>
<td>WITH 문으로 정의한 집합을 물리적으로 생성하지 않고 INLINE 처리하도록 유도</td>
</tr>
<tr>
<td>쿼리 변환</td>
<td>MERGE</td>
<td>뷰 머징 유도</td>
</tr>
<tr>
<td></td>
<td>NO_MERGE</td>
<td>뷰 머징 방지</td>
</tr>
<tr>
<td></td>
<td>UNNEST</td>
<td>서브쿼리 Unnesting 유도</td>
</tr>
<tr>
<td></td>
<td>NO_UNNEST</td>
<td>서브쿼리 Unnesting 방지</td>
</tr>
<tr>
<td></td>
<td>PUSH_PRED</td>
<td>조인조건 Pushdown 유도</td>
</tr>
<tr>
<td></td>
<td>NO_PUSH_PRED</td>
<td>조인조건 Pushdown 방지</td>
</tr>
<tr>
<td></td>
<td>USE_CONCAT</td>
<td>OR 또는 IN-List 조건을 OR-Expansion으로 유도</td>
</tr>
<tr>
<td></td>
<td>NO_EXPAND</td>
<td>OR 또는 IN-List 조건에 대한 OR-Expansion 방지</td>
</tr>
<tr>
<td>병렬 처리</td>
<td>PARALLEL</td>
<td>테이블 스캔 또는 DML을 병렬방식으로 처리하도록 유도</td>
</tr>
<tr>
<td></td>
<td>PARALLEL_INDEX</td>
<td>인덱스 스캔을 병렬방식으로 처리하도록 유도</td>
</tr>
<tr>
<td></td>
<td>PQ_DISTRIBUTE</td>
<td>병렬 수행 시 데이터 분배 방식 결정</td>
</tr>
<tr>
<td>기타</td>
<td>APPEND</td>
<td>Direct-Path Insert 로 유도</td>
</tr>
<tr>
<td></td>
<td>DRIVING_SITE</td>
<td>DB Link Remote 쿼리에 대한 최적화 및 실행 주체 지정</td>
</tr>
<tr>
<td></td>
<td>PUSH_SUBQ</td>
<td>서브쿼리를 가급적 빨리 필터링하도록 유도</td>
</tr>
<tr>
<td></td>
<td>NO_PUSH_SUBQ</td>
<td>서브쿼리를 가급적 늦게 필터링하도록 유도</td>
</tr>
</tbody></table>
<h2 id="sql-공유-및-재사용">SQL 공유 및 재사용</h2>
<h3 id="소프트-파싱-vs-하드-파싱">소프트 파싱 vs 하드 파싱</h3>
<ul>
<li><p><code>라이브러리 캐시</code> : SQL 파싱, 최적화, 로우 소스 생성 과정을 거쳐 생성한 내부 프로시저를 반복 재사용할 수 있도록 캐싱해두는 메모리 공간</p>
</li>
<li><p><code>SGA(System Global Area)</code> : 서버 프로세스와 백그라운드 프로세스가 공통으로 액세스하는 데이터와 제어 구조를 캐싱하는 메모리 공간</p>
</li>
<li><p><code>소프트 파싱</code> : SQL을 캐시에서 찾아 곧바로 실행 단계에서 넘어가는 것</p>
</li>
<li><p><code>하드 파싱</code> : SQL을 캐시에서 찾는데 실패해 최적화 및 로우 소스 생성까지 거치는 것</p>
</li>
<li><p>하나의 쿼리를 수행하는데 있어 후보군이 될만한 무수히 많은 실행경로를 도출하고, 짧은 순간에 딕셔너리와 통계정보를 읽어 각각에 대한 효율성을 판단하는 과정은 무거움</p>
<ul>
<li>DB의 대부분의 작업은 I/O, but 하드 파싱은 CPU를 소비</li>
<li>옵티마이저가 사용하는 정보들<ul>
<li>테이블, 컬럼, 인덱스 구조에 관한 기본 정보</li>
<li>오브젝트 통계 : 테이블 통계, 인덱스 통계, 컬럼 통계</li>
<li>시스템 통계 : CPU 속도, Single Block I/O 속도, Multiblock I/O 속도 등</li>
<li>옵티마이저 관련 파라미터</li>
</ul>
</li>
<li>캐시로 저장해서 하드 파싱 횟수를 줄이기</li>
</ul>
</li>
</ul>
<h3 id="바인드-변수의-중요성">바인드 변수의 중요성</h3>
<ul>
<li><p>라이브러리 캐시에서 SQL을 찾는 법 - <strong>SQL 문 그 자체</strong></p>
<ul>
<li>SQL 텍스트가 변하면 SQL ID도 변함</li>
<li>개발 과정 중에는 수시로 SQL이 바뀜 - 계속 적재되면 캐시에서 찾는게 느려짐</li>
</ul>
</li>
<li><p>로그인 ID를 받는 로그인을 생각</p>
<ul>
<li><p>아래와 같이 로그인ID를 파라미터로 받는 프로시저 하나 공유 → 바인드 변수</p>
<pre><code class="language-sql">  CREATE PROCEDURE LOGIN (login_id in varchar2) { ... }

  -- 라이브러리 캐시 : 하드 파싱 최초 1회만 발생
  SELECT * FROM CUSTOMER WHERE LOGIN_ID = :1</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="데이터-저장-구조-및-io-메커니즘">데이터 저장 구조 및 I/O 메커니즘</h2>
<h3 id="sql이-느린-이유">SQL이 느린 이유</h3>
<ul>
<li>프로세스는 디스크 I/O를 요청할 시에 CPU를 OS에 반환하고 잠시 waiting 상태에서 I/O가 완료될 때까지 대기 진행 → 대기 큐에서 머물러 있음 → 작동해야 될 프로세스가 가만히 대기 → 성능 저하</li>
</ul>
<h3 id="데이터베이스-저장-구조">데이터베이스 저장 구조</h3>
<blockquote>
<p>테이블스페이스 → 세그먼트 → 익스텐트 → 블록 → 로우</p>
</blockquote>
<ul>
<li><code>테이블스페이스</code> : 세그먼트를 담는 컨테이너</li>
<li><code>세그먼트</code> : 테이블, 인덱스, 파티션, LOB(Large Object) 같이 저장 공간이 필요한 영역<ul>
<li>세그먼트의 공간이 부족해지면 익스텐트를 추가로 할당 받음</li>
<li>하지만 할당된 모든 익스텐트가 같은 데이터 파일에 위치하지 않을 수도 있음 - 분산 저장</li>
<li><code>DBA_EXTENTS</code> 뷰에서 세그먼트에 할당된 익스텐트 목록 조회 가능</li>
</ul>
</li>
<li><code>익스텐트</code> : 공간을 확장하는 단위, 여러 블록들로 구성</li>
<li><code>블록</code> : 레코드를 실제로 저장하는 공간, 페이지라고도 불림<ul>
<li>한 블록은 하나의 테이블이 독점 → 한 블록에 저장된 레코드는 모두 같은 테이블 레코드</li>
<li><code>DBA(Data Block Address)</code> : 디스크 상에서 몇 번째 블록인지 나타냄</li>
<li>테이블 스캔 시 : 익스텐트 맵 활용 → 첫 번째 블록 DBA → 블록은 연속이므로 Scan 가능</li>
</ul>
</li>
<li><code>데이터파일</code> : 디스크 상의 물리적인 OS 파일</li>
</ul>
<blockquote>
<p>익스텐트 내 블록은 서로 인접, 익스텐트 끼리는 불연속!</p>
</blockquote>
<ul>
<li><code>DBA(Data Block Address)</code> : 블록의 고유 주소값<ul>
<li>인덱스를 이용해 레코드를 읽을 때는 <code>ROWID</code> (<code>DBA</code> + 로우 번호)로 조회</li>
<li>테이블 스캔 시에는 세그먼트 헤더에 저장된 익스텐트 맵 이용 → 첫번째 <code>DBA</code> 파악</li>
</ul>
</li>
</ul>
<h3 id="블록-단위-io">블록 단위 I/O</h3>
<blockquote>
<p>DBMS에서 데이터를 읽고 쓰는 단위</p>
</blockquote>
<p><code>V$PARAMETER</code> 조회</p>
<pre><code class="language-bash">SQL&gt; show parameter block_size

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
db_block_size                        integer     8192

SQL&gt; select value from v$parameter where name=&#39;db_block_size&#39;;

VALUE
--------------------------------------------------------------------------------
8192</code></pre>
<h3 id="시퀀셜-액세스-vs-랜덤-액세스">시퀀셜 액세스 vs 랜덤 액세스</h3>
<ul>
<li><code>시퀀셜</code> : 연결된 순서에 따라서<ul>
<li>하지만 테이블 블록간 논리적인 연결고리는 없음<ul>
<li>익스텐트 목록을 세그먼트 헤더에 <strong>map</strong>으로 관리</li>
<li>익스텐트 맵 활용 → 첫 번째 블록 DBA → 블록은 연속이므로 Scan 가능</li>
</ul>
</li>
</ul>
</li>
<li><code>랜덤</code> : 순서X, 한 블록씩 접근</li>
</ul>
<h3 id="논리적-io-vs-물리적-io">논리적 I/O vs 물리적 I/O</h3>
<ul>
<li><code>논리적</code> : SQL을 처리하는 과정에 발생한 총 블록 I/O (캐시를 모두 거침 - 사실상 메모리 I/O)</li>
<li><code>물리적</code> : 디스크에서 발생항 총 블록 I/O → 시간 오래 걸림</li>
</ul>
<h3 id="버퍼캐시-히트율buffer-cache-hit-ratio-bchr">버퍼캐시 히트율(Buffer Cache Hit Ratio, BCHR)</h3>
<pre><code>BCHR = (캐시에서 곧바로 찾은 블록 수 / 촌 읽은 블록 수) * 100
    = ((논리적 I/O - 물리적 I/O) / (논리적 I/O)) * 100
    = (1 - (물리적 I/O) / (논리적 I/O)) * 100</code></pre><pre><code>물리적 I/O = 논리적 I/O * (100% - BCHR)</code></pre><ul>
<li>논리적 I/O 줄이기 - <strong>SQL을 튜닝해서 읽는 총 블록 개수 줄이기</strong></li>
</ul>
<h4 id="q-bchr는-sql-성능을-좌우-하지만-bchr이-높다고-효율적인-sql을-의미하진-않는다">Q. BCHR는 SQL 성능을 좌우 하지만, BCHR이 높다고 효율적인 SQL을 의미하진 않는다??</h4>
<ul>
<li>같은 블록을 비효율적으로 반복해서 읽으면 BCHR이 상승<pre><code class="language-sql">SELECT e.empno, e.ename, d.dname
FROM emp e
JOIN dept d ON e.deptno = d.deptno;</code></pre>
</li>
<li>emp가 100만건, dept가 10만건 이라 가정</li>
<li>옵티마이저가 dept 인덱스를 매번 스캔하면, dept 블록은 한번 읽어서 캐시에 있지만, 100만 번의 논리적 I/O가 발생 → BCHR는 높지만, <strong>CPU는 혹사</strong></li>
</ul>
<h3 id="single-block-io-vs-multiblock-io">Single Block I/O vs Multiblock I/O</h3>
<ul>
<li>한번에 한 블록씩 요청해서 메모리 적재 → Single Block I/O<ul>
<li>인덱스를 이용할 시 인덱스와 테이블 블록 모두 Single Block I/O<ul>
<li>인덱스 루트 블록을 읽을 때</li>
<li>인덱스 루트 블록에서 얻은 주소 정보로 브랜치 블록을 읽을 때</li>
<li>인덱스 브랜치 블록에서 얻은 주소 정보로 리프 블록을 읽을 때</li>
<li>인덱스 리프 블록에서 얻은 주소 정보로 테이블 블록을 읽을 때</li>
</ul>
</li>
</ul>
</li>
<li>한번에 여러 블록씩 요청해서 메모리 적재 → Multiblock I/O<ul>
<li>많은 데이터 블록을 읽을 때 유리 → 테이블 풀 스캔</li>
<li>OS 레벨 I/O 단위 : 1 MB, 오라클 레벨 I/O 단위 : 8 KB</li>
<li>확인 - <code>db_file_multiblock_read_count</code> 파라미터</li>
</ul>
</li>
<li>Multiblock I/O 중간에 Single Block I/O 가 나타나는 이유<ul>
<li>익스텐트 맵은 테이블 블록에 대한 인덱스</li>
<li>Multiblock I/O는 Batch I/O</li>
<li>Full Scan 중 Chain 이 발생할 로루를 읽을 때</li>
</ul>
</li>
</ul>
<h3 id="table-full-scan-vs-index-range-scan">Table Full Scan vs Index Range Scan</h3>
<ul>
<li>테이블 전체 스캔 vs 인덱스로 일정량만 스캔</li>
<li>Table Full Scan<ul>
<li>시퀀셜 액세스 &amp; Multi Block I/O 방식으로 디스크 블록 읽음</li>
<li>소량의 데이터만 가져올 때는 불리, 가져오는 데이터의 사이즈가 클 경우 유리</li>
</ul>
</li>
<li>Index Range Scan<ul>
<li>랜덤 액세스 &amp; Single Block I/O</li>
<li>소량의 데이터만 가져올 때는 유리, 가져오는 데이터의 사이즈가 클 경우 불리</li>
</ul>
</li>
</ul>
<h3 id="캐시-탐색-메커니즘">캐시 탐색 메커니즘</h3>
<ul>
<li>Direct Path I/O를 제외한 모든 블록 I/O는 메모리 버퍼 캐시를 경유<ul>
<li>인덱스 루트 블록을 읽을 때</li>
<li>인덱스 루트 블록에서 얻은 주소 정보로 브랜치 블록을 읽을 때</li>
<li>인덱스 브랜치 블록에서 얻은 주소 정보로 리프 블록을 읽을 때</li>
<li>인덱스 리프 블록에서 얻은 주소 정보로 테이블 블록을 읽을 때</li>
<li>테이블 블록을 Full Scan 할 때</li>
</ul>
</li>
<li>해시 체이닝을 통한 해시 구조 관리 : <a href="https://velog.io/@red-sprout/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-HashTable-%EA%B5%AC%ED%98%84">Hash Table</a>을 생각</li>
<li>SGA 내부에 있는 버퍼 캐시는 공유자원이기 때문에 직렬화가 필수<ul>
<li>직렬화 : 줄세우기<ul>
<li>여러 프로세스가 동시에 접근할 수 없도록 순서대로 해시 체인을 탐색</li>
<li>Latch를 통해 접근한 프로세스만 체인에 관여할 수 있도록 하여, 탐색 과정 중 변경을 방지 ⇒ LRU 채인 래치</li>
</ul>
</li>
<li>버퍼 Lock을 통해, 데이터 정합성을 방지<ul>
<li>선행 프로세스가 작업을 완료하고 Latch를 풀기 전, 버퍼 캐시에 I/O 동안 접근하지 못하도록 Lock을 수행</li>
</ul>
</li>
</ul>
</li>
<li>가급적 SQL 튜닝을 통해 쿼리 일량(논리적 I/O) 자체를 줄여야 함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] 입양 시각 구하기(2)]]></title>
            <link>https://velog.io/@red-sprout/SQL-%EC%9E%85%EC%96%91-%EC%8B%9C%EA%B0%81-%EA%B5%AC%ED%95%98%EA%B8%B02</link>
            <guid>https://velog.io/@red-sprout/SQL-%EC%9E%85%EC%96%91-%EC%8B%9C%EA%B0%81-%EA%B5%AC%ED%95%98%EA%B8%B02</guid>
            <pubDate>Wed, 23 Jul 2025 06:30:05 GMT</pubDate>
            <description><![CDATA[<p>Oracle로 해결하였습니다.</p>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/59413">[PRG] 입양 시각 구하기</a></p>
<h2 id="접근-방법">접근 방법</h2>
<p>문제를 쪼개어서 접근하면 다음과 같습니다.</p>
<ul>
<li><code>DATETIME</code> 에서 시간 추출</li>
<li>시간별로 동물 세기</li>
</ul>
<h3 id="시간-추출">시간 추출</h3>
<p>시간을 추출하는 방법은 여러 방법이 있지만, 가장 쉬운 방법은 <code>TO_CHAR(DATETIME, &#39;HH24&#39;)</code> 를 사용하는 것 입니다. 주의할 것은 시간을 추출할 때 <code>&#39;HH&#39;</code> 가 아닌 <code>&#39;HH24&#39;</code> 를 사용하여 24시간을 표현해야 하는 것입니다. 이렇게 해야 <code>[0, 24)</code> 의 값이 나오게 됩니다.</p>
<p>마침 과거에 <a href="https://velog.io/@red-sprout/Oracle-FUNCTION2">간단한 문법</a>을 제가 정리한 내용이 있어 이를 첨부합니다.</p>
<h3 id="시간별로-동물-세기">시간별로 동물 세기</h3>
<p>이는 기본적인 <code>GROUP BY</code> 를 시간에 대해서 진행하면 됩니다.</p>
<p>현재까지 정리한 내용을 SQL로 나타내면 다음과 같습니다.</p>
<pre><code class="language-sql">  SELECT TO_CHAR(DATETIME, &#39;HH24&#39;) AS HOUR,
      COUNT(ANIMAL_ID) AS COUNT
  FROM ANIMAL_OUTS
  GROUP BY TO_CHAR(DATETIME, &#39;HH24&#39;)</code></pre>
<h2 id="문제점">문제점</h2>
<p>하지만 문제가 있습니다. 바로, <code>[0, 24)</code> 까지의 모든 시간이 반영되어 있지 않다는 점입니다.</p>
<p>만약 0시 기록이 없을 경우 <code>COUNT</code> 를 0으로 처리해두어야 하는데, 애초에 시간 데이터 자체가 없어서 표시 자체를 해둘 수가 없습니다.</p>
<p>뭔가 저 데이터와 상관없이 <code>[0, 24)</code> 를 모두 가지는 테이블 하나가 있으면 좋겠다는 생각이 듭니다.</p>
<p>만드는 방법에는 직접 작성하는 방법도 있긴 하겠지만 너무 귀찮습니다. 그렇다고 PL/SQL 까지 가는 것도 무겁습니다. 바로 이 때 재귀 쿼리를 생성하면 됩니다.</p>
<p>재귀 쿼리 역시 정리한 것이 있어 <a href="https://velog.io/@red-sprout/SQL-%EB%A9%B8%EC%A2%85%EC%9C%84%EA%B8%B0%EC%9D%98-%EB%8C%80%EC%9E%A5%EA%B7%A0-%EC%B0%BE%EA%B8%B0">이 포스팅</a>을 참고하면 됩니다. 다만 <strong>해당 포스팅은 MySQL 기반</strong>이라 <strong>Oracle 기반은 여기를 참고하면 됩니다.</strong></p>
<h3 id="oracle-계층형-쿼리">Oracle 계층형 쿼리</h3>
<p>MySQL은 CTE + <code>RECURSIVE</code> 를 사용하는데, Oracle은 <code>CONNECT BY</code> 를 사용합니다. 사용 예시를 <code>[0, 24)</code> 를 모두 가지는 테이블인 <code>TIME_TABLE</code> 을 만들어서 해결해보겠습니다.</p>
<pre><code class="language-sql">WITH TIME_TABLE AS (
    SELECT LEVEL - 1 AS HOUR
    FROM DUAL
    CONNECT BY LEVEL &lt;= 24
)</code></pre>
<p>사실 이 <code>CONNECT BY</code> 는 무려 CTE 없이도 사용 가능합니다!</p>
<pre><code class="language-sql">SELECT column1, column2, ...
FROM table_name
START WITH condition
CONNECT BY [PRIOR] condition</code></pre>
<p>기본적인 문법은 다음과 같이 사용되고, 해당 쿼리를 CTE 말고 단일 쿼리든, 서브 쿼리든 어디든 사용이 가능합니다. Oracle이 <strong>CTE 제공 이전부터 계층형 쿼리가 필요할 때 제공하던 강력한 문법</strong>이고, <strong>다른 RDBMS에서는 적용이 되지 않으므로 주의하기 바랍니다.</strong></p>
<h4 id="start-with">START WITH</h4>
<pre><code class="language-sql">START WITH MGR_ID IS NULL --MGR_ID 가 NULL인 ROW를 최상위로 시작</code></pre>
<p>트리에서 루트 노드 설정하는 것처럼 계층 구조에서 최상위 ROW를 선정하는 조건입니다.</p>
<h4 id="connect-by">CONNECT BY</h4>
<pre><code class="language-sql">CONNECT BY PRIOR 부모노드 = 자식노드  
CONNECT BY 자식노드 = PRIOR 부모노드</code></pre>
<p>어떻게 연결되어 있는지를 명시해주는 부분입니다. 기본적으로 자식은 <strong>PRIOR 부모</strong>라 생각하시면 편합니다.</p>
<h3 id="유용한-함수들">유용한 함수들</h3>
<p>이런 <code>CONNECT BY</code> 와 활용하기좋은 함수들이 몇가지 있는데, 저의 경우에는 <code>LEVEL</code>을 사용해서 계층의 깊이를 표시하였습니다. 이 문제에서는 필수긴 하지만, 알고리즘 풀 때도 깊이 정보 정도는 마킹하는 만큼 습관이 되면 좋습니다.</p>
<h4 id="level">LEVEL</h4>
<p>현재 계층의 깊이를 나타냅니다.</p>
<pre><code class="language-sql">SELECT LEVEL, &#39;Level &#39; || LEVEL AS DESCRIPTION
FROM DUAL
CONNECT BY LEVEL &lt;= 5;
</code></pre>
<h4 id="sys_connect_by_path">SYS_CONNECT_BY_PATH</h4>
<p>루트부터 현재 노드까지의 경로를 보여줍니다.</p>
<pre><code class="language-sql">SELECT EMP_NAME,
       SYS_CONNECT_BY_PATH(EMP_NAME, &#39; -&gt; &#39;) AS PATH
FROM EMPLOYEES
START WITH MGR_ID IS NULL
CONNECT BY PRIOR EMP_ID = MGR_ID;
</code></pre>
<h4 id="connect_by_root">CONNECT_BY_ROOT</h4>
<p>루트 노드의 값을 가져옵니다.</p>
<pre><code class="language-sql">SELECT EMP_NAME,
       CONNECT_BY_ROOT EMP_NAME AS TOP_MANAGER
FROM EMPLOYEES
START WITH MGR_ID IS NULL
CONNECT BY PRIOR EMP_ID = MGR_ID;
</code></pre>
<h2 id="예시-답안">예시 답안</h2>
<pre><code class="language-sql">WITH TIME_TABLE AS (
    SELECT LEVEL - 1 AS HOUR
    FROM DUAL
    CONNECT BY LEVEL &lt;= 24
), ANIMAL_LIST AS (
    SELECT TO_CHAR(DATETIME, &#39;HH24&#39;) AS HOUR,
        COUNT(ANIMAL_ID) AS COUNT
    FROM ANIMAL_OUTS
    GROUP BY TO_CHAR(DATETIME, &#39;HH24&#39;)
)

SELECT T.HOUR,
    NVL(L.COUNT, 0) AS COUNT
FROM TIME_TABLE T
LEFT JOIN ANIMAL_LIST L ON (T.HOUR = L.HOUR)
ORDER BY HOUR</code></pre>
<p>NULL 처리는 Oracle 제공인 <code>NVL</code> 을 사용하였지만, 다음과 같은 <code>COALESCE</code> 사용을 추천하고 있습니다.</p>
<pre><code class="language-sql">-- COALESCE(expr1, expr2, expr3, ...)
-- 첫 번째 NULL이 아닌 값을 반환
-- 여러 개의 표현식 중에서 선택 가능
WITH TIME_TABLE AS (
    SELECT LEVEL - 1 AS HOUR
    FROM DUAL
    CONNECT BY LEVEL &lt;= 24
), ANIMAL_LIST AS (
    SELECT TO_CHAR(DATETIME, &#39;HH24&#39;) AS HOUR,
        COUNT(ANIMAL_ID) AS COUNT
    FROM ANIMAL_OUTS
    GROUP BY TO_CHAR(DATETIME, &#39;HH24&#39;)
)

SELECT T.HOUR,
    COALESCE(L.COUNT, 0) AS COUNT
FROM TIME_TABLE T
LEFT JOIN ANIMAL_LIST L ON (T.HOUR = L.HOUR)
ORDER BY HOUR</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] 멸종위기의 대장균 찾기]]></title>
            <link>https://velog.io/@red-sprout/SQL-%EB%A9%B8%EC%A2%85%EC%9C%84%EA%B8%B0%EC%9D%98-%EB%8C%80%EC%9E%A5%EA%B7%A0-%EC%B0%BE%EA%B8%B0</link>
            <guid>https://velog.io/@red-sprout/SQL-%EB%A9%B8%EC%A2%85%EC%9C%84%EA%B8%B0%EC%9D%98-%EB%8C%80%EC%9E%A5%EA%B7%A0-%EC%B0%BE%EA%B8%B0</guid>
            <pubDate>Mon, 21 Jul 2025 07:50:38 GMT</pubDate>
            <description><![CDATA[<p>비슷한 쿼리를 작성할 일도 있고, CTE에 대한 개념을 정리할 겸 작성하게 되었습니다.</p>
<h2 id="cte">CTE</h2>
<p><strong>CTE(Common Table Expression)</strong>은 <strong>단일 쿼리 내부에서 임시로 쿼리 결과를 저장</strong>하고, 해당 쿼리 내에서 반복적으로 사용 가능한 개념입니다.</p>
<p>이걸 들었을 때 가장 먼저 생각나는건 아무래도 가상 테이블 <strong>VIEW</strong> 인데, 차이점이 있습니다.</p>
<blockquote>
<ul>
<li>VIEW 는 만들기 위해서 특별한 권한이 필요하고, 쿼리 실행 전  사전에 정의 되어 있어야 하며, 쿼리문이 끝나도 사용 가능합니다.</li>
<li>반면 CTE는 특별한 권한은 필요 없고, <strong>쿼리가 끝나면 사라지는 임시 테이블입니다.</strong></li>
</ul>
</blockquote>
<p>이를 쓰는 이유는 서브 쿼리와 달리 <strong>동일 쿼리 내에서 재사용이 가능</strong>하고, 또 <strong>자체 참조로 재귀 쿼리를 작성</strong>할 수 있음에 있습니다.</p>
<h2 id="활용">활용</h2>
<h3 id="기본-활용">기본 활용</h3>
<p>기본적인 문법은 아래와 같습니다. <code>WITH</code> 절을 사용하여 정의합니다.</p>
<pre><code class="language-sql">WITH cte_name AS (
    SELECT column1, column2, ...
    FROM table_name
    WHERE condition
)
SELECT *
FROM cte_name
WHERE additional_condition;</code></pre>
<p>테이블명 옆에 컬럼명을 새로 지정하여 사용할 수도 있습니다.</p>
<pre><code class="language-sql">WITH cte_name (column1, column2, ...) AS (
    SELECT column1, column2, ...
    FROM table_name
    WHERE condition
)
SELECT *
FROM cte_name
WHERE additional_condition;</code></pre>
<h3 id="여러-개-만들기">여러 개 만들기</h3>
<p>여러 개를 만드는 것은 단순히 <code>,</code> 로 이어주면 됩니다.</p>
<pre><code class="language-sql">WITH 
    cte1 AS (
        SELECT column1, column2
        FROM table1
        WHERE condition1
    ),
    cte2 AS (
        SELECT column3, column4
        FROM table2
        WHERE condition2
    )
SELECT c1.column1, c2.column3
FROM cte1 c1
JOIN cte2 c2 ON c1.column2 = c2.column4;</code></pre>
<h3 id="재귀-쿼리">재귀 쿼리</h3>
<p>재귀 쿼리는 재귀 CTE를 사용하여 구현합니다. 재귀 CTE는 자기 자신을 참조하여 반복적으로 실행되는 임시 결과 집합입니다. 쿼리로 알고리즘 문제 풀 듯 DFS가 가능합니다.</p>
<p>가장 대표적으로 쓰이는 사례는 트리 형태로 되어있는 자료 검색입니다.</p>
<p>보통 부서도를 볼 때 다음과 같은 형태로 되어 있는 걸 볼 수 있습니다. 보통 이런 스키마의 경우 <code>ID</code> 와 <code>PARENT_ID</code> 를 따로 두는 편입니다.</p>
<pre><code>CEO 김대표
├── 부장 이부장
│   └── 과장 최과장
│       └── 사원 정사원
└── 부장 박부장</code></pre><p>여기서 <code>최과장</code>을 검색한다고 생각해보면, 아래와 같이 검색 결과가 나오는 것이 자연스럽습니다. 즉 <strong>계층 관계를 모두 표현</strong>해야 됩니다. 이럴 때 사용하는 것이 바로 재귀 쿼리입니다.</p>
<pre><code>CEO 김대표
└── 부장 이부장
    └── 과장 최과장
        └── 사원 정사원</code></pre><p>문법은 <code>WITH RECURSIVE</code> 를 사용하여서 표현합니다.</p>
<pre><code class="language-sql">WITH RECURSIVE recur_cte AS (
    -- 1. Anchor Member (기준점)
    SELECT 초기값들...,
        1 AS DEPTH -- 시작 깊이는 1
    FROM 테이블
    WHERE 시작조건

    UNION ALL

    -- 2. Recursive Member (재귀 부분)
    SELECT 계산값들...,
        R.DEPTH + 1 -- 기존 깊이에 1 추가
    FROM 테이블 T
    JOIN recur_cte R ON 조인조건
    WHERE 종료조건
)
SELECT * FROM recur_cte;</code></pre>
<p>문법을 보시면 아시겠지만, 호출을 위해 <code>recur_cte</code> 내부 본인을 재귀적으로 호출하고 있습니다.</p>
<p>기준점과 재귀 파트가 있는데, 이 둘을 <code>UNION ALL</code> 연산을 통해 모두 합치는 과정을 거칩니다. 합치는 것이 종료되는 시점은 바로 재귀 파트에서 더이상 새로운 row가 없을 때 종료됩니다.</p>
<p>또한 재귀 깊이는 SQL이 아닌 알고리즘 문제에서도 dfs할 때 <code>depth</code> 형태로 많이 쓰이는 만큼 쿼리를 짤 때도 해당 칼럼은 추가해주는 편이 좋습니다.</p>
<h2 id="멸종위기의-대장균-찾기">멸종위기의 대장균 찾기</h2>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/301651">[PRG] 멸종위기의 대장균 찾기</a></p>
<p>문제에서 요구하는 것은 다음과 같습니다.</p>
<blockquote>
<ul>
<li>각 대장균들의 세대를 파악해야 됩니다.</li>
<li>각 대장균들 중 자식이 없는 대장균을 파악해야 됩니다.</li>
<li>각 세대별 자식이 없는 개체의 수(<code>COUNT</code>)와 세대(<code>GENERATION</code>)를 출력합니다.</li>
</ul>
</blockquote>
<h3 id="cte-생성">CTE 생성</h3>
<p>각 세대를 마킹하기 위해서는 알고리즘 문제 풀 듯 dfs 진행하면 됩니다. 즉 재귀 쿼리가 적절한 선택입니다.</p>
<pre><code class="language-sql">WITH RECURSIVE CTE AS (
    SELECT ID,
        PARENT_ID,
        1 AS GENERATION
    FROM ECOLI_DATA
    WHERE PARENT_ID IS NULL
    UNION ALL
    SELECT E.ID,
        E.PARENT_ID,
        C.GENERATION + 1
    FROM ECOLI_DATA E
    JOIN CTE C ON (E.PARENT_ID = C.ID)
)</code></pre>
<p>여기서 <code>depth</code> 역할을 하는 것이 바로 <code>GENERATION</code> 칼럼입니다.</p>
<h3 id="쿼리">쿼리</h3>
<p>자식이 없다라는 말은 해당 대장균과 연결된 <code>PARENT_ID</code> 가 없다는 말과 동일합니다.</p>
<p>이를 구현하는 방법은 크게 두 가지가 있습니다.</p>
<ul>
<li><p><code>CTE</code> 에 원래 테이블(<code>ECOLI_DATA</code>) 을 <code>LEFT JOIN</code> 을 걸어서 <code>ECOLI_DATA</code> 의 <code>ID</code>가 <code>NULL</code> 인 것이 바로 자식이 없는 대장균입니다.</p>
<pre><code class="language-sql">  SELECT 
      COUNT(C.GENERATION) AS COUNT,
      C.GENERATION
  FROM CTE C
  LEFT JOIN ECOLI_DATA E ON (E.PARENT_ID = C.ID)
  WHERE E.ID IS NULL
  GROUP BY GENERATION</code></pre>
</li>
<li><p><code>NOT EXISTS</code> 로 <code>ECOLI_DATA</code> 의 <code>PARENT_ID</code> 에 매핑되는 <code>CTE</code>의 <code>ID</code> 를 제외해줍니다.</p>
<pre><code class="language-sql">  SELECT 
      COUNT(GENERATION) AS COUNT,
      GENERATION
  FROM CTE
  WHERE NOT EXISTS (
      SELECT 1 FROM ECOLI_DATA E WHERE E.PARENT_ID = CTE.ID
  )
  GROUP BY GENERATION</code></pre>
</li>
</ul>
<p>해당 상황은 <code>NOT EXISTS</code> 가 더 적합합니다. 해당 주제로 추후 포스팅 예정이지만, <code>NOT EXISTS</code> 의 동작은 중간에 <code>EXISTS</code> 조건을 만족하는 행을 찾으면 즉시 중단하기에 수행 횟수 측면에서 이득이 있습니다.</p>
<h3 id="전체-예시-답안">전체 예시 답안</h3>
<pre><code class="language-sql">WITH RECURSIVE CTE AS (
    SELECT ID,
        PARENT_ID,
        1 AS GENERATION
    FROM ECOLI_DATA
    WHERE PARENT_ID IS NULL
    UNION ALL
    SELECT E.ID,
        E.PARENT_ID,
        C.GENERATION + 1
    FROM ECOLI_DATA E
    JOIN CTE C ON (E.PARENT_ID = C.ID)
) 

-- NOT EXIST 활용
SELECT 
    COUNT(GENERATION) AS COUNT,
    GENERATION
FROM CTE
WHERE NOT EXISTS (
    SELECT 1 FROM ECOLI_DATA E WHERE E.PARENT_ID = CTE.ID
)
GROUP BY GENERATION</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] #1626 두 번째로 작은 스패닝 트리]]></title>
            <link>https://velog.io/@red-sprout/%EB%B0%B1%EC%A4%80-1626-%EB%91%90-%EB%B2%88%EC%A7%B8%EB%A1%9C-%EC%9E%91%EC%9D%80-%EC%8A%A4%ED%8C%A8%EB%8B%9D-%ED%8A%B8%EB%A6%AC</link>
            <guid>https://velog.io/@red-sprout/%EB%B0%B1%EC%A4%80-1626-%EB%91%90-%EB%B2%88%EC%A7%B8%EB%A1%9C-%EC%9E%91%EC%9D%80-%EC%8A%A4%ED%8C%A8%EB%8B%9D-%ED%8A%B8%EB%A6%AC</guid>
            <pubDate>Sun, 29 Jun 2025 06:55:06 GMT</pubDate>
            <description><![CDATA[<p>C++로 언어 변경한지는 좀 지났는데, C++로 작성하는 첫 포스팅이 되었네요</p>
<p><a href="https://www.acmicpc.net/problem/1626">https://www.acmicpc.net/problem/1626</a>
문제는 아주 간단합니다. MST 가 아닌 SMST(Second Minimum Spanning Tree)를 구하면 됩니다.</p>
<h2 id="개요">개요</h2>
<p>우선 <a href="https://velog.io/@red-sprout/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%9D%AC%EC%86%8C-%EB%B0%B0%EC%97%B4Sparse-Table">희소 배열</a>에 대한 사전 지식이 있어야 하고, <a href="https://www.acmicpc.net/problem/15481">#15481 그래프와 MST</a>를 미리 푸는 것이 좋습니다. </p>
<p>여기서는 희소 배열에 대한 것은 안다고 생각하고, 15481번 과 1626번 순으로 풀이하겠습니다.</p>
<h2 id="15481---그래프와-mstp1">15481 - 그래프와 MST(P1)</h2>
<p>각각의 간선에 대해서 각 간선을 포함하는 MST를 구할 때 그 가중치 합들을 각각 구하면 되는 문제입니다. 정점의 수는 <code>N</code>, 간선의 수는 <code>M</code>이라 가정합니다.</p>
<p>우선 시간 복잡도를 고려하지 않은 풀이를 생각하면 다음과 같습니다.</p>
<ul>
<li>간선 정보를 모두 저장한다.</li>
<li>각 간선에 대해서 해당하는 간선이 연결되어 있다고 생각하고 MST를 구한다</li>
<li>가중치 합을 구한다.</li>
</ul>
<p>MST를 크루스칼 알고리즘으로 구한다고 하면 기본적으로 <code>O(MlogM)</code>이 나오고, 각 간선에 대해서 모두 따지면 <code>O(M * MlogM)</code> 이라는 TLE 받기 좋은 시간 복잡도가 나오게 됩니다. 그래서 조금은 다른 방법을 생각해봅니다.</p>
<p>여기서 크루스칼 알고리즘을 M번 돌리는게 문제라는 걸 알 수 있습니다. 그래서 이를 최소한으로, 가능하면 딱 한번만 돌려서 판단해보면 좋을 것 같습니다. 여기서 파생해서 이러한 아이디어를 생각할 수 있습니다.</p>
<ul>
<li>일단 MST를 구한다.</li>
<li>각 간선에 대해서<ul>
<li>해당 간선이 MST에 포함되면 추가 연산이 불필요하다.</li>
<li>하지만, MST에 포함되지 않으면 추가 연산이 필요하다.</li>
</ul>
</li>
</ul>
<p>추가 연산이 MST 다시 구하는거면 너무 무겁습니다. 하지만 이렇게 생각해볼 수 있습니다. 결국 해당 간선이 MST에 해당하지 않으면 해당하는 정점이 해당 간선으로는 연결이 되지 않았다는 것입니다. 다른 경로가 MST에 있다는 것입니다. 각 정점을 <code>u</code>, <code>v</code>라 하겠습니다.</p>
<p>여기서 MST는 기본적으로 트리이기 때문에 <code>u</code>, <code>v</code> 사이 경로가 유일함이 보장됩니다. 그렇기에 <strong>MST의 <code>u</code>, <code>v</code> 사이 경로 중 최대가 되는 값을 우리가 원하는 간선으로 대치</strong> 해주면 원하는 간선을 포함한 MST가 되는 것입니다. 유일한 경로을 끊어주고 새로운 유일한 경로를 만들어주는 느낌입니다.</p>
<p>즉, MST를 그리고 <strong>각 정점 사이 간선의 최대값</strong> 을 저장해주면 됩니다. 그리고 이를 저장할 때 유용한 자료구조가 바로 <strong>희소 배열</strong>이고, 최소 공통 조상(LCA) 알고리즘을 통해서 저장, 조회를 진행하면 됩니다.</p>
<p>코드에서는 특별히 해당 간선이 MST인지 아닌지를 별도로 구분하진 않았습니다. 어차피 MST에 포함된다면 본인을 끊었다 다시 본인으로 연결하는 꼴이기 때문입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;

using namespace std;

typedef long long ll;
typedef pair&lt;int, ll&gt; pil;

struct Edge {
    int u, v; ll w;
    Edge(int _u, int _v, ll _w) : u(_u), v(_v), w(_w) {}
};

const int MAX = 200&#39;001;
int n, m, p[MAX], depth[MAX], pp[20][MAX];
ll wmax[20][MAX];
vector&lt;pil&gt; g[MAX], mst[MAX];
vector&lt;Edge&gt; edges, sorted_edges;

int find_p(int x) { return p[x] == x ? x : p[x] = find_p(p[x]); }
void union_p(int x, int y) { (x &gt; y ? p[x] : p[y]) = (x &gt; y ? y : x); }
bool comp(Edge e1, Edge e2) { return e1.w &lt; e2.w; }

void dfs(int cur, int d) {
    depth[cur] = d;
    for(auto nxt : mst[cur]) {
        if(depth[nxt.first] == 0) {
            pp[0][nxt.first] = cur;
            wmax[0][nxt.first] = nxt.second;
            dfs(nxt.first, d + 1);
        }
    }
}

ll lca(int u, int v) {
    ll res = 0;
    if(depth[u] &lt; depth[v]) swap(u, v);
    for(int i = 19; i &gt;= 0; --i) {
        if(depth[pp[i][u]] &gt;= depth[v]) {
            res = max(res, wmax[i][u]);
            u = pp[i][u];
        }
    }
    if(u == v) return res;
    for(int i = 19; i &gt;= 0; --i) {
        if(pp[i][u] != pp[i][v]) {
            res = max(res, max(wmax[i][u], wmax[i][v]));
            u = pp[i][u];
            v = pp[i][v];
        }
    }
    res = max(res, max(wmax[0][u], wmax[0][v]));
    return res;
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);

    cin &gt;&gt; n &gt;&gt; m;
    int u, v; ll w;
    for(int i = 0; i &lt; m; ++i) {
        cin &gt;&gt; u &gt;&gt; v &gt;&gt; w;
        g[u].emplace_back(v, w);
        g[v].emplace_back(u, w);
        Edge e(u, v, w);
        edges.emplace_back(e);
        sorted_edges.emplace_back(e);
    }

    ll mst_val = 0;
    int cnt = 0;
    sort(sorted_edges.begin(), sorted_edges.end(), comp);
    for(int i = 1; i &lt;= n; ++i) p[i] = i;
    for(auto&amp; e : sorted_edges) {
        int u = find_p(e.u), v = find_p(e.v);
        if (u != v) {
            union_p(u, v);
            mst_val += e.w;
            mst[e.u].emplace_back(e.v, e.w);
            mst[e.v].emplace_back(e.u, e.w);
            if (++cnt == n - 1) break;
        }
    }

    dfs(1, 1);
    for(int i = 1; i &lt; 20; ++i) {
        for(int j = 1; j &lt;= n; ++j) {
            pp[i][j] = pp[i - 1][pp[i - 1][j]];
            wmax[i][j] = max(wmax[i - 1][j], wmax[i - 1][pp[i - 1][j]]);
        }
    }

    for (auto&amp; e : edges) cout &lt;&lt; mst_val - lca(e.u, e.v) + e.w &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<h2 id="1626---두-번째로-작은-스패닝-트리d4">1626 - 두 번째로 작은 스패닝 트리(D4)</h2>
<p>위 문제를 풀었다면, 이 문제 아이디어도 어렵지 않게 생각할 수 있습니다. 다만 몇가지 실수할 여지가 있습니다.</p>
<p>이 문제는 SMST(Second Minimum Spanning Tree)를 구하면 되는 것이고 핵심은 <strong>MST보다 크다</strong> 에 있습니다. 그렇기에 MST를 만들 수 없거나 아니면 MST 밖에 못만드는 상황에서는 SMST를 만들 수 없습니다.</p>
<h3 id="처음-생각오답">처음 생각(오답)</h3>
<p>위 그래프와 MST 문제처럼 각 간선을 택했을 때 최댓값을 변경하는 방식으로 진행했습니다. 그래서 나온 코드는 아래와 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;

using namespace std;
typedef pair&lt;int, int&gt; pii;
typedef long long ll;

struct Edge {
    int u, v, w;
    Edge(int _u, int _v, int _w) : u(_u), v(_v), w(_w) {}
};

const int MAX = 50&#39;001;

vector&lt;pii&gt; g[MAX], mst[MAX];
vector&lt;Edge&gt; edges;

int n, m;
int par[MAX], dep[MAX], pp[17][MAX], wmax[17][MAX];

int find_p(int x) { return (par[x] == x ? x : par[x] = find_p(par[x])); }
void union_p(int x, int y) { (x &gt; y ? par[x] : par[y]) = (x &gt; y ? y : x); }
bool comp(Edge e1, Edge e2) { return e1.w &lt; e2.w; }

int kruskal() {
    int res = 0, cnt = 0;
    sort(edges.begin(), edges.end(), comp);
    for(int i = 1; i &lt;= n; ++i) par[i] = i;
    for(auto e : edges) {
        int pu = find_p(e.u);
        int pv = find_p(e.v);
        if(pu != pv) {
            union_p(pu, pv);
            res += e.w;
            mst[e.u].emplace_back(e.v, e.w);
            mst[e.v].emplace_back(e.u, e.w);
            if(++cnt == n - 1) return res;
        }
    }
    return -1;
}

void dfs(int cur, int d) {
    dep[cur] = d;
    for(auto nxt : mst[cur]) {
        if(dep[nxt.first] == 0) {
            pp[0][nxt.first] = cur;
            wmax[0][nxt.first] = nxt.second;
            dfs(nxt.first, d + 1);
        }
    }
}

int lca(int u, int v) {
    int res = 0;
    if(dep[u] &lt; dep[v]) swap(u, v);
    for(int i = 16; i &gt;= 0; --i) {
        if(dep[pp[i][u]] &gt;= dep[v]) {
            res = max(res, wmax[i][u]);
            u = pp[i][u];
        }
    }
    if(u == v) return res;
    for(int i = 16; i &gt;= 0; --i) {
        if(pp[i][u] != pp[i][v]) {
            res = max(res, max(wmax[i][u], wmax[i][v]));
            u = pp[i][u];
            v = pp[i][v];
        }
    }
    res = max(res, max(wmax[0][u], wmax[0][v]));
    return res;
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    cin &gt;&gt; n &gt;&gt; m;
    int u, v, w;
    for(int i = 0; i &lt; m; ++i) {
        cin &gt;&gt; u &gt;&gt; v &gt;&gt; w;
        g[u].emplace_back(v, w);
        g[v].emplace_back(u, w);
        Edge e(u, v, w);
        edges.emplace_back(e);
    }

    int mst_val = kruskal();
    if(mst_val == -1) {
        cout &lt;&lt; mst_val &lt;&lt; &#39;\n&#39;;
        return 0;
    }

    dfs(1, 1);
    for(int i = 1; i &lt; 17; ++i) {
        for(int j = 1; j &lt;= n; ++j) {
            pp[i][j] = pp[i - 1][pp[i - 1][j]];
            wmax[i][j] = max(wmax[i - 1][j], wmax[i - 1][pp[i - 1][j]]);
        }
    }

    ll smst = 1e18;
    for(auto e : edges) {
        int l = lca(e.u, e.v);
        if(l &lt; e.w) smst = min(smst, ll(mst_val - l + e.w));
    }

    cout &lt;&lt; (smst == 1e18 ? -1 : smst) &lt;&lt; &#39;\n&#39;;
    return 0;
}
</code></pre>
<h3 id="최종정답">최종(정답)</h3>
<p>하지만, 위 코드로 제출할 경우 반례가 있습니다. 바로 <strong>최댓값에 해당하는 간선의 가중치와 포함하려는 간선의 가중치가 동일한 경우</strong> 입니다. 이런 경우는 SMST가 아닌 그냥 새로운 MST를 구하는 꼴입니다.</p>
<p>이를 해결하기 위해서는 바로 최댓값 뿐만 아니라 <strong>두번째로 큰 간선</strong>도 같이 저장해줍니다. 각각 <code>wmax</code>와 <code>swmax</code>로 저장을 해줍니다.</p>
<p>두번째로 큰 값 구하는게 조금 귀찮은데, 여러 방법이 있지만 다음과 같이 일일이 순회하면서 구현했습니다.</p>
<pre><code class="language-cpp">// {최대, 두번째 최대} 구하기
// vector&lt;int&gt; nxt - 기존 최대, 기존 두번째 최대, 새로운 값들 등을 넣는 인자
pii calc(vector&lt;int&gt; nxt) {
    int mm = -1, sm = -1;
    for(int x : nxt) {
        if(x == -1) continue;
        if(mm &lt; x) {
            sm = mm;
            mm = x;
        } else if(sm &lt; x &amp;&amp; x &lt; mm) {
            sm = x;
        }
    }
    return { mm, sm };
}</code></pre>
<p>그래서 다음과 같이 희소 배열 초기화할 때나</p>
<pre><code class="language-cpp">    dfs(1, 1);
    for(int i = 1; i &lt; 17; ++i) {
        for(int j = 1; j &lt;= n; ++j) {
            pp[i][j] = pp[i - 1][pp[i - 1][j]];
            pii res = calc({ wmax[i - 1][j], swmax[i - 1][j], wmax[i - 1][pp[i - 1][j]], swmax[i - 1][pp[i - 1][j]] });
            wmax[i][j] = res.first;
            swmax[i][j] = res.second;
        }
    }</code></pre>
<p>아니면 lca 구할 때</p>
<pre><code class="language-cpp">pii lca(int u, int v) {
    int mm = -1, sm = -1;
    pii res;
    if(dep[u] &lt; dep[v]) swap(u, v);
    for(int i = 16; i &gt;= 0; --i) {
        if(dep[pp[i][u]] &gt;= dep[v]) {
            res = calc({ mm, sm, wmax[i][u], swmax[i][u] });
            mm = res.first;
            sm = res.second;
            u = pp[i][u];
        }
    }
    if(u == v) return { mm, sm };
    for(int i = 16; i &gt;= 0; --i) {
        if(pp[i][u] != pp[i][v]) {
            res = calc({ mm, sm, wmax[i][u], swmax[i][u], wmax[i][v], swmax[i][v] });
            mm = res.first;
            sm = res.second;
            u = pp[i][u];
            v = pp[i][v];
        }
    }
    res = calc({ mm, sm, wmax[0][u], swmax[0][u], wmax[0][v], swmax[0][v] });
    mm = res.first;
    sm = res.second;
    return { mm, sm };
}</code></pre>
<p>이 때 중간중간 넣어주면서 구현해주면 됩니다.</p>
<h3 id="최종-코드">최종 코드</h3>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;

using namespace std;
typedef pair&lt;int, int&gt; pii;
typedef long long ll;

struct Edge {
    int u, v, w;
    Edge(int _u, int _v, int _w) : u(_u), v(_v), w(_w) {}
};

const int MAX = 50001;

bool check[200000];
vector&lt;pii&gt; g[MAX], mst[MAX];
vector&lt;Edge&gt; edges;

int n, m;
int par[MAX], dep[MAX], pp[17][MAX], wmax[17][MAX], swmax[17][MAX];

int find_p(int x) { return (par[x] == x ? x : par[x] = find_p(par[x])); }
void union_p(int x, int y) { (x &gt; y ? par[x] : par[y]) = (x &gt; y ? y : x); }
bool comp(Edge e1, Edge e2) { return e1.w &lt; e2.w; }

int kruskal() {
    int res = 0, cnt = 0;
    sort(edges.begin(), edges.end(), comp);
    for(int i = 1; i &lt;= n; ++i) par[i] = i;
    for(int i = 0; i &lt; m; ++i) {
        Edge e = edges[i];
        int pu = find_p(e.u);
        int pv = find_p(e.v);
        if(pu != pv) {
            union_p(pu, pv);
            res += e.w;
            mst[e.u].emplace_back(e.v, e.w);
            mst[e.v].emplace_back(e.u, e.w);
            check[i] = true;
            if(++cnt == n - 1) return res;
        }
    }
    return -1;
}

void dfs(int cur, int d) {
    dep[cur] = d;
    for(auto nxt : mst[cur]) {
        if(dep[nxt.first] == 0) {
            pp[0][nxt.first] = cur;
            wmax[0][nxt.first] = nxt.second;
            swmax[0][nxt.first] = -1;
            dfs(nxt.first, d + 1);
        }
    }
}

pii calc(vector&lt;int&gt; nxt) {
    int mm = -1, sm = -1;
    for(int x : nxt) {
        if(x == -1) continue;
        if(mm &lt; x) {
            sm = mm;
            mm = x;
        } else if(sm &lt; x &amp;&amp; x &lt; mm) {
            sm = x;
        }
    }
    return { mm, sm };
}

pii lca(int u, int v) {
    int mm = -1, sm = -1;
    pii res;
    if(dep[u] &lt; dep[v]) swap(u, v);
    for(int i = 16; i &gt;= 0; --i) {
        if(dep[pp[i][u]] &gt;= dep[v]) {
            res = calc({ mm, sm, wmax[i][u], swmax[i][u] });
            mm = res.first;
            sm = res.second;
            u = pp[i][u];
        }
    }
    if(u == v) return { mm, sm };
    for(int i = 16; i &gt;= 0; --i) {
        if(pp[i][u] != pp[i][v]) {
            res = calc({ mm, sm, wmax[i][u], swmax[i][u], wmax[i][v], swmax[i][v] });
            mm = res.first;
            sm = res.second;
            u = pp[i][u];
            v = pp[i][v];
        }
    }
    res = calc({ mm, sm, wmax[0][u], swmax[0][u], wmax[0][v], swmax[0][v] });
    mm = res.first;
    sm = res.second;
    return { mm, sm };
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    cin &gt;&gt; n &gt;&gt; m;
    int u, v, w;
    for(int i = 0; i &lt; m; ++i) {
        cin &gt;&gt; u &gt;&gt; v &gt;&gt; w;
        g[u].emplace_back(v, w);
        g[v].emplace_back(u, w);
        Edge e(u, v, w);
        edges.emplace_back(e);
    }

    int mst_val = kruskal();
    if(mst_val == -1) {
        cout &lt;&lt; mst_val &lt;&lt; &#39;\n&#39;;
        return 0;
    }

    dfs(1, 1);
    for(int i = 1; i &lt; 17; ++i) {
        for(int j = 1; j &lt;= n; ++j) {
            pp[i][j] = pp[i - 1][pp[i - 1][j]];
            pii res = calc({ wmax[i - 1][j], swmax[i - 1][j], wmax[i - 1][pp[i - 1][j]], swmax[i - 1][pp[i - 1][j]] });
            wmax[i][j] = res.first;
            swmax[i][j] = res.second;
        }
    }

    ll smst = 1e18;
    int mm, sm;
    for(int i = 0; i &lt; m; ++i) {
        if(check[i]) continue;
        Edge e = edges[i];
        pii res = lca(e.u, e.v);
        mm = res.first, sm = res.second;
        if(e.w &gt; mm) smst = min(smst, ll(mst_val - mm + e.w));
        else if(e.w &gt; sm &amp;&amp; sm != -1) smst = min(smst, ll(mst_val - sm + e.w));
    }

    cout &lt;&lt; (smst == 1e18 ? -1 : smst) &lt;&lt; &#39;\n&#39;;
    return 0;
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[다이아 달성 후기]]></title>
            <link>https://velog.io/@red-sprout/%EB%8B%A4%EC%9D%B4%EC%95%84-%EB%8B%AC%EC%84%B1-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@red-sprout/%EB%8B%A4%EC%9D%B4%EC%95%84-%EB%8B%AC%EC%84%B1-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 23 Jun 2025 03:38:57 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>조금씩 풀다보니 다이아5가 되었습니다!</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/a37af3a3-4f73-4107-841d-943edb5de633/image.png" alt=""></p>
<p>풀다보니 골드가 없어져 있어서 플레티넘 4이상이 아니라면 제자리 걸음...</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/42887b46-c0ec-4f2b-852d-dba65eb55a7e/image.png" alt=""></p>
<p>나름 1일 1알고를 하려 노력을 했습니다. 8월 중에는 SSAFY에서 SWEA에 집중한다고 백준을 잠깐 손놓긴 했었지만, 프로젝트 기간에 밤을 새더라도 문제는 풀자는 주의로 웬만하면 문제를 풀었습니다.</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/1b13c8ac-acf6-4dee-b519-cc132ff48ad1/image.png" alt=""></p>
<p>아무래도 집중해서 문제만 풀 수는 없는 입장인지라, 플레티넘에서 다이아까지는 굉장히 오랜시간이 걸렸던 것 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/433a5760-10db-469f-bed2-98bac316a8d9/image.png" alt=""></p>
<p>흔히 PS계에서 티어 올리기 쉬운 세그먼트 트리 등 구간 쿼리 문제들도 많이 풀긴 했는데, 확실히 세그먼트 트리가 알면 티어에 비해서는 쉬운편이 맞는 것 같습니다. <del>그래도 <a href="https://velog.io/@red-sprout/%EB%B0%B1%EC%A4%80-10167-%EA%B8%88%EA%B4%91">금광</a>은 어려웠...</del></p>
<p>개인적으로 재밌게 푼 알고리즘 분류들은</p>
<ul>
<li>Dynamic Programing</li>
<li><a href="https://velog.io/@red-sprout/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%9D%AC%EC%86%8C-%EB%B0%B0%EC%97%B4Sparse-Table">Sparse Table</a></li>
<li>Network Flow &amp; MCMF</li>
</ul>
<p>이 분류들이 재밌었습니다.</p>
<p>수학, 기하학을 풀면 사실 좀 더 빨리 찍었을 것 같기도 한데, 사실 기업 코딩 테스트에는 거의 안나오는 유형이기도 해서(라고 하기엔 코딩 테스트에 나오는 알고리즘은 넘어서 푼 것 같긴 하지만) 배제하고 있었습니다. 하지만 이제 DP도 CHT(Convex Hull Trick) 등 최적화를 요하는 문제가 슬슬 나와서 관련 문제도 슬슬 풀어야겠다고 생각은 듭니다. </p>
<p>무엇보다 다각형이 이쁘게 나오고 싶긴 합니다ㅎㅎ</p>
<p>그리고 문자열을 Trie, Rabin-Karp 몇 문제만 풀어봐서 많이 못풀었는데 문자열도 좀 풀어야 될 것 같네요</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/41f5517a-f64b-4507-b146-eedc9992a458/image.png" alt=""></p>
<h2 id="지금까지-기억나는-문제들">지금까지 기억나는 문제들</h2>
<h4 id="승급을-시켜준-마지막-문제--1396---크루스칼의-공p1">승급을 시켜준 마지막 문제 : <a href="https://www.acmicpc.net/problem/1396">1396 - 크루스칼의 공(P1)</a></h4>
<details>
      <p>Sparse Table로 풀이하였습니다. 병렬 이분 탐색의 기본 문제로도 알려져있는 것으로 알고 있지만, 해당 알고리즘 대신 다른 방법으로 해결하였습니다. 개인적으로 되게 참신한 방식이라 생각됩니다.</p>
</details>

<h4 id="티어가-가장-높은-문제--1626---두-번째로-작은-스패닝-트리d4--풀이">티어가 가장 높은 문제 : <a href="https://www.acmicpc.net/problem/1626">1626 - 두 번째로 작은 스패닝 트리(D4)</a> / <a href="https://velog.io/@red-sprout/%EB%B0%B1%EC%A4%80-1626-%EB%91%90-%EB%B2%88%EC%A7%B8%EB%A1%9C-%EC%9E%91%EC%9D%80-%EC%8A%A4%ED%8C%A8%EB%8B%9D-%ED%8A%B8%EB%A6%AC">풀이</a></h4>
<details>
      <p>Sparse Table로 풀이하였습니다. 처음에 한번 틀렸는데, 가장 큰 간선만 저장해서 교환하는 방식을 취했기 때문입니다. 반례를 찾아서 두 번째로 큰 간선도 추가로 저장을 해 주었습니다.</p>
</details>

<h4 id="가장-많이-틀린-문제--10217---kcm-travelp4">가장 많이 틀린 문제 : <a href="https://www.acmicpc.net/problem/10217">10217 - KCM Travel(P4)</a></h4>
<p>얼마나 틀렸냐고요? 사진 한장으로 대체합니다...ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
<img src="https://velog.velcdn.com/images/red-sprout/post/790f869b-3265-482b-b32a-38a570edae04/image.png" alt="KCM travel"></p>
<details>
      <p>딱 봐도 Dijkstra 입니다. 다만 일반 다익스트라를 돌리면 안되고, 노드와 비용에 대해서 정보를 저장하는 Dynamic Programing 이 결합된 형태로 해결해야 됩니다. 하지만 이 문제는 이렇게 해서 TLE가 나기 때문에 우선순위 큐가 아닌 <strong>미리 간선들을 소요 시간에 대해서 정렬을 해놓고</strong> 큐로 풀어야 됩니다.</p>
      <p>너무 힘들게 풀긴 했는데, 삼성 역량 테스트 Professional(B형)을 취득했을 당시에도 Dijkstra + DP 유형이 나와서 해당 문제로 먼저 얻어맞은 저는 역량 테스트 문제는 아주 쉽게 풀 수 있었습니다. 그래서 여러모로 또 가장 생각나는 문제이기도 합니다.</p>
</details>

<h4 id="가장-오래-풀었던-문제--5372---큐빙p5">가장 오래 풀었던 문제 : <a href="https://www.acmicpc.net/problem/5373">5372 - 큐빙(P5)</a></h4>
<details>
      <p>골드 시절 풀었던 문제인데, 푸는데 1주일이나 걸렸습니다. 특별한 건 없습니다. 회전하는 것을 일일이 구현하면 됩니다. 구현 능력이 정말 안좋았을 때라 오래 걸리긴 했는데, 지금 풀면 또 빠르게 풀지않을까 싶습니다. 그래도 빡구현 유형은 아직까지 가장 힘듭니다.</p>
</details>

<h2 id="후기">후기</h2>
<p>다이아는 늦어도 올 중순까지는 찍고 싶긴 했는데 찍게 되어서 우선 기쁩니다.</p>
<p>사실 티어를 올리는 건 이제 크게 의미는 없긴 합니다. 대학생도 아니고, 소소한 일반인 대상 대회 정도는 생각 있지만 그런거 외에는 의미가 없기도 합니다.</p>
<p>개발을 하면서 느끼는 건, 알고리즘 역량이 정말 개발에 도움이 되냐 했을 때 느끼는 건 아직까진 반반인 것 같습니다.</p>
<p>물론 아직 실무에서 핵심 비즈니스 로직을 설계해본 경험은 없어서 이 부분은 모르겠지만, 이렇게 문제 풀면서 구현 능력이나 문제 상황에서 바로 코드부터 작성하는 대신 어떻게 접근을 해야되는지 설계하는 능력은 확실히 많이 늘어난 것 같습니다. </p>
<p>이제 그만 풀 것인가? 라고 하기에는 알고리즘 공부 자체가 많이 재밌어졌습니다. 그냥 게임 대신 한번 스트레스 풀겸 종종 풀 것 같습니다. 대신 백준은 조금 쉬엄쉬엄하고 코드포스도 좀 풀어야겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SSAFY] SSAFY 회고록]]></title>
            <link>https://velog.io/@red-sprout/SSAFY-SSAFY-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@red-sprout/SSAFY-SSAFY-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Mon, 05 May 2025 03:43:30 GMT</pubDate>
            <description><![CDATA[<p>결국 2학기가 끝나기 전 취업을 하게 되면서 일명 싸탈을 하게 되었습니다!</p>
<h1 id="들어오기-전">들어오기 전</h1>
<h2 id="개발의-시작">개발의 시작</h2>
<p>대학원 자퇴생입니다. 대학원을 나온 이유는 명확합니다. 뭔가 해보고 싶은 업무의 방향성이 달랐기 때문입니다.</p>
<p>기계공학을 전공하면서 수학과 물리학은 좋아했지만 정작 기계 그 자체에는 별 흥미가 없던 것이 큽니다. 대신 산업군의 시스템 관리 및 최적화와 관련된 업무를 하고 싶다는 생각을 많이 했습니다.</p>
<p>이를 구현하기 위해서 가장 필요한 것이 <strong>백엔드 개발</strong>이라 생각하였고, 결국은 SSAFY에 지원하게 되었습니다.</p>
<h2 id="한번의-고배">한번의 고배</h2>
<p>사실 전 SSAFY 11기에 지원했었고, 지원이야기가 나올 시점에 <code>Hello World</code> 한번 쳐봤습니다. <strong>컴퓨터공학과는 사실 큰 접점이 없었습니다.</strong> 그리고 시험도 잘 못쳐서 서류 탈락이라는 고배를 마시게 되었습니다.</p>
<p>그래서 SSAFY 탈락 후 뭐라도 해야겠다는 심정으로 국비학원을 다녔고, 학원을 다니면서도 SSAFY를 가고 싶다 생각하였습니다.</p>
<p>그 결과 SSAFY 12기 비전공자로 합격하였습니다!</p>
<p>더불어 <strong>입과시험으로 전공 Java 반에 입과하였고</strong>, 본격적으로 SSAFY 생활이 시작되었습니다.</p>
<h1 id="1학기">1학기</h1>
<h2 id="전공자들과의-만남">전공자들과의 만남</h2>
<p>국비학원 다니면서 완전 기초부터 시작해 알고리즘을 계속 풀어와서 백준 티어가 플레티넘 4인 상태에서 들어왔습니다. 전공자들은 다이아, 루비들이 많겠지라는 막연한 기대감이 있던 것 같습니다.</p>
<p>알고리즘은 의외로 제가 가장 잘했습니다. 다만 문제가 된 부분은 바로 그냥 일상적인 대화였습니다.</p>
<p>전공자들이 개발과 관련된 용어들을 그냥 자연스럽게 이야기하는데 전 이해한 척만 하고 뭔소리지...?를 맘속으로 많이 생각했던 것 같습니다.</p>
<p>그래서 스터디를 좀 진행했었습니다. 싸피 내 스터디를 3개 다니면서 부족한 부분을 채우려고 노력 많이했던 것 같습니다.</p>
<p>그리고 전공자 반이 좋았던게, 인사이트를 얻을 수 있는 사람들이 많았습니다. 사람이 많다는게 싸피 최고의 장점이라 생각하는데, 개발 인맥도 쌓고 취업 정보 및 개발 관련 지식을 쌓는데 좋은 환경이였다 생각합니다.</p>
<p>이렇게 해서 1학기를 <strong>성적 우수(2등)</strong> 으로 마치게 됩니다.</p>
<h2 id="b형">B형</h2>
<p>싸피의 또다른 혜택으로는 삼성 SW 역량테스트 B형 응시 기회가 주어진다는 것입니다. 알고리즘에 어느정도 자신 있는 저로써 B형도 쉽게 취득하리라 생각했던 것 같습니다. 하지만 그렇지 않았습니다.</p>
<p>기본적으로 구현력이 발목을 잡았습니다. 삼성에서 내는 구현 스타일의 문제가 있는데, 이게 익숙하지 않았던 탓이 큽니다. 또한 너무 수준을 얕보고 이정도 시간이면 되겠지... 하고 넘겼던 것도 큽니다.</p>
<p>사실 1학기의 목표는 취업 아니면 B형이였는데, 둘 다 이루지 못하고 2학기에 진학하게 되었습니다.</p>
<h1 id="2학기">2학기</h1>
<h2 id="공통">공통</h2>
<p>임베디드를 맡게 되었고, Java는 MQTT 설정하는 부분을 제외하고는 거의 C, python, shell script만 주구장창 만지게 되었습니다.</p>
<p>공통을 생각하면 아쉬운 부분이 많습니다. 기획을 좀 더 빨리하면 어땠을까, 처음부터 임베 전공자가 있었으면 어땠을까, 시연 실수가 없었으면 어땠을까 등 다양한 아쉬운 부분들이 있습니다.</p>
<p>다만 얻어간 부분이 몇가지 있습니다.</p>
<ul>
<li>스마트팩토리에서 필요한 IoT와 MQTT 에 대해서는 어느 정도 설명을 할 수 있다.</li>
<li>다양한 개발 경험으로 자소서에 어필할 만한 것이 생긴다.</li>
</ul>
<p>그리고 임베디드 개발도 재미있기도 했습니다. 수상은 못했고 아쉬운 부분도 많지만, 분명히 얻어갈 점도 있긴 했습니다.</p>
<h2 id="특화">특화</h2>
<p>특화 기간은 팀장으로 활동했습니다. 그리고 이 기간을 저는 이렇게 표현하고 싶습니다.</p>
<blockquote>
<p>SSAFY 에서의 가장 좋았던 순간, 그리고 나가고 싶었던 순간이 공존하는 상태</p>
</blockquote>
<p>정말 가장 기뻤던 일과 가장 힘들었던 순간이 동시에 왔습니다.</p>
<p>1학기 저는 B형을 따지 못했다는 아쉬움에 바로 뜻이 맞는 사람들과 B형 스터디를 결성하였습니다. </p>
<p>그리고 공통에서 특화가 넘어가는 순간 2025년 1회차 B형을 응시하였고, <strong>운이 좋게도 1시간만에 답을 내고 B형을 합격하였습니다.</strong></p>
<p>하지만 동시에 이런 생각이 들었습니다.</p>
<blockquote>
<p>B형도 땄네...? 난 B형만 보고 왔는데...? 이제 뭐하지...?</p>
</blockquote>
<p>그리고 한동안 Java를 안보고 있었어서 백엔드에 대한 감이 떨어진 상태였고, Spring 거의 처음 본 시점 같은 기분을 느꼈습니다.</p>
<p>계속 앞만 보고 달려와서 번아웃이 와버렸습니다. 물론 팀장이니 팀장으로써 할 일들에 대해서는 해왔지만, 정작 개발에 대한 방향성을 상실했습니다.</p>
<p>하지만 <strong>팀원 운이 너무 좋았습니다.</strong> 팀원들이 긍정에너지가 가득해서 많이 힘든 시기였는데도 도움을 많이 주었습니다. 그리고 나온 결과도 좋아서 <strong>입상도 하게 되었습니다!</strong> 그리고 자율 때는 특화 때 개발적으로 아쉬웠던 부분들 보완하고자 다짐하였습니다.</p>
<h2 id="자율-그리고-싸탈">자율, 그리고 싸탈</h2>
<p>자율 팀원들도 좋은 팀원들과 함께 하게 되었는데, 별로 개발을 진행하지 않은 시점에서 지원했던 기업에 최종합격 소식을 받게 되었습니다.</p>
<p>2025년 상반기를 생각해보면 정말 채용 시장이 얼어있었습니다. 2024년 하반기 때도 역대급 한파다 이런 이야기가 많았는데, 이번에는 더더욱 그랬습니다.</p>
<p>작년 서류합격해서 면접까지 간 기업들이 있는데, 이번에는 서류에서 바로 탈락하였습니다. 당연히 서류는 합격할 줄 알았는데, 바로 떨어지는걸 보고 정말 이번 채용시장 많이 힘들구나 생각했습니다.</p>
<p>그런데, 코딩 테스트야 평소에 자신 있으니 논외로 하고, 다른 사람들이 다 떨어졌다는데 제가 딱 하나 붙고, 1차 면접도 긴장 안했고, 2차 면접은 아쉬웠는데 <strong>운이 좋게 최종 합격까지 가버렸습니다...!!</strong></p>
<p>관심있던 기업에 합격을 해서 싸탈을 하게 되었습니다.</p>
<h1 id="성과">성과</h1>
<ul>
<li>1학기 성적우수(2위)</li>
<li>배틀싸피 전국대항전 준우승</li>
<li>2학기 Best Member 2회(공통, 특화)</li>
<li>특화 프로젝트 입상(3위)</li>
<li>삼성 SW 역량테스트 B형(우수 풀이 선정)</li>
<li>우수 스터디 선정 다수</li>
<li><strong>취업 싸탈</strong></li>
<li>작성 시점 BOJ Platinum 1
<img src="https://velog.velcdn.com/images/red-sprout/post/9db40c09-fbd7-4ff0-8dae-23a7f2103c34/image.png" alt="bojp1"></li>
</ul>
<p>개인적으로 싸피 수료전 다이아는 달고 싶었는데 이건 아쉽긴 합니다. - <a href="https://velog.io/@red-sprout/%EB%8B%A4%EC%9D%B4%EC%95%84-%EB%8B%AC%EC%84%B1-%ED%9B%84%EA%B8%B0">결국 달았네요</a>
<del>그래도 취업이 더 좋으니까..ㅎ</del></p>
<h1 id="결론">결론</h1>
<p>싸피를 다니면서 크게 느낀 장점은 다음과 같습니다.</p>
<ul>
<li>100만원을 포함한 지원</li>
<li>삼성 SW 역량테스트 B형</li>
<li><strong>사람이 많고, 그 중 좋은 사람들이 많아 인맥 쌓기 좋습니다</strong></li>
</ul>
<p>다니면서 다른 것도 다른건데, 정말 사람이 중요하다 생각합니다. 싸피에서 좋은 사람들을 많이 만나 좋은 성과를 많이 낸 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] 도커 환경에서의 MongoDB dump 옮기기(MacOS)]]></title>
            <link>https://velog.io/@red-sprout/Docker-%EB%8F%84%EC%BB%A4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-MongoDB-dump-%EC%98%AE%EA%B8%B0%EA%B8%B0MacOS</link>
            <guid>https://velog.io/@red-sprout/Docker-%EB%8F%84%EC%BB%A4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-MongoDB-dump-%EC%98%AE%EA%B8%B0%EA%B8%B0MacOS</guid>
            <pubDate>Mon, 05 May 2025 02:45:53 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>크롤링을 로컬 PC(Macbook)으로 돌려서 데이터 수집을 진행했었습니다. 다만 저장한 데이터가 로컬 docker로 띄운 MongoDB 안에 있었는데, 이를 EC2 내부 docker로 띄운 개발용 MongoDB와 배포용 MongoDB 모두에 반영해야 되었습니다. Dump 파일을 옮기는 Docker 명령어 및 MongoDB 관련 사용법을 메모하고자 작성하게 되었습니다.</p>
<h2 id="1-로컬-docker-컨테이너에서-mongodb-덤프-생성">1. 로컬 Docker 컨테이너에서 MongoDB 덤프 생성</h2>
<p>로컬에서 실행 중인 Docker MongoDB 컨테이너에서 데이터를 덤프합니다. 예를 들어 컨테이너 이름이 &lt;컨테이너_이름&gt;, 데이터베이스 이름이 &lt;DB_이름&gt;, 계정 정보가 다음과 같다고 가정합니다.</p>
<ul>
<li>사용자명: <code>&lt;USERNAME&gt;</code></li>
<li>비밀번호: <code>&lt;PASSWORD&gt;</code></li>
<li>인증 DB: <code>admin</code></li>
</ul>
<pre><code>docker exec &lt;컨테이너_이름&gt; mongodump \
  -u &lt;USERNAME&gt; -p &lt;PASSWORD&gt; \
  --authenticationDatabase admin \
  --db &lt;DB_이름&gt; \
  --out /dump</code></pre><p>이 명령은 컨테이너 내부의 /dump 디렉토리에 &lt;DB_이름&gt; DB를 .bson 파일로 덤프합니다.</p>
<h2 id="2-컨테이너-내부-덤프-파일을-로컬로-복사">2. 컨테이너 내부 덤프 파일을 로컬로 복사</h2>
<p>컨테이너에서 로컬 PC로 덤프 파일을 복사합니다.</p>
<pre><code>docker cp &lt;컨테이너_이름&gt;:/dump ./&lt;로컬_저장_폴더&gt;</code></pre><p>복사 후 <code>./&lt;로컬_저장_폴더&gt;/&lt;DB_이름&gt;</code> 경로에 .bson 파일이 생성되어 있어야 합니다.</p>
<h2 id="3-ec2로-덤프-파일-전송">3. EC2로 덤프 파일 전송</h2>
<p>.pem 키 파일의 권한이 너무 널널하면 SSH 클라이언트에서 접속을 차단하므로 권한을 제한합니다.</p>
<pre><code>chmod 400 &lt;키_파일_경로&gt;.pem</code></pre><p>권한 설정 후, scp 명령어를 사용해 덤프 폴더를 EC2로 전송합니다.</p>
<pre><code>scp -i &lt;키_파일_경로&gt;.pem -r ./&lt;로컬_저장_폴더&gt; ubuntu@&lt;EC2_IP&gt;:/home/ubuntu/</code></pre><h2 id="4-ec2에서-mongodb-컨테이너로-덤프-복사">4. EC2에서 MongoDB 컨테이너로 덤프 복사</h2>
<p>EC2에서 SSH 접속 후, 덤프 폴더를 MongoDB 컨테이너로 복사합니다.</p>
<pre><code>docker cp ~/ &lt;로컬_저장_폴더&gt; &lt;컨테이너_이름&gt;:/&lt;컨테이너_경로&gt;</code></pre><p>예시:</p>
<pre><code>docker cp ~/dump mongo-dev:/dump
docker cp ~/dump mongo-deploy:/dump</code></pre><h2 id="5-mongodb-덤프-복원-실행">5. MongoDB 덤프 복원 실행</h2>
<h3 id="개발용-컨테이너-복원">개발용 컨테이너 복원</h3>
<pre><code>docker exec -it mongo-dev mongorestore \
  -u &lt;USERNAME&gt; -p &lt;PASSWORD&gt; \
  --authenticationDatabase admin \
  --db &lt;DB_이름&gt; /dump/&lt;DB_이름&gt;</code></pre><h3 id="배포용-컨테이너-복원">배포용 컨테이너 복원</h3>
<pre><code>docker exec -it mongo-deploy mongorestore \
  -u &lt;USERNAME&gt; -p &lt;PASSWORD&gt; \
  --authenticationDatabase admin \
  --db &lt;DB_이름&gt; /dump/&lt;DB_이름&gt;</code></pre><h2 id="6-복원-확인">6. 복원 확인</h2>
<p>복원이 완료되었는지 확인하기 위해 MongoDB에 접속합니다.</p>
<pre><code>docker exec -it mongo-dev mongosh -u &lt;USERNAME&gt; -p &lt;PASSWORD&gt; --authenticationDatabase admin</code></pre><p>MongoDB 셸 내에서 다음 명령을 실행합니다.</p>
<pre><code>use &lt;DB_이름&gt;
show collections</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] JPA의 update 방식]]></title>
            <link>https://velog.io/@red-sprout/JPA-JPA%EC%9D%98-update-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@red-sprout/JPA-JPA%EC%9D%98-update-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Tue, 18 Mar 2025 15:38:48 GMT</pubDate>
            <description><![CDATA[<p>코드 리뷰를 하면서 다음과 같은 피드백을 받았습니다.</p>
<blockquote>
<p>update 할 때 Dirty Checking 방식을 사용하는 것이 유지보수에 좋아요!</p>
</blockquote>
<p>이를 정리하기 위해 블로그 포스팅을 남기게 되었습니다.</p>
<h2 id="1-기존의-코드">1. 기존의 코드</h2>
<p>초기에 JPA의 <code>@Modifying</code>과 <code>@Query</code>를 사용하여 Bulk Update 방식으로 데이터를 업데이트하는 코드를 작성했습니다.</p>
<h3 id="기존-코드">기존 코드</h3>
<h4 id="서비스-코드">서비스 코드</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class TownService {

    private final TownRepository townRepository;

    public void updateTownName(User user, TownNameUpdateDto townNameUpdateDto) {
        int updatedCount = townRepository.updateTownName(user.getId(), townNameUpdateDto.townName());
        if(updatedCount == 0) {
            throw new InvalidArgumentException(&quot;해당하는 마을을 찾을 수 없습니다.&quot;);
        }
    }
}</code></pre>
<h4 id="레포지토리-코드">레포지토리 코드</h4>
<pre><code class="language-java">public interface TownRepository extends JpaRepository&lt;Town, Long&gt; {
    @Modifying(clearAutomatically = true)
    @Query(&quot;UPDATE Town t SET t.name = :townName WHERE t.id = :townId&quot;)
    int updateTownName(Long townId, String townName);
}</code></pre>
<h3 id="기존-코드의-문제점">기존 코드의 문제점</h3>
<p><code>@Modifying</code>을 사용하여 직접 SQL <code>UPDATE</code> 쿼리를 실행하기 때문에 <strong>영속성 컨텍스트와 무관하게 DB가 변경됩니다</strong>. 따라서 <strong>1차 캐시가 갱신되지 않아서, 이후 동일한 엔티티를 조회할 경우 데이터 불일치 문제가 발생</strong>할 수 있습니다.</p>
<p>또한, 코드 가독성이 떨어지고 유지보수성이 낮습니다. 그리고 트랜잭션 롤백이 발생해도 이미 실행된 쿼리는 취소되지 않습니다.</p>
<h2 id="2-dirty-checking">2. Dirty Checking</h2>
<p>위 문제를 해결하기 위해 Dirty Checking 방식으로 변경했습니다. JPA의 영속성 컨텍스트를 활용하여 엔티티의 필드 값이 변경되면 <strong>트랜잭션이 종료될 때 자동으로 업데이트 쿼리가 실행</strong>됩니다.</p>
<h3 id="변경된-코드">변경된 코드</h3>
<h4 id="서비스-코드-1">서비스 코드</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
public class TownService {

    private final TownRepository townRepository;

    public void updateTownName(User user, TownNameUpdateRequest townNameUpdateRequest) {
        user.getTown().updateTownName(townNameUpdateRequest.townName());
    }
}</code></pre>
<ul>
<li><code>townRepository.updateTownName()</code>을 호출하여 직접 쿼리를 실행했던 방식에서 <code>user.getTown().updateTownName()</code>으로 변경되었습니다.</li>
<li>기존에는 <code>@Modifying</code>을 사용하여 즉시 쿼리를 실행했지만, Dirty Checking 방식에서는 <strong>엔티티의 상태를 변경하는 것만으로도 변경 사항이 자동 반영됩니다</strong>.</li>
</ul>
<h4 id="레포지토리-코드-1">레포지토리 코드</h4>
<pre><code class="language-java">public interface TownRepository extends JpaRepository&lt;Town, Long&gt; {
}</code></pre>
<ul>
<li><code>updateTownName()</code>과 같은 명시적인 <code>@Query</code> 메서드가 필요하지 않습니다.</li>
<li>JPA는 영속성 컨텍스트 내에서 <strong>엔티티가 변경되었음을 감지하고 자동으로 <code>UPDATE</code> 쿼리를 실행</strong>하기 때문에 직접 명시하지 않아도 됩니다.</li>
</ul>
<h4 id="엔티티-코드">엔티티 코드</h4>
<pre><code class="language-java">@Entity
public class Town {

    public void updateTownName(String townName) {
        this.name = townName;
    }
}</code></pre>
<ul>
<li><code>townName</code> 필드의 값을 변경하면 JPA는 이를 감지하고, 트랜잭션이 종료될 때 해당 엔티티의 변경 사항을 반영하는 <code>UPDATE</code> 쿼리를 실행합니다.</li>
</ul>
<h3 id="변경된-코드의-특징">변경된 코드의 특징</h3>
<p>엔티티를 조회한 후 <strong>필드 값을 변경</strong>하면, <strong>트랜잭션 종료 시점에 자동으로 <code>UPDATE</code> 쿼리가 실행됩니다</strong>. 여기서 영속성 컨텍스트가 유지되므로 <strong>데이터 불일치 문제가 발생하지 않습니다</strong>.</p>
<p>그리고 <strong>트랜잭션이 롤백될 경우, 변경 사항도 함께 롤백됩니다</strong>. 마지막으로 코드가 간결해지고 유지보수가 쉬워집니다.</p>
<h3 id="jpa의-dirty-checking-동작-원리">JPA의 Dirty Checking 동작 원리</h3>
<p>JPA는 <strong>영속성 컨텍스트</strong>에서 엔티티를 관리하며, <strong>엔티티의 스냅샷(초기 상태)을 저장</strong>합니다.</p>
<p>이후 트랜잭션이 종료될 때 <strong>스냅샷과 현재 엔티티의 상태를 비교하여 변경된 경우 <code>UPDATE</code> 쿼리를 자동 실행</strong>합니다. 따라서 별도의 <code>save()</code> 호출 없이도 변경 사항이 자동 반영됩니다.</p>
<h2 id="3-각각의-장단점">3. 각각의 장단점</h2>
<p>지금까지 보았을 때 Dirty Checking 방식이 무조건 좋아보이지만, 각각의 장단점이 존재합니다.</p>
<h3 id="bulk-update-방식">Bulk Update 방식</h3>
<h4 id="특징">특징</h4>
<ul>
<li><code>@Modifying</code>을 활용하여 직접 SQL 실행</li>
<li>즉시 반영되지만 영속성 컨텍스트와는 무관</li>
</ul>
<h4 id="장점">장점</h4>
<ul>
<li><strong>대량 데이터</strong> 업데이트 시 성능이 뛰어남</li>
<li>한 번의 SQL 실행으로 여러 행을 업데이트 가능</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>영속성 컨텍스트 미반영 문제</li>
<li>트랜잭션 롤백이 되지 않음</li>
<li>코드가 복잡해지고 유지보수가 어려워짐</li>
</ul>
<h3 id="dirty-checking-방식">Dirty Checking 방식</h3>
<h4 id="특징-1">특징</h4>
<ul>
<li>엔티티를 조회한 후 필드 값을 변경하면 트랜잭션 종료 시점에 자동 반영</li>
<li>영속성 컨텍스트를 활용하여 변경 사항을 감지</li>
</ul>
<h4 id="장점-1">장점</h4>
<ul>
<li>데이터 일관성이 보장(영속성 컨텍스트 활용)</li>
<li>트랜잭션 롤백이 쉬움</li>
<li>코드 유지보수성이 높아짐</li>
</ul>
<h4 id="단점-1">단점</h4>
<ul>
<li><strong>대량 업데이트 시 개별 쿼리가 발생</strong>하여 성능이 저하될 수 있음</li>
<li>엔티티 조회 후 변경하는 과정이 필요</li>
</ul>
<h2 id="4-결론">4. 결론</h2>
<p>Bulk Update 방식은 대량 데이터를 빠르게 업데이트할 때 유리하지만, 영속성 컨텍스트를 무시하기 때문에 데이터 일관성이 깨질 위험이 있습니다. 반면 Dirty Checking 방식은 JPA의 변경 감지를 활용하여 코드 유지보수성이 높고, 트랜잭션을 활용할 수 있어 더 안정적입니다.</p>
<h3 id="어떤거-선택해야-되나요">어떤거 선택해야 되나요?</h3>
<ul>
<li><strong>대량 업데이트가 필요한 경우</strong>: Bulk Update 방식이 유리</li>
<li><strong>트랜잭션 내에서 엔티티를 변경하는 경우</strong>: Dirty Checking 방식이 적합</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자료구조 & 알고리즘] Euler Tour Technique]]></title>
            <link>https://velog.io/@red-sprout/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Euler-Tour-Technique</link>
            <guid>https://velog.io/@red-sprout/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Euler-Tour-Technique</guid>
            <pubDate>Mon, 17 Mar 2025 13:54:19 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p><a href="https://www.codetree.ai/ko/frequent-problems/problems/color-tree/description">색깔 트리</a>라는 문제를 풀어보며 다음과 같은 의문이 들었습니다.</p>
<blockquote>
<p>만약 트리의 최대 깊이가 100이라서 단순 dfs로도 구현이 되는데, 만일 LCA 때처럼 10만이란 단위가 나온다면...?</p>
</blockquote>
<p>여러가지 조건을 여기에 적용 가능하지만, 편의를 위해 다음과 같은 조건을 추가해보겠습니다.</p>
<blockquote>
<p>대신 트리의 구조 자체는 변하지 않고 부모 노트 색 칠할 때 자식 노드도 모두 칠해지는 조건만 가져와서 적용해보자</p>
</blockquote>
<p>Segment Tree with Lazy Propagation 을 적용하기에 딱 좋아보입니다. 그리고 마침 백준에 <a href="https://www.acmicpc.net/problem/16404">이에 해당하는 문제</a>가 있습니다. 구조는 안바뀌는데, 뭔가 누적되는 것은 똑같아 보입니다. 하지만, <code>O(depth)</code> 로 처리 시에는 분명히 문제가 됩니다. 뭔가 <code>O(1)</code> 아니면 <code>O(log N)</code> 으로 접근하면 됩니다.</p>
<p>바로 세그먼트 트리로 관리 해주되, 해당되는 트리의 구간을 Lazy Propagation 을 활용해서 <code>O(log N)</code> 으로 진행하면 됩니다. 그런데 문제가 있습니다.</p>
<p>우리가 세그먼트 트리 만들 때는 분명히 완전 이진 트리여서 1차원 배열로 작성이 가능했습니다. 근데 저건 그런게 아니라 완전히 일반화된 트리입니다. 일반화된 트리를 저렇게 1차원 배열 형식으로 가능…은 할까요…?</p>
<p>네 가능합니다. 대신 특별한 기술이 들어갑니다. 바로 Euler Tour Technique(ETT) 입니다.</p>
<h2 id="euler-tour-techniqueett">Euler Tour Technique(ETT)</h2>
<p>ETT는 <strong>트리를 1차원 배열로 변환</strong>하는 기법으로, 트리에서 <strong>특정 노드의 서브트리를 연속된 구간으로 변환</strong>하여 세그먼트 트리와 같은 자료구조를 활용할 수 있도록 합니다.</p>
<p>트리는 일반적으로 계층적 구조를 가지고 있어서 1차원 배열을 기반으로 동작하는 세그먼트 트리나 펜윅 트리를 직접 적용하기 어렵습니다.</p>
<p>하지만 <strong>dfs를 수행하며 트리를 펼쳐서 1차원 배열로 표현</strong>하면 <code>O(log N)</code>의 시간 복잡도로 서브트리 연산이 가능해집니다.</p>
<h3 id="ett-과정">ETT 과정</h3>
<p>ETT의 과정은 다음과 같습니다.</p>
<blockquote>
<ol>
<li>dfs를 수행하며 정점을 방문하는 순서를 기록합니다.</li>
<li>각 노드의 &quot;들어온 시간 (In-Time)&quot;과 &quot;나가는 시간 (Out-Time)&quot;을 저장합니다.</li>
<li>In-Time과 Out-Time을 이용해 서브트리를 하나의 연속된 구간으로 표현합니다.</li>
</ol>
</blockquote>
<p>뭔가 복잡해 보이지만, 사실은 단순한 개념입니다. 트리를 dfs로 순회하며 각 노드가 언제 탐색을 시작하고 끝나는지만 기록하면 됩니다.</p>
<p>이 방식은 <code>dfs numbering</code> 이라고도 부릅니다. 코드로도 아주 간단합니다.</p>
<pre><code class="language-cpp">int cnt = 0;
void dfs(int node) {
    in[node] = ++cnt; // 시작 번호 마킹
    // 탐색 부분
    out[node] = cnt;
}</code></pre>
<p>아래는 트리에 대해서 dfs numbering을 진행한 결과입니다.</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/010660db-f5aa-477a-b789-b77876f0c121/image.png" alt=""></p>
<p>표로 나타내면 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>1번 노드</th>
<th>2번 노드</th>
<th>3번 노드</th>
<th>4번 노드</th>
<th>5번 노드</th>
</tr>
</thead>
<tbody><tr>
<td>in</td>
<td>1</td>
<td>2</td>
<td>5</td>
<td>3</td>
<td>4</td>
</tr>
<tr>
<td>out</td>
<td>5</td>
<td>4</td>
<td>5</td>
<td>3</td>
<td>4</td>
</tr>
<tr>
<td>idx(id)</td>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
</tr>
</tbody></table>
<p>이렇게 만들면 트리를 1차원 배열로 만들 준비가 끝났습니다. 적용은 다음과 같이 하면 됩니다.</p>
<table>
<thead>
<tr>
<th>idx(in/out)</th>
<th>ㅤㅤ0ㅤㅤ</th>
<th>ㅤㅤ1ㅤㅤ</th>
<th>ㅤㅤ2ㅤㅤ</th>
<th>ㅤㅤ3ㅤㅤ</th>
<th>ㅤㅤ4ㅤㅤ</th>
<th>ㅤㅤ5ㅤㅤ</th>
</tr>
</thead>
<tbody><tr>
<td>1번 루트 서브 트리</td>
<td></td>
<td>in[1]</td>
<td></td>
<td></td>
<td></td>
<td>out[1]</td>
</tr>
<tr>
<td>2번 루트 서브 트리</td>
<td></td>
<td></td>
<td>in[2]</td>
<td></td>
<td>out[2]</td>
<td></td>
</tr>
<tr>
<td>3번 루트 서브 트리</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>in[3] out[3]</td>
</tr>
<tr>
<td>4번 루트 서브 트리</td>
<td></td>
<td></td>
<td></td>
<td>in[4] out[4]</td>
<td></td>
<td></td>
</tr>
<tr>
<td>5번 루트 서브 트리</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>in[5] out[5]</td>
<td></td>
</tr>
</tbody></table>
<p>이렇게 구간으로 관리할 수 있습니다. 이제는 Lazy Propagation 활용해서 구간 관리하면 됩니다.</p>
<h2 id="boj-16404--주식회사-승범이네">[BOJ] 16404 / 주식회사 승범이네</h2>
<p>위 과정으로 얻은 서브트리 구간을 Segment tree with Lazy Propagation 을 통해 구간 갱신, 현재 노드 값은 시작 값을 가져와서 그대로 Segment tree에서 가져오면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

using namespace std;
typedef long long ll;

const int MAX = 100&#39;001;

int n;
int num = 0;
int depth[MAX], s[MAX], e[MAX];
ll lazy[4 * MAX], tree[4 * MAX];
vector&lt;int&gt; g[MAX];

void dfs(int cur, int d) {
    depth[cur] = d;
    s[cur] = ++num;
    for (int nxt : g[cur]) {
        if (depth[nxt] == 0) {
            dfs(nxt, d + 1);
        }
    }
    e[cur] = num;
}

void updateLazy(int node, int s, int e) {
    if (lazy[node] == 0) return;
    tree[node] += (e - s + 1) * lazy[node];
    if (s != e) {
        lazy[node &lt;&lt; 1] += lazy[node];
        lazy[node &lt;&lt; 1 | 1] += lazy[node];
    }
    lazy[node] = 0;
}

void updateTree(int node, int s, int e, int ts, int te, int val) {
    updateLazy(node, s, e);
    if (te &lt; s || e &lt; ts) return;
    if (ts &lt;= s &amp;&amp; e &lt;= te) {
        tree[node] += val;
        if (s != e) {
            lazy[node &lt;&lt; 1] += val;
            lazy[node &lt;&lt; 1 | 1] += val;
        }
        return;
    }

    int m = (s + e) &gt;&gt; 1;
    updateTree(node &lt;&lt; 1, s, m, ts, te, val);
    updateTree(node &lt;&lt; 1 | 1, m + 1, e, ts, te, val);
    tree[node] = tree[node &lt;&lt; 1] + tree[node &lt;&lt; 1 | 1];
}

ll get(int node, int s, int e, int ts, int te) {
    updateLazy(node, s, e);
    if (te &lt; s || e &lt; ts) return 0;
    if (ts &lt;= s &amp;&amp; e &lt;= te) return tree[node];

    int m = (s + e) &gt;&gt; 1;
    return get(node &lt;&lt; 1, s, m, ts, te) + get(node &lt;&lt; 1 | 1, m + 1, e, ts, te);
}

void update(int i, int w) {
    updateTree(1, 1, n, s[i], e[i], w);
}

ll balance(int i) {
    return get(1, 1, n, s[i], s[i]);
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int m, p;
    cin &gt;&gt; n &gt;&gt; m;
    for (int i = 1; i &lt;= n; ++i) {
        cin &gt;&gt; p;
        if (p == -1) continue;
        g[p].emplace_back(i);
    }

    dfs(1, 1);
    fill(lazy, lazy + (4 * n + 1), 0);
    fill(tree, tree + (4 * n + 1), 0);

    int q, i, w;
    while (m--) {
        cin &gt;&gt; q;
        if (q == 1) {
            cin &gt;&gt; i &gt;&gt; w;
            update(i, w);
        }
        else {
            cin &gt;&gt; i;
            cout &lt;&lt; balance(i) &lt;&lt; &#39;\n&#39;;
        }
    }

    return 0;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MQTT] Spring Boot MQTT Subscriber (with Raspberry PI)]]></title>
            <link>https://velog.io/@red-sprout/MQTT-Spring-Boot-MQTT-Subscriber-with-Raspberry-PI</link>
            <guid>https://velog.io/@red-sprout/MQTT-Spring-Boot-MQTT-Subscriber-with-Raspberry-PI</guid>
            <pubDate>Wed, 29 Jan 2025 13:33:12 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-boot-에서-데이터-수집">Spring Boot 에서 데이터 수집</h1>
<p>백엔드로 센서 데이터를 보내서 처리를 할 필요가 있어 Spring Boot로 보내는 작업에 대해 작성합니다.</p>
<h2 id="의존성">의존성</h2>
<pre><code class="language-gradle">plugins {
    java
    id(&quot;org.springframework.boot&quot;) version &quot;3.4.2&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.7&quot;
}

group = &quot;com.example&quot;
version = &quot;0.0.1-SNAPSHOT&quot;

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
    testRuntimeOnly(&quot;org.junit.platform:junit-platform-launcher&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-integration&quot;)
    implementation(&quot;org.springframework.integration:spring-integration-mqtt&quot;)
}

tasks.withType&lt;Test&gt; {
    useJUnitPlatform()
}</code></pre>
<p><code>build.gradle</code> 입니다. 모바일 환경을 고려하고 있어 우선 Kotlin 형식으로 받기는 했지만, 다른 방식도 크게 다르지는 않습니다. 다만, 기존 <code>Spring Web</code> 이외에 다른 의존성이 몇가지 필요합니다.</p>
<ul>
<li><code>spring-boot-starter-integration</code> : HTTP, TCP 등과 같은 전송 로직을 추상화하는 역할을 합니다.</li>
<li><code>spring-integration-mqtt</code> : 이름 그대로 MQTT를 사용할 수 있습니다.</li>
</ul>
<h2 id="패키지-구조">패키지 구조</h2>
<p>실제 프로젝트를 진행하면 이것보다 훨씬 복잡한 구조를 가지겠지만, 현재는 메세지 테스트만을 진행하므로 간단하게 구성하였습니다.</p>
<pre><code>src
 ├── main
 │   ├── java/com/example/demo
 │   │   ├── config
 │   │   │   ├── MqttConfig.java  &lt;-- MQTT 설정 파일
 │   │   ├── handler
 │   │   │   ├── MqttMessageHandler.java  &lt;-- MQTT 메시지 처리 핸들러
 │   │   ├── DemoApplication.java  &lt;-- Spring Boot 메인 애플리케이션
 │   ├── resources
 │   │   ├── application.properties  &lt;-- MQTT 브로커 설정</code></pre><h3 id="mqttconfig">MqttConfig</h3>
<ul>
<li>현재 서버로 데이터가 들어오는 것을 확인하는 것이기에 inboundChannel로 구성</li>
<li>공식 문서 등에 여러 구현체들이 나와 있어 해당 문서를 참고하면서 구현하는 것이 좋습니다.</li>
</ul>
<pre><code class="language-java">package com.example.demo.config;

import com.example.demo.handler.MqttMessageSubscriber;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;

@Configuration
public class MqttConfig {

    @Value(&quot;${mqtt.broker.url}&quot;)
    private String brokerUrl;

    @Value(&quot;${mqtt.client.id}&quot;)
    private String clientId;

    @Value(&quot;${mqtt.topic}&quot;)
    private String topic;

    @Value(&quot;${mqtt.username}&quot;)
    private String username;

    @Value(&quot;${mqtt.password}&quot;)
    private String password;

    @Bean
    public MqttPahoClientFactory mqttClientFactory() { // MQTT 클라이언트 관련 설정
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        MqttConnectOptions options = new MqttConnectOptions();
        options.setServerURIs(new String[]{brokerUrl});
        options.setUserName(username);
        options.setPassword(password.toCharArray());
        options.setAutomaticReconnect(true);
        factory.setConnectionOptions(options);
        return factory;
    }

    @Bean
    public MessageProducer inboundChannel(
            MqttPahoClientFactory mqttClientFactory,
            MessageChannel mqttInputChannel
    ) { // inboundChannel 어댑터
        MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(
                brokerUrl,
                clientId,
                mqttClientFactory,
                topic
        );
        adapter.setCompletionTimeout(5000);
        adapter.setConverter(new DefaultPahoMessageConverter());
        adapter.setQos(1);
        adapter.setOutputChannel(mqttInputChannel);
        return adapter;
    }

    @Bean
    public MessageChannel mqttInputChannel() { // MQTT 구독 채널 생성
        return new DirectChannel();
    }

    @Bean
    @ServiceActivator(inputChannel = &quot;mqttInputChannel&quot;) // MQTT 구독 핸들러
    public MessageHandler inboundMessageHandler() {
        return new MqttMessageSubscriber();
    }
}</code></pre>
<h3 id="mqttmessagesubscriber">MqttMessageSubscriber</h3>
<ul>
<li>메세지 핸들러를 받아 단순히 받은 메세지를 출력</li>
</ul>
<pre><code class="language-java">package com.example.demo.handler;

import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Component;

@Component
public class MqttMessageSubscriber implements MessageHandler {

    @Override
    public void handleMessage(Message&lt;?&gt; message) throws MessagingException {
        System.out.println(&quot;Received MQTT message: &quot; + message.getPayload());
    }
}</code></pre>
<h3 id="applicationproperties">application.properties</h3>
<pre><code>mqtt.broker.url = tcp://[브로커 IP 주소]:1883
mqtt.client.id = test-client
mqtt.topic = floor/room/temp
mqtt.username = [유저 이름]
mqtt.password = [비밀 번호]</code></pre><h2 id="test">Test</h2>
<p>우선 Raspberry PI에 <a href="https://velog.io/@red-sprout/MQTT-Raspberry-PI-5-MQTT-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-ipc9t19r">mosquitto 설정</a>을 마쳤으면 다음과 같이 실행해줍니다. Raspberry PI가 브로커의 역할을 합니다.</p>
<h3 id="raspberry-pi">Raspberry PI</h3>
<h4 id="브로커-설정">브로커 설정</h4>
<pre><code class="language-bash">cd /etc/mosquitto # 경로 변경
sudo /etc/init.d/mosquitto stop # 실행 중인 브로커 중지
sudo mosquitto -c mosquitto.conf -v # 설정 파일에 맞게 브로커 실행</code></pre>
<h4 id="publisher">Publisher</h4>
<pre><code class="language-bash">mosquitto_pub -h [브로커의 IP 주소] -t floor/room/temp -m &quot;Hello Spring Boot!&quot;</code></pre>
<h3 id="spring-boot">Spring Boot</h3>
<h4 id="console">console</h4>
<pre><code>Received MQTT message: Hello Spring Boot!</code></pre><h1 id="참고">참고</h1>
<ul>
<li><a href="https://godekdls.github.io/Spring%20Boot/spring-integration/">https://godekdls.github.io/Spring%20Boot/spring-integration/</a></li>
<li><a href="https://docs.spring.io/spring-integration/reference/mqtt.html">https://docs.spring.io/spring-integration/reference/mqtt.html</a></li>
<li><a href="https://junhyunny.github.io/spring-boot/integration/mqtt-subscriber-spring-boot/">https://junhyunny.github.io/spring-boot/integration/mqtt-subscriber-spring-boot/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MQTT] Raspberry PI 5 MQTT 환경 설정]]></title>
            <link>https://velog.io/@red-sprout/MQTT-Raspberry-PI-5-MQTT-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-ipc9t19r</link>
            <guid>https://velog.io/@red-sprout/MQTT-Raspberry-PI-5-MQTT-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95-ipc9t19r</guid>
            <pubDate>Tue, 28 Jan 2025 15:15:07 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<h2 id="mqtt">MQTT</h2>
<ul>
<li>MQTT(Message Queuing Telemetry Transport)는 발행-구독(Publish-Subscribe) 기반의 메시지 송수신 프로토콜</li>
<li>IoT와 같이 제한된 또는 대규모 트래픽 전송을 위해 만들어진 프로토콜</li>
</ul>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/ccdfc688-358a-4f7a-976c-7fd1937fddef/image.png" alt=""></p>
<h3 id="구성요소">구성요소</h3>
<ul>
<li>발행자(Publisher) - 메시지를 발행(메시지를 브로커로 전송)하는 역할을 담당하는 구성요소</li>
<li>구독자(Subscriber) - 메시지를 구독(브로커로부터 메시지를 수신)하는 역할을 담당하는 구성요소</li>
<li>브로커 - 발행자가 발행한 메시지를 수신하여 구독자에게 전달하는 중계장 역할을 담당하는 구성요소</li>
</ul>
<h3 id="특징">특징</h3>
<ol>
<li><p>연결 지향적 (Connection Oriented)</p>
<ul>
<li>TCP/IP 소켓 연결 이휴 연결이 끊어질 때까지 상태 유지</li>
<li>Live라는 하트비트와 Topic에 발행되는 메시지를 통해 연결을 유지하고 메시지 송수신을 하게 됨</li>
<li>연결이 끊어지면 재접속 가능</li>
</ul>
</li>
<li><p>브로커를 통한 통신</p>
<ul>
<li>오로지 브로커를 통해서만 통신이 가능</li>
<li>개설된 Topic에 메시지를 발행 시 해당 Topic를 구독하는 클라이언트들에게 메시지를 발행</li>
<li>일대일, 일대다 통신 모두 가능</li>
</ul>
</li>
<li><p>QoS(Quality of Service)</p>
<ul>
<li><p><code>0</code> : 보낸 다음 잊어버림</p>
<ul>
<li>최대 1회 전송.</li>
<li>Topic을 통해 메시지를 전송할 뿐 보장은 하지 않음. </li>
</ul>
</li>
<li><p><code>1</code> : 확인 응답을 거치는 전달</p>
<ul>
<li>최소 1회 전송.</li>
<li>구독하는 클라이언트가 메시지를 받았는지 불확실하면 정해진 횟수만큼 재전송.</li>
<li>메시지의 핸드셰이킹 과정을 엄밀하게 추적하지는 않으므로 중복의 위험성 존재.</li>
</ul>
</li>
<li><p><code>2</code> : 보장된 전달</p>
<ul>
<li>구독하는 클라이언트가 요구된 메시지를 정확히 한 번 수신할 수 있도록 보장</li>
<li>메시지의 핸드셰이킹 과정을 추적</li>
<li>높은 품질을 보장, but 성능 요구</li>
</ul>
</li>
<li><p>TCP/IP 데이터 전송의 처리에 영향을 주지 않으며, MQTT 송신자와 수신자 간에만 사용</p>
</li>
<li><p>글자 수 제한이 없으므로, 긴 메시지나 JSON 포맷 또는 파일 전송 가능</p>
</li>
<li><p>0에 가까울수록 메시지 처리에 대한 부하가 줄어들고, 메시지 손실의 위험이 높음.</p>
</li>
</ul>
</li>
<li><p>메시지 유형</p>
<ul>
<li>연결하기 - 서버와의 연결 수립을 기다린 다음 노드 간 링크</li>
<li>연결 끊기 - MQTT 클라이언트가 해야 할 일을 기다리고 인터넷 프로토콜 스위트 세션의 연결이 끊어지기를 기다림</li>
<li>발행하기 - MQTT 클라이언트에 요청이 전달된 직후 어플리케이션 스레드에 즉시 반환</li>
<li>각각의 메시지의 event에 따라 MQTT 브로커가 알림을 주어 대응</li>
</ul>
</li>
<li><p>다양한 개발언어의 다양한 클라이언트 지원</p>
</li>
</ol>
<h1 id="mosquitto">mosquitto</h1>
<h2 id="mosquitto-1">mosquitto</h2>
<ul>
<li><p>대표적인 오픈소스 MQTT 브로커 중 하나</p>
</li>
<li><p>Raspberry Pi에 mosquitto 설치</p>
<pre><code class="language-bash">  sudo apt install mosquitto mosquitto-clients</code></pre>
</li>
<li><p>브로커 동작 확인</p>
<pre><code class="language-bash">  sudo systemctl status mosquitto</code></pre>
<p>  결과 예시 : </p>
<pre><code class="language-bash">  ● mosquitto.service - Mosquitto MQTT Broker
       Loaded: loaded (/lib/systemd/system/mosquitto.service; enabled; preset: enabled)
       Active: active (running) since Thu 2025-01-23 19:41:01 GMT; 12s ago
         Docs: man:mosquitto.conf(5)
               man:mosquitto(8)
      Process: 724 ExecStartPre=/bin/mkdir -m 740 -p /var/log/mosquitto (code=exited, status=0/SUCCESS)
      Process: 731 ExecStartPre=/bin/chown mosquitto /var/log/mosquitto (code=exited, status=0/SUCCESS)
      Process: 733 ExecStartPre=/bin/mkdir -m 740 -p /run/mosquitto (code=exited, status=0/SUCCESS)
      Process: 735 ExecStartPre=/bin/chown mosquitto /run/mosquitto (code=exited, status=0/SUCCESS)
     Main PID: 736 (mosquitto)
        Tasks: 1 (limit: 4443)
          CPU: 17ms
       CGroup: /system.slice/mosquitto.service
               └─736 /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf

  Jan 23 19:41:01 raspberrypi systemd[1]: Starting mosquitto.service - Mosquitto MQTT Broker...
  Jan 23 19:41:01 raspberrypi systemd[1]: Started mosquitto.service - Mosquitto MQTT Broker.</code></pre>
</li>
</ul>
<h3 id="브로커">브로커</h3>
<p><code>/etc/mosquitto</code> 경로에 mosquitto 관련 설정 파일이 있습니다. 브로커를 실행하기 위해서는 해당 경로로 들어가서 해당 설정 파일에 있는 내용에 맞게 실행해야 되므로 경로를 바꾸고 브로커를 실행합니다.</p>
<h4 id="트러블-슈팅">트러블 슈팅</h4>
<p>단 그냥 접속 시, <code>Error: Connection refused</code> 를 볼 수있습니다. 따라서 다음과 같이 수정을 해줍니다.</p>
<ol>
<li><p><code>sudo vi /etc/mosquitto/mosquitto.conf</code> 를 통해 설정 파일을 vi editor로 열어줍니다.</p>
</li>
<li><p>다음 내용을 해당 파일 제일 마지막에 추가해줍니다.</p>
<pre><code class="language-vi"> port 1883
 allow_anonymous true</code></pre>
</li>
</ol>
<h4 id="브로커-실행">브로커 실행</h4>
<pre><code class="language-bash">cd /etc/mosquitto # 경로 변경
sudo /etc/init.d/mosquitto stop # 실행 중인 브로커 중지
sudo mosquitto -c mosquitto.conf -v # 설정 파일에 맞게 브로커 실행</code></pre>
<p>이렇게 실행하면 <code>The &#39;port&#39; option is now deprecated and will be removed in a future</code> 라고 해서 port 관련 설정을 준 것이 deprecated 되었다고 경고가 뜨는데, 지금은 크게 신경쓸 필요는 없습니다.</p>
<h3 id="mosquitto_sub">mosquitto_sub</h3>
<pre><code class="language-bash">mosquitto_sub -h [MQTT 브로커 접속주소(IP)] -p [통신포트 : 기본값 1883] -t [구독할 토픽 : &quot;#&quot;는 모든 토픽을 구독]</code></pre>
<p>참고로 <code>[MQTT 브로커 접속주소(IP)]</code> 는 라즈베리파이에서 <code>ifconfig</code> 로 확인할 수 있는 그 IP로 접속하면 됩니다.</p>
<h3 id="mosquitto_pub">mosquitto_pub</h3>
<pre><code class="language-bash">mosquitto_pub -h [MQTT 브로커 접속주소(IP)] -p [통신포트 : 기본값 1883] -t [발행할 메시지의 토픽] -m [발행할 메시지]</code></pre>
<h3 id="test">Test</h3>
<ul>
<li>Raspberry Pi 터미널을 열어서 실행</li>
</ul>
<pre><code class="language-bash">mosquitto_sub -h [MQTT 브로커 접속주소(IP)] -p 1883 -t &quot;#&quot; 
# #은 topic에 해당하는 하위 항목 모두 다 포함
# mosquitto_pub 입력마다 Hello 출력</code></pre>
<pre><code class="language-bash">mosquitto_pub -h [MQTT 브로커 접속주소(IP)] -p 1883 -t &quot;test&quot; -m &quot;Hello&quot;</code></pre>
<h1 id="참고">참고</h1>
<p><a href="https://underflow101.tistory.com/22?category=826163">https://underflow101.tistory.com/22?category=826163</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SSAFY] SSAFY 12기 1학기 회고록]]></title>
            <link>https://velog.io/@red-sprout/SSAFY-SSAFY-12%EA%B8%B0-1%ED%95%99%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@red-sprout/SSAFY-SSAFY-12%EA%B8%B0-1%ED%95%99%EA%B8%B0-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Sun, 19 Jan 2025 17:02:40 GMT</pubDate>
            <description><![CDATA[<h1 id="1학기를-마치며">1학기를 마치며</h1>
<p>1학기를 마치며, 느낀 사실이 있습니다.</p>
<p>개발자라는 직업이라는 것을 생각하기 이전에는 취업이라는 선택지가 저에게 있어서는 전혀 존재하지 않았습니다. 학계에 남고 싶어 대학원 준비를 하였으니 말입니다. 학점은 나름 높았지만, 취업 준비와는 한 활동들이 거리가 멀었습니다.</p>
<p>하지만, 뜻하지 않게 대학원 학위를 얻지 못하였고, 여러가지 분야들을 공부하다 개발 분야에 뜻하지 않게 개발자 진로를 희망하게 되었습니다. </p>
<p>어쩌다보니 본의아니게 취업과 관련된 첫 면접이 SSAFY가 되었고, 운좋게 합격하여 비전공자인데 분반 테스트 결과도 잘 나오게 되어 전공반에 가게되었습니다.</p>
<p>또 거기서도 운좋게 성적우수(2등)을 하게 되었고, 배틀싸피라는 새로 생긴 SSAFY 내 게이미피케이션 프로그램에서도 전국대항전 준우승이란 성과를 달성하게 되었습니다.</p>
<p>이런 것을 보면 새옹지마라는 표현이 딱 떠오릅니다.</p>
<p>그 결과...</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/216c1524-ca89-42ac-9bdc-07da3c9b0961/image.png" alt=""></p>
<p>이런 상품도 얻었습니다...ㅎㅎ</p>
<h2 id="about-상품">About 상품</h2>
<p>상품에 대해 저건 뭐야? 하실까봐 간단하게 언급하고 지나가겠습니다.</p>
<p>평소 클래식 음악을 즐겨듣는터라 비싼 스피커를 받게 되어서 기쁜 마음이 컸습니다. 다만 받은 순간에는 저게 꽤 무거워서 (체감상 5kg) 저거 들고 집에 어떻게 가지... 란 생각을 했습니다.</p>
<p>블루투스 스피커인데, 기본적으로 모듈 자체가 커서 성능 특히 저음부를 잘 살릴 수 있지 않을까 생각을 했습니다. 그리고 곡은 3가지 정도를 먼저 들어봤습니다.</p>
<ul>
<li>F.Liszt, Réminiscences des Norma - 피아노 솔로</li>
<li>M.Ravel, La valse - 피아노 솔로 &amp; 오케스트라</li>
<li>A.Bruckner, 교향곡 9번 1악장 - 오케스트라</li>
</ul>
<p>특히, 두번째 La valse의 경우에는 컨트라바순으로 시작하는 초저음부터 확인 가능하고, 모리스 라벨의 오케스트레이션으로 화려하고 귀가 즐거운 음악이기에 제가 항상 음질 테스트시 사용하던 작품입니다.</p>
<p>기본적으로 손잡이가 달려있어 들고 다니기는 무게에 비해서 편하긴 하였고, 기본적으로 유선 포트가 따로 있는 것으로 확인되어서 일반적인 블루투스 스피커에 비해서 유선 스피커에 가깝게 음질이 좋았습니다.</p>
<p>음 해상도가 명확한 편이고 저음부 역시 빵빵한 편이여서 여건만 괜찮으면 크게 듣고 싶은데... 환경이 고시원이라 제한적이긴 한게 아쉽긴 합니다. Mahler나 Shostakovich 작품들 들으면 기깔날 것 같다는 생각이 드네요.</p>
<p>내장 마이크가 있어서 주변 환경에 적응을 하는 측면도 있고, 기기도 하나만 페어링 되는 것이 아닌 두개가 가능해서 기술적으로도 볼만한 사항이 있던 것 같습니다. 안그래도 이번 프로젝트를 IoT쪽으로 구상하고 있어 사용해보면서 어느정도 참고할 만한 측면이 있는 것 같습니다.</p>
<h2 id="ssafy의-장점">SSAFY의 장점?</h2>
<p>이제는 프로젝트 주간입니다. 프로젝트 주간에서 팀과 논의를 해보면서 아직 새롭게 배울 것이 많아감을 요즘 느끼고 있습니다.</p>
<p>SSAFY에서 가장 크게 얻어가고 있는 것(또는 얻어갈 것)이 무엇인가 라고 한다면 다음과 같습니다.</p>
<h3 id="진로">진로</h3>
<p>한번도 취업준비에 대해 생각해본적 없는 제 입장에서 가장 막막한 것은 능력도, 비전공도 아닌 뭘 하고 싶은지에 대한 고민이였습니다. SSAFY 에서 사람들을 많이 많나고 많은 도메인을 가진 사람들과 교류하다보니 지금은 어느정도 안정화되는 것 같습니다. 또한 취업지원프로그램 역시도 많이 있다보니 취업준비로 무엇을 해야되는지 정도 감이 잡혀가는 것 같습니다.</p>
<h3 id="겸손">겸손</h3>
<p>이에 따라 많은 능력자들을 보게 되었습니다. 그런 능력자들을 볼 때마다 항상 작아지기도 하지만, 가끔 자만할 때 계영배의 역할을 해 주는 것 같습니다.</p>
<h3 id="아직은-못딴-b형">(아직은 못딴...) B형</h3>
<p>결국 원하는 직무가 삼성전자에 있어 B형이 거의 저에게 있어서는 무엇보다도 필수적입니다. B형 스터디를 운영하고 있는만큼, 또 지금까지 B형 탈락한 만큼 이왕 딸꺼 최상위권으로 통과하고 싶습니다.</p>
<h3 id="프로젝트-경험">프로젝트 경험</h3>
<p>기본적으로 자소서에 쓸 프로젝트 3개(관통 프로젝트까지 하면 4개)가 생긴다는 점이 경쟁력이 확실히 있는 것 같습니다. 전문적인 코칭을 현재 받고 있는데, 열심히해서 좋은 성과 있으면 좋겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] #10167 금광]]></title>
            <link>https://velog.io/@red-sprout/%EB%B0%B1%EC%A4%80-10167-%EA%B8%88%EA%B4%91</link>
            <guid>https://velog.io/@red-sprout/%EB%B0%B1%EC%A4%80-10167-%EA%B8%88%EA%B4%91</guid>
            <pubDate>Sat, 18 Jan 2025 09:21:44 GMT</pubDate>
            <description><![CDATA[<h1 id="문제">문제</h1>
<blockquote>
<p>다이아는 정말... 힘드네요</p>
</blockquote>
<p><a href="https://www.acmicpc.net/problem/10167">https://www.acmicpc.net/problem/10167</a>
점과 가중치가 있을 때 x, y축과 평행한 직사각형으로 감싼 최대 2차원 연속합을 구하는 문제입니다.</p>
<h1 id="풀이">풀이</h1>
<h2 id="연습-문제">연습 문제</h2>
<p>연습문제로 <a href="https://www.acmicpc.net/problem/16993">[BOJ] 16992 / 연속합과 쿼리</a> 문제를 먼저 풀이하는 것을 추천 드립니다.</p>
<h3 id="연습-문제-풀이">연습 문제 풀이</h3>
<p>연속합은 떨어지는 구간이 발생해서는 안됩니다. 따라서 기본적으로 연속합에 해당하는 구간이 왼쪽 끝, 오른쪽 끝에 닿는지 여부를 판단하는 것이 중요합니다. 이 여부에 따라 두 구간이 붙을 수 있는지 없는지에 대한 판단 기준이 됩니다.</p>
<p>결국 관리해야 되는 것은 4가지 입니다.</p>
<h4 id="왼쪽-끝이-닿는-구간합left">왼쪽 끝이 닿는 구간합(left)</h4>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/7b42a53c-01de-4d93-b19c-ade2807661cf/image.png" alt=""></p>
<h4 id="오른쪽-끝이-닿는-구간합right">오른쪽 끝이 닿는 구간합(right)</h4>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/65107c96-595e-4313-b020-b4012d7149c7/image.png" alt=""></p>
<h4 id="중간-부분에-걸친-구간합mid">중간 부분에 걸친 구간합(mid)</h4>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/3a0905d3-37c1-4ba8-88f2-26fca2c75c3f/image.png" alt=""></p>
<h4 id="전체-합all">전체 합(all)</h4>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/70277b66-1e88-4e1e-a0d2-f0a4c42f5aa5/image.png" alt=""></p>
<p>여기서 구간 내 최댓값은 사실상 모든 기준을 포함하는 <code>mid</code> 가 됩니다. 따라서 마지막에 get 할 때 <code>mid</code> 값을 가져오면 됩니다.</p>
<p>그리고 4개의 값을 가져와야 되므로 트리를 4개 만들어야 되는가 생각할 수 있는데, 4개나 따로 구현하기란 너무 귀찮은 일입니다. 이 4개를 하나로 통합하는 <code>Node</code> 를 만들어 관리하면 됩니다.</p>
<p>이제 남은 것은 두 구간합을 합치는 연산입니다. <code>n1</code> 노드와 <code>n2</code> 노드를 합치는 연산은 다음과 같습니다.</p>
<ul>
<li>left<ul>
<li>왼쪽 노드만 생각할 것인가 → <code>n1.left</code></li>
<li>오른쪽 노드도 고려할 것인가 → 왼쪽 전체 선택하고 오른쪽 노드의 left → <code>n1.all + n2.left</code><pre><code>max(n1.left, n1.all + n2.left);</code></pre></li>
</ul>
</li>
<li>right<ul>
<li>오른쪽 노드만 생각할 것인가 → <code>n1.right</code></li>
<li>왼쪽 노드도 고려할 것인가 → 오른쪽 전체 선택하고 왼쪽 노드의 right → <code>n1.right + n2.all</code><pre><code>max(n2.right, n1.right + n2.all);</code></pre></li>
</ul>
</li>
<li>mid<ul>
<li>왼쪽 노드만 생각할 것인가 → <code>n1.mid</code></li>
<li>오른쪽 노드만 생각할 것인가 → <code>n2.mid</code></li>
<li>둘을 이어 붙일 것인가 → 왼쪽 노드의 right + 오른쪽 노드의 left → <code>n1.right + n2.left</code><pre><code>max(n1.mid, n2.mid, n1.right + n2.left);</code></pre></li>
</ul>
</li>
<li>all<ul>
<li>둘 다 전체 선택한 거 더하면 됩니다.      <pre><code>n1.all + n2.all</code></pre></li>
</ul>
</li>
</ul>
<p>이 연산에 대한 항등원(세그먼트 트리 범위 밖일 경우 반환)도 생각해보면 다음과 같습니다.</p>
<ul>
<li><code>left</code>, <code>right</code>, <code>mid</code> : max 연산의 항등원 → <code>-INF</code></li>
<li><code>all</code> : + 연산의 항등원 → <code>0</code></li>
</ul>
<p>참고로 정식 명칭은 <a href="https://en.wikipedia.org/wiki/Maximum_subarray_problem">MSP(Maximum Subarray Problem)</a>라 하는데, 국내에서는 아래에 풀이할 금광 덕분에 <strong>금광세그</strong>라는 이름으로 더 많이 알려져 있습니다.</p>
<h3 id="연습-문제-코드">연습 문제 코드</h3>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    static class Node {
        int left, right, mid, all;

        Node() {
            this(-1_000_000_000);
            all = 0;
        }

        Node(int val) {
            left = right = mid = all = val;
        }
    }

    static int n, size;
    static int[] arr;
    static Node[] tree;

    static Node merge(Node n1, Node n2) {
        Node node = new Node();
        node.left = Math.max(n1.left, n1.all + n2.left);
        node.right = Math.max(n2.right, n1.right + n2.all);
        node.mid = Math.max(Math.max(n1.mid, n2.mid), n1.right + n2.left);
        node.all = n1.all + n2.all;
        return node;
    }

    static void init(int node, int s, int e) {
        if (s == e) {
            tree[node] = new Node(arr[s]);
            return;
        }
        int mid = (s + e) &gt;&gt; 1;
        init(node &lt;&lt; 1, s, mid);
        init(node &lt;&lt; 1 | 1, mid + 1, e);
        tree[node] = merge(tree[node &lt;&lt; 1], tree[node &lt;&lt; 1 | 1]);
    }

    static int query(int s, int e) {
        return get(1, 1, n, s, e).mid;
    }

    static Node get(int node, int s, int e, int ts, int te) {
        if (e &lt; ts || te &lt; s) return new Node();
        if (ts &lt;= s &amp;&amp; e &lt;= te) return tree[node];
        int mid = (s + e) &gt;&gt; 1;
        Node l = get(node &lt;&lt; 1, s, mid, ts, te);
        Node r = get(node &lt;&lt; 1 | 1, mid + 1, e, ts, te);
        return merge(l, r);
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        StringTokenizer st;

        n = Integer.parseInt(br.readLine());
        size = 1 &lt;&lt; ((int) Math.ceil(Math.log(n) / Math.log(2)) + 1);
        arr = new int[n + 1];
        tree = new Node[size];

        st = new StringTokenizer(br.readLine(), &quot; &quot;);
        for (int i = 1; i &lt;= n; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        init(1, 1, n);
        int m = Integer.parseInt(br.readLine());
        while (m-- &gt; 0) {
            st = new StringTokenizer(br.readLine(), &quot; &quot;);
            int i = Integer.parseInt(st.nextToken());
            int j = Integer.parseInt(st.nextToken());
            sb.append(query(i, j)).append(&#39;\n&#39;);
        }

        System.out.print(sb);
        br.close();
    }
}</code></pre>
<h2 id="금광-풀이">금광 풀이</h2>
<p>지금까지 금광세그라는 것을 알아보았습니다. 하지만 이 세그를 알아도 풀이가 쉽지는 않습니다. 세그를 일단 생각하지 않고 접근 해보겠습니다.</p>
<h3 id="처음-든-생각">처음 든 생각</h3>
<p><code>[x1, x2]</code> , <code>[y1, y2]</code>  범위의 점들을 선택해서 이 범위에 해당하는 점들의 합을 구합니다.  </p>
<ul>
<li><code>[x1, x2]</code> : <code>O(N^2)</code></li>
<li><code>[y1, y2]</code> : <code>O(N^2)</code></li>
<li>구간 내 점들의 합 : <code>O(N^2)</code></li>
<li>전체 : <code>O(N^6)</code></li>
</ul>
<p>당연히 이렇게 풀면 TLE입니다. 여기서 힌트가 되는 것이 바로 서브테스크가 됩니다.</p>
<ol>
<li>N ≤ 100 : O(N^6) 가능</li>
<li>N ≤ 500 : 조금 더 줄여야 됨</li>
<li>wi &lt; 0 (1 개) : 연속합 중간에 끊기는 부분이 딱 하나 존재</li>
<li>y = 0 : 일직선 상에 존재하는 연속합의 최대, 위에서 설명한 금광세그 사용</li>
<li>제약조건 없음 : 종합</li>
</ol>
<h3 id="점들-선택하는-과정">점들 선택하는 과정</h3>
<p>문제가 되는 부분은 바로 <code>[x1, x2]</code> , <code>[y1, y2]</code>  범위를 모두 본다는 것에 있습니다. 이중에서 하나만 보고 나머지는 뭔가 최적화하는 방법이 있을 것인지 생각해보는 것이 어떨까요?</p>
<p>마침 서브테스크 중에 y값이 같을 경우 어떻게 해결할 것인지 보는 것이 있습니다. <code>[y1, y2]</code> 에 대한 선택만 하도록 합니다 → <code>O(N^2)</code></p>
<p>그러면 점들에 대한 순서 역시 필요하게 되고, 그 과정은 다음과 같습니다.</p>
<ul>
<li>y 기준으로 정렬</li>
<li>y1을 고정하여 세그먼트 트리를 초기화</li>
<li>정점들을 하나씩 넣어서 세그먼트 트리를 갱신</li>
</ul>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/6c2c2644-4554-4c54-8415-9c211d430846/image.png" alt=""></p>
<h3 id="반례-잡기">반례 잡기</h3>
<p>이렇게 풀이하면 되는데, 반례 하나가 있습니다.</p>
<pre><code>4
2 2 4
2 1 6
1 2 7
1 1 -1000</code></pre><p>최대가 나올 수 있는 부분은 (1, 2) 와 (2, 2) 을 선택하면 됩니다. (답 : 11)</p>
<p>하지만 현재 풀이대로만 진행하면 17이 나옵니다. 문제의 원인은 다음과 같습니다.</p>
<ul>
<li>정렬 순서 (1, 1) → (1, 2) → (2, 1) → (2, 2)</li>
<li>(1, 1) 에서 4개의 점 삽입 시에는 문제 없음</li>
<li>(1, 2) 에서 같은 높이에 있는 (1, 1)이 누락 → (1, 2), (2, 1), (2, 2) 만 포함된 범위가 계산됨 → error</li>
</ul>
<p>즉, 같은 높이일 때는 해당하는 같은 높이의 모든 점을 다 삽입해주어야 됩니다.</p>
<p>다만, 조금 관점을 다르게 생각해서 x 좌표가 제일 작은 시점에서만 모두 넣어주고, 나머지는 모두 무시해보는 방법도 생각해볼 수 있습니다. 즉 <strong>중복은 무시</strong>해주면 됩니다.</p>
<pre><code class="language-java">for (int i = 0; i &lt; n; i++) {
    // 같은 높이는 중복 처리 X
    if (i &gt; 0 &amp;&amp; point[i - 1].y == point[i].y) continue;
    init();
    for (int j = i; j &lt; n; j++) {
        update((int) point[j].x, point[j].v);
        // 같은 높이는 모두 삽입된 후 처리
        if (j == n - 1 || point[j].y != point[j + 1].y) {
            res = Math.max(res, tree[1].mid);
        }
    }
}
</code></pre>
<h3 id="코드">코드</h3>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    static class Point implements Comparable&lt;Point&gt; {
        long x, y, v;
        int i;

        public Point(long x, long y, long v, int i) {
            this.x = x;
            this.y = y;
            this.v = v;
            this.i = i;
        }

        @Override
        public String toString() {
            return &quot;[&quot; + x + &quot;, &quot; + y + &quot;, &quot; + v + &quot;, &quot; + i + &quot;]&quot;;
        }

        @Override
        public int compareTo(Point p) {
            return this.y == p.y ? Long.compare(this.x, p.x) : Long.compare(this.y, p.y);
        }
    }

    static class Pair {
        int idx;
        long val;

        public Pair(int idx, long val) {
            this.idx = idx;
            this.val = val;
        }

        @Override
        public String toString() {
            return &quot;[&quot; + idx + &quot;, &quot; + val + &quot;]&quot;;
        }
    }

    static class Node {
        long left, right, mid, all;
    }

    static int n, m, size;
    static Node[] tree;
    static Point[] point;
    static Pair[] tmpX, tmpY;

    static void compression() {
        tmpX = new Pair[n];
        tmpY = new Pair[n];
        for (Point p : point) {
            tmpX[p.i] = new Pair(p.i, p.x);
            tmpY[p.i] = new Pair(p.i, p.y);
        }

        Arrays.sort(tmpX, (o1, o2) -&gt; Long.compare(o1.val, o2.val));
        Arrays.sort(tmpY, (o1, o2) -&gt; Long.compare(o1.val, o2.val));

        int ix = 0, iy = 0;
        for (int i = 0; i &lt; n - 1; i++) {
            point[tmpX[i].idx].x = ix;
            if (tmpX[i].val != tmpX[i + 1].val) ix++;
        }
        point[tmpX[n - 1].idx].x = ix;

        for (int i = 0; i &lt; n - 1; i++) {
            point[tmpY[i].idx].y = iy;
            if (tmpY[i].val != tmpY[i + 1].val) iy++;
        }
        point[tmpY[n - 1].idx].y = iy;

        Arrays.sort(point);
    }

    static void init() {
        for (int i = 0; i &lt; size; i++) {
            tree[i].left = tree[i].right = tree[i].mid = (long) (-1e14);
            tree[i].all = 0;
        }
    }

    static Node merge(Node n1, Node n2) {
        Node node = new Node();
        node.left = Math.max(n1.left, n1.all + n2.left);
        node.right = Math.max(n2.right, n1.right + n2.all);
        node.mid = Math.max(Math.max(n1.mid, n2.mid), n1.right + n2.left);
        node.all = n1.all + n2.all;
        return node;
    }

    static void update(int idx, long val) {
        idx += m;
        tree[idx].all += val;
        tree[idx].left = tree[idx].right = tree[idx].mid = tree[idx].all;
        while ((idx &gt;&gt;= 1) &gt; 0) {
            tree[idx] = merge(tree[idx &lt;&lt; 1], tree[idx &lt;&lt; 1 | 1]);
        }
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

        n = Integer.parseInt(br.readLine());
        m = 1 &lt;&lt; (int) Math.ceil(Math.log(n) / Math.log(2));
        size = m &lt;&lt; 1;
        tree = new Node[size];
        for (int i = 0; i &lt; size; i++) tree[i] = new Node();
        point = new Point[n];
        for (int i = 0; i &lt; n; i++) {
            st = new StringTokenizer(br.readLine(), &quot; &quot;);
            long x = Long.parseLong(st.nextToken());
            long y = Long.parseLong(st.nextToken());
            long v = Long.parseLong(st.nextToken());
            point[i] = new Point(x, y, v, i);
        }
        compression();

        long res = 0;
        for (int i = 0; i &lt; n; i++) {
            if (i &gt; 0 &amp;&amp; point[i - 1].y == point[i].y) continue;
            init();
            for (int j = i; j &lt; n; j++) {
                update((int) point[j].x, point[j].v);
                if (j == n - 1 || point[j].y != point[j + 1].y) {
                    res = Math.max(res, tree[1].mid);
                }
            }
        }

        System.out.println(res);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CODETREE] 코드트리 메신저]]></title>
            <link>https://velog.io/@red-sprout/CODETREE-%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EB%A9%94%EC%8B%A0%EC%A0%80</link>
            <guid>https://velog.io/@red-sprout/CODETREE-%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EB%A9%94%EC%8B%A0%EC%A0%80</guid>
            <pubDate>Fri, 03 Jan 2025 07:51:48 GMT</pubDate>
            <description><![CDATA[<h1 id="문제">문제</h1>
<blockquote>
<ul>
<li>update 과정은 가급적 O(depth)로 처리!</li>
<li>런타임 에러 조심할 것!</li>
</ul>
</blockquote>
<h2 id="구현할-api-설명">구현할 API 설명</h2>
<h3 id="void-init">void init()</h3>
<ul>
<li>최초 1회 실행</li>
<li>0번 root 노드는 주어지지 않음</li>
<li>알람은 부모로 전파 됨</li>
</ul>
<pre><code>Q : [1, 100000] // 쿼리 수
N : [1, 100000] // 0번 제외 노드 수
D : [1, 20] // 이진 트리 최대 깊이, depth
pi : [0, N] // 부모 노드 번호
ai : [1, N] // 권한 세기</code></pre><h3 id="void-updatealarmint-c">void updateAlarm(int c)</h3>
<ul>
<li>c번 노드 알람 전파 ON/OFF toggle</li>
</ul>
<pre><code>c : [1, N]</code></pre><h3 id="void-updateauthint-c-int-power">void updateAuth(int c, int power)</h3>
<ul>
<li>c번 노드 권한 세기 변경</li>
</ul>
<pre><code>c : [1, N]
power : [1, N]</code></pre><h3 id="void-updateparentsint-c1-int-c2">void updateParents(int c1, int c2)</h3>
<ul>
<li>c1번 노드, c2번 노드 부모 변경</li>
<li>c1번 노드와 c2번 노드는 동일한 depth</li>
</ul>
<pre><code>c1, c2 : [1, N]</code></pre><h3 id="int-getint-c">int get(int c)</h3>
<ul>
<li>c번 노드에 알림을 보낼 수 있는 하위 채팅방(노드) 개수(본인은 제외)</li>
</ul>
<pre><code>c : [1, N]</code></pre><h1 id="풀이">풀이</h1>
<h2 id="권한-설정">권한 설정</h2>
<h3 id="전파-거리-관리">전파 거리 관리</h3>
<p>어떻게 전파 거리를 고려하여 값을 가지고 있는가에 대한 고민을 하면 됩니다. 사실 가장 기초적인 방법으로는 <code>Node</code> 구현 시 <code>List&lt;Node&gt;</code> 형태로 해당 노드로 전파 가능한 하위 노드들을 들고 있으면 되긴 합니다.</p>
<p>하지만 삽입 삭제 과정이 빈번하게 일어나는 상황에서 이는 바람직하지 않습니다. 어떤 방법을 쓰면 될까요?</p>
<p>이 때 우리는 조건 <code>D &lt;= 20</code> 이 수상합니다. 이걸 활용해서 가능합니다.</p>
<p>어차피 우리에게 필요한 건 하위 노드의 종류가 아닌 전파 거리만이 필요합니다. 그래서 이를 관리해주는 카운팅 배열 <code>cnt[D]</code> 를 만들어 줍니다.</p>
<pre><code>cnt[i] : 전파 거리가 i 만큼 남은 하위 노드의 개수
ex) 0 - 1(auth = 1) - 2(auth = 1) - 3(auth = 2) 와 같은 트리에서
3 - cnt : [0, 0, 0, 0, ...]
2 - cnt : [0, 1, 0, 0, ...] // 노드 3의 전파 거리 2 - 1
1 - cnt : [2, 0, 0, 0, ...] // 노드 2의 전파 거리 1 - 1, 노드 3의 전파 거리 2 - 1 - 1
0 - cnt : [1, 0, 0, 0, ...] // 음수가 되면 사라짐, 노드 1의 전파 거리 1 - 1</code></pre><p>그리고 update나 get을 할 때 이 배열 전체를 순회하면 됩니다. 배열의 사이즈는 <code>D</code> 로 잡으면 충분합니다. 어차피 제일 깊은 곳에서 root 까지 올라와도 겨우 20회 확인하기 때문입니다.</p>
<h3 id="setvalue--setsub">setValue &amp; setSub</h3>
<p>각각의 역할은 다음과 같습니다.</p>
<ul>
<li><code>setValue</code> : 전파 거리 딱 하나 바뀌었을 때 update 수행</li>
<li><code>setSub</code> : 서브 트리 자체가 바뀌었을 때 update 수행</li>
</ul>
<h3 id="setvalueint-id-int-val-int-power">setValue(int id, int val, int power)</h3>
<ul>
<li>id번 노드의 auth 값이 power 로 변경이 될 때 val 만큼 cnt를 변경해주는 update</li>
<li>단일 값이 수정 될 경우 사용하고, root거나 전파 거리가 부족하거나 알람 OFF라면 break</li>
<li>자식에서 부모로 이동하는 것이 핵심 포인트!</li>
<li><code>O(D)</code></li>
</ul>
<pre><code class="language-java">    static void setValue(int id, int val, int power) {
        while(true) {
            if(parents[id] == -1 || power-- == 0 || alarm[id] == -1) break;
            node[parents[id]].cnt[power] += val;
            id = parents[id];
        }
    }</code></pre>
<h3 id="setsubint-id-int-val">setSub(int id, int val)</h3>
<ul>
<li>id번 노드를 root로 하는 서브트리를 그 부모 노드에 할당 또는 제거하는 연산</li>
<li>알람 ON/OFF, parents 바꾸기 등에 사용</li>
<li><code>setValue</code> 를 root(id번 노드) 의 auth를 한번 적용한 것과 cnt의 크기(D) 만큼 수행</li>
<li><code>O(D^2)</code></li>
</ul>
<pre><code class="language-java">    static void setSub(int id, int val) {
        setValue(id, val, node[id].auth);
        for(int power = 1; power &lt; 20; power++) {
            setValue(id, val * node[id].cnt[power], power);
        }
    }</code></pre>
<h3 id="추가-메소드의-사용처">추가 메소드의 사용처</h3>
<ul>
<li><code>setValue</code> : <code>updateAuth</code> - <code>O(D)</code></li>
<li><code>setSub</code> : <code>updateAlarm</code> , <code>updateParents</code> - <code>O(D^2)</code></li>
</ul>
<h2 id="주의-사항">주의 사항</h2>
<h3 id="권한-세기">권한 세기</h3>
<ul>
<li>권한 세기를 정할 때 p 가 D 보다도 크게 나오는 경우가 존재 - 런타임 에러</li>
<li>setter 를 통해서 값을 변경하도록 함 - <strong>캡슐화 필요</strong></li>
</ul>
<pre><code class="language-java">    static class Node {
        int id, auth;
        int[] cnt;

        public Node(int id, int auth) {
            this.id = id;
            setAuth(auth);
            cnt = new int[20];
        }

        public void setAuth(int auth) { // setter를 만들어 두기
            this.auth = auth &gt; 20 ? 20 : auth;
        }

        @Override
        public String toString() {
            return &quot;Node [id=&quot; + id + &quot;, auth=&quot; + auth + &quot;, cnt=&quot; + Arrays.toString(cnt) + &quot;]&quot;;
        }
    }</code></pre>
<ul>
<li>값을 바꿀 때는 setter를 거칩니다.</li>
</ul>
<pre><code class="language-java">    static void updateAuth(int c, int power) {
        setValue(c, -1, node[c].auth);
        node[c].setAuth(power); // setter를 통한 값 변경
        setValue(c, 1, node[c].auth);
    }</code></pre>
<h3 id="알람-onoff">알람 ON/OFF</h3>
<ul>
<li><code>setSub</code> 도 사용하는 <code>setValue</code> 를 보면 알겠지만 알람 OFF의 경우에는 update X<ul>
<li>알람 ON : <code>alarm[i] == 1</code> , 알람 OFF : <code>alarm[i] == -1</code></li>
</ul>
</li>
<li>알람의 초기 상태에 따라 동작 과정이 다름<ul>
<li>알람 ON → OFF<ul>
<li><code>setSub(id, -1)</code> : 상위 노드 cnt 하나씩 빼주기</li>
<li><code>alarm[id] = -1</code> : 알람 OFF</li>
</ul>
</li>
<li>알람 OFF → ON<ul>
<li><code>alarm[id] = 1</code> : 알람 ON</li>
<li><code>setSub(id, 1)</code> : 상위 노드 cnt 하나씩 더해주기</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="시간-복잡도">시간 복잡도</h2>
<h3 id="api-별">API 별</h3>
<ul>
<li><code>init</code> : <code>O(D * N)</code></li>
<li><code>updateAlarm</code> : <code>O(D^2)</code></li>
<li><code>updateAuth</code> : <code>O(D)</code></li>
<li><code>updateParents</code> : <code>O(D^2)</code></li>
<li><code>get</code> : <code>O(D)</code></li>
</ul>
<h3 id="전체">전체</h3>
<p><code>O(D^2 * Q + D * N)</code></p>
<h1 id="코드">코드</h1>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    static class Node {
        int id, auth;
        int[] cnt;

        public Node(int id, int auth) {
            this.id = id;
            setAuth(auth);
            cnt = new int[20];
        }

        public void setAuth(int auth) {
            this.auth = auth &gt; 20 ? 20 : auth;
        }

        @Override
        public String toString() {
            return &quot;Node [id=&quot; + id + &quot;, auth=&quot; + auth + &quot;, cnt=&quot; + Arrays.toString(cnt) + &quot;]&quot;;
        }
    }

    static int N, Q;
    static int[] parents, auth, alarm;
    static Node[] node;

    static void init() {
        parents[0] = -1;
        node = new Node[N + 1];
        alarm = new int[N + 1];
        Arrays.fill(alarm, 1);
        for(int i = 0; i &lt;= N; i++) {
            node[i] = new Node(i, auth[i]);
        }
        for(int i = 0; i &lt;= N; i++) {
            setValue(i, 1, node[i].auth);
        }
    }

    static void setValue(int id, int val, int power) {
        while(true) {
            if(parents[id] == -1 || power-- == 0 || alarm[id] == -1) break;
            node[parents[id]].cnt[power] += val;
            id = parents[id];
        }
    }

    static void setSub(int id, int val) {
        setValue(id, val, node[id].auth);
        for(int power = 1; power &lt; 20; power++) {
            setValue(id, val * node[id].cnt[power], power);
        }
    }

    static void updateAlarm(int c) {
        if(alarm[c] == -1) {
            alarm[c] = 1; setSub(c, 1);
        } else {
            setSub(c, -1); alarm[c] = -1;
        }
    }

    static void updateAuth(int c, int power) {
        setValue(c, -1, node[c].auth);
        node[c].setAuth(power);
        setValue(c, 1, node[c].auth);
    }

    static void updateParents(int c1, int c2) {
        setSub(c1, -1); setSub(c2, -1);
        int p1 = parents[c1], p2 = parents[c2];
        parents[c1] = p2; parents[c2] = p1;
        setSub(c1, 1); setSub(c2, 1);
    }

    static int get(int c) {
        int ans = 0;
        int[] cnt = node[c].cnt;
        for(int i : cnt) {
            ans += i;
        }
        return ans;
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = null;
        st = new StringTokenizer(br.readLine(), &quot; &quot;);
        N = Integer.parseInt(st.nextToken());
        Q = Integer.parseInt(st.nextToken());
        parents = new int[N + 1];
        auth = new int[N + 1];
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i &lt; Q; i++) {
            st = new StringTokenizer(br.readLine());
            int query = Integer.parseInt(st.nextToken());
            int c, c1, c2, power;
            switch(query) {
            case 100:
                for(int j = 1; j &lt;= N; j++) parents[j] = Integer.parseInt(st.nextToken());
                for(int j = 1; j &lt;= N; j++) auth[j] = Integer.parseInt(st.nextToken());
                init();
                break;
            case 200:
                c = Integer.parseInt(st.nextToken());
                updateAlarm(c);
                break;
            case 300:
                c = Integer.parseInt(st.nextToken());
                power = Integer.parseInt(st.nextToken());
                updateAuth(c, power);
                break;
            case 400:
                c1 = Integer.parseInt(st.nextToken());
                c2 = Integer.parseInt(st.nextToken());
                updateParents(c1, c2);
                break;
            case 500:
                c = Integer.parseInt(st.nextToken());
                sb.append(get(c)).append(&#39;\n&#39;);
                break;
            }
        }
        System.out.print(sb);
        br.close();
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자료구조 & 알고리즘] Dynamic Segment Tree with Java]]></title>
            <link>https://velog.io/@red-sprout/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Dynamic-Segment-Tree-with-Java</link>
            <guid>https://velog.io/@red-sprout/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Dynamic-Segment-Tree-with-Java</guid>
            <pubDate>Thu, 26 Dec 2024 08:11:49 GMT</pubDate>
            <description><![CDATA[<h1 id="문제">문제</h1>
<p>다음 문제를 바로 살펴봅시다. <a href="https://www.acmicpc.net/problem/14577">문제 링크</a></p>
<p>문제에서 처리하는 쿼리들을 정리하면 다음과 같습니다.</p>
<blockquote>
<ul>
<li><code>1 i x</code> : i번 지역에 x 만큼 추가</li>
<li><code>2 i y</code> : i번 지역에 y 만큼 추가</li>
<li><code>3 L R</code> : <code>[L, R]</code> 범위의 지역 수</li>
<li><code>4 T</code> : 내림차순으로 T번째 높이</li>
</ul>
</blockquote>
<p>이거만 보면 일반적인 세그먼트 트리(이하 세그) 문제라 생각할 수 있습니다. k번째 찾는 쿼리라 <a href="https://velog.io/@red-sprout/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Segment-Tree-Kth">k번째 찾는 방법</a>을 쓰면 풀릴 것 같습니다.
(단 내림차순이라 N - T + 1 번째를 찾는 것임에 유의)</p>
<p>그런데... 높이 범위가 10의 18승으로... 일반적인 방법으로 저장은 절대 불가능하다는 것을 알 수 있습니다.</p>
<p>방법으로는 오프라인 쿼리를 활용하여 좌표 압축을 진행하는 방법도 있습니다. 쿼리에 나오는 모든 높이를 받아서 이에 해당하는 배열만 만드는 방법입니다.</p>
<p>하지만, 온라인으로 문제를 풀어야 되는 상황이라면...? 트리의 노드를 동적으로 저장해야됩니다. 이를 <strong>다이나믹 세그먼트 트리</strong>라 합니다.</p>
<h1 id="다이나믹-세그먼트-트리">다이나믹 세그먼트 트리</h1>
<p>동작원리는 일반 세그먼트 트리를 잘 안다면 쉽게 이해할 수 있습니다. 딱 두가지만 지키면 됩니다.</p>
<blockquote>
<ul>
<li>update 시 필요한 곳의 노드만 만들어 주자</li>
<li>탐색 했는데 하위 노드 없으면 그냥 return 해주자</li>
</ul>
</blockquote>
<p>일반 세그먼트 트리는 이미 트리를 완전 이진 트리 형태로 다 만들어 놓고 거기에서 값만 변경하는 방식입니다.</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/c8021a29-fe67-48a1-8509-2b620b728c67/image.png" alt="기본세그"></p>
<p>하지만 다이나믹 세그먼트 트리는 update 쿼리 수행시 필요한 곳에서만 노드를 만들어 주고 있기에 이진 트리는 이진 트리인데 완전 이진 트리는 아니고 많이 비어 있는 모습을 띄게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/red-sprout/post/93b0f476-abad-4521-ba1a-840e6fd58fa0/image.png" alt="다이나믹세그"></p>
<p>이렇게 필요한 부분만 만들기에 메모리를 많이 절약할 수 있게 됩니다.</p>
<p>그리고 눈치가 빠른 분들은 기본 세그의 노드 번호는 고정이 되어있지만, 다이나믹 세그 노드 번호는 이와 다르다는게 보일겁니다(사진상의 노드 번호가 보면 서로 다릅니다.)</p>
<p>이는 노드가 새롭게 부여될 때마다 노드 번호가 증가하는 방식입니다. 그래서 노드 번호는 할당된 순으로 부여됩니다.</p>
<p>따라서 노드 선언할 때 값 뿐만 아니라 <strong>왼쪽과 오른쪽 노드 정보도 같이 들고 있어야 한다는 것이 핵심입니다.</strong></p>
<h1 id="구현">구현</h1>
<p>이 구현은 <a href="https://justicehui.github.io/medium-algorithm/2020/02/28/DynamicSeg/">JusticeHui</a>님의 구현 방식을 많이 참고하였습니다. 자바로 구현한 포스팅은 못보았기에 구현해보게 되었습니다.</p>
<h2 id="node">Node</h2>
<p>우선 Node를 정의합니다.</p>
<pre><code class="language-java">class Node {
    long val = 0;
    int l = -1, r = -1;
}</code></pre>
<p>그리고 Node를 관리할 리스트를 만들어 둡니다.</p>
<pre><code class="language-java">int size = 1; // 초기 트리는 루트 노트 딱 하나만 존재
List&lt;Node&gt; tree;</code></pre>
<pre><code class="language-java">// 초기화 시
tree = new ArrayList&lt;&gt;();
tree.add(new Node()); // 루트 노드 저장</code></pre>
<p>즉 필요한 노드만 관리하고, 노드 내에서 저장을 해당하는 노드의 인덱스를 저장하는 방식을 택합니다. 여기서는 <strong>root는 0번 노드</strong>로 설정했으며, <strong>-1은 노드가 할당되지 않음</strong>을 의미합니다.</p>
<h2 id="update">update</h2>
<p>우선 기존 세그입니다.</p>
<pre><code class="language-java">void update(int node, int s, int e, int idx, long val) {
    if(e &lt; idx || idx &lt; s) return;
    if(s == e) {
        tree[node] += val;
        return;
    }
    int mid = (s + e) / 2;
    update(2 * node, s, mid, idx, val);
    update(2 * node + 1, mid + 1, e, idx, val);
    tree[node] = tree[2 * node] + tree[2 * node + 1];
}</code></pre>
<p>위 코드는 update 하기 위해서 양쪽 모두를 탐색하고 있습니다. 하지만 다이나믹 세그의 경우는 필요한 곳만 할당하기에 탐색 과정에서 조건문을 하나 추가로 적어줍니다.</p>
<pre><code class="language-java">void update(int node, long s, long e, long idx, long val) {
    if(s == e) {
        tree.get(node).val += val;
        return;
    }
    long mid = (s + e) / 2;
    if(idx &lt;= mid) {
        if(tree.get(node).l &lt; 0) {
            tree.add(new Node());
            tree.get(node).l = size++;
        }
        update(tree.get(node).l, s, mid, idx, val);
    } else {
        if(tree.get(node).r &lt; 0) {
            tree.add(new Node());
            tree.get(node).r = size++;
        }
        update(tree.get(node).r, mid + 1, e, idx, val);
    }
    int left = tree.get(node).l;
    int right = tree.get(node).r;
    long lval = left &lt; 0 ? 0 : tree.get(left).val;
    long rval = right &lt; 0 ? 0 : tree.get(right).val;
    tree.get(node).val = lval + rval;
}</code></pre>
<h2 id="get">get</h2>
<p>get은 비슷하지만 없는 노드가 나오면 탐색을 중단한다는 점만 조심하면 됩니다.</p>
<pre><code class="language-java">long getTotal(int node, long s, long e, long ts, long te) {
    if(node &lt; 0) return 0;
    if(e &lt; ts || te &lt; s) return 0;
    if(ts &lt;= s &amp;&amp; e &lt;= te) return tree.get(node).val;
    long mid = (s + e) / 2;
    long left = getTotal(tree.get(node).l, s, mid, ts, te);
    long right = getTotal(tree.get(node).r, mid + 1, e, ts, te);
    return left + right;
}</code></pre>
<h2 id="t번째-항목-얻기">T번째 항목 얻기</h2>
<p>이게 처음 생각하면 잘 생각이 안날껀데 의외로 기존 세그와 비슷합니다. 다만, 없는 노드는 탐색하지 않는다는 점만 주의하면 됩니다.</p>
<pre><code class="language-java">long getTth(int node, long s, long e, long t) {
    if (s == e) return s;
    long mid = (s + e) / 2;
    long lval = (tree.get(node).l &lt; 0) ? 0 : tree.get(tree.get(node).l).val;
    if (lval &gt;= t) return getTth(tree.get(node).l, s, mid, t);
    return getTth(tree.get(node).r, mid + 1, e, t - lval);
}</code></pre>
<h1 id="문제-풀이">문제 풀이</h1>
<h2 id="14577-일기-예보---p3">14577 일기 예보 - P3</h2>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    static class Node {
        long val = 0;
        int l = -1, r = -1;
    }

    static int N, M, size;
    static final long MAX_VALUE = (long) 1e18;
    static long[] arr;
    static List&lt;Node&gt; tree;

    static void update(long idx, long val) {
        update(0, 0, MAX_VALUE, idx, val);
    }

    static long getTotal(long l, long r) {
        return getTotal(0, 0, MAX_VALUE, l, r);
    }

    static long getTth(long t) {
        return getTth(0, 0, MAX_VALUE, t);
    }

    static void update(int node, long s, long e, long idx, long val) {
        if(s == e) {
            tree.get(node).val += val;
            return;
        }
        long mid = (s + e) / 2;
        if(idx &lt;= mid) {
            if(tree.get(node).l &lt; 0) {
                tree.add(new Node());
                tree.get(node).l = size++;
            }
            update(tree.get(node).l, s, mid, idx, val);
        } else {
            if(tree.get(node).r &lt; 0) {
                tree.add(new Node());
                tree.get(node).r = size++;
            }
            update(tree.get(node).r, mid + 1, e, idx, val);
        }
        int left = tree.get(node).l;
        int right = tree.get(node).r;
        long lval = left &lt; 0 ? 0 : tree.get(left).val;
        long rval = right &lt; 0 ? 0 : tree.get(right).val;
        tree.get(node).val = lval + rval;
    }

    static long getTotal(int node, long s, long e, long ts, long te) {
        if(node &lt; 0) return 0;
        if(e &lt; ts || te &lt; s) return 0;
        if(ts &lt;= s &amp;&amp; e &lt;= te) return tree.get(node).val;
        long mid = (s + e) / 2;
        long left = getTotal(tree.get(node).l, s, mid, ts, te);
        long right = getTotal(tree.get(node).r, mid + 1, e, ts, te);
        return left + right;
    }

    static long getTth(int node, long s, long e, long t) {
        if (s == e) return s;
        long mid = (s + e) / 2;
        long lval = (tree.get(node).l &lt; 0) ? 0 : tree.get(tree.get(node).l).val;
        if (lval &gt;= t) return getTth(tree.get(node).l, s, mid, t);
        return getTth(tree.get(node).r, mid + 1, e, t - lval);
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        StringTokenizer st = null;
        st = new StringTokenizer(br.readLine(), &quot; &quot;);
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        size = 1;
        arr = new long[N + 1];
        tree = new ArrayList&lt;&gt;();
        tree.add(new Node());

        st = new StringTokenizer(br.readLine(), &quot; &quot;);
        for(int i = 1; i &lt;= N; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
            update(arr[i], 1);
        }

        int q, idx;
        long val, l, r, t;
        while(M-- &gt; 0) {
            st = new StringTokenizer(br.readLine(), &quot; &quot;);
            q = Integer.parseInt(st.nextToken());
            switch(q) {
            case 1:
                idx = Integer.parseInt(st.nextToken());
                val = Long.parseLong(st.nextToken());
                update(arr[idx], -1);
                arr[idx] += val;
                update(arr[idx], 1);
                break;
            case 2:
                idx = Integer.parseInt(st.nextToken());
                val = Long.parseLong(st.nextToken());
                update(arr[idx], -1);
                arr[idx] -= val;
                update(arr[idx], 1);
                break;
            case 3:
                l = Long.parseLong(st.nextToken());
                r = Long.parseLong(st.nextToken());
                sb.append(getTotal(l, r)).append(&#39;\n&#39;);
                break;
            case 4:
                t = Long.parseLong(st.nextToken());
                sb.append(getTth(N - t + 1)).append(&#39;\n&#39;);
                break;
            }
        }

        System.out.print(sb);
        br.close();
    }
}</code></pre>
<h1 id="참고">참고</h1>
<ul>
<li><a href="https://justicehui.github.io/medium-algorithm/2020/02/28/DynamicSeg/">https://justicehui.github.io/medium-algorithm/2020/02/28/DynamicSeg/</a></li>
<li><a href="https://nicotina04.tistory.com/265">https://nicotina04.tistory.com/265</a></li>
<li><a href="https://www.acmicpc.net/blog/view/86">https://www.acmicpc.net/blog/view/86</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[컴퓨터 구조] CPU Cache]]></title>
            <link>https://velog.io/@red-sprout/%EC%BB%B4%ED%93%A8%ED%84%B0-%EA%B5%AC%EC%A1%B0-CPU-Cache</link>
            <guid>https://velog.io/@red-sprout/%EC%BB%B4%ED%93%A8%ED%84%B0-%EA%B5%AC%EC%A1%B0-CPU-Cache</guid>
            <pubDate>Thu, 12 Dec 2024 06:30:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>먼저 읽어보기 - <a href="https://velog.io/@red-sprout/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%9D%AC%EC%86%8C-%EB%B0%B0%EC%97%B4Sparse-Table">Sparse Table</a></p>
</blockquote>
<p><a href="https://velog.io/@red-sprout/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%9D%AC%EC%86%8C-%EB%B0%B0%EC%97%B4Sparse-Table">Sparse Table</a> 포스팅을 작성하고 관련 피드백을 하나 들었습니다.</p>
<blockquote>
<p>관련해서 <a href="https://namnamseo.tistory.com/entry/Sparse-Table">이 포스팅</a> 한번 참고하시면 좋을 것 같아요!</p>
</blockquote>
<p>바로 CPU Cache 때문에 배열의 선언 방법에 따라 TLE가 날 수도 있다고 합니다. 관련 이슈 및 개념을 정리하면서 이를 확인해보기 위해 <a href="https://www.acmicpc.net/problem/11438">최소 공통 조상</a>에서 어떻게 성능 차이가 나는지 알아보겠습니다.</p>
<h1 id="cpu-캐시cache란">CPU 캐시(Cache)란?</h1>
<p>캐시에 대해서 간단하게 먼저 짚고 넘어가겠습니다.</p>
<p>캐시 메모리(Cache Memory)는 데이터를 저장하는 <strong>임시 저장소</strong>로, CPU가 데이터를 더 빠르고 효율적으로 액세스할 수 있도록 돕습니다. 이는 CPU와 메모리 사이에 위치하며, 속도가 빠른 <strong>SRAM</strong> 기반으로 설계된 저장 장치입니다. 캐시는 특히 동일한 데이터에 <strong>반복적으로 접근</strong>하거나 많은 연산이 필요한 경우 컴퓨터 성능을 크게 향상시킵니다.</p>
<h2 id="캐시-메모리의-동작-원리">캐시 메모리의 동작 원리</h2>
<ol>
<li>시스템이 데이터 요청 시, 먼저 캐시에서 해당 데이터를 찾습니다.</li>
<li>캐시에 데이터가 있으면(Cache Hit), 해당 데이터를 바로 제공합니다.</li>
<li>데이터가 없거나 오래되어 최신성을 잃은 경우(Cache Miss), 원본 데이터 소스에 접근합니다.</li>
<li>캐시 공간이 부족하면, 잘 사용되지 않는 데이터를 삭제하여 공간을 확보합니다.</li>
</ol>
<h2 id="캐시의-계층-구조">캐시의 계층 구조</h2>
<p>캐시는 보통 <strong>L1, L2, L3 캐시</strong>로 나뉘며, 각 계층은 속도와 용량이 다릅니다.</p>
<ul>
<li><strong>L1 캐시</strong>: 코어별로 존재하며, 가장 작고 빠름<ul>
<li>명령어 저장(L1I)과 데이터 저장(L1D)으로 나뉘기도 함 - 분리형 캐시</li>
</ul>
</li>
<li><strong>L2 캐시</strong>: L1보다 크고 느리고, 코어별로 존재.</li>
<li><strong>L3 캐시</strong>: 여러 코어가 공유, 가장 크고 느림.</li>
</ul>
<h2 id="참조-지역성locality-of-reference">참조 지역성(Locality of Reference)</h2>
<p><strong>시간 지역성(Time Locality)</strong> : 최근에 접근한 데이터에 다시 접근하는 경향.</p>
<pre><code class="language-java">   for (int i = 0; i &lt; 10; i++) {
       arr[i] = i; // 변수 i를 여러 번 참조.
   }</code></pre>
<p><strong>공간 지역성(Spatial Locality)</strong> : 최근 접근한 데이터 주변 공간에 다시 접근하는 경향.</p>
<pre><code class="language-java">   int[] arr = new int[10];
   for (int i = 0; i &lt; 10; i++) {
       arr[i] = i; // 배열 요소에 연속적으로 접근.
   }</code></pre>
<h2 id="캐시의-필요성">캐시의 필요성</h2>
<ol>
<li><p><strong>병목 현상 완화</strong>:
CPU와 메인 메모리(RAM) 사이의 병목 현상을 줄이기 위해, 크기는 작지만 속도가 빠른 캐시를 사용합니다. CPU가 자주 사용하는 데이터를 캐시에 저장하여, 데이터를 더 빠르게 가져옵니다.</p>
<ul>
<li>병목 현상: 시스템 성능이 특정 구성 요소의 제한으로 인해 저하되는 현상.</li>
</ul>
</li>
<li><p><strong>데이터 접근 시간 감소</strong>:
캐싱은 데이터를 빠르게 액세스할 수 있는 위치에 저장함으로써, 디스크나 데이터베이스와 같은 느린 저장 장치에 대한 접근을 줄입니다.</p>
</li>
<li><p><strong>서버 부하 분산</strong>:
캐시에서 데이터를 제공함으로써 데이터베이스 서버나 파일 시스템에 가해지는 부하를 줄이고, 전체 시스템 성능을 향상시킵니다.</p>
</li>
</ol>
<h2 id="캐시-관련-용어">캐시 관련 용어</h2>
<ul>
<li><strong>캐시 히트(Cache Hit)</strong>: CPU가 요청한 데이터가 캐시에 있는 경우.</li>
<li><strong>캐시 미스(Cache Miss)</strong>: CPU가 요청한 데이터가 캐시에 없는 경우.</li>
<li><strong>캐시 적중률(Cache Hit Ratio)</strong>: <code>캐시 히트 / (캐시 히트 + 캐시 미스)</code>.</li>
<li><strong>캐시 일관성(Cache Coherence)</strong>: 멀티 코어 환경에서 각 코어의 캐시에 저장된 데이터가 동일하게 유지되는 것을 보장.</li>
</ul>
<h2 id="캐시와-이차원-배열-성능">캐시와 이차원 배열 성능</h2>
<p>다음과 같이 간단하게 c++ 로 배열 원소의 주소를 출력하면 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

using namespace std;

int arr[20][101010];

int main() {
    for(int i = 0; i &lt; 3; i++) {
        for(int j = 0; j &lt; 3; j++) {
            cout &lt;&lt; &quot;arr[&quot; &lt;&lt; i &lt;&lt; &quot;]&quot; &lt;&lt; &quot;[&quot; &lt;&lt; j &lt;&lt; &quot;]&quot; &lt;&lt; &quot; : &quot; &lt;&lt; &amp;arr[i][j] &lt;&lt; &#39;\n&#39;;
        }
    }
    return 0;
}</code></pre>
<pre><code>arr[0][0] : 0x7ff6a3957040
arr[0][1] : 0x7ff6a3957044
arr[0][2] : 0x7ff6a3957048
arr[1][0] : 0x7ff6a39b9a88
arr[1][1] : 0x7ff6a39b9a8c
arr[1][2] : 0x7ff6a39b9a90
arr[2][0] : 0x7ff6a3a1c4d0
arr[2][1] : 0x7ff6a3a1c4d4
arr[2][2] : 0x7ff6a3a1c4d8</code></pre><p><code>arr[0][0], arr[0][1], arr[0][2]</code> 와 같은 경우에는 <code>int</code> 가 4 byte 이므로 연속적입니다. 배열에 원소가 연속적으로 있으면 앞서 언급한 <strong>참조 지역성</strong>으로 캐시는 자주 사용되거나 인접한 데이터를 미리 가져와서 처리 속도를 높이고, <strong>캐시 히트율이 높습니다.</strong> 즉 효율적입니다.</p>
<p>단, 2차원 배열에서 두번째 항목의 크기가 101010으로 크기에, <code>arr[0]</code> 에서 <code>arr[1]</code> 로 넘어가면 기존 캐시에 저장되어 있는 데이터가 존재하지 않아 캐시 미스가 일어나게 됩니다. 이렇게 <strong>불연속적이면 캐시 미스가 일어날 가능성이 높습니다.</strong></p>
<h4 id="행-열-순서">행-열 순서</h4>
<pre><code class="language-java">for (int i = 0; i &lt; 1000; i++) {
    for (int j = 0; j &lt; 1000; j++) {
        a[i][j] = 0; // 연속된 메모리 공간 접근.
    }
}</code></pre>
<p>배열의 행은 메모리에 연속적으로 배치되기 때문에, CPU는 한 번 데이터를 가져올 때 여러 인접 요소를 캐시에 저장합니다. 결과적으로 <strong>캐시 히트율이 높아지고 성능이 향상</strong>됩니다.</p>
<h4 id="열-행-순서">열-행 순서</h4>
<pre><code class="language-java">for (int i = 0; i &lt; 1000; i++) {
    for (int j = 0; j &lt; 1000; j++) {
        a[j][i] = 0; // 비연속적인 메모리 공간 접근.
    }
}</code></pre>
<p>반대로, 열 단위로 접근할 경우 메모리의 비연속적으로 띄엄띄엄 참조하게 됩니다. 이는 캐시에 저장된 데이터 블록을 효과적으로 활용하지 못하므로, <strong>캐시 미스가 발생할 확률이 높아집니다.</strong></p>
<h1 id="sparse-table을-다시-보자">Sparse Table을 다시 보자!</h1>
<p>아래 두 코드는 모두 제가 제출한 <a href="https://www.acmicpc.net/problem/11438">최소 공통 조상</a> 문제의 해설코드입니다.</p>
<ol>
<li><a href="http://boj.kr/5974934546014b85adc0b7faaa2ac017">http://boj.kr/5974934546014b85adc0b7faaa2ac017</a></li>
<li><a href="http://boj.kr/9cef16a5874343e58676c88709ba6a61">http://boj.kr/9cef16a5874343e58676c88709ba6a61</a></li>
</ol>
<p>두 코드 변수명, 공백까지 다 같은데 딱 다른 점이 하나 있습니다. </p>
<ul>
<li>table을 <code>[size + 1][N + 1]</code> 로 생성한 것이 1번, </li>
<li>table을 <code>[N + 1][size + 1]</code> 로 생성한 것이 2번입니다. </li>
</ul>
<p><a href="https://velog.io/@red-sprout/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%9D%AC%EC%86%8C-%EB%B0%B0%EC%97%B4Sparse-Table">Sparse Table</a> 에서는 1번 방식으로 표기하였고, 실제 시간도 1번(680 ms)가 2번(724 ms)보다 더 빠른 것을 확인할 수 있었습니다. 단순한 우연일지 확인해보도록 해보겠습니다.</p>
<h2 id="table-초기화">table 초기화</h2>
<pre><code class="language-java">    // 1번 코드
    dfs(1, 1);
    for(int i = 1; i &lt;= size; i++) {
        for(int j = 1; j &lt;= N; j++) {
            dp[i][j] = dp[i - 1][dp[i - 1][j]];
        }
    }</code></pre>
<pre><code class="language-java">    // 2번 코드
    dfs(1, 1);
    for (int i = 1; i &lt;= size; i++) {
        for (int j = 1; j &lt;= N; j++) {
            dp[j][i] = dp[dp[j][i - 1]][i - 1];
          }
    }</code></pre>
<p>벌써 눈치채신 분들도 있겠지만, 바로 <code>i</code>, <code>j</code>가 1, 2번 코드가 서로 바뀌어 있습니다. sparse table의 초기화 시에는 이동 횟수별로 각 노드가 이동했을 때 도착 노드가 초기화 된 뒤 다음 이동 횟수로 넘어가야 초기화가 되기에, <strong>for-loop 자체는 table 생성 순서와 관계 없이 동일</strong>해야 됩니다. 그러면 행과 열을 반대로 작성해야 되는데, 그러면 <strong>이차원 배열 예시와 동일한 이유</strong>로 <strong>1번 코드</strong>가 2번보다 <strong>빠르게 됩니다.</strong></p>
<h1 id="참고">참고</h1>
<ul>
<li><a href="https://namnamseo.tistory.com/entry/Sparse-Table">https://namnamseo.tistory.com/entry/Sparse-Table</a></li>
<li><a href="https://product.kyobobook.co.kr/detail/S000061584886">혼자 공부하는 컴퓨터 구조+운영체제</a></li>
<li><a href="https://frogred8.github.io/docs/014_cache_line/">https://frogred8.github.io/docs/014_cache_line/</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>