<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yujin_1219.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 22 Oct 2026 04:27:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yujin_1219.log</title>
            <url>https://velog.velcdn.com/images/yujin_1219/profile/a1be0fc2-ba69-4e09-9333-877ac23adfa5/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yujin_1219.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yujin_1219" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[오래된 저장 요청이 최신 문서를 덮어쓰지 않게 만든 과정]]></title>
            <link>https://velog.io/@yujin_1219/%EC%98%A4%EB%9E%98%EB%90%9C-%EC%A0%80%EC%9E%A5-%EC%9A%94%EC%B2%AD%EC%9D%B4-%EC%B5%9C%EC%8B%A0-%EB%AC%B8%EC%84%9C%EB%A5%BC-%EB%8D%AE%EC%96%B4%EC%93%B0%EC%A7%80-%EC%95%8A%EA%B2%8C-%EB%A7%8C%EB%93%A0-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@yujin_1219/%EC%98%A4%EB%9E%98%EB%90%9C-%EC%A0%80%EC%9E%A5-%EC%9A%94%EC%B2%AD%EC%9D%B4-%EC%B5%9C%EC%8B%A0-%EB%AC%B8%EC%84%9C%EB%A5%BC-%EB%8D%AE%EC%96%B4%EC%93%B0%EC%A7%80-%EC%95%8A%EA%B2%8C-%EB%A7%8C%EB%93%A0-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Thu, 22 Oct 2026 04:27:59 GMT</pubDate>
            <description><![CDATA[<p>문서를 편집하고 저장한 뒤 다시 열었을 때, 마지막으로 수정한 내용이 그대로 남아 있어야 하는게, 편집 기능을 만들면서 가장 기본적으로 필요한 동작이다.</p>
<p>처음에는 한 사람이 한 화면에서 문서를 수정하는 흐름을 중심으로 생각했엇다.
근ㄷ데 한 사용자가 같은 문서를 브라우저 탭 두 개에서 열어 두는 것만으로도 상황이 달라진다. 실시간 공동 편집 기능이 없어도 두 탭은 서로 다른 시점의 문서를 가지고 있기 때문이다.</p>
<p>만약, 한 탭에서 최신 내용을 저장한 뒤 오래전에 열어 둔 다른 탭에서 저장한다면... 서버가 요청을 받은 순서대로 모두 반영한다면 먼저 저장한 변경이 오래된 내용으로 덮일 수 있다. 
두 저장 요청이 모두 성공하기 때문에 사용자는 문제를 바로 알아채기도 어렵다</p>
<h2 id="같은-문서를-두-탭에서-열었을-때">같은 문서를 두 탭에서 열었을 때</h2>
<p>탭 A와 탭 B가 같은 문서를 열었다. 두 탭이 문서를 읽은 시점의 상태는 <code>R1</code>이다.</p>
<pre><code class="language-text">DB의 현재 revision: R1

탭 A가 읽은 revision: R1
탭 B가 읽은 revision: R1</code></pre>
<p>여기서 <code>R1</code>, <code>R2</code>는 흐름을 설명하기 위한 기호다. 실제 구현에서는 문서 초안마다 UUID 형태의 <code>revision</code>을 사용했다.</p>
<p>탭 A에서 먼저 내용을 수정하고 저장하면 DB의 문서는 <code>R2</code>가 된다.</p>
<pre><code class="language-text">탭 A 저장
R1 → R2</code></pre>
<p>문제는 탭 B다. 탭 B는 아직 <code>R1</code>을 보고 있다. 이 상태에서 저장하면 요청은 나중에 도착한다. 하지만 요청을 만들 때 기준으로 삼은 문서는 더 오래된 상태다.</p>
<p>서버가 별다른 확인 없이 마지막 요청을 저장하면 탭 A에서 추가한 내용이 사라질 수 있다.</p>
<p>이 문제를 알아보고 나니 단순히 “마지막으로 저장한 요청이 최신이다”라고 볼 수 없었다. 요청이 도착한 시간과 그 요청이 바라본 문서의 시간은 서로 달랐다.</p>
<h2 id="마지막-저장-요청이-이기는-구조">마지막 저장 요청이 이기는 구조</h2>
<p>가장 간단한 방식은 마지막에 도착한 요청을 그대로 반영하는 것이다. 흔히 <code>Last Write Wins</code>라고 부르는 방식이다.</p>
<p>구현은 쉽다! 요청이 들어온 순서대로 내용을 저장만 하면되니께</p>
<p>하지만 오래된 탭에서 보낸 요청도 정상적인 저장으로 처리된다. 서버 입장에서는 마지막 요청이다. 사용자 입장에서는 최신 변경을 지워버린 요청일 수 있다.</p>
<p>더 불편한 점은 두 요청이 모두 성공한다는 것이었다. 오류가 발생하지 않으니 사용자는 자신의 내용이 사라졌다는 사실을 나중에야 알게 된다.</p>
<p>다른 방법으로 실시간 병합도 생각할 수 있었다. 다만 구조화된 문서를 자동으로 병합하려면 단순히 문자열 두 개를 합치는 것으로 끝나지 않는다.</p>
<p>같은 블록을 동시에 수정했을 때 무엇을 남길지, 한쪽에서 삭제한 블록을 다른 쪽에서 수정했다면 어떻게 처리할지, 블록 순서가 동시에 바뀌면 어떤 결과가 맞는지까지 정해야 한다.</p>
<p>당시 필요한 범위는 공동 편집 기능 전체가 아니었다. 우선 오래된 요청이 최신 문서를 조용히 덮어쓰는 상황부터 막기로 했다.</p>
<h2 id="저장-시점의-revision-확인">저장 시점의 revision 확인</h2>
<p>문서 초안마다 <code>revision</code>을 뒀다. 클라이언트는 저장 요청을 보낼 때 마지막으로 읽은 <code>revision</code>도 함께 보낸다.</p>
<pre><code class="language-json">{
  &quot;content&quot;: &quot;오늘의 회의 내용을 정리했습니다. 다음 회의는 금요일입니다.&quot;,
  &quot;revision&quot;: &quot;R1&quot;
}</code></pre>
<p>여기서 요청의 <code>revision</code>은 새로 저장할 문서의 값이 아니다.</p>
<p>“나는 <code>R1</code> 상태의 문서를 읽고 이 내용을 작성했다”라는 의미에 가깝다.</p>
<p>서버는 요청을 받은 뒤 DB에 저장된 현재 <code>revision</code>과 요청의 <code>revision</code>을 비교한다.</p>
<ul>
<li>두 값이 같으면 그동안 다른 변경이 없었다고 보고 저장한다.</li>
<li>두 값이 다르면 다른 저장이 먼저 반영된 것이므로 현재 요청을 거부한다.</li>
<li>저장에 성공하면 서버가 새로운 UUID <code>revision</code>을 발급한다.</li>
<li>충돌한 요청에는 HTTP <code>409 Conflict</code>를 반환한다.</li>
</ul>
<p>이 구조를 적용한 뒤에야 저장 요청이 최신 상태를 기준으로 만들어졌는지 확인할 수 있었다.</p>
<p>숫자 버전을 증가시키는 대신 UUID를 사용했지만 역할은 비슷하다. 저장하기 전에 내가 읽었던 버전이 아직도 현재 버전인지 확인한다는 점에서 낙관적 동시성 제어 방식에 가깝다.</p>
<h2 id="행-잠금은-순서-revision은-최신-여부">행 잠금은 순서, revision은 최신 여부</h2>
<p>여기서 가장 헷갈렸던 부분은 행 잠금과 <code>revision</code> 검사의 차이였다.</p>
<p>처음에는 같은 문서 행에 잠금을 걸면 덮어쓰기 문제도 해결되는 것처럼 느껴졌다. 저장 요청을 하나씩 처리하니 안전해 보였기 때문이다.</p>
<p>하지만 행 잠금은 두 저장 처리가 동시에 같은 행을 수정하지 못하도록 순서를 정해 줄 뿐이다.</p>
<pre><code class="language-text">요청 A 처리 중
→ 문서 행 잠금
→ 요청 B는 잠시 대기</code></pre>
<p>행 잠금만으로는 요청 B가 오래된 문서를 기준으로 작성됐는지 알 수 없다. A의 저장이 끝난 뒤 차례가 돌아오면 B 역시 정상적으로 저장될 수 있다.</p>
<p><code>revision</code> 검사는 다른 질문에 답한다.</p>
<pre><code class="language-text">행 잠금
같은 문서의 저장 처리가 겹치지 않는가?

revision 검사
이 요청이 현재 문서를 기준으로 작성됐는가?</code></pre>
<p>둘 중 하나가 다른 하나를 대신하는 구조가 아니었다.</p>
<p>먼저 행 잠금으로 확인과 수정 사이에 다른 저장이 끼어들지 못하게 했다. 잠금을 잡은 상태에서 DB의 현재 <code>revision</code>과 요청의 <code>revision</code>을 비교했다.</p>
<p>값이 같을 때만 문서를 저장하고 새로운 <code>revision</code>을 발급했다. 다르면 오래된 요청으로 보고 저장하지 않았다.</p>
<h2 id="a는-성공하고-b는-거절되는-흐름">A는 성공하고 B는 거절되는 흐름</h2>
<p>두 탭이 모두 <code>R1</code>을 읽은 상태에서 탭 A의 요청이 먼저 도착했다고 가정해 보자.</p>
<pre><code class="language-text">요청 A
- 요청 revision: R1
- 새 문서 내용: A에서 수정한 내용</code></pre>
<p>서버는 문서 행을 잠근 뒤 DB의 현재 값과 요청의 값을 비교한다.</p>
<pre><code class="language-text">DB: R1
요청 A: R1
결과: 일치</code></pre>
<p>두 값이 같으므로 내용을 저장하고 새 <code>revision</code>인 <code>R2</code>를 발급한다.</p>
<p>그다음 탭 B의 요청이 도착한다.</p>
<pre><code class="language-text">요청 B
- 요청 revision: R1
- 새 문서 내용: B에서 수정한 내용</code></pre>
<p>이번에는 비교 결과가 다르다.</p>
<pre><code class="language-text">DB: R2
요청 B: R1
결과: 불일치</code></pre>
<p>서버는 요청 B를 저장하지 않고 HTTP <code>409 Conflict</code>를 반환한다.</p>
<p>탭 B의 내용이 틀렸다고 판단해서 거부한 게 아니다. 탭 B가 문서를 읽은 뒤 다른 변경이 먼저 저장됐으니 서버가 어느 내용을 남길지 임의로 결정하지 않은 것이다.</p>
<p>저장에 성공한 탭 A는 응답으로 받은 <code>R2</code>를 기억한다. 다음 저장부터는 <code>R2</code>를 요청에 포함한다.</p>
<h2 id="구현하며-배운-점">구현하며 배운 점</h2>
<p>처음에는 저장 요청을 순서대로 처리하면 충분하다고 생각했다. 행 잠금을 걸면 같은 문서가 동시에 수정되는 문제도 끝나는 줄 알았다.</p>
<p>하지만 실제로는</p>
<blockquote>
<p>저장 처리 사이의 순서를 지키는 것, 
그리고 각 요청이 어떤 문서 상태를 보고 만들어졌는지 확인하는 것</p>
</blockquote>
<p>이 필요했다.</p>
<p>행 잠금은 첫 번째 문제를 다뤘고 <code>revision</code> 검사는 두 번째 문제를 다뤘다.</p>
<p>이번 구현을 하면서 동시성 문제는 요청이 정확히 동시에 들어오는 상황에만 한정되지 않는다는 점을 배웠다. 오래된 상태를 들고 있던 요청이 나중에 도착하는 것만으로도 데이터는 사라질 수 있었다.</p>
<p>물론 저장을 거부하는 것만으로 사용자 경험까지 완성되지는 않는다. 
그래도 충돌을 모른 척하고 성공시키는 것보다, 지금은 저장할 수 없다고 명시적으로 알려주는 편이 다음 대응을 설계하기 쉬웠다!</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/edffdba8-f0f3-46ff-8743-5d7d59feb533/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI 시대의 새로운 병목과 고민]]></title>
            <link>https://velog.io/@yujin_1219/ai-new-bottleneck-speed-of-understanding</link>
            <guid>https://velog.io/@yujin_1219/ai-new-bottleneck-speed-of-understanding</guid>
            <pubDate>Thu, 15 Oct 2026 00:45:45 GMT</pubDate>
            <description><![CDATA[<p>요즘은 일을 하다가 막히는 시간이 많이 줄었다.</p>
<p>처음 보는 오류가 나와도 AI가 오류를 다 파악해서 원인을 알려준다( 항상 맞진 않지만..)
낯선 코드도 파일 몇 개만 보여주면 구조를 정리해주고 구현 방향부터 수정할 코드까지 빠르게 받을 수 있다.</p>
<p>분명 편해졌다. 
이전보다 할 수 있는 일도 많아졌고 작업 속도도 빨라졌다.</p>
<p>그런데 어느 순간부터일은 계속 앞으로 나아가지만 나는 그 속도를 제대로 따라가고 있는지 잘 모르겠다는 불안감이 들기 시작했다
<img src="https://velog.velcdn.com/images/yujin_1219/post/9e8ed420-b305-4479-b091-abfc5bd07f1d/image.png" alt=""></p>
<h2 id="작업은-빨라졌지만-모두-이해하지-못하고-있다">작업은 빨라졌지만 모두 이해하지 못하고 있다</h2>
<p>AI에게 질문하면 하나를 물어봐도 많은 답이 한꺼번에 돌아온다.</p>
<p>문제의 원인, 확인할 파일, 수정 방법, 다른 선택지, 주의할 점까지 몇 초 만에 나온다. 예전 같으면 하나씩 검색하면서 며칠에 걸쳐 발견했을 내용이다.</p>
<p>문제는 내가 그만큼 빠르게 이해하지는 못한다는 것이다.</p>
<p>답을 읽는 동안에도 다음 정보가 들어온다. 첫 번째 설명을 충분히 생각하기도 전에 두 번째 해결 방법을 보게 된다. 코드를 이해하기 전에 수정 결과부터 확인할 때도 있다.</p>
<p>처리해야 하는 정보는 많아졌는데 한 가지 생각에 오래 머무는 시간은 줄었다.</p>
<p>그래서 작업은 빨라졌지만 몰입은 오히려 어려워졌다. 하나를 끝까지 파고들기보다 여러 답 사이를 이동하는 시간이 많아졌다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/9647eb6f-e448-4967-8fb2-a19fb8360e59/image.png" alt="">
요즘 집중이 너무 안 돼서 ADHD 생겼나 까지 고민했다;;;</p>
<p>예전에는 막히더라도 한 문제를 붙잡고 있었는데, 요즘은 답 하나를 읽다가 다른 질문을 하고 그 답에서 나온 단어를 다시 검색한다. 
그러다 처음에 뭘 하려고 했는지 놓칠 때도 너무 많았다.</p>
<p>AI를 켜면 할 수 있는 일이 계속 생긴다. 코드를 봐달라고 할수도 있고 다른 방식도 물어볼 수 있고 이해가 안 되는 문장은 다시 풀어달라고 하면 된다. 답이 끊이지 않으니 나도 계속 다음 질문으로 넘어가게 된다.</p>
<p>가만히 생각해 보니 집중력이 갑자기 사라졌다기보다, 한곳에 오래 머물 필요가 없는 방식에 익숙해진 건 아닐까 싶었다.</p>
<p>물론 이게 전부 AI 때문이라고 단정할 수는 없다. 
원래도 집중이 흔들리는 날은 있었다. 
그래도 AI를 많이 쓰기 시작한 뒤 한 문제에서 다른 문제로 넘어가는 속도가 훨씬 빨라진 건 확실히 느낀다.</p>
<p>답을 기다리는 시간조차 거의 없다. 막히면 고민하기보다 바로 새 질문을 만들 수 있다. 이 편리함이 내 집중 방식과 꽤 크게 연결되어 있다는 생각이 들었다.</p>
<h2 id="답이-늦어서-생각했던-시간">답이 늦어서 생각했던 시간</h2>
<p>예전에는 문제가 생기면 먼저 내가 알고 있는 내용을 떠올렸다.</p>
<p>기억이 나지 않으면 검색해서 여러 글을 비교했다. 찾은 내용을 코드에 직접 적용해 봤다. 실패하면 다시 원인을 추측했다.</p>
<pre><code class="language-text">내가 아는 내용 떠올리기
→ 원인 추측하기
→ 검색하기
→ 직접 적용하기
→ 실패한 이유 다시 생각하기</code></pre>
<p>돌이켜 보면 꽤 느린 과정이었다. 엉뚱한 검색어로 한참 헤매기도 했다. 공식 문서를 읽고도 뭔말인지 몰라서 다른 글을 다시 찾아본 적도 많았다.</p>
<p>그런데 그 과정에서 기억에 남는 것이 분명 있었다.</p>
<p>처음에 무엇을 의심했는지, 어떤 방법이 왜 실패했는지가 기억에 남았다. 마지막 해결책이 이전 방법과 어떻게 달랐는지도 연결되어 남았다. 정답과 함께 그곳까지 가는 동안 생긴 생각도 기억했다.</p>
<p>하지만 지금은 질문만 잘하면 그 과정이 크게 줄어든다.</p>
<pre><code class="language-text">상황 설명하기
→ 답 받기
→ 적용하기
→ 정상 동작 확인하기</code></pre>
<p>훨씬 효율적이다 !! 그치만..너무 빠르게 끝나서 내가 무엇을 배웠는지 설명하기 어려운 경우가 많아지고있다.</p>
<h2 id="ai가-만든-새로운-병목">AI가 만든 새로운 병목</h2>
<p>처음에는 AI가 병목을 없애 주는 도구라고 생각했다.</p>
<p>검색하는 시간도 줄고 반복 코드를 작성하는 시간도 줄었다. 모르는 기술 때문에 멈춰 있는 시간도 짧아졌다.</p>
<p>하지만 한쪽이 빨라지자 다른 곳에서 병목이 생겼다.</p>
<p>이제는 답을 찾는 속도보다 그 답을 이해하고 내 상황에 연결하는 속도가 병목이었다.</p>
<p>AI는 여러 가능성을 한 번에 제시할 수 있고, 
나는 그중 어떤 설명이 지금 상황에 맞는지 판단해야 한다. 
코드를 만들어 줄 수는 있지만 이 코드가 기존 구조에서 어떤 영향을 만들지는 내가 확인해야하는 몫이라고 생각한다.</p>
<p>받을 수 있는 답의 양은 거의 제한이 없는데 내가 집중해서 검토할 수 있는 양에는 분명한 한계가 있었다.</p>
<p>그 한계를 인정하지 않고 계속 다음 답을 받다 보면 많이 본 것과 제대로 이해한 것을 헷갈리게 된다.</p>
<h2 id="로직보다-먼저-남겨야-하는-생각">로직보다 먼저 남겨야 하는 생각</h2>
<p>회사에서도 비슷한 이야기를 들었다.</p>
<p>어떤 로직 하나를 만들더라도 처음에 무엇을 문제라고 봤는지가 중요하다는 말이었다. 어떤 생각이 다음 판단으로 이어졌는지, 왜 이 방법을 선택했는지도 분명히 직접 인간이. 우리가 사고해야한다고 하셨다.</p>
<p>이 말을 듣고 나니 머리가 띵해졋다
AI가 알려준 방법을 이해하는 것만으로 충분한지 다시 생각하게 됐다.</p>
<p>요즘 AI는 단순히 코드를 작성하는 데서 끝나지 않는다.</p>
<ul>
<li>무엇이 문제인지 추측한다.</li>
<li>확인할 순서를 정한다.</li>
<li>적절한 구현 방식을 제안한다.</li>
<li>코드를 수정한다.</li>
<li>테스트 결과를 보고 다시 고친다.</li>
</ul>
<p>잘 안 되는 이유를 물으면 원인과 해결 순서까지 만들어 준다. 내가 생각을 시작하기 전에 이미 그럴듯한 사고 과정이 완성되어 있다.</p>
<p>이 과정은 물론 혼자 생각할 때보다 빠르고 정확한 경우도 많다.</p>
<p>다만 완성된 사고 과정만 계속 받아보다 보면 문제를 만났을 때 어디서부터 생각해야 하는지 점점 잊을 수도 있겠다는 생각이 들었다.. 실제로 나도 점점 그 과정 속에 있는 것 같았다.</p>
<p>문제가 생겼을 때는 원인을 파악하고 가설을 세운다. 
틀린 가설을 버리는 과정도 여전히 중요하다. 
결과가 맞는지 판단하려면 적어도 내가 무엇을 확인하고 있는지는 알아야 하기 때문이다.</p>
<h2 id="어디까지-이해해야-하는가">어디까지 이해해야 하는가</h2>
<p>여기서 가장 어려운 질문이 생겼다.</p>
<p>그렇다면 어디까지 내가 직접 이해해야 할까??</p>
<p>모든 코드를 한 줄씩 설명할 수 있을 때까지 붙잡고 있으면 작업 속도는 다시 느려진다. 한 번 사용하고 끝날 도구의 내부 구현까지 모두 공부하는 것도 항상 좋은 선택은 아닐 것이다.</p>
<p>반대로 결과만 확인하고 계속 넘어가면 당장은 빠르다. 하지만 비슷한 문제가 생겼을 때 다시 처음부터 AI에게 물어보게 된다. 무엇이 잘못됐는지 판단할 기준도 약해질 수 있다.</p>
<p>속도와 이해 중 하나만 선택할 수는 없었다.</p>
<p>AI는 이미 너무 유용해서 일부러 덜 쓸 생각은 없다. 앞으로는 더 많은 일을 맡기게 될 것 같다.</p>
<p>생각해 보니 내가 고민할 문제는 사용 여부보다 <strong>무엇을 남기고 무엇을 넘길지 정하는 일</strong>에 가까웠다.</p>
<h2 id="어디까지-ai를-맡겨야할까">어디까지 AI를 맡겨야할까</h2>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/c5059d26-dfdf-40db-b65c-f1d778adf177/image.png" alt=""></p>
<p>아직 명확한 기준은 없지만 지금은 이렇게 나눠 보려고 한다.</p>
<p>반복되는 코드와 문서 정리, 문법과 명령어 확인, 처음 보는 코드의 구조 요약은 AI에 적극적으로 맡겨도 된다고 생각한다. 가능한 해결 방법의 목록을 만들거나 빠뜨린 예외와 테스트 항목을 찾는 일도 마찬가지다.</p>
<p>대신 지금 해결하려는 문제가 정확히 무엇인지, 현재 상황에서 가장 먼저 의심할 원인이 무엇인지는 가능하면 내가 먼저 생각하고 싶다. 선택지마다 생기는 장단점과 실패했을 때 영향을 받는 범위, 최종적으로 이 방법을 선택한 이유도 직접 남기려고 한다.</p>
<p>구현 방법은 넘길 수 있어도 문제를 정의하는 일까지 완전히 넘기고 싶지는 않다.</p>
<p>AI가 여러 선택지를 제안할 수는 있지만 우리 상황에서 무엇이 중요한지 결정하는 일은 맥락을 가진 사람의 몫이다. 적어도 그 판단을 설명할 수 있을 정도의 이해는 남겨야 한다고 생각한다.</p>
<h2 id="당분간의-작은-기준">당분간의 작은 기준</h2>
<p>거창한 규칙을 만들면 며칠 지나지 않아 지키지 못할 것 같다. 그래서 당분간은 아주 작은 기준만 두려고 한다.</p>
<p>AI에게 묻기 전에 내가 생각한 원인을 한 줄이라도 먼저 적는다.</p>
<pre><code class="language-text">나는 지금 이 문제가 상태 변경 순서 때문에 발생했다고 생각한다.</code></pre>
<p>답을 받은 뒤에는 왜 그 방법이 동작하는지 내 말로 짧게 정리한다.</p>
<pre><code class="language-text">상태를 먼저 저장하는 것이 아니라, 외부 작업의 성공을 확인한 뒤 상태를 바꿔야 불일치가 줄어든다.</code></pre>
<p>마지막으로 비슷한 문제가 생기면 무엇부터 확인할지 하나만 남긴다.</p>
<p>다 외울 생각은 없다. 다음 문제에서 다시 사용할 수 있는 생각 하나만 가져가려 한다.</p>
<p>이 정도 과정조차 생략하면 나는 답을 사용했을 뿐, 판단한 사람이라고 하기는 어려울 것 같다.</p>
<h2 id="아직-잘-모르겠다">아직 잘 모르겠다!!</h2>
<p>솔직히 어디까지 직접 생각하고 어디부터 맡겨야 하는지는 아직 모르겠다.</p>
<p>AI에게 맡길 수 있는 범위는 계속 넓어진다. 그러니 오늘 세운 기준도 나중에는 달라질 수 있다. 지금 중요하다고 여기는 능력도 있다. 그중 일부는 언젠가 굳이 사람이 하지 않아도 되는 일이 될지 모른다.</p>
<p>그러다 보니 속도를 포기하고 예전 방식으로 돌아가는 것도 답은 아닌 것 같다.</p>
<p>다만 빠르게 해결했다는 이유만으로 제대로 이해했다고 착각하지는 않으려고 한다. 많은 답을 읽었다는 이유로 많이 배웠다고 생각하지도 않으려고 한다.</p>
<p>앞으로 더 고민해야 할 것은 AI를 얼마나 많이 쓰느냐가 아닐 것이다.</p>
<p>무엇을 직접 기억할지, 무엇을 직접 의심할지, 어떤 판단만큼은 내가 내릴지에 대한 기준일 것 같다.</p>
<p>작업의 속도는 이미 충분히 빨라졌다.</p>
<p>이제는 그 속도 안에서 내 생각을 어디에 남길지 고민해야 할 때가 된 것 같다.</p>
<p>다들 화이팅.....<img src="https://velog.velcdn.com/images/yujin_1219/post/2b12972b-02c6-49ce-9d7c-6b31a3433534/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes를 알아보자: Pod, Deployment, Service와 YAML]]></title>
            <link>https://velog.io/@yujin_1219/kubernetes-beginner-pod-deployment-service-yaml</link>
            <guid>https://velog.io/@yujin_1219/kubernetes-beginner-pod-deployment-service-yaml</guid>
            <pubDate>Wed, 14 Oct 2026 04:09:12 GMT</pubDate>
            <description><![CDATA[<p>Kubernetes를 직접 구축하거나 운영해 본 경험은 없었다. 그런데 회사 프로젝트가 이미 Kubernetes 환경으로 구성되어 있었고, 기능을 개발하는 과정에서 기존 배포 구조를 읽고 일부 설정을 수정해야 했다.</p>
<p>처음에는 내가 맡은 기능 코드만 잘 작성하면 된다고 생각했는데 막상 배포 설정을 따라가 보니 애플리케이션이 어떤 이미지로 만들어지는지, 어디에서 실행되는지, 설정값은 어떻게 전달되는지 알아야 변경의 영향을 판단할 수 있었다.</p>
<p>YAML 파일에는 익숙하지 않은 객체가 계속 등장했다. <code>Pod</code>, <code>Deployment</code>, <code>Service</code>, <code>ConfigMap</code>, <code>Job</code>이 서로 어떤 관계인지 모른 채 값을 수정할순 없으니...^^ 당장 필요한 설정만 복사하기보다 기존 구조를 읽을 수 있을 정도로 기초부터 공부해 보기로 했다.</p>
<blockquote>
<p>궁금한점!!</p>
</blockquote>
<ul>
<li>컨테이너가 죽으면 누가 다시 실행할까?</li>
<li>요청이 많아지면 같은 컨테이너를 어떻게 늘릴까?</li>
<li>여러 서버 중 어디에 컨테이너를 배치할까?</li>
<li>새 버전을 배포할 때 서비스를 어떻게 끊지 않을까?</li>
<li>컨테이너 주소가 바뀌어도 다른 서비스는 어떻게 찾아갈까?</li>
</ul>
<p>Kubernetes는 이런 문제를 다루는 플랫폼이었다. 처음에는 YAML 파일이 많고 용어도 낯설어 어디서부터 읽어야 할지 막막했다. 하나씩 따라가 보니 각 파일은 명령을 늘어놓은 것이 아니라, 애플리케이션이 어떤 상태로 실행되어야 하는지를 설명하고 있었다.</p>
<p>Docker가 컨테이너 이미지를 만들고 실행하는 데 집중한다면 _<strong>Kubernetes는 여러 서버 위에서 컨테이너화된 애플리케이션의 원하는 상태를 유지</strong>_한다.</p>
<h2 id="여러-컨테이너의-운영을-맡는-kubernetes">여러 컨테이너의 운영을 맡는 Kubernetes</h2>
<p>컨테이너 하나가 멈추면 <code>docker run</code>으로 다시 실행할 수 있다. 컨테이너가 두세 개고 서버도 한 대라면 사람이 관리하거나 Docker Compose를 사용할 수도 있다.</p>
<p>규모가 커지면 판단할 일이 많아진다.</p>
<pre><code class="language-text">API 컨테이너 5개
작업자 컨테이너 3개
여러 대의 서버
배포 중인 새 버전과 기존 버전
계속 바뀌는 컨테이너 상태</code></pre>
<p>어떤 서버에 여유가 있는지 살피고, 실패한 컨테이너를 다시 만들고, 요청을 정상 컨테이너로 보내는 일을 계속 사람이 하기 어렵다. Kubernetes는 선언한 상태를 읽고 실제 상태가 그 모습에 가까워지도록 반복해서 조정한다. 공식 문서에서는 이를 컨테이너화된 워크로드와 서비스를 관리하는 이식 가능하고 확장 가능한 플랫폼으로 설명한다. <a href="https://kubernetes.io/docs/concepts/overview/">Kubernetes 공식 문서: Overview</a></p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/ac36b4e2-9257-4587-9848-a653db7acddc/image.png" alt="Docker와 Kubernetes가 맡는 범위를 비교한 구성도"></p>
<h2 id="docker와-kubernetes의-역할-차이">Docker와 Kubernetes의 역할 차이</h2>
<p>처음에는 Kubernetes가 Docker의 상위 버전이라고 생각했다. 하지만 Kubernetes가 Dockerfile을 읽어 이미지를 만들어 주는 것은 아니다.</p>
<p>일반적인 흐름은 다음과 같다.</p>
<ol>
<li>Dockerfile 등으로 컨테이너 이미지를 만든다.</li>
<li>이미지를 레지스트리에 올린다.</li>
<li>Kubernetes에 어떤 이미지를 몇 개 실행할지 선언한다.</li>
<li>Kubernetes가 적절한 Node에 Pod를 배치한다.</li>
</ol>
<p>즉 컨테이너 이미지가 배송할 상자라면 Kubernetes는 상자를 여러 창고에 배치하고, 부족하면 보충하며, 목적지까지 연결하는 운영 시스템에 가깝다.</p>
<p>Kubernetes는 다양한 컨테이너 런타임과 함께 동작한다. 그래서 “Kubernetes 안에서 Docker 명령이 계속 실행된다”라고 단순화하면 현재 구조를 정확히 설명하지 못할 수 있다.</p>
<h2 id="cluster와-node">Cluster와 Node</h2>
<p>Kubernetes가 관리하는 전체 환경을 <strong>Cluster</strong>라고 한다. Cluster는 크게 Control Plane과 Worker Node로 나뉜다.</p>
<table>
<thead>
<tr>
<th>구성</th>
<th>하는 일</th>
</tr>
</thead>
<tbody><tr>
<td>Control Plane</td>
<td>사용자가 선언한 상태를 받고, 어디에 무엇을 실행할지 판단하고, 전체 상태를 조정한다.</td>
</tr>
<tr>
<td>Worker Node</td>
<td>실제 애플리케이션 Pod가 실행되는 서버다.</td>
</tr>
</tbody></table>
<p>Control Plane 안에는 API Server, Scheduler, Controller Manager, etcd 같은 구성 요소가 있다. 처음부터 내부 동작을 모두 외울 필요는 없었지만 역할은 대략 구분해 두는 편이 좋았다.</p>
<ul>
<li><strong>API Server</strong>: Kubernetes 요청이 드나드는 중심 입구다.</li>
<li><strong>etcd</strong>: Cluster 상태 데이터를 저장하는 키-값 저장소다.</li>
<li><strong>Scheduler</strong>: 새 Pod를 어느 Node에 둘지 정한다.</li>
<li><strong>Controller Manager</strong>: 원하는 상태와 실제 상태의 차이를 계속 줄인다.</li>
<li><strong>kubelet</strong>: 각 Node에서 Pod가 실제로 실행되도록 관리한다.</li>
<li><strong>Container runtime</strong>: 컨테이너를 실제로 실행한다.</li>
</ul>
<p>공식 구성 요소 문서와 그림을 함께 보면 각 역할을 연결하기 쉽다. <a href="https://kubernetes.io/docs/concepts/overview/components/">Kubernetes 공식 문서: Components</a></p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/7a3750af-0872-4dda-a708-4fa6190653c9/image.png" alt="Control Plane과 Worker Node로 구성된 Kubernetes Cluster 구조"></p>
<h2 id="pod는-컨테이너를-담는-가장-작은-배포-단위">Pod는 컨테이너를 담는 가장 작은 배포 단위</h2>
<p>Kubernetes에서는 컨테이너를 곧바로 배포 단위로 다루지 않고 <strong>Pod</strong> 안에서 실행한다. Pod는 Kubernetes가 만들고 배치할 수 있는 가장 작은 단위다.</p>
<p>Pod 하나에는 컨테이너 하나 이상이 들어갈 수 있다. 같은 Pod의 컨테이너들은 네트워크와 일부 저장 공간을 공유한다. 하지만 처음 공부할 때는 “Pod 하나에 주 애플리케이션 컨테이너 하나”인 경우부터 이해하는 편이 쉬웠다.</p>
<pre><code class="language-text">Pod
└── API 컨테이너</code></pre>
<p>보조 기능이 반드시 같은 생명주기와 네트워크를 공유해야 할 때 사이드카 컨테이너를 둘 수 있다.</p>
<pre><code class="language-text">Pod
├── API 컨테이너
└── 보조 컨테이너</code></pre>
<p>Pod는 영구적인 서버처럼 다루지 않는 편이 좋다. 장애나 배포로 사라졌다가 새 이름과 새 IP를 가진 Pod가 만들어질 수 있다. <a href="https://kubernetes.io/docs/concepts/workloads/pods/">Kubernetes 공식 문서: Pods</a></p>
<h2 id="pod의-실행-상태를-관리하는-deployment">Pod의 실행 상태를 관리하는 Deployment</h2>
<p>Pod YAML을 작성해 직접 실행할 수는 있다. 하지만 그 Pod가 죽었을 때 같은 Pod를 계속 유지해 줄 관리자가 없다.</p>
<p>웹 API처럼 계속 실행되어야 하는 애플리케이션에는 보통 <strong>Deployment</strong>를 사용한다. Deployment에 원하는 Pod 개수와 이미지 버전을 적으면 관련 Controller가 실제 상태를 계속 맞춘다.</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-api
  template:
    metadata:
      labels:
        app: sample-api
    spec:
      containers:
        - name: api
          image: registry.example/sample-api:1.0.0
          ports:
            - containerPort: 8080</code></pre>
<p>이 YAML은 명령을 위에서 아래로 한 번 실행하는 스크립트라기보다 다음과 같은 요청에 가깝다.</p>
<blockquote>
<p><code>sample-api:1.0.0</code> 이미지로 실행되는 Pod가 세 개인 상태를 유지해 줘.</p>
</blockquote>
<p>Pod 하나가 사라져 두 개가 되면 Deployment가 관리하는 ReplicaSet Controller가 새 Pod를 만들어 다시 세 개에 가깝게 맞춘다. 새 이미지로 바꾸면 Deployment가 교체 과정을 관리한다. <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/">Kubernetes 공식 문서: Deployments</a></p>
<h2 id="yaml은-실행-순서가-아니라-원하는-상태">YAML은 실행 순서가 아니라 원하는 상태</h2>
<p>Kubernetes YAML에는 자주 반복되는 네 가지 필드가 있다.</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-api
spec:
  # 원하는 상태</code></pre>
<table>
<thead>
<tr>
<th>필드</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>apiVersion</code></td>
<td>이 객체가 사용하는 Kubernetes API 버전</td>
</tr>
<tr>
<td><code>kind</code></td>
<td>만들 객체의 종류</td>
</tr>
<tr>
<td><code>metadata</code></td>
<td>이름, Namespace, Label 같은 식별 정보</td>
</tr>
<tr>
<td><code>spec</code></td>
<td>사용자가 원하는 상태</td>
</tr>
</tbody></table>
<p>객체가 만들어지면 Kubernetes는 현재 상태를 <code>status</code>로 관찰한다. 사용자는 <code>spec</code>에 원하는 상태를 적고, Controller는 실제 상태가 그에 가까워지도록 반복해서 조정한다.</p>
<pre><code class="language-text">원하는 상태: Pod 3개
실제 상태: Pod 2개
조정: Pod 1개 추가</code></pre>
<p>이 동작을 이해하니 Kubernetes를 명령 실행기가 아니라 <strong>계속 관찰하고 차이를 줄이는 시스템</strong>으로 볼 수 있었다. <a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/">Kubernetes 공식 문서: Objects</a></p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/c083b2db-f508-45ff-8ef1-326bf58bad34/image.png" alt="Kubernetes Controller가 원하는 상태와 실제 상태의 차이를 조정하는 흐름"></p>
<h2 id="바뀌는-pod를-연결하는-안정적인-접근점-service">바뀌는 Pod를 연결하는 안정적인 접근점, Service</h2>
<p>Deployment가 Pod를 새로 만들면 Pod 이름과 IP가 바뀔 수 있다. 클라이언트가 특정 Pod IP를 기억하면 배포나 장애 복구 때 연결이 깨진다.</p>
<p>이 문제를 해결하는 기본 객체가 <strong>Service</strong>다. Service는 조건에 맞는 Pod 집합 앞에 비교적 안정적인 네트워크 주소와 이름을 제공한다.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: sample-api
spec:
  selector:
    app: sample-api
  ports:
    - port: 80
      targetPort: 8080</code></pre>
<p>위 Service는 <code>app: sample-api</code> Label을 가진 Pod를 찾는다. 클라이언트는 개별 Pod IP 대신 Service 이름과 포트를 사용한다.</p>
<pre><code class="language-text">클라이언트 -&gt; sample-api Service -&gt; 정상 Pod 중 하나</code></pre>
<p>Service가 있다고 인터넷에 자동 공개되는 것은 아니다. <code>ClusterIP</code>, <code>NodePort</code>, <code>LoadBalancer</code> 등 Service 유형과 Ingress 또는 Gateway 구성에 따라 접근 범위가 달라진다. 처음에는 Cluster 내부의 안정적인 주소를 만든다는 역할부터 이해하는 것으로 충분했다. <a href="https://kubernetes.io/docs/concepts/services-networking/service/">Kubernetes 공식 문서: Service</a></p>
<h2 id="kubernetes-핵심-객체-정리">Kubernetes 핵심 객체 정리</h2>
<p>Kubernetes는 객체 종류가 많아서 처음부터 모두 외우려다 더 헷갈렸다. “계속 실행할 것인가, 끝나야 하는 작업인가, 상태를 어디에 둘 것인가”로 나누니 조금 선명해졌다.</p>
<table>
<thead>
<tr>
<th>객체</th>
<th>필요한 상황</th>
<th>처음 이해한 역할</th>
</tr>
</thead>
<tbody><tr>
<td>Deployment</td>
<td>웹 API처럼 계속 실행되는 무상태 애플리케이션</td>
<td>Pod 수와 배포 버전을 관리</td>
</tr>
<tr>
<td>StatefulSet</td>
<td>안정적인 식별자와 저장 공간이 중요한 상태 저장 워크로드</td>
<td>순서와 고유성을 고려해 Pod 관리</td>
</tr>
<tr>
<td>Service</td>
<td>바뀌는 Pod 앞에 안정적인 접근점이 필요할 때</td>
<td>Pod 집합을 이름과 네트워크 주소로 연결</td>
</tr>
<tr>
<td>Job</td>
<td>한 번 실행하고 성공적으로 끝나야 할 작업</td>
<td>완료될 때까지 Pod 실행을 관리</td>
</tr>
<tr>
<td>CronJob</td>
<td>정해진 시간마다 반복할 작업</td>
<td>일정에 따라 Job 생성</td>
</tr>
<tr>
<td>ConfigMap</td>
<td>비밀이 아닌 설정을 애플리케이션과 분리할 때</td>
<td>문자열 설정을 환경 변수나 파일로 전달</td>
</tr>
<tr>
<td>Secret</td>
<td>비밀번호나 토큰 같은 민감한 값을 분리할 때</td>
<td>민감 데이터를 별도 객체로 전달</td>
</tr>
</tbody></table>
<p>Secret이라는 이름만으로 완벽히 암호화된 금고라고 단정하면 안 된다. Kubernetes 공식 문서에 따르면 Secret은 기본 설정에서 etcd에 암호화되지 않은 형태로 저장될 수 있어, 저장 시 암호화와 접근 제어를 별도로 구성해야 한다. <a href="https://kubernetes.io/docs/concepts/configuration/secret/">Kubernetes 공식 문서: Secrets</a></p>
<p>ConfigMap도 비밀값을 넣는 곳이 아니다. 환경별 일반 설정을 코드나 이미지에서 분리하는 데 사용한다. <a href="https://kubernetes.io/docs/concepts/configuration/configmap/">Kubernetes 공식 문서: ConfigMaps</a></p>
<h2 id="docker-compose와-kubernetes의-차이">Docker Compose와 Kubernetes의 차이</h2>
<p>Compose와 Kubernetes 모두 YAML로 여러 컨테이너 구성을 표현해서 처음에는 같은 종류의 도구처럼 보였다. 범위와 운영 모델에는 차이가 있다.</p>
<table>
<thead>
<tr>
<th>질문</th>
<th>Docker Compose</th>
<th>Kubernetes</th>
</tr>
</thead>
<tbody><tr>
<td>주 사용 범위</td>
<td>개발 환경, 단일 호스트의 여러 컨테이너</td>
<td>여러 Node로 구성된 Cluster</td>
</tr>
<tr>
<td>실행 단위</td>
<td>Service와 Container</td>
<td>Pod와 Controller</td>
</tr>
<tr>
<td>장애 복구</td>
<td>구성과 환경에 따라 직접 보완</td>
<td>Controller가 원하는 상태를 지속적으로 조정</td>
</tr>
<tr>
<td>확장과 배포</td>
<td>비교적 단순한 구성</td>
<td>Replica, 롤링 업데이트 등 선언적 관리</td>
</tr>
<tr>
<td>학습과 운영 비용</td>
<td>비교적 낮음</td>
<td>구성 요소가 많고 운영 복잡도가 높음</td>
</tr>
</tbody></table>
<p>그래서 작은 개인 프로젝트라면 Kubernetes가 항상 정답은 아니다. 단일 서버의 Compose만으로 충분한 상황에서 Kubernetes를 도입하면 학습량과 운영 지점만 늘 수 있다.</p>
<p>반대로 여러 서비스와 서버, 반복 배포, 자동 복구, 표준화된 운영 방식이 필요한 상황에서는 Kubernetes의 선언적 모델이 도움이 될 수 있다.</p>
<h2 id="정리하자면">정리하자면...</h2>
<h3 id="pod와-서버의-차이">Pod와 서버의 차이</h3>
<p>Pod는 서버 장비나 가상 머신이 아니다. Node 위에 스케줄되어 실행되는 비교적 짧은 수명의 워크로드 단위다.</p>
<p>Pod가 교체될 수 있다는 전제로 Service, Deployment, Volume 같은 다른 객체와 함께 설계해야 한다.</p>
<h3 id="deployment와-service의-역할">Deployment와 Service의 역할</h3>
<p>Deployment는 원하는 Pod 템플릿과 개수를 관리한다. Service는 실행된 Pod 중 Label이 맞는 대상을 찾아 네트워크 접근점을 제공한다.</p>
<p>Service가 Pod를 만들지는 않는다.</p>
<h3 id="kubernetes-yaml의-동작-방식">Kubernetes YAML의 동작 방식</h3>
<p>Kubernetes는 YAML의 줄 순서를 셸 스크립트처럼 실행하지 않는다. 객체를 API에 제출하면 각 Controller가 자신의 대상을 관찰하고 조정한다.</p>
<p>객체 사이에 의존성이 있다면 준비 상태 검사, 재시도, 초기화 작업 등을 별도로 설계해야 한다.</p>
<h3 id="configmap-변경값의-반영-방식">ConfigMap 변경값의 반영 방식</h3>
<p>전달 방식과 애플리케이션 구현에 따라 다르다. 환경 변수로 주입한 값은 실행 중인 컨테이너에 자동으로 다시 들어가지 않는다. 파일로 마운트한 값은 갱신될 수 있지만 애플리케이션이 다시 읽는지도 확인해야 한다.</p>
<p>설정 변경 시 Pod 재시작이 필요한지, 자동 재배포를 어떻게 할지 정해야 한다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://kubernetes.io/docs/concepts/overview/">Kubernetes Overview</a></li>
<li><a href="https://kubernetes.io/docs/concepts/overview/components/">Kubernetes Components</a></li>
<li><a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/">Kubernetes Objects</a></li>
<li><a href="https://kubernetes.io/docs/concepts/workloads/pods/">Pods</a></li>
<li><a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/">Deployments</a></li>
<li><a href="https://kubernetes.io/docs/concepts/services-networking/service/">Services</a></li>
<li><a href="https://kubernetes.io/docs/concepts/configuration/configmap/">ConfigMaps</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker: 이미지, 컨테이너, Dockerfile]]></title>
            <link>https://velog.io/@yujin_1219/docker-beginner-image-container-dockerfile</link>
            <guid>https://velog.io/@yujin_1219/docker-beginner-image-container-dockerfile</guid>
            <pubDate>Wed, 14 Oct 2026 02:59:17 GMT</pubDate>
            <description><![CDATA[<p>웹 서비스를 만들다 보면 어느 순간 이런 말을 듣는다.</p>
<blockquote>
<p>&quot;로컬에 직접 설치하지 말고 도커로 띄우면 돼&quot;</p>
</blockquote>
<p>처음에는 Docker가 가상 머신의 가벼운 버전쯤이라고 생각했다. 명령어 몇 개로 서버를 만드는 도구인가 싶기도 했다. 막상 공부해 보니 Docker가 해결하는 핵심 문제는 서버를 새로 만드는 것보다 <strong>애플리케이션과 실행 환경을 함께 묶어 어디서든 비슷하게 실행하는 것</strong>에 가까웠다.</p>
<h2 id="docker를-왜-쓰는지부터-궁금했다">Docker를 왜 쓰는지부터 궁금했다</h2>
<p>Docker 없이도 애플리케이션은 실행할 수 있다. Java나 Node.js를 설치하고, 데이터베이스를 설치하고, 필요한 환경 변수를 넣으면 된다.</p>
<p>문제는 사람과 컴퓨터가 늘어날 때 생겼다.</p>
<ul>
<li>내 컴퓨터에서는 되는데 다른 컴퓨터에서는 실행되지 않는다.</li>
<li>같은 언어라도 런타임과 라이브러리 버전이 다르다.</li>
<li>데이터베이스 설치 방법이 운영체제마다 다르다.</li>
<li>프로젝트를 지운 뒤에도 설치한 프로그램과 설정이 남는다.</li>
<li>새로운 팀원이 같은 환경을 만드는 데 시간이 오래 걸린다.</li>
</ul>
<p>결국 소스 코드만 같다고 같은 프로그램이 되는 것은 아니었다. 실행하려면 런타임, 라이브러리, 설정 같은 주변 환경도 맞아야 했다.</p>
<p>Docker는 이 실행 환경을 <strong>이미지</strong>라는 형태로 포장하고, 그 이미지를 <strong>컨테이너</strong>로 실행한다. 공식 문서도 컨테이너를 애플리케이션의 각 구성 요소를 격리된 프로세스로 실행하는 방식으로 설명한다. <a href="https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-a-container/">Docker 공식 문서: What is a container?</a></p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/5693e673-b6c7-4939-8037-0a374f03eb57/image.png" alt="직접 설치 방식과 Docker 이미지 기반 실행 방식을 비교한 구성도"></p>
<h2 id="이미지와-컨테이너">이미지와 컨테이너??</h2>
<p>처음에는 이미지와 컨테이너를 같은 뜻으로 섞어 썼다. 둘의 관계는 프로그램 설치 파일과 실행 중인 프로그램을 떠올리니 이해하기 쉬웠다.</p>
<table>
<thead>
<tr>
<th>개념</th>
<th>쉽게 생각하면</th>
<th>실제 역할</th>
</tr>
</thead>
<tbody><tr>
<td>이미지</td>
<td>실행에 필요한 재료를 묶은 틀</td>
<td>코드, 런타임, 라이브러리, 기본 설정을 담은 읽기 전용 패키지</td>
</tr>
<tr>
<td>컨테이너</td>
<td>그 틀로 실제 실행한 프로세스</td>
<td>이미지를 기반으로 격리되어 실행되는 인스턴스</td>
</tr>
</tbody></table>
<p>같은 이미지로 여러 컨테이너를 만들 수 있다. 예를 들어 같은 API 이미지를 컨테이너 세 개로 실행할 수도 있다.</p>
<p>여기서 중요한 점은 이미지가 실행 중인 서버 그 자체는 아니라는 것이다. 이미지는 실행을 위한 설계도에 가깝고, 컨테이너가 실제 실행 단위다. Docker 공식 문서는 이미지를 변경 불가능한 계층형 패키지, 컨테이너를 이미지의 실행 가능한 인스턴스로 구분한다. <a href="https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-an-image/">Docker 공식 문서: What is an image?</a></p>
<h2 id="dockerfile은-이미지를-만드는-설명서다">Dockerfile은 이미지를 만드는 설명서다</h2>
<p>이미지를 직접 만들 때는 보통 <code>Dockerfile</code>을 작성한다. 처음에는 셸스크립트와 비슷해 보였지만 목적이 다르다. Dockerfile은 어떤 기반 이미지에서 시작하고, 무엇을 복사하고, 어떤 명령으로 애플리케이션을 실행할지 차례로 적는 파일이다.</p>
<p>예시를 들오보자면</p>
<pre><code class="language-dockerfile">FROM &lt;runtime-image&gt;

WORKDIR /app

COPY &lt;dependency-files&gt; ./
RUN &lt;install-dependencies&gt;

COPY . .

CMD [&quot;&lt;start-command&gt;&quot;]</code></pre>
<ul>
<li><code>FROM</code>: 출발점이 되는 기반 이미지를 정한다.</li>
<li><code>WORKDIR</code>: 이후 명령이 실행될 작업 폴더를 정한다.</li>
<li><code>COPY</code>: 호스트의 파일을 이미지 안으로 복사한다.</li>
<li><code>RUN</code>: 이미지를 만드는 동안 필요한 명령을 실행한다.</li>
<li><code>CMD</code>: 컨테이너가 시작될 때 실행할 기본 명령을 정한다.</li>
</ul>
<p>대략적인 흐름은 다음과 같다.</p>
<pre><code class="language-bash">docker build -t sample-api .
docker run sample-api</code></pre>
<p><code>build</code>는 Dockerfile을 읽어 이미지를 만들고, <code>run</code>은 그 이미지로 새 컨테이너를 실행한다. Dockerfile 작성법과 각 명령의 역할은 공식 가이드에서 더 자세히 확인할 수 있다. <a href="https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/">Docker 공식 문서: Writing a Dockerfile</a></p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/ca9a14ec-e5e9-4b56-906b-f0632254d6f0/image.png" alt="소스 코드와 Dockerfile이 이미지와 컨테이너로 이어지는 흐름"></p>
<h2 id="컨테이너-안에서-실행되면-바로-접속할-수-있을까">컨테이너 안에서 실행되면 바로 접속할 수 있을까</h2>
<p>API 서버가 컨테이너 안에서 <code>8080</code> 포트를 사용한다고 해도 브라우저가 자동으로 접근할 수 있는 것은 아니다. 컨테이너의 포트와 내 컴퓨터의 포트를 연결해야 한다.</p>
<pre><code class="language-bash">docker run -p 8080:8080 sample-api</code></pre>
<p><code>-p 8080:8080</code>은 다음 의미다.</p>
<pre><code class="language-text">내 컴퓨터의 8080 포트 -&gt; 컨테이너의 8080 포트</code></pre>
<p>앞 숫자는 호스트 포트, 뒤 숫자는 컨테이너 포트다. 두 숫자가 꼭 같을 필요는 없다.</p>
<pre><code class="language-bash">docker run -p 9000:8080 sample-api</code></pre>
<p>이 경우 브라우저는 <code>localhost:9000</code>으로 접속하고, Docker가 요청을 컨테이너의 <code>8080</code>으로 전달한다. 포트를 외부에 공개하면 접근 범위도 달라질 수 있으므로 개발 환경과 운영 환경의 바인딩 주소와 방화벽 정책은 따로 확인해야 한다. <a href="https://docs.docker.com/engine/network/port-publishing/">Docker 공식 문서: Port publishing</a></p>
<h2 id="컨테이너를-지우면-데이터도-사라질-수-있다">컨테이너를 지우면 데이터도 사라질 수 있다</h2>
<p>컨테이너 안에 데이터베이스 파일을 저장하고 컨테이너를 삭제해 보니 데이터까지 사라졌다. 컨테이너의 쓰기 영역은 컨테이너 수명에 묶여 있기 때문이다.</p>
<p>오래 보관해야 하는 데이터는 컨테이너 바깥에 둬야 한다. 이때 자주 쓰는 것이 <strong>볼륨</strong>이다.</p>
<pre><code class="language-bash">docker volume create sample-data

docker run \
  --mount source=sample-data,target=/var/lib/sample \
  sample-database</code></pre>
<p>볼륨은 Docker가 관리하는 저장 공간이다. 컨테이너를 새로 만들어도 같은 볼륨을 다시 연결하면 데이터를 이어서 사용할 수 있다.</p>
<p>소스 코드를 컨테이너 안에서 바로 확인하려고 호스트 폴더를 연결할 때는 <strong>바인드 마운트</strong>도 사용한다. 볼륨과 바인드 마운트는 둘 다 파일을 연결하지만 관리 주체가 다르다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>저장 위치 관리</th>
<th>주로 쓰는 곳</th>
</tr>
</thead>
<tbody><tr>
<td>볼륨</td>
<td>Docker가 관리</td>
<td>데이터베이스처럼 오래 남길 애플리케이션 데이터</td>
</tr>
<tr>
<td>바인드 마운트</td>
<td>내가 지정한 호스트 경로</td>
<td>개발 중인 소스 코드나 로컬 설정 파일</td>
</tr>
</tbody></table>
<p>볼륨이 있으면 백업, 권한, 용량, 장애 복구까지 자동으로 해결되는 것은 아니다. 데이터가 컨테이너 삭제와 분리될 뿐, 운영 정책은 별도로 필요하다. <a href="https://docs.docker.com/engine/storage/volumes/">Docker 공식 문서: Volumes</a></p>
<h2 id="컨테이너끼리는-이름으로-찾을-수-있다">컨테이너끼리는 이름으로 찾을 수 있다</h2>
<p>API와 데이터베이스를 각각 컨테이너로 실행하면 API가 DB에 어떻게 접속하는지도 궁금했다. 처음에는 DB 주소를 <code>localhost</code>로 넣었다.</p>
<p>하지만 API 컨테이너 안에서 <code>localhost</code>는 내 컴퓨터도, DB 컨테이너도 아니다. <strong>API 컨테이너 자기 자신</strong>을 가리킨다.</p>
<p>같은 Docker 네트워크에 연결된 컨테이너는 보통 서비스 이름으로 서로를 찾을 수 있다.</p>
<pre><code class="language-text">API 컨테이너 -&gt; db:5432 -&gt; DB 컨테이너</code></pre>
<p>컨테이너 IP를 직접 저장하기보다 이름을 사용하는 이유도 여기에 있다. 컨테이너가 다시 만들어지면 IP는 바뀔 수 있지만 서비스 이름은 유지할 수 있다. <a href="https://docs.docker.com/compose/how-tos/networking/">Docker 공식 문서: Compose networking</a></p>
<h2 id="컨테이너가-여러-개면-docker-compose가-편함">컨테이너가 여러 개면 Docker Compose가 편함</h2>
<p>API, DB, 객체 저장소를 매번 긴 <code>docker run</code> 명령으로 실행하면 포트, 환경 변수, 볼륨, 네트워크 옵션을 기억하기 어렵다. Docker Compose는 여러 컨테이너 구성을 YAML 파일 하나에 적고 함께 실행하게 해 준다.</p>
<pre><code class="language-yaml">services:
  api:
    build: .
    ports:
      - &quot;8080:8080&quot;
    environment:
      DATABASE_HOST: db
    depends_on:
      - db

  db:
    image: &lt;database-image&gt;
    volumes:
      - db-data:/var/lib/database

volumes:
  db-data:</code></pre>
<pre><code class="language-bash">docker compose up</code></pre>
<p>여기서 Dockerfile과 Compose 파일의 역할도 구분할 수 있었다.</p>
<table>
<thead>
<tr>
<th>파일</th>
<th>답하는 질문</th>
</tr>
</thead>
<tbody><tr>
<td>Dockerfile</td>
<td>이 애플리케이션 이미지를 어떻게 만들까?</td>
</tr>
<tr>
<td>Compose 파일</td>
<td>어떤 컨테이너들을 어떤 설정과 관계로 함께 실행할까?</td>
</tr>
</tbody></table>
<p>Compose는 개발 환경이나 단일 서버에서 여러 컨테이너를 재현하는 데 특히 편했다. 다만 여러 서버에 걸친 자동 복구, 확장, 배포까지 모두 해결하는 도구로 이해하면 곤란하다. <a href="https://docs.docker.com/compose/">Docker 공식 문서: Docker Compose</a></p>
<h2 id="docker를-쓰면-모두-같은-환경이-될까">Docker를 쓰면 모두 같은 환경이 될까..?</h2>
<p>Docker는 차이를 크게 줄여즈지만 완전히 없애지는 않는다.</p>
<ul>
<li>CPU 아키텍처가 다르면 이미지가 지원하는 플랫폼을 확인해야 한다.</li>
<li>macOS와 Windows의 Docker Desktop은 내부적으로 Linux 가상 환경을 사용한다.</li>
<li>파일 권한, 시간대, 줄바꿈, 볼륨 성능은 호스트 영향을 받을 수 있다.</li>
<li>환경 변수와 외부 서비스 주소는 실행 환경별로 달라진다.</li>
<li><code>latest</code> 같은 움직이는 태그만 쓰면 시점에 따라 다른 이미지를 받을 수 있다.</li>
</ul>
<p>즉 Docker는 실행 환경을 재현하기 쉽게 만들지만, 아무 설정 없이 모든 차이를 지워 주는 도구는 아니다.</p>
<h2 id="처음-공부하며-자주-헷갈렸던-점">처음 공부하며 자주 헷갈렸던 점</h2>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/f5e3b2e8-794a-40f8-8f44-c4172cb85c8d/image.png" alt=""></p>
<h3 id="컨테이너는-작은-가상-머신인가">컨테이너는 작은 가상 머신인가</h3>
<p>사용하는 느낌은 비슷한 부분이 있지만 구조는 다르다. 가상 머신은 보통 게스트 운영체제 전체를 실행한다. 컨테이너는 호스트 커널을 공유하면서 격리된 프로세스로 실행된다.</p>
<p>이 차이 덕분에 컨테이너는 비교적 빠르게 만들고 지울 수 있다. 반대로 컨테이너마다 완전히 독립된 커널을 갖는다고 생각하면 안 된다.</p>
<h3 id="이미지-안에-비밀번호를-넣어도-될까">이미지 안에 비밀번호를 넣어도 될까</h3>
<p>이미지는 레지스트리나 캐시에 남고 다른 사람에게 전달될 수 있다. 비밀번호, API 키, 인증서 같은 값은 Dockerfile에 직접 적거나 이미지 레이어에 복사하지 않는 편이 안전하다.</p>
<p>실행 시점의 환경 변수나 비밀 관리 기능을 사용해야 한다. 환경 변수를 쓰는 것만으로 모든 노출 위험이 사라지는 것도 아니므로 로그, 프로세스 정보, 배포 도구의 저장 방식도 확인해야 한다.</p>
<h3 id="depends_on이면-db가-준비될-때까지-기다릴까"><code>depends_on</code>이면 DB가 준비될 때까지 기다릴까</h3>
<p>컨테이너가 먼저 시작됐다는 것과 애플리케이션이 요청을 받을 준비가 됐다는 것은 다르다. DB 프로세스가 시작되어도 초기화가 끝나지 않았을 수 있다.</p>
<p>헬스 체크와 애플리케이션 재시도 로직을 함께 고려해야 한다.</p>
<h2 id="마무우리">마무우리</h2>
<p>Docker를 처음에는 서버를 간단히 만드는 도구라고 생각했다. 공부하고 나니 핵심은 애플리케이션과 실행에 필요한 환경을 이미지로 묶고, 이를 격리된 컨테이너로 반복 실행하는 데 있었다.</p>
<p>이미지와 컨테이너의 차이, 포트와 볼륨, 컨테이너 네트워크까지 이해하니 Compose 파일도 단순한 설정 모음이 아니라 실행 환경을 설명하는 문서처럼 보이기 시작했다.</p>
<p>그다음 궁금증은 자연스럽게 이어졌다. 컨테이너가 몇 개가 아니라 수십 개가 되고, 여러 서버에 흩어져 있으며, 하나가 죽었을 때 자동으로 다시 띄워야 한다면 누가 관리할까? 다음 글에서는 이 문제를 해결하기 위해 등장한 Kubernetes를 처음부터 정리해 보려고 한다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://docs.docker.com/get-started/docker-overview/">Docker Overview</a></li>
<li><a href="https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-a-container/">What is a container?</a></li>
<li><a href="https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-an-image/">What is an image?</a></li>
<li><a href="https://docs.docker.com/compose/">Docker Compose</a></li>
<li><a href="https://docs.docker.com/engine/storage/volumes/">Volumes</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[QGIS 오류를 원인부터 좁혀 본 기록: 인코딩, 필드 충돌, invalid geometry]]></title>
            <link>https://velog.io/@yujin_1219/qgis-%EC%98%A4%EB%A5%98%EB%A5%BC-%EC%9B%90%EC%9D%B8%EB%B6%80%ED%84%B0-%EC%A2%81%ED%98%80-%EB%B3%B8-%EA%B8%B0%EB%A1%9D-%EC%9D%B8%EC%BD%94%EB%94%A9-%ED%95%84%EB%93%9C-%EC%B6%A9%EB%8F%8C-invalid-geometry</link>
            <guid>https://velog.io/@yujin_1219/qgis-%EC%98%A4%EB%A5%98%EB%A5%BC-%EC%9B%90%EC%9D%B8%EB%B6%80%ED%84%B0-%EC%A2%81%ED%98%80-%EB%B3%B8-%EA%B8%B0%EB%A1%9D-%EC%9D%B8%EC%BD%94%EB%94%A9-%ED%95%84%EB%93%9C-%EC%B6%A9%EB%8F%8C-invalid-geometry</guid>
            <pubDate>Tue, 13 Oct 2026 23:55:21 GMT</pubDate>
            <description><![CDATA[<p>QGIS 실습 중에는 오류 문구가 원인보다 결과만 알려 주는 경우가 많았다. <code>교차 영역 분석 실패</code>라고 떠도 실제 원인은 특정 폴리곤 하나의 자기 교차일 수 있었고, 필드를 추가할 수 없다는 메시지는 편집 모드나 Shapefile 형식 제한에서 시작되기도 했다.</p>
<p>그래서 오류를 만날 때마다 <strong>증상 → 확인할 것 → 해결 → 다시 검증할 것</strong> 순서로 기록했다.</p>
<h2 id="빠르게-보는-오류-표">빠르게 보는 오류 표</h2>
<table>
<thead>
<tr>
<th>증상</th>
<th>먼저 확인할 것</th>
<th>처리 방향</th>
</tr>
</thead>
<tbody><tr>
<td>한글이 깨짐</td>
<td>속성값도 깨졌는지, 라벨만 깨졌는지</td>
<td>인코딩과 폰트를 분리해 확인</td>
</tr>
<tr>
<td>새 필드 추가 실패</td>
<td>편집 가능 여부, 파일 권한, 데이터 형식</td>
<td>복사본 또는 GeoPackage로 내보내 재시도</td>
</tr>
<tr>
<td>필드가 이미 존재함</td>
<td>필드명, 잘린 이름, 타입</td>
<td>기존 필드 갱신 또는 명확한 새 이름 사용</td>
</tr>
<tr>
<td>Intersection 결과가 비어 있음</td>
<td>입력 레이어, CRS, 공간 범위</td>
<td>각 입력을 지도에서 따로 확인</td>
</tr>
<tr>
<td>GEOS / invalid geometry</td>
<td>유효성 검사 결과</td>
<td>복사본에 Fix Geometries 실행</td>
</tr>
<tr>
<td>편집 저장 실패</td>
<td>변경 내역과 제공자 오류</td>
<td>원인 수정 후 저장하거나 변경 버리기</td>
</tr>
</tbody></table>
<h2 id="한글이-깨질-때-인코딩과-폰트를-분리한다">한글이 깨질 때: 인코딩과 폰트를 분리한다</h2>
<p>속성 테이블에서도 깨진다면 원본 문자열을 잘못 해석한 가능성이 크다. Shapefile은 속성을 <code>.dbf</code>에 보관하며, <code>.cpg</code>나 DBF 코드 페이지 정보가 정확하지 않으면 QGIS/GDAL이 다른 인코딩으로 읽을 수 있다.</p>
<p>GDAL 문서에 따르면 Shapefile 드라이버는 <code>.cpg</code>를 우선 보고, 없으면 DBF의 LDID 정보를 참고해 UTF-8로 변환한다. 이 정보가 없거나 실제 인코딩과 다르면 사용자가 해석을 덮어써야 할 수 있다. <a href="https://gdal.org/en/stable/drivers/vector/shapefile.html#encoding">GDAL Shapefile 인코딩 문서</a></p>
<p>내가 정한 확인 순서는 다음과 같다.</p>
<ol>
<li>원본 파일 묶음에 <code>.cpg</code>가 있는지 확인한다.</li>
<li>데이터 제공 문서에 인코딩이 적혀 있는지 확인한다.</li>
<li>원본의 복사본을 후보 인코딩으로 열어 비교한다.</li>
<li>정상적으로 읽힌 결과를 UTF-8 기반 GeoPackage 등으로 새로 저장한다.</li>
</ol>
<p>라벨만 깨진다면 파일을 다시 저장하기 전에 폰트부터 바꿔 본다. 한글 글리프가 없는 폰트는 속성값이 정상이어도 네모로 표시할 수 있다.</p>
<h2 id="제공자에-새-필드를-추가할-수-없습니다">“제공자에 새 필드를 추가할 수 없습니다”</h2>
<p>이 메시지만으로 원인을 하나로 정할 수는 없다. 다음을 순서대로 확인했다.</p>
<ul>
<li>레이어가 편집 가능한 데이터 소스인가?</li>
<li>파일과 폴더에 쓰기 권한이 있는가?</li>
<li>다른 프로그램이 파일을 잠그고 있지는 않은가?</li>
<li>조인 또는 가상 레이어처럼 직접 필드를 추가할 수 없는 상태인가?</li>
<li>선택한 필드 타입을 현재 형식이 지원하는가?</li>
</ul>
<p>급하다고 원본 파일의 권한과 구조를 계속 바꾸기보다, 먼저 새 GeoPackage로 내보낸 복사본에서 편집해 보는 방법이 안전했다.</p>
<h2 id="area-필드가-이미-존재한다는-오류"><code>AREA</code> 필드가 이미 존재한다는 오류</h2>
<p>Shapefile의 속성 필드는 DBF 제약을 받는다. GDAL 문서에서는 필드명이 기본적으로 10자로 제한되며, 중복되거나 10자로 잘린 이름이 충돌할 때 고유한 이름으로 조정된다고 설명한다. 대소문자만 다른 이름도 드라이버에서 충돌하거나 예상과 다르게 바뀔 수 있다. <a href="https://gdal.org/en/stable/drivers/vector/shapefile.html#field-sizes">GDAL Shapefile 필드 제한 문서</a></p>
<p>실습에서는 이미 <code>area</code> 필드가 있는데 <code>AREA</code>를 새로 만들려고 해서 문제가 생겼다. 해결할 때는 다음 중 하나를 선택한다.</p>
<ul>
<li>기존 필드의 타입이 목적에 맞으면 <strong>기존 필드 갱신</strong>을 사용한다.</li>
<li>다른 값이어야 한다면 10자 이내에서도 의미가 구분되는 이름을 쓴다.</li>
<li>장기적으로 필드명과 타입 제약이 불편하면 GeoPackage 같은 형식으로 옮긴다.</li>
</ul>
<p>필드 계산기는 새 필드를 만들 수도 있고 기존 필드를 갱신할 수도 있다. 실행 전 대상 범위가 전체인지 선택 피처만인지도 확인해야 한다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/working_with_vector/attribute_table.html#using-the-field-calculator">QGIS 필드 계산기 문서</a></p>
<h2 id="intersection이-비어-있을-때">Intersection이 비어 있을 때</h2>
<p>빈 결과가 나왔다고 곧바로 도구 오류로 판단하지 않는다.</p>
<ol>
<li>입력 레이어를 하나씩 켜서 실제로 겹치는지 본다.</li>
<li>각 레이어의 CRS가 올바르게 <strong>정의</strong>되어 있는지 확인한다.</li>
<li>버퍼 거리가 현재 레이어 단위와 맞는지 확인한다.</li>
<li>필터 때문에 일부 피처가 숨겨지지 않았는지 본다.</li>
<li>임시 결과가 아닌 의도한 중간 레이어를 골랐는지 확인한다.</li>
</ol>
<p>CRS를 잘못 지정한 데이터는 QGIS의 즉석 변환 덕분에 얼핏 맞아 보이기도 한다. 좌표값 범위와 레이어 메타데이터까지 확인해야 한다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/working_with_projections/working_with_projections.html">QGIS 좌표계 문서</a></p>
<p>또한 Intersection이 반드시 버퍼만 받는 것은 아니다. 이번 실습에서 버퍼들만 골랐던 이유는 여러 거리 조건의 공통 영역이 필요했기 때문이다.</p>
<h2 id="geos-오류와-무결하지-않은-지오메트리">GEOS 오류와 “무결하지 않은 지오메트리”</h2>
<p>폴리곤의 선이 자기 자신과 교차하거나, 링 구성이 잘못되거나, 같은 꼭짓점이 비정상적으로 반복되면 일부 공간 연산이 실패할 수 있다.</p>
<p>처음에는 메뉴에서 <code>Fix Geometries</code>를 찾지 못했다. Geometry Tools 메뉴에 항상 보이는 도구라고 생각했기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/ad703ac5-a6ab-452f-88cb-17a702992ddc/image.png" alt="메뉴에서 Fix Geometries를 찾다가 처리 도구상자를 확인한 화면"></p>
<p>버전과 화면 구성에 따라 메뉴 위치가 달라질 수 있으므로 <strong>처리 도구상자(Processing Toolbox)</strong> 검색창에서 <code>Fix geometries</code>를 찾는 편이 확실했다.</p>
<p>내가 사용한 처리 순서는 이렇다.</p>
<ol>
<li>가능하면 <code>Check validity</code>로 어떤 피처와 위치가 잘못됐는지 확인한다.</li>
<li>원본을 보존하고 복사본에 <code>Fix geometries</code>를 실행한다.</li>
<li>수정 결과의 피처 수, 지오메트리 유형, 면적 합계를 원본과 비교한다.</li>
<li>문제가 된 위치를 확대해 모양이 의도대로 복구됐는지 본다.</li>
<li>수정 결과로 Intersection을 다시 실행한다.</li>
</ol>
<p>QGIS의 <code>Fix geometries</code>는 유효한 표현을 만들려고 시도하며, 이미 유효한 피처는 그대로 반환한다. 결과는 멀티 지오메트리 레이어가 되고 M 값이 제거될 수 있으므로 “오류가 사라졌다”만 보고 원본을 덮어쓰면 안 된다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/vectorgeometry.html#fix-geometries">QGIS Fix geometries 문서</a></p>
<h2 id="저장이-실패해-편집-모드를-끌-수-없을-때">저장이 실패해 편집 모드를 끌 수 없을 때</h2>
<p>저장 실패 상태에서 편집 모드를 끄려고 하면 저장·취소 여부를 묻는다. 이때 <code>버리기(Discard)</code>는 오류를 고치는 기능이 아니라 <strong>이번 편집 세션의 변경을 포기하는 선택</strong>이다.</p>
<p>따라서 먼저 해야 할 일은 변경이 필요한지 판단하는 것이다.</p>
<ul>
<li>변경을 살려야 한다면 오류 메시지를 복사하고, 가능한 경우 임시 레이어나 다른 형식으로 내보낸다.</li>
<li>잘못 추가한 필드처럼 되돌려도 되는 변경이라면 버리기로 편집을 종료한다.</li>
<li>중요한 원본이라면 파일 복사본을 만든 뒤 복구를 시도한다.</li>
</ul>
<p>“저장이 안 되면 버리기를 누른다”로 외우면 필요한 편집까지 잃을 수 있다.</p>
<h2 id="최단-경로-결과의-cost가-이상할-때">최단 경로 결과의 <code>cost</code>가 이상할 때</h2>
<p>네트워크 분석에서는 도구가 실행됐다는 사실보다 네트워크가 올바른지가 중요했다.</p>
<ul>
<li>선들이 교차해 보이기만 하고 실제 노드를 공유하지 않을 수 있다.</li>
<li>일방통행 방향 필드가 반대로 지정될 수 있다.</li>
<li>속도 필드 단위가 설정과 다를 수 있다.</li>
<li>시작점이나 도착점이 도로에서 멀어도 자동으로 가까운 선에 스냅될 수 있다.</li>
<li>Centroid가 폴리곤 밖에 만들어질 수 있다.</li>
</ul>
<p><code>Shortest path (point to layer)</code>는 <code>Shortest</code>와 <code>Fastest</code> 전략을 제공하고, 점과 네트워크의 최대 거리 및 위상 허용오차도 설정할 수 있다. <code>cost</code>는 이 설정을 함께 보고 해석해야 한다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/networkanalysis.html#shortest-path-point-to-layer">QGIS 네트워크 분석 문서</a></p>
<h2 id="내가-만든-qgis-오류-점검-순서">내가 만든 QGIS 오류 점검 순서</h2>
<p>앞으로는 오류 문구를 검색하기 전에 다음 순서로 확인하려고 한다.</p>
<pre><code class="language-text">1. 활성 레이어와 입력 레이어가 맞는가?
2. 공간 범위와 CRS가 맞는가?
3. 단위와 데이터 타입이 맞는가?
4. 지오메트리가 유효한가?
5. 파일 형식의 제약에 걸리지 않았는가?
6. 임시 결과와 원본을 혼동하지 않았는가?
7. 결과 건수·범위·면적이 상식적인가?</code></pre>
<p>QGIS 트러블슈팅에서 가장 크게 배운 점은 오류 메시지 하나에 원인을 바로 연결하지 않는 것이었다. 입력, 좌표계, 형식, 지오메트리를 차례로 분리해 확인하면 “안 된다”를 재현 가능한 문제로 바꿀 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[QGIS에서 자꾸 막혔던 것: 피처 선택, 한글 깨짐, 버퍼와 교차 영역]]></title>
            <link>https://velog.io/@yujin_1219/qgis%EC%97%90%EC%84%9C-%EC%9E%90%EA%BE%B8-%EB%A7%89%ED%98%94%EB%8D%98-9%EA%B0%80%EC%A7%80-%ED%94%BC%EC%B2%98-%EC%84%A0%ED%83%9D-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%EB%B2%84%ED%8D%BC%EC%99%80-%EA%B5%90%EC%B0%A8-%EC%98%81%EC%97%AD</link>
            <guid>https://velog.io/@yujin_1219/qgis%EC%97%90%EC%84%9C-%EC%9E%90%EA%BE%B8-%EB%A7%89%ED%98%94%EB%8D%98-9%EA%B0%80%EC%A7%80-%ED%94%BC%EC%B2%98-%EC%84%A0%ED%83%9D-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%EB%B2%84%ED%8D%BC%EC%99%80-%EA%B5%90%EC%B0%A8-%EC%98%81%EC%97%AD</guid>
            <pubDate>Tue, 13 Oct 2026 23:54:54 GMT</pubDate>
            <description><![CDATA[<p>QGIS 실습을 하면서 가장 많이 한 말은 “왜 아무것도 안 되지?!!??”였다.
<img src="https://velog.velcdn.com/images/yujin_1219/post/e2de686a-a2f8-4119-b539-df8a910b7014/image.png" alt="">
나중에 보니 큰 오류보다 현재 활성 레이어, 선택 도구, 인코딩처럼 작은 상태를 놓친 경우가 많았다.</p>
<p>이번 글에서는 실제로 헷갈렸던 지점을 질문과 답 형태로 다시 정리했다. 화면 이름은 QGIS 버전과 언어 설정에 따라 조금 다를 수 있다.</p>
<h2 id="1-속성-테이블에-폴리곤-열이-왜-없을까">1. 속성 테이블에 폴리곤 열이 왜 없을까?</h2>
<p>처음에는 속성 테이블 안에 좌표나 폴리곤이 그대로 들어 있는 열이 있을 거라고 생각했다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/cf89d470-d4a8-44c3-977c-0190a30343f1/image.png" alt="건물 레이어의 속성 테이블을 확인한 화면"></p>
<p>벡터 레이어의 각 피처는 <strong>지오메트리와 속성값을 함께 가진다.</strong> 속성 테이블은 주로 속성 부분을 행과 열로 보여 주기 때문에 화면에 <code>geometry</code> 열이 항상 보이는 것은 아니다. 한 행을 선택하면 같은 피처의 도형이 지도에서 선택되는 이유가 여기에 있다.</p>
<p>외부 테이블을 공간 레이어와 연결할 때는 건물관리번호 같은 공통 식별자를 조인 키로 사용할 수 있다. 다만 식별자와 지오메트리는 역할이 다르다. 식별자는 대상을 연결하고, 실제 공간 연산은 레이어가 가진 지오메트리로 수행한다.</p>
<h2 id="2-피처-선택과-표현식으로-선택은-무엇이-다를까">2. “피처 선택”과 “표현식으로 선택”은 무엇이 다를까?</h2>
<p>아래처럼 속성 테이블 툴바만 보고 지도에서 클릭하는 선택 도구를 찾으려니 계속 헷갈렸다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/c3966d92-01e1-49f4-838e-e3e89e8f3e7d/image.png" alt="속성 테이블의 선택 및 필터 도구를 확인한 화면"></p>
<ul>
<li>지도 캔버스의 <strong>피처 선택 도구</strong>는 화면의 도형을 직접 클릭하거나 영역으로 드래그해 선택한다.</li>
<li><strong>표현식으로 선택</strong>은 <code>용도 = &#39;아파트&#39;</code>처럼 속성과 함수를 이용해 조건에 맞는 피처를 선택한다.</li>
<li>필터는 표시하거나 조회할 대상을 제한하는 기능이라 선택과 결과가 다를 수 있다.</li>
</ul>
<p>속성 테이블의 한 행이 한 피처를 나타내며 지도 선택과 표 선택이 연결된다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/working_with_vector/attribute_table.html">QGIS 속성 테이블 문서</a></p>
<h2 id="3-도형을-클릭해도-선택되지-않는다면">3. 도형을 클릭해도 선택되지 않는다면?</h2>
<p>다음 순서로 확인하면 원인을 빨리 줄일 수 있었다.</p>
<ol>
<li>레이어 패널에서 선택하려는 레이어가 활성화되어 있는가?</li>
<li>현재 도구가 이동 도구가 아니라 피처 선택 도구인가?</li>
<li>축척 때문에 너무 얇게 보이는 선이나 작은 도형을 정확히 클릭했는가?</li>
<li>선택 대상이 잠겨 있거나 선택 불가능한 레이어는 아닌가?</li>
<li>다른 레이어를 활성화한 채 클릭하고 있지는 않은가?</li>
</ol>
<p>“위 레이어가 클릭을 가로챈다”라고 외우기보다 <strong>현재 어떤 레이어를 대상으로 선택하는지</strong>를 먼저 보는 편이 정확했다.</p>
<h2 id="4-한글이-□□처럼-깨지는-이유는">4. 한글이 <code>□?□</code>처럼 깨지는 이유는?</h2>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/d18392b9-e4df-4c20-a592-154491cb711b/image.png" alt="한글 라벨이 깨져 보이는 지도 화면"></p>
<p>화면 증상은 비슷하지만 원인은 크게 두 갈래였다.</p>
<h3 id="속성-테이블의-한글부터-깨진-경우">속성 테이블의 한글부터 깨진 경우</h3>
<p>Shapefile의 문자열은 <code>.dbf</code>에 저장된다. GDAL은 <code>.cpg</code> 파일 또는 DBF 헤더의 코드 페이지 정보를 이용해 인코딩을 판단하지만, 정보가 없거나 잘못되면 오인식할 수 있다. 국내의 오래된 데이터는 CP949나 EUC-KR로 작성된 경우도 있다. <a href="https://gdal.org/en/stable/drivers/vector/shapefile.html#encoding">GDAL Shapefile 인코딩 문서</a></p>
<p>이때는 무조건 EUC-KR로 바꾸기보다 다음처럼 확인한다.</p>
<ol>
<li>원본과 함께 <code>.cpg</code> 파일이 있는지 본다.</li>
<li>QGIS의 <code>Extract Shapefile encoding</code> 결과나 데이터 제공자의 메타데이터를 확인한다.</li>
<li>복사본에서 후보 인코딩으로 열어 실제 문자열을 비교한다.</li>
<li>정상적으로 읽힌 뒤 GeoPackage 또는 UTF-8을 명시한 새 파일로 내보낸다.</li>
</ol>
<h3 id="속성값은-정상인데-라벨만-깨진-경우">속성값은 정상인데 라벨만 깨진 경우</h3>
<p>이 경우에는 라벨에 사용한 폰트가 한글 글리프를 지원하지 않는지 확인한다. 인코딩과 폰트를 같은 문제로 보고 파일을 계속 변환하면 오히려 원본을 망가뜨릴 수 있다.</p>
<h2 id="5-표현식을-그대로-입력했는데-결과가-없는-이유는">5. 표현식을 그대로 입력했는데 결과가 없는 이유는?</h2>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/166d3b56-2710-4eb8-a985-a0d874fd745b/image.png" alt="특정 지하철역을 찾기 위한 표현식 지시문"></p>
<p>표현식이 비어 있는 결과를 만들 때는 다음 항목을 하나씩 확인했다.</p>
<ul>
<li>필드명을 문자열 값처럼 따옴표로 감싸지 않았는가?</li>
<li>문자열 따옴표가 일반 작은따옴표인지, 문서 편집기가 바꾼 곡선 따옴표인지?</li>
<li>실제 값 앞뒤에 공백이 들어 있지 않은가?</li>
<li>대소문자나 한글 인코딩이 달라지지 않았는가?</li>
<li>NULL과 빈 문자열을 같은 것으로 생각하지 않았는가?</li>
</ul>
<p>긴 조건을 한 번에 입력하기보다 한 조건으로 몇 건이 선택되는지 확인한 뒤 <code>AND</code> 조건을 추가하는 방식이 디버깅하기 쉬웠다. QGIS 표현식은 필드 계산기, 선택, 라벨과 스타일 등 여러 곳에서 공통으로 사용된다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/working_with_vector/expression.html">QGIS 표현식 문서</a></p>
<h2 id="6-버퍼-두-개를-한-번에-만들-수-있을까">6. 버퍼 두 개를 한 번에 만들 수 있을까?</h2>
<p>기본 Buffer 알고리즘의 입력 레이어는 하나다. 지하철역 500m와 기준 건물 3km처럼 입력도 거리도 다르면 각각 실행한다.</p>
<p>한 번 만든 버퍼의 거리를 바꾸고 싶을 때는 결과 폴리곤을 손으로 밀기보다 원본 레이어에서 알고리즘을 다시 실행한다. 처리 이력을 모델이나 로그로 남기면 나중에 같은 결과를 재현하기도 쉽다.</p>
<h2 id="7-intersection-결과가-비거나-이상한-이유는">7. Intersection 결과가 비거나 이상한 이유는?</h2>
<p>처음에는 건물 원본을 <code>Intersection (multiple)</code>의 입력 레이어로 넣었다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/44904c81-5c3e-44f1-ba5e-bd3d6e72beeb/image.png" alt="교차 영역 다중 도구에서 원본 건물 레이어를 입력한 화면"></p>
<p>도구가 틀렸다기보다 내가 풀려는 질문과 입력이 달랐다. 목표가 “도로 50m, 역 500m, 기준점 3km를 모두 만족하는 <strong>영역</strong>”이라면 세 조건을 영역으로 표현한 버퍼가 입력되어야 한다. 원본 건물을 먼저 넣으면 “그 조건 영역들과 겹치는 건물 조각”이라는 다른 결과가 된다.</p>
<p>중첩 레이어 목록에서도 원본 건물과 행정구역까지 모두 선택해 버렸다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/e7702b5c-4bc6-4614-ab02-1e2994b76ed9/image.png" alt="Intersection 중첩 레이어에 불필요한 원본 레이어까지 선택한 화면"></p>
<p>이때는 이름이 비슷한 레이어를 모두 체크하지 말고, 각 입력이 어떤 공간 조건을 나타내는지 적어 보는 게 도움이 됐다.</p>
<pre><code class="language-text">도로 접근 조건  -&gt; 도로 버퍼
대중교통 조건   -&gt; 지하철역 버퍼
기준점 거리 조건 -&gt; 기준점 버퍼</code></pre>
<p>교차 결과는 입력 레이어들의 공통 부분과 속성을 가진 새 지오메트리를 만든다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/vectoroverlay.html#intersection">QGIS Intersection 문서</a></p>
<h2 id="8-intersection과-extract-by-location-중-무엇을-써야-할까">8. Intersection과 Extract by Location 중 무엇을 써야 할까?</h2>
<p>원하는 결과를 문장으로 쓰면 선택하기 쉽다.</p>
<ul>
<li>“겹친 <strong>부분</strong>을 잘라 새 도형으로 만들고 싶다” → Intersection</li>
<li>“조건을 만족하는 <strong>원본 건물 전체</strong>를 가져오고 싶다” → Extract by Location</li>
</ul>
<p>이번 실습에서는 버퍼들의 공통 영역을 Intersection으로 만든 뒤, 그 영역과 교차하는 원본 건물을 Extract by Location으로 추출했다. 두 단계를 한 도구로 해결하려고 하면 결과 지오메트리가 예상과 달라졌다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/vectorselection.html#extract-by-location">QGIS Extract by Location 문서</a></p>
<h2 id="9-레이어-스타일-수치는-어디에-넣을까">9. 레이어 스타일 수치는 어디에 넣을까?</h2>
<p>실습 자료에는 색상과 투명도가 숫자로 주어졌는데, 심볼 색상과 레이어 전체 불투명도를 섞어서 적용하기 쉬웠다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/dc2a292d-0d59-41a5-b1c8-374e407fbca7/image.png" alt="건물 레이어의 색상과 불투명도 설정 지시 화면"></p>
<ul>
<li><strong>심볼 색상/채우기 불투명도</strong>는 개별 심볼의 표현을 바꾼다.</li>
<li><strong>레이어 불투명도</strong>는 라벨이나 여러 심볼 레이어를 포함한 최종 레이어 표현 전체에 영향을 줄 수 있다.</li>
<li>RGB와 HSV는 같은 색을 서로 다른 방식으로 지정한다. 둘을 동시에 따로 적용하는 옵션이 아니다.</li>
</ul>
<p>분석 결과가 맞는지 확인할 때는 장식적인 스타일보다 입력·중간·결과 레이어가 구분되는 단순한 색과 투명도가 더 유용했다.</p>
<h2 id="마무리">마무리</h2>
<p>QGIS에서 막혔을 때는 같은 버튼을 반복해서 누르기보다 현재 상태를 문장으로 적는 편이 빨랐다.</p>
<blockquote>
<p>지금 활성 레이어는 무엇이고, 어떤 피처를 어떤 조건으로 선택하거나 변환하려는가?</p>
</blockquote>
<p>이 질문에 답하지 못하면 도구 이름을 알아도 입력을 잘못 고르기 쉽다. 반대로 목적과 입력·출력을 말로 설명할 수 있으면 메뉴 위치가 조금 달라져도 다시 찾을 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[QGIS 실습 전에 알았으면 좋았을 용어들: 버퍼부터 최단 경로까지]]></title>
            <link>https://velog.io/@yujin_1219/qgis-%EC%8B%A4%EC%8A%B5-%EC%A0%84%EC%97%90-%EC%95%8C%EC%95%98%EC%9C%BC%EB%A9%B4-%EC%A2%8B%EC%95%98%EC%9D%84-%EC%9A%A9%EC%96%B4%EB%93%A4-%EB%B2%84%ED%8D%BC%EB%B6%80%ED%84%B0-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@yujin_1219/qgis-%EC%8B%A4%EC%8A%B5-%EC%A0%84%EC%97%90-%EC%95%8C%EC%95%98%EC%9C%BC%EB%A9%B4-%EC%A2%8B%EC%95%98%EC%9D%84-%EC%9A%A9%EC%96%B4%EB%93%A4-%EB%B2%84%ED%8D%BC%EB%B6%80%ED%84%B0-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Tue, 13 Oct 2026 23:51:49 GMT</pubDate>
            <description><![CDATA[<p>QGIS를 처음 켰을 때 가장 힘들었던 건 버튼 위치보다 용어였다. 버퍼, 디졸브, 교차 영역처럼 이름만 봐서는 결과를 상상하기 어려운 도구가 계속 나왔다. 설명을 하나씩 외우면 금방 섞였고, 실제 작업 흐름 안에 놓고 보니 그제야 차이가 보였다.
<img src="https://velog.velcdn.com/images/yujin_1219/post/a6d5e2f0-b800-4f29-8bb0-9956af8dfd9d/image.png" alt=""></p>
<p>이번 글은 건물·도로·지하철역 데이터를 이용해 조건에 맞는 후보지를 찾는 실습을 하면서 새로 배운 용어를 정리한 기록이다.</p>
<h2 id="전체-흐름">전체 흐름</h2>
<p>목표는 여러 공간 조건을 만족하는 건물을 추린 뒤, 도로망을 따라 이동 거리가 짧은 후보를 찾는 것이었다.</p>
<ol>
<li>건물, 도로, 행정구역 레이어를 불러온다.</li>
<li>프로젝트와 레이어의 좌표계를 확인한다.</li>
<li>속성 조건으로 필요한 건물과 지하철역만 추출한다.</li>
<li>도로·역·기준 지점 주변에 각각 버퍼를 만든다.</li>
<li>버퍼가 모두 겹치는 영역을 구한다.</li>
<li>그 영역과 공간적으로 만나는 건물만 추린다.</li>
<li>면적 조건을 적용한다.</li>
<li>건물의 대표점을 만든 뒤 도로망 최단 경로를 계산한다.</li>
</ol>
<p>단계를 먼저 보고 용어를 붙이면 <code>버퍼는 범위를 만드는 도구</code>, <code>Intersection은 범위를 겹치는 도구</code>처럼 역할이 분명해진다.</p>
<h2 id="벡터-타일과-타일맵">벡터 타일과 타일맵</h2>
<p>웹 지도는 세계 전체를 한 장의 거대한 이미지로 내려받지 않는다. 화면에 필요한 구역을 정사각형 타일 단위로 요청한다.</p>
<ul>
<li><strong>래스터 타일</strong>은 완성된 지도 이미지를 조각내 전송한다.</li>
<li><strong>벡터 타일</strong>은 점·선·면과 속성 정보를 타일 단위로 전송하고 클라이언트가 스타일을 적용해 그린다.</li>
<li><strong>타일맵</strong>은 줌과 행·열 규칙에 따라 이 타일들을 요청하고 이어 붙이는 방식 또는 서비스다.</li>
</ul>
<p>벡터 타일은 화면에서 다시 그리므로 일반적으로 확대했을 때 글자와 선이 선명하다. 그렇다고 원본 데이터가 무한히 정밀한 것은 아니다. 줌 단계별로 데이터가 단순화되거나 생략될 수 있다.</p>
<p>QGIS에서는 데이터 소스 관리자나 브라우저 패널에서 XYZ 타일 서비스를 연결할 수 있다. 다만 서비스 URL을 안다고 해서 아무 배경지도나 자유롭게 재사용할 수 있는 것은 아니다. 제공자의 이용 조건과 표시 의무를 먼저 확인해야 한다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/working_with_vector/supported_data.html">QGIS의 XYZ·벡터 데이터 불러오기 문서</a></p>
<p>처음 브라우저 패널을 열었을 때 원하는 배경지도가 기본 목록에 없어서 프로그램이 잘못 설치된 줄 알았다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/755a67c9-d5ce-4a8f-8717-8bb9648e80c3/image.png" alt="XYZ Tiles 목록에 원하는 배경지도가 등록되어 있지 않은 화면"></p>
<p>하지만 기본 등록 여부와 QGIS의 지원 여부는 다른 문제였다. 새 연결을 만들 수 있는 입력창은 다음처럼 별도로 열렸다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/1facb13a-e4e9-4a90-98ff-9c7d59f8a3a1/image.png" alt="QGIS에서 XYZ 타일 연결 정보를 입력하는 창"></p>
<h2 id="투영-타원체-crs">투영, 타원체, CRS</h2>
<p>지구는 둥글지만 화면과 종이는 평평하다. <strong>투영(Projection)</strong>은 지구 위 위치를 평면 좌표로 펼치는 규칙이다. 귤껍질을 찢지 않고 책상 위에 완전히 펴기 어려운 것처럼, 어떤 투영도 거리·면적·각도·방향을 모두 완벽하게 보존하지는 못한다.</p>
<p><strong>타원체(Ellipsoid)</strong>는 울퉁불퉁한 지구를 계산 가능한 회전타원체로 단순화한 모델이다. GRS 1980과 WGS 84는 수치가 매우 가깝지만 동일한 정의는 아니다. 대부분의 일반 지도 작업에서는 차이가 매우 작더라도, 정밀 측량에서는 둘을 같은 것으로 단정하면 안 된다.</p>
<p><strong>CRS(Coordinate Reference System)</strong>는 타원체, 기준면, 좌표축과 투영법 등을 묶어 좌표를 해석하는 규칙이다. <code>EPSG:5179</code>는 Korea 2000 / Unified CS라는 국내 투영좌표계다. 단순히 “한국 중부원점 좌표계”라고 부르기보다, 작업 지역과 데이터의 공식 메타데이터를 보고 알맞은 CRS를 선택하는 편이 안전하다.</p>
<p>QGIS는 서로 다른 CRS의 레이어를 화면에서 즉석 변환해 겹쳐 보여 줄 수 있다. 그래서 화면이 맞아 보인다는 사실만으로 원본 CRS가 같다고 판단하면 안 된다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/working_with_projections/working_with_projections.html">QGIS 좌표계와 즉석 변환 문서</a></p>
<h2 id="직교좌표-거리와-타원체-거리">직교좌표 거리와 타원체 거리</h2>
<p>측정 도구에서 <code>Cartesian</code>과 <code>Ellipsoidal</code>을 보고 처음에는 단순히 빠른 방식과 정확한 방식의 차이라고 생각했다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/19ca7302-4465-4105-b1e9-fe6110b3ce09/image.png" alt="QGIS 측정 도구에서 직교좌표와 타원체 계산을 확인한 화면"></p>
<p>조금 더 정확히 말하면 다음과 같다.</p>
<ul>
<li><strong>직교좌표 계산</strong>은 현재 좌표 공간을 평면으로 보고 피타고라스 방식으로 길이와 면적을 계산한다.</li>
<li><strong>타원체 계산</strong>은 프로젝트에 설정한 타원체를 사용해 지구 곡률을 반영한다.</li>
</ul>
<p>어느 쪽이 맞는지는 작업 목적과 CRS에 달려 있다. 좁은 지역을 그 지역에 맞는 투영좌표계로 분석한다면 평면 거리도 실용적이다. 넓은 범위나 지표면 거리가 중요하면 타원체 설정을 확인해야 한다. QGIS의 거리·면적 계산은 프로젝트의 타원체와 단위 설정의 영향을 받는다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/introduction/qgis_configuration.html">QGIS 측정 설정 문서</a></p>
<h2 id="버퍼와-디졸브">버퍼와 디졸브</h2>
<p><strong>버퍼(Buffer)</strong>는 점·선·면 주변에 지정한 거리만큼 영역을 만든다. 역에서 500m 이내, 도로에서 50m 이내 같은 조건을 폴리곤으로 바꾸는 셈이다.</p>
<p>버퍼 도구는 한 번에 하나의 입력 레이어를 받는다. 서로 다른 레이어에 서로 다른 거리를 적용한다면 각각 실행해야 한다. 결과가 마음에 들지 않을 때는 생성된 버퍼를 손으로 고치기보다 원본 레이어에서 거리 값을 바꿔 다시 실행하는 편이 재현하기 쉽다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/vectorgeometry.html#buffer">QGIS Buffer 문서</a></p>
<p><strong>디졸브(Dissolve)</strong>는 여러 피처의 내부 경계를 없애 하나 또는 그룹별 멀티 지오메트리로 합친다. 여러 역에서 만든 500m 버퍼가 서로 겹칠 때, 역별 경계를 유지할 필요가 없다면 디졸브해 하나의 검색 범위처럼 다룰 수 있다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/vectorgeometry.html#dissolve">QGIS Dissolve 문서</a></p>
<p>실습 중 만들어진 버퍼 레이어가 맞는지 확인했던 화면이다. 이름만 보고 넘기지 말고 입력 레이어, 거리, 디졸브 여부를 함께 기록해야 나중에 다시 확인하기 쉽다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/f2cdbde1-5b31-4269-9f27-3a816366350e/image.png" alt="도로·지하철역·기준 지점의 디졸브 버퍼 레이어를 확인한 화면"></p>
<h2 id="intersection과-extract-by-location">Intersection과 Extract by Location</h2>
<p>두 도구는 비슷해 보여도 결과가 다르다.</p>
<table>
<thead>
<tr>
<th>도구</th>
<th>무엇을 만드는가</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>Intersection</td>
<td>실제로 겹친 부분을 새 지오메트리로 생성</td>
<td>세 버퍼가 모두 겹치는 영역</td>
</tr>
<tr>
<td>Extract by Location</td>
<td>공간 조건을 만족하는 원본 피처를 통째로 추출</td>
<td>교차 영역과 만나는 건물</td>
</tr>
</tbody></table>
<p>여기서 중요한 정정이 하나 있다. <strong>Intersection은 버퍼 레이어끼리만 가능한 도구가 아니다.</strong> 점·선·면 등 조건에 맞는 여러 벡터 레이어를 교차할 수 있다. 다만 이번 실습에서는 “세 거리 조건을 동시에 만족하는 범위”가 목적이었기 때문에 디졸브된 버퍼들끼리 교차해야 했다. 원본 건물을 끼워 넣으면 도구 오류라기보다 질문 자체가 바뀌어 버린다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/vectoroverlay.html">QGIS Vector overlay 문서</a></p>
<p><code>Extract by Location</code>은 입력 피처와 비교 레이어 사이의 <code>intersect</code>, <code>contain</code>, <code>within</code>, <code>touch</code> 같은 공간 관계를 기준으로 새 레이어를 만든다. 여러 조건을 고르면 QGIS 문서 기준으로 하나 이상을 만족하는 OR 조건이라는 점도 주의해야 한다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/vectorselection.html#extract-by-location">QGIS Extract by Location 문서</a></p>
<h2 id="필드-계산기와-지오메트리-수정">필드 계산기와 지오메트리 수정</h2>
<p><strong>필드 계산기(Field Calculator)</strong>는 표현식으로 새 속성값을 만들거나 기존 필드를 갱신한다. <code>$area</code>, <code>$length</code> 같은 지오메트리 함수로 면적과 길이를 계산할 수 있다.</p>
<p>다만 값의 단위와 계산 방식은 CRS와 프로젝트 측정 설정을 확인해야 한다. 숫자가 나왔다고 곧바로 제곱미터라고 가정하면 안 된다. 기존 필드를 덮어쓸지, 새 필드를 만들지도 실행 전에 확인한다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/working_with_vector/attribute_table.html#using-the-field-calculator">QGIS 필드 계산기 문서</a></p>
<p><strong>Fix Geometries</strong>는 자기 교차나 잘못된 링처럼 유효하지 않은 지오메트리를 가능한 유효한 표현으로 고친다. “원본과 완전히 같은 모양을 보장하는 만능 복구”는 아니다. 결과가 멀티 지오메트리가 될 수 있고, 일부 차원 값이 사라지는 등 후속 확인이 필요하다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/vectorgeometry.html#fix-geometries">QGIS Fix geometries 문서</a></p>
<h2 id="중심점과-네트워크-최단-경로">중심점과 네트워크 최단 경로</h2>
<p>폴리곤 후보를 도로망 분석의 도착점으로 쓰기 위해 중심점 레이어를 만들었다. 여기서 <strong>Centroid가 항상 폴리곤 안에 있는 것은 아니다.</strong> 오목한 폴리곤이나 멀티폴리곤에서는 바깥에 생길 수 있다. 도착점이 반드시 내부에 있어야 한다면 <code>Point on Surface</code>가 더 적합할 수 있다. <a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/vectorgeometry.html#centroids">QGIS Centroids 문서</a></p>
<p><code>Shortest path (point to layer)</code>는 시작점 하나에서 여러 도착점까지 도로 같은 선 네트워크를 따라 최단 또는 최빠른 경로를 계산한다. 결과의 <code>cost</code>는 무조건 “실제 이동 거리”가 아니다.</p>
<ul>
<li><code>Shortest</code> 전략이면 거리 비용으로 해석한다.</li>
<li><code>Fastest</code> 전략이면 속도 설정을 반영한 시간 비용으로 해석한다.</li>
<li>선이 실제로 연결되어 있는지, 방향 필드와 속도 필드가 맞는지 확인해야 한다.</li>
<li>네트워크에서 떨어진 점은 설정에 따라 가까운 선으로 스냅될 수 있어 허용 거리를 점검해야 한다.</li>
</ul>
<p><a href="https://docs.qgis.org/3.44/en/docs/user_manual/processing_algs/qgis/networkanalysis.html#shortest-path-point-to-layer">QGIS 네트워크 분석 문서</a></p>
<h2 id="정리">정리</h2>
<p>QGIS 용어는 정의만 외울 때보다 <code>범위를 만들고 → 범위를 합치고 → 겹치는 곳을 구하고 → 원본 피처를 추리는</code> 흐름으로 연결했을 때 이해가 쉬웠다.</p>
<p>다음 실습부터는 도구를 실행하기 전에 세 가지를 먼저 적어 보려고 한다.</p>
<ol>
<li>입력 피처는 무엇인가?</li>
<li>결과 지오메트리는 무엇이어야 하는가?</li>
<li>거리·면적·비용의 단위는 무엇인가?</li>
</ol>
<p>이 세 질문만 확인해도 결과는 만들어졌지만 의미가 틀린 상황을 꽤 줄일 수 있을 것 같다.
<img src="https://velog.velcdn.com/images/yujin_1219/post/4f2b6afc-4492-4ff3-b4eb-917cbf803bb6/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PNU 19자리 읽는 법: 필지고유번호를 데이터 연계 키로 사용할 때]]></title>
            <link>https://velog.io/@yujin_1219/pnu-19%EC%9E%90%EB%A6%AC-%EC%9D%BD%EB%8A%94-%EB%B2%95-%ED%95%84%EC%A7%80%EA%B3%A0%EC%9C%A0%EB%B2%88%ED%98%B8%EB%A5%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%B0%EA%B3%84-%ED%82%A4%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EB%95%8C</link>
            <guid>https://velog.io/@yujin_1219/pnu-19%EC%9E%90%EB%A6%AC-%EC%9D%BD%EB%8A%94-%EB%B2%95-%ED%95%84%EC%A7%80%EA%B3%A0%EC%9C%A0%EB%B2%88%ED%98%B8%EB%A5%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%B0%EA%B3%84-%ED%82%A4%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EB%95%8C</guid>
            <pubDate>Tue, 13 Oct 2026 08:38:20 GMT</pubDate>
            <description><![CDATA[<p>토지/건축 공간 데이터를 다루다 보면 <code>PNU</code>라는 19자리 문자열을 자주 만난다. 처음에는 주소를 숫자로 바꾼 코드라고 생각했지만, 실제로는 법정동 코드와 지번 구조를 결합해 필지를 식별하는 키였다.</p>
<p>이번 글에서는 PNU의 자릿수를 나눠 읽는 법과 데이터 조인에 사용할 때 주의할 점을 정리한다.</p>
<h2 id="pnu는-무엇인가">PNU는 무엇인가</h2>
<p>PNU(Parcel Number)는 필지를 식별하기 위한 19자리 코드다. 국내 토지 관련 데이터에서 서로 다른 테이블을 필지 단위로 연결하는 키로 널리 사용된다.</p>
<p>구조는 크게 네 부분이다.</p>
<pre><code class="language-text">법정동코드 10자리 + 필지구분 1자리 + 본번 4자리 + 부번 4자리</code></pre>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/1ba69ee9-dc7d-4f90-a831-689729f79740/image.png" alt="PNU 19자리의 시도/시군구/법정동/리/필지구분/본번/부번 구조"></p>
<h2 id="110자리-법정동-코드">1~10자리: 법정동 코드</h2>
<p>앞의 10자리는 법정동을 나타낸다.</p>
<table>
<thead>
<tr>
<th>위치</th>
<th align="right">길이</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>1~2</td>
<td align="right">2</td>
<td>시/도</td>
</tr>
<tr>
<td>3~5</td>
<td align="right">3</td>
<td>시/군/구</td>
</tr>
<tr>
<td>6~8</td>
<td align="right">3</td>
<td>읍/면/동</td>
</tr>
<tr>
<td>9~10</td>
<td align="right">2</td>
<td>리</td>
</tr>
</tbody></table>
<p>리가 없는 지역은 마지막 두 자리가 <code>00</code>인 경우가 있다. 여기서 행정동과 법정동을 혼동하면 안 된다. PNU 앞부분은 법정동 코드 기준이며, 주민센터 업무를 위한 행정동 체계와 경계/코드가 항상 같지 않다.</p>
<h2 id="11자리-필지구분">11자리: 필지구분</h2>
<p>11번째 자리는 일반 지번과 산 지번 같은 필지 유형을 구분한다. 실무에서 가장 흔히 보는 값은 다음과 같다.</p>
<ul>
<li><code>1</code>: 일반</li>
<li><code>2</code>: 산</li>
</ul>
<p>데이터 명세에 따라 가번지/블록번지 등 추가 코드가 사용될 수 있다. 모든 공급자의 값을 임의로 같은 의미라고 가정하지 말고 해당 데이터셋의 코드 정의서를 확인하는 편이 안전하다.</p>
<h2 id="1215자리-본번-1619자리-부번">12<del>15자리: 본번, 16</del>19자리: 부번</h2>
<p>본번과 부번은 각각 네 자리이며 앞을 <code>0</code>으로 채운다.</p>
<pre><code class="language-text">150번지      → 본번 0150, 부번 0000
150-12번지   → 본번 0150, 부번 0012</code></pre>
<p>숫자처럼 보이지만 PNU 전체는 문자열로 저장하는 것이 안전하다. 정수형으로 바꾸면 앞의 0이 사라져 19자리 구조가 깨질 수 있다.</p>
<h2 id="코드-파싱-예시">코드 파싱 예시</h2>
<p>특정 PNU가 아래와 같다고 가정하자.</p>
<pre><code class="language-text">1111010100101500012</code></pre>
<p>설명을 위한 의사 코드로 분해하면 다음과 같다.</p>
<pre><code class="language-javascript">function parsePnu(pnu) {
  if (!/^\d{19}$/.test(pnu)) {
    throw new Error(&#39;PNU는 숫자로 구성된 19자리 문자열이어야 한다.&#39;);
  }

  return {
    legalDongCode: pnu.slice(0, 10),
    parcelType: pnu.slice(10, 11),
    mainNumber: Number(pnu.slice(11, 15)),
    subNumber: Number(pnu.slice(15, 19)),
  };
}</code></pre>
<p>원본 PNU는 문자열 그대로 보존하고, 화면 표시나 검색에 필요한 본번/부번만 별도 값으로 파싱하는 편이 좋다.</p>
<h2 id="pnu가-주소와-같은-것은-아니다">PNU가 주소와 같은 것은 아니다</h2>
<p>PNU는 필지를 가리키고 도로명주소는 건물을 찾아가는 주소 체계다. 하나의 필지에 여러 건물이 있거나 한 건물이 여러 필지에 걸칠 수 있으므로 언제나 1:1 관계라고 생각하면 안 된다.</p>
<p>건축물 데이터와 결합할 때는 대표필지 여부, 관련지번, 건물관리번호 같은 추가 키와 관계를 확인해야 한다. 단순 조인 뒤 행 수가 늘어났다면 중복 데이터라고 바로 제거하기보다 실제 1:N 관계인지 먼저 살펴봐야 한다.</p>
<h2 id="데이터-연계에서-자주-만나는-문제">데이터 연계에서 자주 만나는 문제</h2>
<h3 id="1-폐지되거나-변경된-법정동-코드">1. 폐지되거나 변경된 법정동 코드</h3>
<p>행정구역 개편으로 법정동 코드가 바뀔 수 있다. 서로 다른 기준일의 데이터를 조인하면 같은 지역이 매칭되지 않을 수 있다. 코드 이력과 데이터 기준일을 함께 관리해야 한다.</p>
<h3 id="2-앞자리-0-손실">2. 앞자리 0 손실</h3>
<p>CSV나 스프레드시트에서 숫자로 자동 변환되면 자릿수가 달라질 수 있다. 스키마에서 처음부터 문자열 길이 19를 강제하는 것이 좋다.</p>
<h3 id="3-일반산-지번-표기-차이">3. 일반/산 지번 표기 차이</h3>
<p>사람이 읽는 주소의 <code>산</code> 표시는 PNU의 필지구분 코드로 들어간다. 주소 문자열만 잘라 만든 키는 예외를 놓치기 쉽다.</p>
<h3 id="4-하나의-키로-모든-공간-객체를-식별하려는-시도">4. 하나의 키로 모든 공간 객체를 식별하려는 시도</h3>
<p>PNU는 필지 키다. 건물 동, 도로 시설물, 행정구역 등 다른 단위의 객체에는 각 도메인의 식별자를 사용해야 한다.</p>
<h2 id="db에-저장할-때-확인할-것">DB에 저장할 때 확인할 것</h2>
<pre><code class="language-sql">CREATE TABLE parcel_link (
  pnu char(19) PRIMARY KEY,
  legal_dong_code char(10) NOT NULL,
  parcel_type char(1) NOT NULL,
  main_number integer NOT NULL,
  sub_number integer NOT NULL,
  source_date date NOT NULL,
  CHECK (pnu ~ &#39;^[0-9]{19}$&#39;)
);</code></pre>
<p>고정 길이 문자열, 형식 검증, 기준일을 함께 두면 조인 오류를 조금 일찍 발견할 수 있다. 원천 데이터가 변경될 수 있으므로 소스와 갱신 시점도 남긴다.</p>
<h2 id="마무리">마무리</h2>
<p>PNU는 주소를 표시하기 위한 문자열보다 <strong>필지 단위 데이터를 연결하기 위한 구조화된 식별자</strong>로 이해하는 편이 정확했다. 10자리 법정동 코드, 1자리 필지구분, 4자리 본번과 4자리 부번을 알면 값 자체를 읽을 수 있다.</p>
<p>하지만 코드 구조를 아는 것만으로 안전한 조인이 보장되지는 않는다. 법정동 코드 이력, 기준일, 1:N 관계, 공급자별 코드 명세를 함께 확인해야 한다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://www.nsdi.go.kr/">국가공간정보포털</a></li>
<li><a href="https://www.codil.or.kr/filebank/original/RK/OTKCRK240565/OTKCRK240565.pdf">국토교통 분야 자료의 PNU 구성 설명</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[PostGIS 공간 쿼리 입문: 포함, 교차, 거리, 버퍼를 SQL로 다루기]]></title>
            <link>https://velog.io/@yujin_1219/postgis-%EA%B3%B5%EA%B0%84-%EC%BF%BC%EB%A6%AC-%EC%9E%85%EB%AC%B8-%ED%8F%AC%ED%95%A8-%EA%B5%90%EC%B0%A8-%EA%B1%B0%EB%A6%AC-%EB%B2%84%ED%8D%BC%EB%A5%BC-sql%EB%A1%9C-%EB%8B%A4%EB%A3%A8%EA%B8%B0</link>
            <guid>https://velog.io/@yujin_1219/postgis-%EA%B3%B5%EA%B0%84-%EC%BF%BC%EB%A6%AC-%EC%9E%85%EB%AC%B8-%ED%8F%AC%ED%95%A8-%EA%B5%90%EC%B0%A8-%EA%B1%B0%EB%A6%AC-%EB%B2%84%ED%8D%BC%EB%A5%BC-sql%EB%A1%9C-%EB%8B%A4%EB%A3%A8%EA%B8%B0</guid>
            <pubDate>Tue, 13 Oct 2026 08:37:45 GMT</pubDate>
            <description><![CDATA[<p>PostGIS를 처음 접했을 때는 지도 도형을 PostgreSQL에 저장하는 확장 정도로 생각했다. 실제로는 “이 점이 어느 행정구역 안에 있는가”, “도로와 하천이 교차하는가”, “주변 500m 안에 무엇이 있는가”를 SQL로 묻는 도구에 가까웠다.</p>
<p>이번 글에서는 자주 쓰는 공간 관계와 거리 함수를 예시로 정리하고, 처음 작성한 쿼리에서 놓치기 쉬웠던 SRID와 인덱스 문제를 함께 살펴본다.</p>
<h2 id="먼저-geometry와-srid를-확인한다">먼저 geometry와 SRID를 확인한다</h2>
<p>예시 테이블은 다음과 같다고 하자.</p>
<pre><code class="language-sql">CREATE TABLE stores (
  id bigint PRIMARY KEY,
  name text NOT NULL,
  geom geometry(Point, 5179) NOT NULL
);

CREATE INDEX stores_geom_gix
ON stores USING GIST (geom);</code></pre>
<p><code>geometry(Point, 5179)</code>는 점 도형이며 SRID가 5179라는 뜻이다. 공간 연산에 넣는 두 geometry는 같은 SRID를 사용해야 한다. 숫자가 비슷해 보여도 좌표계가 다르면 비교 전에 <code>ST_Transform</code>으로 맞춰야 한다.</p>
<p><code>ST_SetSRID</code>와 <code>ST_Transform</code>도 구분해야 한다.</p>
<pre><code class="language-sql">-- 좌표값은 바꾸지 않고 해석 기준만 지정
ST_SetSRID(geom, 5179)

-- 좌표값을 목표 CRS에 맞게 실제 계산
ST_Transform(geom, 4326)</code></pre>
<h2 id="포함-관계-st_contains-st_within-st_covers">포함 관계: <code>ST_Contains</code>, <code>ST_Within</code>, <code>ST_Covers</code></h2>
<p>매장이 어느 행정구역 안에 있는지 찾을 수 있다.</p>
<pre><code class="language-sql">SELECT s.id, s.name, d.name AS district_name
FROM stores s
JOIN districts d
  ON ST_Within(s.geom, d.geom);</code></pre>
<p><code>ST_Within(A, B)</code>는 A가 B 안에 있는지를 본다. 인수를 뒤집어 <code>ST_Contains(B, A)</code>로 표현할 수도 있다.</p>
<p>경계 위의 점을 내부로 볼 것인지가 중요하면 <code>ST_Covers</code>도 검토해야 한다. <code>Contains</code>는 내부점 조건 때문에 경계에만 놓인 객체에서 예상과 다른 결과가 나올 수 있다. 함수 이름만 보고 고르지 말고 경계 규칙을 공식 문서와 샘플로 확인해야 한다.</p>
<h2 id="겹침과-교차-st_intersects-st_crosses">겹침과 교차: <code>ST_Intersects</code>, <code>ST_Crosses</code></h2>
<p>침수 예상 구역과 건물이 조금이라도 공간을 공유하는지 찾는 쿼리는 다음처럼 작성할 수 있다.</p>
<pre><code class="language-sql">SELECT b.id, b.name
FROM buildings b
JOIN flood_zones f
  ON ST_Intersects(b.geom, f.geom);</code></pre>
<p><code>ST_Intersects</code>는 두 객체가 한 점이라도 공유하면 참이다. 반대로 전혀 만나지 않으면 <code>ST_Disjoint</code>가 참이다.</p>
<p><code>ST_Crosses</code>는 선이 다른 선이나 영역을 가로지르는 것처럼 더 구체적인 위상 관계를 검사한다. 단순히 닿는 것과 가로지르는 것은 다르다.</p>
<h2 id="겹치는-영역을-실제-도형으로-만들기-st_intersection">겹치는 영역을 실제 도형으로 만들기: <code>ST_Intersection</code></h2>
<p>관계 함수는 참/거짓을 돌려주지만, 중첩된 부분의 도형이 필요할 때는 <code>ST_Intersection</code>을 사용한다.</p>
<pre><code class="language-sql">SELECT
  l.id AS landuse_id,
  f.id AS flood_id,
  ST_Intersection(l.geom, f.geom) AS overlap_geom
FROM landuse l
JOIN flood_zones f
  ON ST_Intersects(l.geom, f.geom);</code></pre>
<p>먼저 <code>ST_Intersects</code>로 후보를 좁힌 뒤 교차 도형을 계산한다. 결과가 GeometryCollection이 되거나 아주 작은 조각이 생길 수 있으므로 후처리 조건도 필요할 수 있다.</p>
<h2 id="반경-검색은-st_dwithin부터-생각한다">반경 검색은 <code>ST_DWithin</code>부터 생각한다</h2>
<p>처음에는 기준점을 <code>ST_Buffer</code>로 원으로 만든 다음 그 안에 있는 점을 찾았다.</p>
<pre><code class="language-sql">-- 동작할 수 있지만 반경 검색만을 위한 최선의 표현은 아닐 수 있다.
WHERE ST_Intersects(
  s.geom,
  ST_Buffer(:center, 500)
)</code></pre>
<p>PostGIS에는 거리 조건을 바로 검사하는 <code>ST_DWithin</code>이 있다.</p>
<pre><code class="language-sql">SELECT s.id, s.name
FROM stores s
WHERE ST_DWithin(s.geom, :center_geom, 500);</code></pre>
<p><code>ST_DWithin</code>은 공간 인덱스를 활용할 수 있는 경계 상자 비교를 포함해 반경 검색에 적합하다. 실제 버퍼 폴리곤이 필요한 것이 아니라 “일정 거리 안인가”만 묻는다면 우선 이 함수를 검토한다.</p>
<h2 id="거리-단위는-타입과-crs에-달려-있다">거리 단위는 타입과 CRS에 달려 있다</h2>
<p>geometry의 거리 단위는 좌표계 단위다. EPSG:5179 같은 미터 단위 투영좌표계에서 <code>500</code>은 500m로 해석할 수 있다. EPSG:4326 geometry에서 같은 숫자는 도 단위라 의미가 완전히 달라진다.</p>
<p>전 지구 위경도를 다룰 때는 geography 타입을 사용할 수 있다.</p>
<pre><code class="language-sql">SELECT id, name
FROM stores_wgs84
WHERE ST_DWithin(
  geom::geography,
  ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography,
  500
);</code></pre>
<p>geography 거리 인수는 미터다. 편리하지만 모든 연산이 geometry와 같은 성능/기능 특성을 갖는 것은 아니므로 데이터 범위와 쿼리 패턴에 맞춰 선택한다.</p>
<h2 id="공간-인덱스를-썼는데도-느린-이유">공간 인덱스를 썼는데도 느린 이유</h2>
<p>GiST 인덱스를 만들었다고 모든 쿼리가 자동으로 빨라지는 것은 아니다.</p>
<ul>
<li>인덱스 지원 관계 함수인지 확인한다.</li>
<li>인덱스 컬럼에 불필요한 변환을 매 행 적용하지 않는다.</li>
<li>자주 쓰는 변환 결과라면 별도 컬럼이나 표현식 인덱스를 검토한다.</li>
<li><code>EXPLAIN (ANALYZE, BUFFERS)</code>로 실제 계획을 본다.</li>
<li>큰 도형은 세분화나 단순화 전략이 필요할 수 있다.</li>
</ul>
<p>예를 들어 <code>ST_Distance(a, b) &lt; 500</code>은 거리 값을 모두 계산한 뒤 비교하게 될 수 있다. 단순 반경 조건이라면 <code>ST_DWithin</code>이 의도와 인덱스 활용 면에서 더 분명하다.</p>
<h2 id="유효하지-않은-도형도-확인한다">유효하지 않은 도형도 확인한다</h2>
<p>자기 교차 폴리곤처럼 유효하지 않은 도형은 관계 연산에서 오류나 예상 밖 결과를 만들 수 있다.</p>
<pre><code class="language-sql">SELECT id, ST_IsValidReason(geom)
FROM parcels
WHERE NOT ST_IsValid(geom);</code></pre>
<p><code>ST_MakeValid</code>로 고칠 수 있지만 결과의 지오메트리 유형이 달라질 수 있다. 자동 수정 전에 원본 오류의 원인과 허용할 결과를 정해야 한다.</p>
<h2 id="마무리">마무리</h2>
<p>PostGIS 공간 쿼리에서 중요한 것은 함수 목록을 많이 외우는 일이 절대 아니었다. 관계를 정확히 정의하고, CRS와 단위를 맞추고(이게 진짜 중요), 인덱스가 적용되는 형태로 질문하는 것이 핵심이었다.</p>
<p><code>ST_Intersects</code>, <code>ST_Within</code>, <code>ST_DWithin</code>부터 익히고 실제 실행 계획을 확인하면 지도 서버 바깥에서도 상당한 공간 분석을 SQL로 처리할 수 있다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://postgis.net/docs/reference.html#Spatial_Relationships">PostGIS 공간 관계 함수</a></li>
<li><a href="https://postgis.net/docs/ST_Intersects.html">ST_Intersects</a></li>
<li><a href="https://postgis.net/docs/ST_Within.html">ST_Within</a></li>
<li><a href="https://postgis.net/documentation/faq/radius-search/">PostGIS 반경 검색 FAQ</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[QGIS 뒤에서 일하는 GDAL: 변환, 재투영, 자르기 도구 정리]]></title>
            <link>https://velog.io/@yujin_1219/qgis-%EB%92%A4%EC%97%90%EC%84%9C-%EC%9D%BC%ED%95%98%EB%8A%94-gdal-%EB%B3%80%ED%99%98-%EC%9E%AC%ED%88%AC%EC%98%81-%EC%9E%90%EB%A5%B4%EA%B8%B0-%EB%8F%84%EA%B5%AC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@yujin_1219/qgis-%EB%92%A4%EC%97%90%EC%84%9C-%EC%9D%BC%ED%95%98%EB%8A%94-gdal-%EB%B3%80%ED%99%98-%EC%9E%AC%ED%88%AC%EC%98%81-%EC%9E%90%EB%A5%B4%EA%B8%B0-%EB%8F%84%EA%B5%AC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 13 Oct 2026 08:36:43 GMT</pubDate>
            <description><![CDATA[<p>QGIS에서 파일 형식을 바꾸거나 래스터를 자를 때 버튼 몇 번이면 작업이 끝난다. 같은 작업을 반복하거나 서버에서 자동화하려고 보니 그 뒤에서 GDAL 도구가 움직이고 있었다.</p>
<p>이번 글에서는 GDAL이 무엇인지, QGIS와 어떤 관계인지, 자주 쓰는 명령을 어떤 기준으로 구분하는지 정리한다.</p>
<h2 id="gdal은-공간-데이터-입출력과-변환을-맡는-기반-도구다">GDAL은 공간 데이터 입출력과 변환을 맡는 기반 도구다</h2>
<p>GDAL은 래스터와 벡터 지리공간 데이터 형식을 읽고 쓰는 오픈소스 라이브러리다. GeoTIFF, GeoPackage, Shapefile, GeoJSON, NetCDF와 데이터베이스 등 많은 드라이버를 제공한다.</p>
<p>예전 설명에서는 래스터용 GDAL과 벡터용 OGR을 별도 라이브러리처럼 구분한다. 현재는 프로젝트가 통합되어 공식 문서도 전체를 GDAL로 부른다. 다만 역사적인 이유로 벡터 API와 도구 이름에 <code>OGR</code>이 남아 있다.</p>
<p>GDAL은 다음 세 방식으로 만날 수 있다.</p>
<ul>
<li>C/C++ 라이브러리와 Python 등 언어 바인딩</li>
<li><code>gdalinfo</code>, <code>gdalwarp</code>, <code>ogr2ogr</code> 같은 명령행 프로그램</li>
<li>QGIS Processing Toolbox 등 다른 프로그램 안의 처리 엔진</li>
</ul>
<h2 id="qgis와-gdal은-경쟁-제품이-아니다">QGIS와 GDAL은 경쟁 제품이 아니다!!</h2>
<p>QGIS는 지도를 보고 편집하고 분석하는 데 필요한 GUI와 프로젝트 환경을 제공한다. GDAL은 QGIS가 여러 데이터 형식을 읽고 변환할 때 사용하는 핵심 기반 중 하나다.</p>
<p>QGIS Processing Toolbox에서 GDAL 항목을 실행하면 입력창 아래에 대응하는 명령이 보이는 경우가 있다. 처음에는 GUI로 옵션을 익히고, 반복 작업이 생기면 같은 명령을 스크립트로 옮길 수 있다.</p>
<p>다만 QGIS의 모든 기능이 GDAL 하나로 구현되는 것은 아니다. QGIS 자체 알고리즘과 GRASS GIS, SAGA 등의 처리 제공자도 함께 사용할 수 있다.</p>
<h2 id="파일-상태-확인-gdalinfo와-ogrinfo">파일 상태 확인: <code>gdalinfo</code>와 <code>ogrinfo</code></h2>
<p>변환부터 시도하기 전에 원본을 확인하는 것이 먼저다.</p>
<pre><code class="language-bash">gdalinfo elevation.tif
ogrinfo -so -al parcels.gpkg</code></pre>
<p><code>gdalinfo</code>에서는 래스터 크기, 밴드, 좌표계, 지리 변환, NoData, 경계 등을 확인한다. <code>ogrinfo</code>에서는 벡터 레이어 목록, 지오메트리 유형, CRS와 필드 스키마를 확인할 수 있다.</p>
<p>확인 없이 명령부터 실행하면 CRS가 잘못 지정된 파일을 그럴듯하게 재투영하거나, 문자 인코딩과 필드 타입이 바뀐 사실을 놓치기 쉽다.</p>
<h2 id="벡터-변환-ogr2ogr">벡터 변환: <code>ogr2ogr</code></h2>
<p>GeoJSON을 GeoPackage로 변환하는 기본 예시는 다음과 같다.</p>
<pre><code class="language-bash">ogr2ogr -f GPKG output.gpkg input.geojson</code></pre>
<p>좌표계를 변환하려면 원본 CRS와 목표 CRS를 구분한다.</p>
<pre><code class="language-bash">ogr2ogr \
  -s_srs EPSG:4326 \
  -t_srs EPSG:5179 \
  output.gpkg input.geojson</code></pre>
<p>원본 파일에 올바른 CRS 정보가 이미 있다면 <code>-s_srs</code>는 생략할 수 있다. 원본 CRS를 모른 채 추측해서 지정하면 결과가 틀린다.</p>
<p>공간 범위와 속성 조건으로 필요한 피처만 가져올 수도 있다.</p>
<pre><code class="language-bash">ogr2ogr \
  -spat 126.8 37.4 127.2 37.7 \
  -where &quot;category = &#39;park&#39;&quot; \
  parks.gpkg source.gpkg parks</code></pre>
<h2 id="래스터-재투영과-자르기-gdalwarp">래스터 재투영과 자르기: <code>gdalwarp</code></h2>
<p><code>gdalwarp</code>는 래스터 재투영, 워핑, 모자이크, 영역 자르기에 사용한다.</p>
<pre><code class="language-bash">gdalwarp \
  -s_srs EPSG:4326 \
  -t_srs EPSG:5179 \
  -r bilinear \
  input.tif output_5179.tif</code></pre>
<p><code>-r</code>은 리샘플링 방법이다. 연속값 영상에는 bilinear나 cubic을 고려할 수 있지만, 토지피복 분류처럼 범주형 값에는 보통 nearest를 사용한다. 보간으로 존재하지 않는 클래스 값이 생기면 안 되기 때문이다.</p>
<p>벡터 경계로 자르려면 다음처럼 쓸 수 있다.</p>
<pre><code class="language-bash">gdalwarp \
  -cutline boundary.gpkg \
  -crop_to_cutline \
  -dstnodata -9999 \
  input.tif clipped.tif</code></pre>
<h2 id="변환과-복사-gdal_translate">변환과 복사: <code>gdal_translate</code></h2>
<p><code>gdal_translate</code>는 래스터 형식 변환, 밴드 선택, 해상도 조정, 부분 영역 추출 등에 자주 사용한다.</p>
<pre><code class="language-bash">gdal_translate \
  -of COG \
  -co COMPRESS=DEFLATE \
  input.tif output_cog.tif</code></pre>
<p>여기서 중요한 점은 형식 변환과 재투영을 구분하는 것이다. <code>gdal_translate</code>로 CRS 메타데이터를 지정하는 것만으로 픽셀 위치가 새 좌표계에 맞게 재계산되는 것은 아니다. 재투영은 <code>gdalwarp</code>를 사용한다.</p>
<h2 id="지오리퍼런싱은-기준점-품질이-핵심이다">지오리퍼런싱은 기준점 품질이 핵심이다</h2>
<p>좌표가 없는 스캔 지도에 지상기준점(GCP)을 지정하고 <code>gdalwarp</code>로 변형해 공간 위치를 부여할 수 있다. 명령이 성공해도 기준점이 한쪽에 몰렸거나 잘못 찍혔다면 결과가 뒤틀린다.</p>
<p>기준점 개수만 늘리기보다 화면 전체에 고르게 배치하고, 독립된 검사점으로 오차를 확인하는 편이 중요하다.</p>
<h2 id="자동화할-때-확인할-체크리스트">자동화할 때 확인할 체크리스트</h2>
<ul>
<li>입력 드라이버와 출력 드라이버가 필요한 데이터 타입을 지원하는가?</li>
<li>원본 CRS와 목표 CRS가 정확한가?</li>
<li>래스터의 NoData와 데이터 타입이 유지되는가?</li>
<li>범주형/연속형에 맞는 리샘플링을 골랐는가?</li>
<li>벡터 필드명, 문자 인코딩, 날짜와 숫자 타입이 보존되는가?</li>
<li>결과의 범위, 피처 수, 밴드 통계와 샘플 위치를 비교했는가?</li>
</ul>
<h2 id="마무리">마무리</h2>
<p>QGIS는 사람이 공간 데이터를 보고 조작하기 좋은 작업대이고, GDAL은 다양한 형식을 읽고 변환하는 엔진에 가깝다. GUI에서 한 번 성공한 작업을 명령행으로 옮기면 반복 가능성과 기록성이 좋아진다.</p>
<p>반대로 명령이 오류 없이 끝났다는 이유만으로 결과가 맞다고 생각하면 안 된다. GIS 변환에서는 CRS, 단위, NoData, 리샘플링, 스키마를 전후로 검증하는 단계가 작업 자체만큼 중요했다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://gdal.org/en/stable/programs/index.html">GDAL 프로그램 목록</a></li>
<li><a href="https://gdal.org/en/latest/programs/ogr2ogr.html">ogr2ogr 공식 문서</a></li>
<li><a href="https://gdal.org/en/stable/programs/gdalwarp.html">gdalwarp 공식 문서</a></li>
<li><a href="https://gdal.org/en/stable/drivers/raster/gtiff.html">GDAL GeoTIFF driver</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[벡터와 래스터부터 Shapefile, GeoPackage, GeoTIFF까지]]></title>
            <link>https://velog.io/@yujin_1219/%EB%B2%A1%ED%84%B0%EC%99%80-%EB%9E%98%EC%8A%A4%ED%84%B0%EB%B6%80%ED%84%B0-shapefile-geopackage-geotiff%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@yujin_1219/%EB%B2%A1%ED%84%B0%EC%99%80-%EB%9E%98%EC%8A%A4%ED%84%B0%EB%B6%80%ED%84%B0-shapefile-geopackage-geotiff%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Tue, 13 Oct 2026 08:35:39 GMT</pubDate>
            <description><![CDATA[<p>GIS 데이터를 처음 받았을 때는 확장자부터 헷갈렸다. <code>.shp</code> 파일을 열려고 했더니 옆에 있는 <code>.dbf</code>와 <code>.shx</code>도 필요했고, <code>.gpkg</code> 하나에는 여러 레이어가 들어 있었다. 위성영상처럼 보이는 <code>.tif</code>는 일반 이미지와 달리 지도 위의 정확한 위치에 놓였다.</p>
<p>파일 형식을 외우기 전에 벡터와 래스터가 공간을 어떻게 표현하는지부터 이해하니 차이가 선명해졌다.</p>
<h2 id="벡터와-래스터는-공간을-나누는-두-가지-방식">벡터와 래스터는 공간을 나누는 두 가지 방식</h2>
<p>벡터는 객체의 경계를 좌표로 표현한다. 정류장은 점, 도로는 선, 행정구역은 면처럼 모델링할 수 있다. 래스터는 공간을 같은 크기의 셀로 나누고 셀마다 값을 저장한다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/ad973fa5-477c-4df6-8739-0e2003ccbf22/image.png" alt="점/선/면 벡터 레이어와 여러 래스터 레이어가 쌓이는 모습"></p>
<table>
<thead>
<tr>
<th>기준</th>
<th>벡터</th>
<th>래스터</th>
</tr>
</thead>
<tbody><tr>
<td>표현</td>
<td>점/선/면의 좌표</td>
<td>행/열 격자의 셀 값</td>
</tr>
<tr>
<td>잘 맞는 대상</td>
<td>도로, 건물, 필지, 시설물</td>
<td>위성영상, 고도, 기온, 강수량</td>
</tr>
<tr>
<td>강점</td>
<td>경계와 객체 단위 속성 표현</td>
<td>연속 현상과 영상 연산</td>
</tr>
<tr>
<td>비용</td>
<td>복잡한 도형 연산</td>
<td>해상도가 높을수록 큰 용량</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/d8a6b8d8-1d49-4bde-a772-7c5dcaa26bba/image.png" alt="포인트, 라인, 폴리곤의 좌표 구성"></p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/0de3c3fa-e771-4d46-99cb-10f59bde2ad7/image.png" alt="우물은 점, 하천은 선, 호수는 면으로 표현한 벡터 지도 예시"></p>
<p>어느 쪽이 더 좋은 것이 아니라 분석하려는 현상에 맞는 모델을 선택해야 한다. 하천 중심선은 벡터 선으로 표현하기 좋지만, 강수량처럼 공간 전체에서 연속적으로 변하는 값은 래스터가 자연스럽다.</p>
<h2 id="도형과-속성은-한-객체를-설명하는-두-부분">도형과 속성은 한 객체를 설명하는 두 부분</h2>
<p>벡터 레이어의 한 행은 보통 도형과 속성을 함께 가진 피처(feature)다.</p>
<pre><code class="language-text">Feature
├─ geometry: POINT(127.0 37.5)
└─ attributes: {name: &quot;관측소 A&quot;, type: &quot;기상&quot;}</code></pre>
<p>속성 테이블의 행과 지도 위 객체는 내부 식별자로 연결된다. 외부 CSV를 행정구역 코드 같은 공통 키로 조인할 수도 있다. 이때 키가 중복되거나 누락되면 1:1이라고 생각한 결합이 1:N으로 불어나거나 데이터가 사라질 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/feed68b7-a441-4b82-9338-7524f50eddb9/image.png" alt="지도 객체와 속성 테이블 행이 1대1로 연결되는 예시"></p>
<h2 id="shapefile은-왜-파일-하나가-아닐까">Shapefile은 왜 파일 하나가 아닐까</h2>
<p>Shapefile은 오래되고 호환성이 높은 벡터 형식이다. 이름 때문에 <code>.shp</code> 하나만 있으면 될 것 같지만 실제로는 여러 파일이 한 세트를 이룬다.</p>
<ul>
<li><code>.shp</code>: 도형 정보</li>
<li><code>.shx</code>: 도형 인덱스</li>
<li><code>.dbf</code>: 속성 테이블</li>
<li><code>.prj</code>: 좌표계 정의</li>
<li><code>.cpg</code>: 문자 인코딩 정보</li>
</ul>
<p>필수 구성 파일 하나를 빼고 전달하면 레이어가 열리지 않거나 속성이 깨질 수 있다. DBF 제약 때문에 필드명이 짧아지고 문자 인코딩 문제도 자주 만난다. 한 Shapefile에는 하나의 지오메트리 유형을 저장하는 것이 기본이다.</p>
<p>높은 호환성은 여전히 장점이지만, 새 작업의 기본 저장 형식으로는 제약을 감수할 이유가 있는지 먼저 확인하게 됐다.</p>
<h2 id="geopackage는-sqlite-파일-안에-공간-구조를-담는다">GeoPackage는 SQLite 파일 안에 공간 구조를 담는다</h2>
<p>GeoPackage는 OGC 표준이며 SQLite 데이터베이스 컨테이너를 사용한다. <code>.gpkg</code> 파일 하나에 여러 벡터 레이어, 비공간 속성 테이블, 타일 행렬 형태의 영상/래스터 지도를 담을 수 있다.</p>
<p>단일 파일이라 전달하기 쉽고 긴 필드명, 여러 레이어, 공간 인덱스 같은 기능을 활용할 수 있다. SQLite 기반이므로 일부 SQL 조회와 트랜잭션도 가능하다.</p>
<p>다만 “모든 종류의 래스터를 아무 형태로나 저장한다”라고 이해하면 안 된다. GeoPackage 핵심 표준의 래스터 지도는 타일 행렬 구조이며, 수치형 격자 커버리지는 확장 규격을 확인해야 한다. 사용하는 GIS가 해당 확장을 지원하는지도 별도 문제다.</p>
<h2 id="geotiff는-픽셀과-공간-참조를-함께-저장한다">GeoTIFF는 픽셀과 공간 참조를 함께 저장한다</h2>
<p>일반 TIFF는 픽셀을 저장한다. GeoTIFF는 여기에 좌표계, 픽셀 크기, 원점 또는 변환 정보 같은 지리 참조 태그를 담을 수 있다. 그래서 위성영상이나 DEM 파일을 GIS에 추가하면 정해진 위치에 놓인다.</p>
<p>GeoTIFF는 여러 밴드를 저장할 수 있다. RGB 영상은 세 밴드를 사용하고, 위성자료는 근적외선이나 열적외선 밴드를 더 포함할 수 있다. LZW, Deflate, JPEG 등 다양한 압축 옵션도 있지만 손실 여부와 데이터 유형에 맞게 선택해야 한다.</p>
<p>큰 GeoTIFF를 웹이나 클라우드에서 부분적으로 읽을 목적이라면 내부 타일, 오버뷰와 바이트 배치를 최적화한 Cloud Optimized GeoTIFF(COG)도 검토할 수 있다.</p>
<h2 id="어떤-형식을-고를까">어떤 형식을 고를까</h2>
<pre><code class="language-text">오래된 시스템과 교환 호환성이 최우선 → Shapefile
여러 벡터 레이어를 한 파일에서 관리 → GeoPackage
좌표가 있는 위성영상/DEM → GeoTIFF
원격 객체 저장소에서 부분 읽기 → COG 검토</code></pre>
<p>확장자만 보지 말고 CRS, 문자 인코딩, NoData, 밴드 의미, 스키마와 데이터 유효성을 함께 확인해야 한다. 형식 변환이 성공했다고 정보가 모두 보존됐다는 보장도 없다.</p>
<h2 id="마무리">마무리</h2>
<p>벡터는 개별 객체와 경계를, 래스터는 격자 위의 연속 현상을 표현하는 데 강하다. Shapefile, GeoPackage, GeoTIFF는 이 모델을 저장하고 교환하는 서로 다른 그릇이다.</p>
<p>파일을 열 수 있는지만 확인하는 단계에서 한 걸음 더 나아가, 어떤 정보가 들어 있고 변환 과정에서 무엇이 손실될 수 있는지를 보는 습관이 필요했다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://gdal.org/en/stable/user/vector_data_model.html">GDAL Vector Data Model</a></li>
<li><a href="https://www.ogc.org/standards/geopackage/">OGC GeoPackage Standard</a></li>
<li><a href="https://portal.ogc.org/files/83583">OGC GeoTIFF Standard</a></li>
<li><a href="https://gdal.org/en/stable/drivers/raster/gtiff.html">GDAL GeoTIFF driver</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[좌표계 문자열만 바꾸면 될까: WKT, PROJ와 측지계 변환 흐름]]></title>
            <link>https://velog.io/@yujin_1219/%EC%A2%8C%ED%91%9C%EA%B3%84-%EB%AC%B8%EC%9E%90%EC%97%B4%EB%A7%8C-%EB%B0%94%EA%BE%B8%EB%A9%B4-%EB%90%A0%EA%B9%8C-wkt-proj%EC%99%80-%EC%B8%A1%EC%A7%80%EA%B3%84-%EB%B3%80%ED%99%98-%ED%9D%90%EB%A6%84</link>
            <guid>https://velog.io/@yujin_1219/%EC%A2%8C%ED%91%9C%EA%B3%84-%EB%AC%B8%EC%9E%90%EC%97%B4%EB%A7%8C-%EB%B0%94%EA%BE%B8%EB%A9%B4-%EB%90%A0%EA%B9%8C-wkt-proj%EC%99%80-%EC%B8%A1%EC%A7%80%EA%B3%84-%EB%B3%80%ED%99%98-%ED%9D%90%EB%A6%84</guid>
            <pubDate>Tue, 13 Oct 2026 08:35:05 GMT</pubDate>
            <description><![CDATA[<p>좌표가 어긋났을 때 처음에는 <code>+proj</code>, <code>+datum</code>, <code>+towgs84</code> 같은 값을 복사해 넣으면 해결되는 줄 알았다. 하지만 좌표계 정의와 좌표 변환은 같은 일이 아니었고, 문자열 형식만 바꿔도 일부 정보가 사라질 수 있었다.</p>
<p>이번 글에서는 WKT와 PROJ 문자열의 역할, 그리고 서로 다른 측지 기준 사이에서 좌표가 실제로 어떻게 변환되는지 정리한다.</p>
<h2 id="wkt와-proj-문자열은-좌표-자체가-아니다">WKT와 PROJ 문자열은 좌표 자체가 아니다</h2>
<p>WKT(Well-Known Text)는 좌표참조계와 좌표 연산을 구조적인 텍스트로 표현하는 표준 형식이다. WKT2에는 기준계, 타원체, 좌표축, 단위, 투영법, 사용 영역 같은 내용을 자세히 담을 수 있다.</p>
<pre><code class="language-text">PROJCRS[&quot;KGD2002 / Unified CS&quot;,
  BASEGEOGCRS[&quot;KGD2002&quot;, ...],
  CONVERSION[&quot;Korea Unified Belt&quot;, ...],
  CS[Cartesian, 2],
  AXIS[&quot;northing (X)&quot;, north],
  AXIS[&quot;easting (Y)&quot;, east],
  LENGTHUNIT[&quot;metre&quot;, 1],
  ID[&quot;EPSG&quot;, 5179]]</code></pre>
<p>PROJ 문자열은 PROJ 라이브러리에서 CRS나 변환 단계를 간결하게 표현해 온 문법이다.</p>
<pre><code class="language-text">+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996
+x_0=1000000 +y_0=2000000 +ellps=GRS80 +units=m +type=crs</code></pre>
<p>짧아서 설정 파일이나 명령행에서 편하지만 WKT2가 표현하는 모든 정보를 보존하지 못할 수 있다. PROJ 공식 문서도 WKT와 PROJ 문자열 사이 변환에서 정보 손실 가능성을 경고하고 가능하면 WKT2 사용을 권한다.</p>
<p>가능하면 임의 문자열을 직접 조립하기보다 <code>EPSG:5179</code>처럼 권위 기관 코드로 CRS를 식별하고, 필요할 때 <code>projinfo</code>로 정의와 후보 좌표 연산을 확인하는 편이 안전하다.</p>
<h2 id="crs-정의와-재투영을-구분해야-한다">CRS 정의와 재투영을 구분해야 한다</h2>
<p>파일에 CRS가 누락됐을 때 원래 기준을 지정하는 것은 <strong>정의(assign)</strong>다. 좌표 숫자는 바뀌지 않는다.</p>
<p>한 CRS의 좌표를 다른 CRS의 숫자로 계산하는 것은 <strong>재투영(reproject)</strong>이다. 이때 실제 좌표값이 바뀐다.</p>
<pre><code class="language-text">정의:      (x, y)는 그대로 + &quot;이 숫자는 EPSG:5179&quot;라고 설명
재투영:    EPSG:5179의 (x, y) → EPSG:4326의 (경위도)로 계산</code></pre>
<p>원본 CRS를 잘못 지정한 뒤 재투영하면 계산은 정상 완료돼도 결과는 틀린 위치로 간다.</p>
<h2 id="서로-다른-pcs를-변환할-때-내부에서-일어나는-일">서로 다른 PCS를 변환할 때 내부에서 일어나는 일</h2>
<p>개념적으로는 다음 흐름으로 볼 수 있다.</p>
<pre><code class="language-text">원본 평면 좌표
  ↓ 역투영
원본 측지 기준의 경위도
  ↓ 기준 프레임 변환
목표 측지 기준의 경위도
  ↓ 투영
목표 평면 좌표</code></pre>
<p>하지만 실제 라이브러리는 항상 WGS 84를 중간에 거치는 단순한 방식만 쓰지 않는다. 최신 PROJ는 EPSG 데이터베이스와 변환 그리드를 바탕으로 출발 CRS와 목표 CRS 사이에서 사용할 수 있는 좌표 연산 후보를 찾는다. 사용 지역, 필요한 그리드의 설치 여부, 정확도에 따라 선택 결과가 달라질 수 있다.</p>
<h2 id="37-파라미터-helmert-변환">3/7 파라미터 Helmert 변환</h2>
<p>서로 다른 3차원 기준 프레임을 맞추는 대표적인 방법이 Helmert 변환이다.</p>
<h3 id="3파라미터">3파라미터</h3>
<p>X, Y, Z축 방향의 평행이동 세 값만 사용한다.</p>
<pre><code class="language-text">dX, dY, dZ</code></pre>
<p>구조가 단순하지만 두 기준 프레임 사이의 회전과 축척 차이는 표현하지 못한다.</p>
<h3 id="7파라미터">7파라미터</h3>
<p>평행이동 3개에 회전 3개와 축척 1개를 더한다.</p>
<pre><code class="language-text">dX, dY, dZ, rX, rY, rZ, scale</code></pre>
<p>여기서 회전 부호 규약이 <code>coordinate frame</code>인지 <code>position vector</code>인지 반드시 확인해야 한다. 같은 숫자도 규약이 다르면 회전 부호 해석이 달라진다.</p>
<p>과거 PROJ 문자열의 <code>+towgs84</code>는 3개 또는 7개 값을 사용해 WGS 84로 가는 변환을 표현했다. 최신 작업에서는 이 값만 수동으로 붙이기보다 CRS 코드와 PROJ 데이터베이스가 제공하는 좌표 연산을 우선 검토하는 편이 낫다. WGS 84의 어떤 실현을 뜻하는지 명확하지 않아 불확실성이 생길 수 있기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/56a1aebd-8036-43c7-b408-9ef5bf0f72ef/image.png" alt="평행이동, 회전량, 축척변화를 사용하는 7파라미터 변환식 예시"></p>
<h2 id="10파라미터-방식은-무엇이-다를까">10파라미터 방식은 무엇이 다를까</h2>
<p>Molodensky–Badekas 방식은 7개 Helmert 파라미터에 회전 중심 좌표 3개를 더한다. 지구 중심이 아니라 관심 지역 가까운 점을 회전 중심으로 삼아 지역 변환 모델을 만들 수 있다.</p>
<p>다만 파라미터가 많다고 언제나 더 정확한 것은 아니다. 값이 어떤 관측망과 지역에서 추정됐는지, 데이터가 그 유효 범위 안에 있는지가 중요하다.</p>
<h2 id="그리드-변환이-더-적합한-경우도-있다">그리드 변환이 더 적합한 경우도 있다</h2>
<p>3/7파라미터 방식은 기준 프레임 사이의 차이를 하나의 전역 모델로 근사한다. 지역마다 변형이 다르면 위치별 보정값을 담은 그리드 변환이 더 정확할 수 있다.</p>
<p>PROJ는 수평/수직 변환 그리드와 변환 파이프라인을 지원한다. 그러나 필요한 그리드 파일이 설치되지 않으면 더 낮은 정확도의 대체 연산이 선택되거나 변환이 실패할 수 있다. 결과 좌표뿐 아니라 사용한 연산 이름과 예상 정확도도 로그에 남기는 편이 좋다.</p>
<h2 id="projinfo로-확인하는-습관"><code>projinfo</code>로 확인하는 습관</h2>
<pre><code class="language-bash">projinfo EPSG:4326
projinfo -s EPSG:5179 -t EPSG:4326 --spatial-test intersects</code></pre>
<p>첫 번째 명령은 CRS 정의를 WKT와 PROJ 형식으로 보여 준다. 두 번째는 출발 CRS와 목표 CRS 사이에서 가능한 좌표 연산을 확인하는 데 쓸 수 있다.</p>
<p>확인할 항목은 다음과 같다.</p>
<ul>
<li>축 순서와 단위</li>
<li>area of use</li>
<li>선택된 좌표 연산과 정확도</li>
<li>필요한 그리드의 설치 여부</li>
<li>변환 방향과 회전 규약</li>
</ul>
<h2 id="정리">정리</h2>
<p>WKT와 PROJ 문자열은 CRS를 표현하는 형식이지 좌표 변환의 품질을 자동으로 보장하는 주문이 아니다. WKT2가 더 풍부한 정보를 담을 수 있고, 둘 사이 변환은 정보 손실이 생길 수 있다.</p>
<p>좌표 변환은 <code>역투영 → 기준 프레임 변환 → 투영</code>으로 이해할 수 있지만, 실제로는 적용 지역과 가용한 변환 모델까지 고려해 연산을 선택해야 한다. 좌표가 변환됐다는 사실보다 <strong>어떤 연산으로 어느 정확도에서 변환됐는지</strong>가 중요하다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://proj.org/en/stable/faq.html">PROJ FAQ</a></li>
<li><a href="https://proj.org/en/stable/usage/transformation.html">PROJ Geodetic transformation</a></li>
<li><a href="https://proj.org/en/stable/operations/transformations/helmert.html">PROJ Helmert transform</a></li>
<li><a href="https://proj.org/en/stable/apps/projinfo.html">PROJ projinfo</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[EPSG:4326, 3857, 5179, 5186은 언제 쓸까: 국내 GIS 좌표계]]></title>
            <link>https://velog.io/@yujin_1219/epsg4326-3857-5179-5186%EC%9D%80-%EC%96%B8%EC%A0%9C-%EC%93%B8%EA%B9%8C-%EA%B5%AD%EB%82%B4-gis-%EC%A2%8C%ED%91%9C%EA%B3%84-%EC%9E%85%EB%AC%B8</link>
            <guid>https://velog.io/@yujin_1219/epsg4326-3857-5179-5186%EC%9D%80-%EC%96%B8%EC%A0%9C-%EC%93%B8%EA%B9%8C-%EA%B5%AD%EB%82%B4-gis-%EC%A2%8C%ED%91%9C%EA%B3%84-%EC%9E%85%EB%AC%B8</guid>
            <pubDate>Tue, 13 Oct 2026 08:33:56 GMT</pubDate>
            <description><![CDATA[<p>GIS 파일을 처음 열었을 때 가장 당황스러웠던 것은 <code>EPSG:4326</code>, <code>EPSG:3857</code>, <code>EPSG:5179</code> 같은 숫자였다. 모두 좌표계 이름처럼 쓰이지만 역할은 꽤 다르다. 코드를 잘못 지정하면 데이터가 바다로 날아가고, 표시가 정상이어도 거리와 면적이 틀릴 수 있다.</p>
<p>이번 글은 국내 GIS 작업에서 자주 만나는 코드를 “무엇인지”보다 “언제 선택하는지” 중심으로 정리한다.</p>
<h2 id="epsg-코드는-좌표-변환-공식-하나가-아니다">EPSG 코드는 좌표 변환 공식 하나가 아니다</h2>
<p>EPSG 코드는 좌표참조계(CRS), 좌표 변환, 단위 같은 측지 항목을 식별하는 번호다. CRS 코드에는 기준계, 타원체, 축, 단위, 투영법, 적용 지역 같은 정보가 연결되어 있다.</p>
<p>따라서 파일에 EPSG 코드를 지정한다는 것은 좌표 숫자를 바꾸는 일이 아니다. 기존 숫자를 어떤 규칙으로 해석할지 선언하는 일이다. 실제 좌표 숫자를 다른 CRS에 맞게 바꾸는 것은 재투영(reprojection)이다.</p>
<p>이 차이를 놓치면 가장 흔한 사고가 생긴다.</p>
<pre><code class="language-text">잘못된 처리: 원래 좌표는 그대로 두고 CRS 라벨만 4326 → 5179로 변경
올바른 처리: 원본 CRS를 정확히 지정한 뒤 5179로 재투영</code></pre>
<h2 id="epsg4326--gps와-데이터-교환에서-자주-만나는-위경도">EPSG:4326 : GPS와 데이터 교환에서 자주 만나는 위경도</h2>
<p>EPSG:4326은 WGS 84 기반의 지리좌표계다. 좌표 단위는 도이며 전 세계 위치를 표현할 수 있다. GPS와 GeoJSON 기반 API, 위치 데이터 교환에서 매우 흔하다.</p>
<p>다만 두 가지를 주의해야 한다.</p>
<p>첫째, 도는 미터가 아니다. 위도 1도와 경도 1도의 실제 길이는 같지 않고 경도의 길이는 위도에 따라 달라진다. 단순한 평면 거리/면적 계산에 그대로 쓰면 안 된다.</p>
<p>둘째, 축 순서다. EPSG 정의의 축 순서는 위도, 경도지만 많은 개발 도구와 GeoJSON은 경도, 위도를 사용한다. <code>x=경도, y=위도</code>라는 관습이 언제나 공식 축 순서와 같은 것은 아니다.</p>
<h2 id="epsg3857--웹-지도-화면에-강한-좌표계">EPSG:3857 : 웹 지도 화면에 강한 좌표계</h2>
<p>EPSG:3857은 WGS 84 / Pseudo-Mercator로, 대부분의 웹 지도 타일에서 사용한다. 세계 지도를 정사각형 타일로 나누고 확대 수준별로 캐시하기 편해 빠른 화면 표시에 적합하다.</p>
<p>하지만 위도가 높아질수록 크기가 과장된다. 지도에서 그린란드가 실제보다 매우 커 보이는 현상이 대표적이다. 따라서 3857은 “단위가 미터니까 실제 거리와 면적도 정확하다”라고 해석하면 안 된다.</p>
<p>화면의 베이스맵과 겹쳐 보여 주는 용도에는 편하지만, 정확한 지역 면적 산정은 목적에 맞는 다른 CRS에서 수행하는 편이 낫다.</p>
<h2 id="epsg5179--대한민국-전역을-하나로-다루기-편한-통합-좌표계">EPSG:5179 : 대한민국 전역을 하나로 다루기 편한 통합 좌표계</h2>
<p>EPSG:5179는 <code>KGD2002 / Unified CS</code>다. Korea 2000 측지 기준과 GRS 80 타원체를 사용하며 대한민국 육상과 해상을 하나의 좌표계로 다룬다. 단위는 미터다.</p>
<p>전국 단위 데이터나 서로 다른 지역 자료를 하나의 평면 좌표로 통합할 때 편하다. 중앙자오선은 127.5도이고 false easting과 false northing을 사용해 좌표를 양수 영역으로 옮긴다.</p>
<p>다만 “국내 데이터면 무조건 5179”는 아니다. EPSG 등록 정보에는 사용 범위와 목적이 있고, 더 좁은 지역에서 높은 정밀도가 필요하면 지역별 원점 좌표계가 적합할 수 있다.</p>
<h2 id="epsg51855188--지역별-원점을-둔-korea-2000-좌표계">EPSG:5185~5188 : 지역별 원점을 둔 Korea 2000 좌표계</h2>
<p><img src="blob:https://velog.io/dcd2c20d-1bff-40d0-8da2-702ed51c5378" alt="업로드중.."></p>
<p>이 네 좌표계는 KGD2002와 GRS 80을 기반으로 하며 횡메르카토르(Transverse Mercator) 투영을 사용한다. 서로 다른 중앙자오선을 사용해 관심 지역의 왜곡을 줄인다.</p>
<table>
<thead>
<tr>
<th>코드</th>
<th>이름</th>
<th align="right">중앙자오선</th>
</tr>
</thead>
<tbody><tr>
<td>EPSG:5185</td>
<td>Korea 2000 / West Belt 2010</td>
<td align="right">125°</td>
</tr>
<tr>
<td>EPSG:5186</td>
<td>Korea 2000 / Central Belt 2010</td>
<td align="right">127°</td>
</tr>
<tr>
<td>EPSG:5187</td>
<td>Korea 2000 / East Belt 2010</td>
<td align="right">129°</td>
</tr>
<tr>
<td>EPSG:5188</td>
<td>Korea 2000 / East Sea Belt 2010</td>
<td align="right">131°</td>
</tr>
</tbody></table>
<p>지역 이름만 보고 감으로 고르기보다 데이터 제공기관의 메타데이터와 EPSG의 area of use를 확인해야 한다. 과거 Bessel 기반 좌표계나 2010 이전 정의도 함께 유통되기 때문에 숫자 범위가 비슷하다는 이유로 추정하면 위험하다.</p>
<h2 id="실무에서-선택하는-순서">실무에서 선택하는 순서</h2>
<p>좌표계를 고를 때는 다음 순서가 도움이 됐다.</p>
<ol>
<li>원본 데이터의 CRS를 확인한다. <code>.prj</code>, GeoPackage 메타데이터, GeoTIFF 태그, API 문서를 본다.</li>
<li>결과의 목적을 정한다. 화면 표시인지, 데이터 교환인지, 거리/면적 분석인지 구분한다.</li>
<li>분석 지역을 확인한다. 전 세계, 대한민국 전역, 특정 지역 중 어디인지 본다.</li>
<li>좌표계의 단위, 축 순서, 유효 지역을 확인한다.</li>
<li>변환할 때 사용된 좌표 연산과 예상 정확도를 기록한다.</li>
</ol>
<h2 id="좌표가-그럴듯하다는-검증이-아니다"><code>좌표가 그럴듯하다</code>는 검증이 아니다</h2>
<p>국내 평면 좌표는 수십만~수백만 단위 숫자로 보이는 경우가 많다. 하지만 그 범위만으로 EPSG 코드를 단정할 수 없다. false easting과 false northing, 축 순서, 지역 원점에 따라 유사한 값이 나올 수 있다.</p>
<p>샘플 지점을 하나 골라 알려진 위치와 대조하고, 데이터 전체의 경계 상자(bounding box)를 확인하는 것이 좋다.</p>
<pre><code class="language-text">원본 CRS 확인 → 재투영 → 지도 중첩 확인 → 거리/면적 샘플 검증</code></pre>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://epsg.io/5179">EPSG:5179 : KGD2002 / Unified CS</a></li>
<li><a href="https://proj.org/en/stable/faq.html">PROJ FAQ : CRS와 축 순서</a></li>
<li><a href="https://developers.arcgis.com/documentation/spatial-references/">Esri Spatial references</a></li>
<li><a href="https://www.iogp.org/bookstore/wp-content/uploads/sites/2/woocommerce_uploads/2017/01/373-23.pdf">IOGP Guidance Note 23 : Web Mercator</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[위도와 경도는 어디를 기준으로 할까: 지오이드, 타원체, GCS와 PCS]]></title>
            <link>https://velog.io/@yujin_1219/%EC%9C%84%EB%8F%84%EC%99%80-%EA%B2%BD%EB%8F%84%EB%8A%94-%EC%96%B4%EB%94%94%EB%A5%BC-%EA%B8%B0%EC%A4%80%EC%9C%BC%EB%A1%9C-%ED%95%A0%EA%B9%8C-%EC%A7%80%EC%98%A4%EC%9D%B4%EB%93%9C-%ED%83%80%EC%9B%90%EC%B2%B4-gcs%EC%99%80-pcs</link>
            <guid>https://velog.io/@yujin_1219/%EC%9C%84%EB%8F%84%EC%99%80-%EA%B2%BD%EB%8F%84%EB%8A%94-%EC%96%B4%EB%94%94%EB%A5%BC-%EA%B8%B0%EC%A4%80%EC%9C%BC%EB%A1%9C-%ED%95%A0%EA%B9%8C-%EC%A7%80%EC%98%A4%EC%9D%B4%EB%93%9C-%ED%83%80%EC%9B%90%EC%B2%B4-gcs%EC%99%80-pcs</guid>
            <pubDate>Tue, 13 Oct 2026 08:31:58 GMT</pubDate>
            <description><![CDATA[<p>처음 위도와 경도를 배울 때는 지구에 원래부터 그어진 절대 좌표라고 생각했다. 하지만 같은 장소도 어떤 지구 모델과 기준을 사용하느냐에 따라 좌표가 달라질 수 있다. 좌표는 숫자만으로 완성되지 않고 <strong>그 숫자를 해석하는 규칙</strong>까지 있어야 한다.</p>
<p>이 글에서는 지오이드와 타원체에서 시작해 지리좌표계(GCS)와 투영좌표계(PCS)가 왜 필요한지 연결해서 정리한다.</p>
<h2 id="울퉁불퉁한-지구를-그대로-계산하기-어려웠다">울퉁불퉁한 지구를 그대로 계산하기 어려웠다</h2>
<p>지구는 완전한 구가 아니다. 자전 때문에 적도 쪽이 조금 부풀어 있고 지형과 중력 분포도 고르지 않다. 이 복잡한 모양을 그대로 계산에 쓰기 어려워 서로 다른 두 기준면을 사용한다.</p>
<h3 id="지오이드-물이-중력만-따라-놓였다고-상상한-면">지오이드: 물이 중력만 따라 놓였다고 상상한 면</h3>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/c10569e0-3925-45ff-86b1-337828f3c99e/image.png" alt=""></p>
<p>지오이드(geoid)는 바람과 조류 같은 영향을 제외한 평균 해수면을 육지 아래까지 연장한 것에 가까운 중력 등퍼텐셜면이다. 중력장이 고르지 않기 때문에 표면이 매끈한 타원체처럼 단순하지 않다.</p>
<p>우리가 일상에서 말하는 “해발 100m”는 보통 지오이드에 가까운 기준면에서 잰 정표고(orthometric height)다.</p>
<h3 id="타원체-계산하기-쉽게-만든-매끈한-모델">타원체: 계산하기 쉽게 만든 매끈한 모델</h3>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/a7dc1e49-eeda-4c10-8be9-7fe1f5b55d22/image.png" alt=""></p>
<p>타원체(ellipsoid)는 타원을 회전시켜 만든 수학적 표면이다. 장반경과 편평률 같은 몇 개의 값으로 표현할 수 있어 위도/경도 계산과 지도 투영에 적합하다. WGS 84와 GRS 80은 자주 만나는 기준 타원체다.</p>
<p>GNSS가 제공하는 높이는 일반적으로 타원체고(ellipsoidal height)다. 지도에서 원하는 해발 높이와는 기준이 다르다.</p>
<p>높이 관계는 보통 다음처럼 쓴다.</p>
<pre><code class="language-text">h = H + N

h: 타원체고
H: 정표고(해발 높이에 가까운 값)
N: 지오이드고, 타원체와 지오이드 사이의 간격</code></pre>
<p>정밀 작업에서는 사용하는 지오이드 모델과 높이 기준을 함께 확인해야 한다.</p>
<h2 id="타원체만-정하면-좌표계가-완성될까">타원체만 정하면 좌표계가 완성될까</h2>
<p>같은 모양의 타원체도 지구에 어떻게 배치하고 방향을 맞출지 정해야 한다. 이 기준을 측지 기준계 또는 데이텀(datum)이라고 부른다. 실무에서는 타원체, 기준 프레임, 기준 시점, 좌표계가 함께 얽혀 있으므로 “WGS 84 좌표”처럼 기준을 붙여 말한다.</p>
<p>예전의 지역 측지계는 특정 지역에 잘 맞도록 타원체를 배치했다. 위성 관측을 사용하는 전 지구 기준과 겹치면 같은 위치에 숫자 차이가 생길 수 있다. 과거 도면을 GPS 데이터와 합칠 때 단순히 EPSG 코드만 바꾸면 안 되는 이유다.</p>
<h2 id="gcs-지구-표면의-위치를-각도로-표현한다">GCS: 지구 표면의 위치를 각도로 표현한다</h2>
<p>GCS(Geographic Coordinate System), 즉 지리좌표계는 기준 타원체 위의 위치를 위도와 경도로 나타낸다.</p>
<ul>
<li>위도: 적도를 기준으로 북쪽과 남쪽의 각도</li>
<li>경도: 본초자오선을 기준으로 동쪽과 서쪽의 각도</li>
<li>단위: 보통 도(degree)</li>
</ul>
<p>예를 들어 <code>37.4979, 127.0276</code>이라는 값만 보면 서울 부근처럼 보인다. 하지만 순서가 위도/경도인지 경도/위도인지, CRS가 무엇인지 모르면 안전하게 사용할 수 없다.</p>
<p>특히 EPSG 정의에서 EPSG:4326의 공식 축 순서는 위도, 경도다. 반면 GeoJSON과 많은 웹 API는 관습적으로 경도, 위도 순서를 쓴다. 라이브러리가 어떤 규칙을 따르는지 확인하지 않으면 한국의 좌표가 전혀 다른 곳으로 갈 수 있다.</p>
<h2 id="pcs-곡면을-평면에-투영한다">PCS: 곡면을 평면에 투영한다</h2>
<p>PCS(Projected Coordinate System), 즉 투영좌표계는 지구의 곡면을 평면으로 옮기고 위치를 X, Y 같은 선형 좌표로 나타낸다. 단위는 흔히 미터지만 좌표계에 따라 다른 단위도 가능하다.</p>
<p>평면 좌표를 쓰면 버퍼, 거리, 면적 같은 연산을 다루기 편해진다. 그러나 오렌지 껍질을 찢거나 늘리지 않고 평평하게 펼칠 수 없듯이, 곡면을 평면으로 옮기면 왜곡이 반드시 생긴다.</p>
<p>투영법마다 보존하려는 성질이 다르다.</p>
<table>
<thead>
<tr>
<th>목적</th>
<th>중요하게 보는 성질</th>
</tr>
</thead>
<tbody><tr>
<td>항해/방향</td>
<td>각도와 국소 형상</td>
</tr>
<tr>
<td>통계 지도</td>
<td>면적</td>
</tr>
<tr>
<td>특정 경로 비교</td>
<td>거리</td>
</tr>
<tr>
<td>지역 실무 지도</td>
<td>관심 지역 안의 왜곡 최소화</td>
</tr>
</tbody></table>
<p>모든 위치에서 거리, 면적, 방향, 모양을 동시에 완벽하게 보존하는 투영법은 없다.</p>
<h2 id="언제-gcs를-쓰고-언제-pcs를-쓸까">언제 GCS를 쓰고 언제 PCS를 쓸까</h2>
<p>“표시는 GCS, 분석은 PCS”라고 외우기 쉽지만 조금 더 정확히는 작업 목적과 라이브러리의 계산 방식을 봐야 한다.</p>
<ul>
<li>위치 교환과 저장: EPSG:4326 같은 지리좌표계를 자주 사용한다.</li>
<li>웹 지도 타일 표시: EPSG:3857이 흔하다.</li>
<li>지역 단위 거리/면적/버퍼: 해당 지역과 목적에 맞는 투영좌표계를 선택한다.</li>
<li>전 지구 거리: 구면/타원체 계산을 지원하는 geography 타입이나 측지 연산을 사용할 수 있다.</li>
</ul>
<p>위도/경도를 미터처럼 빼서 거리를 계산하거나, Web Mercator의 평면 면적을 실제 면적으로 단정하면 안 된다.</p>
<h2 id="좌표를-받으면-먼저-확인할-것">좌표를 받으면 먼저 확인할 것</h2>
<pre><code class="language-text">1. CRS 또는 EPSG 코드는 무엇인가?
2. 축 순서는 위도-경도인가, 경도-위도인가?
3. 좌표 단위는 도인가, 미터인가?
4. 높이가 있다면 타원체고인가, 정표고인가?
5. 데이터의 유효 지역과 기준 시점은 무엇인가?</code></pre>
<p>좌표값이 그럴듯하게 보인다는 이유만으로 기준을 추측하면 오류가 늦게 발견된다. 원본 데이터의 메타데이터를 보존하는 습관이 중요하다.</p>
<h2 id="정리">정리</h2>
<p>지오이드는 중력을 반영한 높이의 기준면이고 타원체는 위치 계산을 위한 수학적 모델이다. GCS는 타원체 위 위치를 각도로 표현하고, PCS는 그 곡면을 평면에 투영해 선형 좌표로 표현한다.</p>
<p>결국 좌표는 숫자가 아니라 <code>숫자 + 기준면 + 기준계 + 축 + 단위</code>의 묶음이다. 다음 글에서는 국내 데이터에서 자주 마주치는 EPSG 코드를 이 관점으로 비교한다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://geodesy.noaa.gov/PUBS_LIB/Geodesy4Layman/geo4lay.pdf">NOAA, Geodesy for the Layman</a></li>
<li><a href="https://proj.org/en/stable/faq.html">PROJ FAQ: CRS와 축 순서</a></li>
<li><a href="https://proj.org/en/stable/development/quickstart.html">PROJ API 좌표 순서 설명</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[GIS를 처음 공부하며 정리한 공간 데이터와 공간정보의 차이]]></title>
            <link>https://velog.io/@yujin_1219/gis%EB%A5%BC-%EC%B2%98%EC%9D%8C-%EA%B3%B5%EB%B6%80%ED%95%98%EB%A9%B0-%EC%A0%95%EB%A6%AC%ED%95%9C-%EA%B3%B5%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%99%80-%EA%B3%B5%EA%B0%84%EC%A0%95%EB%B3%B4%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@yujin_1219/gis%EB%A5%BC-%EC%B2%98%EC%9D%8C-%EA%B3%B5%EB%B6%80%ED%95%98%EB%A9%B0-%EC%A0%95%EB%A6%AC%ED%95%9C-%EA%B3%B5%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%99%80-%EA%B3%B5%EA%B0%84%EC%A0%95%EB%B3%B4%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Tue, 13 Oct 2026 08:27:52 GMT</pubDate>
            <description><![CDATA[<p>지도 위에 점을 찍는 기능을 만들면 곧 GIS를 다루는 것이라고 생각했다. 그런데 자료를 찾아볼수록 지도는 결과를 보여 주는 화면일 뿐이었다. GIS에서 더 중요한 질문은 <strong>무엇을 어디에 저장하고, 위치를 기준으로 어떤 관계를 계산할 것인가</strong>였다.</p>
<p>이번 글에서는 GIS를 처음 접할 때 자주 섞어 쓰는 공간 데이터와 공간정보를 구분하고, 실제 서비스에서는 두 개념이 어떻게 이어지는지 정리한다.</p>
<h2 id="공간-데이터는-어디-속성-데이터는-무엇">공간 데이터는 <code>어디</code>, 속성 데이터는 <code>무엇</code></h2>
<p>카페 한 곳을 예로 들어 보자.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>담는 내용</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>공간 데이터</td>
<td>위치와 모양</td>
<td>경도/위도, 건물 경계</td>
</tr>
<tr>
<td>속성 데이터</td>
<td>대상의 특성</td>
<td>상호, 업종, 영업시간, 매출</td>
</tr>
</tbody></table>
<p>지도 위의 좌표만으로는 그 점이 무엇인지 알기 어렵다. 반대로 카페 이름과 영업시간만 있고 위치가 없으면 주변 상권이나 접근성을 분석할 수 없다. 두 데이터가 같은 객체를 가리키도록 연결되어야 비로소 “반경 500m 안에 경쟁 매장이 몇 개인가?” 같은 질문을 할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/e5f0a9c5-3373-4e04-b96c-81f278ebef3d/image.png" alt="현실의 대상을 벡터와 래스터로 표현한 비교 이미지"></p>
<p>공간 데이터가 원재료라면 공간정보는 그 데이터를 수집/가공/분석해 의사결정에 쓸 수 있게 만든 결과에 가깝다. 국내 법령에서도 공간정보를 공간에 존재하는 객체의 위치 정보와 공간 인지/의사결정에 필요한 정보로 정의한다.</p>
<h2 id="점-선-면만-알면-끝일까">점, 선, 면만 알면 끝일까</h2>
<p>벡터 데이터는 현실의 객체를 점, 선, 면으로 단순화한다.</p>
<ul>
<li>점(Point): 정류장, 매장, 관측소처럼 하나의 위치</li>
<li>선(LineString): 도로, 하천, 이동 경로처럼 이어진 대상</li>
<li>면(Polygon): 건물, 필지, 행정구역처럼 경계를 가진 영역</li>
</ul>
<p>여기서 “현실의 대상을 어떤 도형으로 볼 것인가”는 정답이 하나가 아니다. 전국 규모 지도에서 도시는 점으로 충분할 수 있지만, 도시계획을 할 때는 행정구역 면이 필요하다. 도로도 길 찾기에서는 선이지만 도로 점유 면적을 계산할 때는 면으로 다뤄야 한다.</p>
<p>즉, 공간 모델은 현실을 그대로 복사한 것이 아니라 <strong>분석 목적에 맞게 현실을 추상화한 것</strong>이다.</p>
<h2 id="연속적으로-변하는-현상은-래스터가-자연스럽다">연속적으로 변하는 현상은 래스터가 자연스럽다</h2>
<p>기온, 고도, 위성영상처럼 공간 전체에 값이 이어지는 현상은 격자 형태의 래스터 데이터가 잘 맞는다. 래스터는 바둑판처럼 공간을 같은 크기의 셀로 나누고 각 셀에 값을 저장한다.</p>
<p>셀을 작게 만들수록 세부 표현은 좋아지지만 파일 크기와 처리량이 커진다. 반대로 셀이 크면 가볍지만 작은 지형이나 경계가 뭉개질 수 있다. 래스터의 해상도는 단순한 화질 옵션이 아니라 분석 결과를 좌우하는 설계 값이다.</p>
<h2 id="gis-서비스에서-실제로-일어나는-일">GIS 서비스에서 실제로 일어나는 일</h2>
<p>처음에는 GIS 서비스를 “지도를 보여 주는 서비스”라고 이해했다. 실제 흐름은 조금 더 길다.</p>
<ol>
<li>GPS, 측량, 센서, 행정자료, 위성영상 등에서 데이터를 수집한다.</li>
<li>서로 다른 파일 형식과 좌표계를 정리한다.</li>
<li>도형과 속성을 연결해 공간 데이터셋을 만든다.</li>
<li>거리, 포함, 교차, 밀도, 경로 등을 분석한다.</li>
<li>분석 결과를 지도, 표, 알림 또는 API로 제공한다.</li>
</ol>
<p>예를 들어 침수 위험 서비스는 단순히 침수 지도를 한 장 띄우는 것으로 끝나지 않는다. 고도, 하천, 배수 시설, 강우량, 건물과 도로 데이터를 같은 공간 기준으로 맞추고, 위험 지역과 생활 시설이 겹치는지를 분석해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/d27109be-a52e-4c95-a65a-a176520918b8/image.png" alt="침수지도, 산불 현황, 지적 기준점, 지명정보 등 공간정보 서비스 사례"></p>
<h2 id="어디에-쓰일까">어디에 쓰일까</h2>
<p>공간정보는 생각보다 일상 가까이에 있다.</p>
<ul>
<li>내비게이션: 도로망을 그래프로 보고 이동 시간과 통행 조건을 반영해 경로를 찾는다.</li>
<li>재난 대응: 산불/침수 예상 범위와 인구/시설 데이터를 겹쳐 우선 대응 지역을 정한다.</li>
<li>상권 분석: 유동인구, 점포, 교통, 주거 데이터를 위치 기준으로 결합한다.</li>
<li>도시 관리: 건물, 도로, 지하시설물의 위치와 상태를 함께 관리한다.</li>
<li>물류: 배송지와 도로망을 이용해 차량별 경로를 배분한다.</li>
</ul>
<p>공통점은 일반 데이터에 위치라는 축이 하나 더 생겼다는 것이다. 위치가 붙으면 단순 집계뿐 아니라 가까움, 겹침, 연결, 이동 같은 관계를 계산할 수 있다.</p>
<h2 id="개발자-관점에서-달라지는-점">개발자 관점에서 달라지는 점</h2>
<p>일반적인 목록 API만 생각하고 공간 기능을 붙이면 몇 가지 문제를 만나기 쉽다.</p>
<p>첫째, 좌표에는 좌표참조계(CRS)가 필요하다. 숫자 두 개가 같아도 기준 좌표계가 다르면 서로 다른 위치일 수 있다.</p>
<p>둘째, 거리의 단위를 확인해야 한다. 위도/경도는 각도이고 투영 좌표는 보통 미터다. 좌표만 보고 무조건 피타고라스 공식을 적용하면 안 된다.</p>
<p>셋째, 공간 인덱스가 필요하다. 모든 도형을 하나씩 비교하는 쿼리는 데이터가 늘면 급격히 느려진다. PostGIS 같은 공간 데이터베이스는 R-tree 계열 탐색을 지원하는 GiST 인덱스를 활용할 수 있다.</p>
<p>넷째, 화면에 보이는 모양과 분석에 적합한 좌표계가 다를 수 있다. 웹 지도 표시에 흔히 쓰는 좌표계를 거리/면적 계산에도 그대로 쓰면 왜곡이 결과에 섞인다.</p>
<h2 id="정리">정리</h2>
<p>GIS를 공부하며 가장 먼저 바뀐 생각은 “지도 데이터”가 곧 하나의 이미지가 아니라는 점이었다. 현실의 객체를 점/선/면이나 격자로 표현하고, 그 도형에 속성을 연결하고, 위치 관계를 계산한 결과가 서비스가 된다.!</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://www.law.go.kr/%EB%B2%95%EB%A0%B9/%EA%B5%AD%EA%B0%80%EA%B3%B5%EA%B0%84%EC%A0%95%EB%B3%B4%EA%B8%B0%EB%B3%B8%EB%B2%95">국가공간정보 기본법</a></li>
<li><a href="https://gdal.org/en/stable/user/vector_data_model.html">GDAL Vector Data Model</a></li>
<li><a href="https://postgis.net/docs/reference.html#Spatial_Relationships">PostGIS 공간 관계 함수</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[큰 동영상 업로드를 설계하며 배운 것: 객체 저장소부터 상태 머신까지]]></title>
            <link>https://velog.io/@yujin_1219/%ED%81%B0-%EB%8F%99%EC%98%81%EC%83%81-%EC%97%85%EB%A1%9C%EB%93%9C%EB%A5%BC-%EC%84%A4%EA%B3%84%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B4-%EA%B2%83-%EA%B0%9D%EC%B2%B4-%EC%A0%80%EC%9E%A5%EC%86%8C%EB%B6%80%ED%84%B0-%EC%83%81%ED%83%9C-%EB%A8%B8%EC%8B%A0%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@yujin_1219/%ED%81%B0-%EB%8F%99%EC%98%81%EC%83%81-%EC%97%85%EB%A1%9C%EB%93%9C%EB%A5%BC-%EC%84%A4%EA%B3%84%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B4-%EA%B2%83-%EA%B0%9D%EC%B2%B4-%EC%A0%80%EC%9E%A5%EC%86%8C%EB%B6%80%ED%84%B0-%EC%83%81%ED%83%9C-%EB%A8%B8%EC%8B%A0%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Tue, 13 Oct 2026 07:48:27 GMT</pubDate>
            <description><![CDATA[<h2 id="한-문단-요약">한 문단 요약</h2>
<p>웹 서비스에 이미지와 동영상 업로드를 붙일 때 처음에는 파일을 API 서버가 받아 저장소로 넘기면 된다고 생각했다. 작은 이미지는 이 방식으로도 충분했지만 동영상이 커지자 서버 메모리, 네트워크 대역폭, 요청 시간, 실패 복구가 한꺼번에 문제가 됐다. 해결 과정에서 실제 파일은 객체 저장소에 두고 DB에는 식별자와 상태를 기록하는 구조, presigned URL을 이용한 브라우저 직접 업로드, FFprobe와 FFmpeg를 이용한 검증과 remux, 체크섬 확인, 상태 머신과 정리 작업이 서로 따로 떨어진 기능이 아니라 하나의 흐름이라는 점을 배웠다.</p>
<blockquote>
<p><strong>먼저 결론부터 정리하면</strong></p>
<ul>
<li>큰 파일의 바이트는 API 서버를 거치지 않고 객체 저장소로 직접 보낸다.</li>
<li>업로드된 원본은 곧바로 서비스하지 않고 검사와 처리를 거쳐 <code>READY</code>로 승격한다.</li>
<li>외부에는 저장 키 대신 <code>mediaId</code>를 노출하고 실제 접근 권한은 매번 서버에서 확인한다.</li>
<li>DB와 객체 저장소의 불일치는 상태, 재시도, 정리 작업으로 복구한다.</li>
</ul>
</blockquote>
<h2 id="문제를-만나게-된-배경">문제를 만나게 된 배경</h2>
<p>처음 구현한 이미지 업로드는 단순했다. 브라우저가 multipart/form-data 요청을 보내면 API 서버가 파일을 받고 객체 저장소에 올렸다. 파일도 작고 요청 수도 많지 않을 때는 이해하기 쉽고 권한 검사도 한곳에서 할 수 있는 괜찮은 선택이었다.</p>
<p>같은 방식을 동영상에 적용하자 얘기가 달라졌다. 브라우저에서 API 서버로 한 번, API 서버에서 객체 저장소로 다시 한 번 데이터가 이동했다. 서버가 요청 본문을 버퍼에 담으면 메모리가 흔들렸고 스트리밍으로 넘겨도 연결을 오래 붙잡고 네트워크 대역폭을 사용한다는 사실은 바뀌지 않았다. 중간에 연결이 끊어지면 큰 파일을 처음부터 다시 보내야 하는 상황도 생겼다.</p>
<p>파일을 저장한 뒤에도 질문이 이어졌다. 파일 이름과 URL을 DB에 그대로 넣어도 되는지, 비공개 동영상을 브라우저에 어떻게 보여줄지, 확장자가 <code>.mp4</code>인 파일을 정말 동영상이라고 믿어도 되는지 정해야 했다. DB 저장은 성공했는데 객체 업로드가 실패하거나 반대 상황이 생기면 무엇을 기준으로 복구할지도 애매했다.</p>
<p>이때부터 업로드를 단일 API 호출로 보지 않고 여러 단계로 진행되는 작업으로 다시 보기 시작했다.</p>
<table>
<thead>
<tr>
<th>처음 생각한 방식</th>
<th>실제로 만난 문제</th>
<th>바꾼 방향</th>
</tr>
</thead>
<tbody><tr>
<td>모든 파일을 API 서버로 받기</td>
<td>큰 동영상이 서버 연결과 대역폭을 오래 점유</td>
<td>브라우저에서 객체 저장소로 직접 업로드</td>
</tr>
<tr>
<td>업로드 성공이면 바로 사용</td>
<td>확장자와 <code>Content-Type</code>만으로 실제 형식을 판단하기 어려움</td>
<td>임시 저장 후 FFprobe 검사와 remux</td>
</tr>
<tr>
<td>파일 URL을 문서에 저장</td>
<td>저장 구조 변경과 권한 검사가 문서에 얽힘</td>
<td>문서에는 <code>mediaId</code>만 저장</td>
</tr>
<tr>
<td>요청 한 번으로 끝내기</td>
<td>DB와 객체 저장소가 서로 다른 시점에 실패</td>
<td>상태 머신, 재시도, 정리 작업 도입</td>
</tr>
</tbody></table>
<h2 id="전체-구조">전체 구조</h2>
<p>구조를 나누고 나니 각 구성 요소가 맡을 일이 선명해졌다.</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>맡은 일</th>
</tr>
</thead>
<tbody><tr>
<td>브라우저</td>
<td>파일 선택, 체크섬 계산, 객체 저장소로 직접 업로드, 완료 통지</td>
</tr>
<tr>
<td>API 서버</td>
<td>사용자 권한과 정책 확인, 업로드 세션과 상태 관리, presigned URL 발급</td>
</tr>
<tr>
<td>객체 저장소</td>
<td>원본과 최종 미디어의 바이트 저장, 부분 다운로드 응답</td>
</tr>
<tr>
<td>DB</td>
<td><code>mediaId</code>, 소유자, 파일 크기, 체크섬, 처리 상태, 메타데이터 저장</td>
</tr>
<tr>
<td>미디어 처리 작업자</td>
<td>FFprobe 검사, FFmpeg remux, 최종본 저장, 실패 기록</td>
</tr>
<tr>
<td>정리 작업</td>
<td>만료된 업로드, 실패 파일, 삭제 대기 파일, 미완료 multipart 정리</td>
</tr>
</tbody></table>
<p>여기서 파일 바이트가 지나는 길과 상태가 바뀌는 길을 구분하는 게 도움이 됐다. 브라우저와 객체 저장소 사이에는 큰 파일이 오가지만 API 서버와 DB에는 권한, 식별자, 상태처럼 작은 정보만 오간다.</p>
<p>문서나 게시물에는 저장 키나 URL을 넣지 않고 서비스 식별자인 <code>mediaId</code>만 남긴다.</p>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;VIDEO&quot;,
  &quot;mediaId&quot;: &quot;...&quot;
}</code></pre>
<p>이렇게 두면 저장소 위치나 URL 발급 방식이 바뀌어도 문서 형식을 고칠 필요가 없다.</p>
<h2 id="핵심-개념별-설명">핵심 개념별 설명</h2>
<h3 id="파일-시스템처럼-보이지만-파일-시스템은-아니다">파일 시스템처럼 보이지만 파일 시스템은 아니다</h3>
<p>객체 저장소를 처음 봤을 때는 인터넷에 연결된 큰 디스크처럼 생각했다. 일반 파일 시스템에서는 디렉터리 안에 파일과 하위 디렉터리가 있고 파일을 열어 일부를 덮어쓸 수 있다. 객체 저장소는 보통 버킷 안에서 키로 객체 전체를 저장하고 읽는 형태에 가깝다.</p>
<p>비유하면 파일 시스템은 서랍과 폴더가 실제로 중첩된 캐비닛이고 객체 저장소는 번호표가 붙은 물품 보관소다. <code>media/123/video.mp4</code>가 폴더처럼 보여도 많은 객체 저장소에서 슬래시는 키 문자열의 일부이며 관리 도구가 보기 좋게 계층처럼 보여줄 뿐이다.</p>
<p>버킷은 객체를 담는 큰 컨테이너다. 객체는 파일 바이트와 메타데이터의 묶음이고 저장 키는 버킷 안에서 객체를 찾는 이름이다. 버킷과 키의 조합으로 객체를 찾는다고 이해하면 된다. AWS 문서도 S3를 버킷 안에서 고유한 키로 객체를 다루는 저장소로 설명한다.</p>
<h3 id="s3와-s3-호환-저장소">S3와 S3 호환 저장소</h3>
<p>S3는 AWS가 제공하는 객체 저장소 서비스다. 업로드, 조회, 삭제, multipart upload 같은 API가 널리 쓰이면서 다른 객체 저장소도 이 API 형식을 구현하기 시작했다. 이런 제품을 S3 호환 저장소라고 부른다.</p>
<p>호환이라는 말이 모든 동작이 완전히 같다는 뜻은 아니다. 같은 SDK와 요청 형식을 상당 부분 재사용할 수 있다는 의미에 가깝다. 지원하는 API, 체크섬 방식, 최대 크기, 일관성, presigned URL 제약은 공급자별 문서를 다시 확인해야 한다. 실제 코드도 endpoint, region, 자격 증명, path-style 사용 여부처럼 달라질 수 있는 값을 설정으로 분리하는 편이 낫다.</p>
<h3 id="파일은-객체-저장소에-상태는-db에">파일은 객체 저장소에, 상태는 DB에</h3>
<p>처음에는 DB에 파일 바이트까지 넣는 방법도 떠올렸다. 하지만 큰 바이너리는 DB 백업과 복구, 복제, 쿼리 성능, 비용에 부담을 줄 수 있다. 객체 저장소는 큰 비정형 데이터를 저장하고 전달하는 데 맞춰져 있고 DB는 관계, 검색 조건, 권한, 상태 전이를 다루는 데 강하다.</p>
<p>그래서 실제 바이트는 객체 저장소에 두고 관리 정보는 DB에 기록한다.</p>
<pre><code class="language-text">mediaId, ownerId, mediaType, status
originalName, contentType, size, checksum
duration, width, height, videoCodec, audioCodec
createdAt, updatedAt, failureReason</code></pre>
<p>내부 저장 키도 DB의 관리 정보에는 필요할 수 있다. 다만 외부 문서와 API의 장기 식별자로 저장 키를 그대로 노출하면 저장 구조 변경이 곧 API 변경이 된다. <code>mediaId</code>를 한 겹 두면 권한 검사, 저장소 이전, 키 규칙 변경, 재처리를 서비스 내부에서 흡수할 수 있다.</p>
<h3 id="api-서버-경유-업로드와-브라우저-직접-업로드">API 서버 경유 업로드와 브라우저 직접 업로드</h3>
<p>API 서버 경유 방식은 서버가 파일을 직접 받는다. 구현이 단순하고 서버에서 요청을 통제하기 쉽지만 큰 파일의 모든 바이트가 API 서버를 지나간다. 직접 업로드 방식은 API 서버가 권한과 정책만 확인한 뒤 제한된 업로드 권한을 브라우저에 잠시 빌려준다. 실제 바이트는 브라우저에서 객체 저장소로 이동한다.</p>
<p>직접 업로드가 API 서버를 완전히 빼는 방식은 아니다. 세션 생성, 권한 확인, 저장 키 결정, 완료 검증, 상태 변경은 여전히 서버가 맡는다. 브라우저가 완료했다고 보낸 말만 믿어서도 안 된다. 서버는 객체의 존재 여부와 크기, 체크섬 등 확인 가능한 정보를 다시 조회해야 한다.</p>
<h3 id="presigned-url은-임시로-서명된-한-번의-행동">Presigned URL은 임시로 서명된 한 번의 행동</h3>
<p>Presigned URL은 서버가 가진 저장소 자격 증명으로 특정 객체와 HTTP 메서드, 만료 시각을 묶어 서명한 URL이다. 브라우저는 저장소 자격 증명을 직접 받지 않고도 지정된 키에 <code>PUT</code>하거나 지정된 객체를 <code>GET</code>할 수 있다. URL을 만든 주체가 원래 갖고 있던 권한보다 더 큰 권한이 생기는 것은 아니다.</p>
<p>저장 키는 비밀번호가 아니다. 비공개 버킷에서는 키를 안다고 바로 객체를 읽을 수 있는 것이 아니며 별도의 권한이 필요하다. 반대로 presigned URL 전체에는 서명과 만료 정보가 들어 있으므로 유효기간 동안 전달받은 사람이 해당 작업을 수행할 수 있다. 접근 로그, 분석 도구, 예외 메시지, 채팅, 리퍼러에 전체 URL이 남지 않도록 다뤄야 한다.</p>
<p>업로드 URL을 만들 때 서명에 포함한 <code>Content-Type</code>이나 체크섬 헤더가 있다면 브라우저 요청도 같은 값을 보내야 한다. 브라우저 직접 업로드에는 객체 저장소의 CORS 설정도 필요하다. 이 설정은 인증을 대신하지 않고 브라우저가 허용된 출처에서 요청할 수 있게 하는 규칙이다.</p>
<h3 id="원본을-임시-영역에-두는-이유">원본을 임시 영역에 두는 이유</h3>
<p>브라우저가 올린 파일은 아직 신뢰할 수 없는 입력이다. 확장자와 <code>Content-Type</code>은 사용자가 바꿀 수 있고 업로드가 끝까지 완료됐는지도 확인 전에는 모른다. 그래서 원본은 먼저 다음과 같은 임시 키에 둔다.</p>
<pre><code class="language-text">media/uploads/{uploadId}/source</code></pre>
<p>검사와 처리가 끝난 파일만 최종 키로 옮긴다.</p>
<pre><code class="language-text">media/{mediaId}/video.mp4</code></pre>
<p>임시 영역과 최종 영역을 나누면 <code>READY</code>가 아닌 파일이 재생 경로에 섞이는 것을 막기 쉽다. 실패 파일의 보관 기한과 정리 규칙도 따로 적용할 수 있다. 다만 키 이름만 나눴다고 접근이 차단되는 것은 아니므로 버킷 정책과 애플리케이션 권한 검사가 함께 있어야 한다.</p>
<h3 id="ffprobe는-파일-이름이-아니라-내부-스트림을-본다">FFprobe는 파일 이름이 아니라 내부 스트림을 본다</h3>
<p><code>.mp4</code> 확장자와 <code>video/mp4</code> 헤더만으로 실제 파일을 판별할 수는 없다. FFprobe는 미디어 컨테이너를 열어 내부 스트림 정보를 기계가 읽기 쉬운 형태로 출력한다. 컨테이너 형식, 비디오와 오디오 코덱, 재생 시간, 해상도, 스트림 수 등을 정책과 비교할 수 있다.</p>
<pre><code class="language-bash">ffprobe -v error \
  -show_format \
  -show_streams \
  -of json \
  input-file</code></pre>
<p>의사 코드로 옮기면 대략 이런 모습이다.</p>
<pre><code class="language-text">probe = runFfprobe(tempObject)

reject if probe cannot be parsed
reject if video stream is missing
reject if container or codec is outside the allowlist
reject if duration, width, height, or stream count exceeds policy</code></pre>
<p>FFprobe가 성공했다고 파일이 모든 면에서 안전하다고 단정할 수는 없다. 손상된 뒤쪽 프레임까지 확인하려면 더 깊은 디코딩 검사가 필요할 수 있고 악성 파일 검사는 별도 영역이다. 어느 수준까지 읽고 검사할지는 서비스 위험도와 처리 비용을 보고 정해야 한다.</p>
<h3 id="stream-copy-remux와-재인코딩은-다르다">Stream-copy remux와 재인코딩은 다르다</h3>
<p>remux는 인코딩된 비디오와 오디오 스트림을 다른 컨테이너에 다시 담는 작업이다. <code>-c copy</code>를 사용한 stream copy는 패킷을 디코딩하고 다시 인코딩하지 않는다.</p>
<pre><code class="language-bash">ffmpeg -i input-file \
  -map 0:v:0 -map 0:a:0? \
  -c copy \
  -movflags +faststart \
  output.mp4</code></pre>
<p>재인코딩보다 빠르고 화질 손실도 만들지 않지만 코덱을 변환하지 않는다는 제한이 있다. 입력 코덱이 목표 컨테이너나 브라우저에서 지원되지 않으면 remux만으로 해결되지 않는다. 해상도 변경, 비트레이트 조정, 코덱 통일이 필요할 때는 재인코딩 파이프라인을 따로 설계해야 한다.</p>
<p><code>+faststart</code>는 MP4의 재생에 필요한 메타데이터를 파일 앞쪽으로 옮겨 다운로드가 끝나기 전에 재생을 시작하기 쉽게 만든다. 이 옵션도 코덱을 바꾸지는 않는다.</p>
<h3 id="sha-256-체크섬은-같은-바이트가-도착했는지-확인한다">SHA-256 체크섬은 같은 바이트가 도착했는지 확인한다</h3>
<p>파일 크기가 같다고 내용까지 같다는 보장은 없다. 브라우저가 원본 바이트의 SHA-256을 계산해 업로드 세션에 전달하고 객체 저장소가 검증한 체크섬 또는 서버가 다시 계산한 값과 비교하면 전송 중 바이트가 달라졌는지 확인할 수 있다.</p>
<pre><code class="language-text">clientChecksum = SHA256(selectedFile)
upload(file, signedChecksumHeader = clientChecksum)

storedChecksum = readVerifiedChecksumFromStorage()
reject if storedChecksum != clientChecksum</code></pre>
<p>체크섬은 파일 형식이나 콘텐츠의 안전성을 판정하지 않는다. 같은 파일인지 확인하는 무결성 도구다. 또 multipart upload의 ETag는 전체 파일의 MD5라고 가정하면 안 된다. 사용 중인 저장소와 SDK가 SHA-256을 단일 객체, 파트별 값, 전체 객체 값 중 어떤 형태로 검증하고 반환하는지 확인해야 한다.</p>
<h3 id="상태는-화면-표시용이-아니라-복구의-기준이다">상태는 화면 표시용이 아니라 복구의 기준이다</h3>
<p>업로드와 처리가 여러 요청과 작업으로 나뉘면 중간 상태를 기록해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/472965bd-845f-4f2a-ad2c-437e21ca72d8/image.png" alt="PENDING_UPLOAD에서 PROCESSING과 READY로 진행하고 실패 시 FAILED, 삭제 시 DELETE_PENDING으로 이동하는 미디어 상태도"></p>
<ul>
<li><code>PENDING_UPLOAD</code>: 세션은 만들었지만 원본 업로드를 확인하기 전이다.</li>
<li><code>PROCESSING</code>: 원본을 확인했고 검사와 remux를 진행 중이다.</li>
<li><code>READY</code>: 검증된 최종본을 권한 확인 후 재생할 수 있다.</li>
<li><code>FAILED</code>: 실패 사유를 남기고 재시도나 정리를 기다린다.</li>
<li><code>DELETE_PENDING</code>: 삭제 요청은 받았지만 객체 삭제를 아직 확인하지 못했다.</li>
</ul>
<p>상태 변경에는 조건을 두는 편이 안전하다. 예를 들어 <code>PENDING_UPLOAD</code>인 행만 <code>PROCESSING</code>으로 바꾸면 같은 완료 요청이 여러 번 와도 처리 작업을 중복 실행할 가능성을 줄일 수 있다.</p>
<h2 id="업로드-처리-흐름">업로드 처리 흐름</h2>
<p>아래 HTTP 경로와 필드는 설명을 위한 가상 예시다.</p>
<pre><code class="language-http">POST &lt;업로드 세션 생성 API&gt;
Content-Type: application/json

{
  &quot;type&quot;: &quot;VIDEO&quot;,
  &quot;size&quot;: 123456,
  &quot;contentType&quot;: &quot;video/mp4&quot;,
  &quot;checksumSha256&quot;: &quot;...&quot;
}</code></pre>
<p>전체 흐름을 순서대로 정리하면 이렇다.</p>
<ol>
<li>브라우저가 API 서버에 업로드 세션 생성을 요청한다.</li>
<li>서버가 사용자 권한과 허용 크기, 미디어 종류 같은 제한을 확인하고 DB에 <code>PENDING_UPLOAD</code> 상태를 만든다.</li>
<li>서버가 <code>media/uploads/{uploadId}/source</code>에만 쓸 수 있는 짧은 유효기간의 presigned PUT URL을 발급한다.</li>
<li>브라우저가 객체 저장소에 원본을 직접 업로드한다.</li>
<li>브라우저가 API 서버에 업로드 완료를 알린다. 이 요청은 중복 호출돼도 같은 결과가 나오도록 설계하는 편이 좋다.</li>
<li>서버가 객체 존재 여부, 크기, 체크섬을 확인한다. 브라우저가 보낸 완료 신호만으로 성공 처리하지 않는다.</li>
<li>조건부 상태 변경으로 <code>PENDING_UPLOAD</code>를 <code>PROCESSING</code>으로 바꾼다.</li>
<li>작업자가 FFprobe로 형식과 코덱, 길이, 해상도를 검사하고 FFmpeg로 stream-copy remux를 시도한다.</li>
<li>검증된 최종본을 <code>media/{mediaId}/video.mp4</code>에 저장하고 필요한 메타데이터와 함께 상태를 <code>READY</code>로 바꾼다.</li>
<li>임시 원본을 삭제한다. 삭제 실패는 본 처리 성공을 되돌리기보다 별도 재시도 대상으로 기록할 수 있다.</li>
<li>실패하거나 중단된 파일은 주기적인 정리 작업이 상태와 생성 시각을 기준으로 삭제한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/949ebc27-e014-4bc1-ab80-cff3ed72b478/image.png" alt="브라우저가 업로드 세션을 만든 뒤 객체 저장소에 직접 업로드하고, 서버와 작업자가 검증과 처리를 거쳐 READY 상태로 바꾸는 순서도"></p>
<p>처리 작업은 요청 스레드에서 끝까지 기다리기보다 큐와 작업자로 분리하는 편이 대용량 파일에 잘 맞았다. 완료 API는 처리 접수를 알리고 클라이언트는 <code>mediaId</code>로 상태를 조회할 수 있다.</p>
<h2 id="재생-처리-흐름">재생 처리 흐름</h2>
<p>비공개 객체의 영구 URL을 문서에 넣지 않는다. 클라이언트는 저장 키 대신 <code>mediaId</code>로 재생 권한을 요청한다.</p>
<ol>
<li>클라이언트가 <code>mediaId</code>로 재생 권한을 요청한다.</li>
<li>서버가 현재 사용자의 접근 권한과 미디어의 <code>READY</code> 상태를 확인한다.</li>
<li>서버가 최종 객체를 읽을 수 있는 짧은 유효기간의 presigned GET URL을 반환한다.</li>
<li>브라우저가 객체 저장소에 HTTP Range 요청을 보내 필요한 구간부터 재생한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/1f142abe-9106-4634-900f-d259e37e0d9e/image.png" alt="브라우저가 mediaId로 재생 권한을 받고 presigned GET URL을 이용해 객체 저장소에 Range 요청을 보내는 순서도"></p>
<pre><code class="language-http">GET &lt;presigned GET URL&gt;
Range: bytes=1048576-2097151</code></pre>
<p>Range 요청이 지원되면 브라우저가 동영상 전체를 처음부터 다 받지 않고 필요한 바이트 구간을 요청할 수 있다. 탐색 위치를 옮기거나 재생을 재개할 때 유리하다. 성공한 부분 요청은 보통 <code>206 Partial Content</code>와 <code>Content-Range</code>로 응답한다.</p>
<p>GET URL의 유효기간은 재생 시간과 같아야 한다는 공식은 없다. 브라우저가 언제 새 Range 요청을 보낼지, 만료 뒤 재발급을 어떻게 처리할지, CDN이나 캐시를 쓰는지에 따라 달라진다. 짧게 잡되 실제 재생이 끊기지 않는지 측정하고 필요하면 URL 재발급 흐름을 둬야 한다.</p>
<h2 id="단일-put과-multipart-upload">단일 PUT과 multipart upload</h2>
<p>단일 PUT은 파일 하나를 요청 한 번으로 보낸다. 구현이 단순하지만 연결이 끝부분에서 끊어져도 전체를 다시 올려야 한다. multipart upload는 파일을 여러 파트로 나눠 독립적으로 전송한 뒤 마지막 완료 요청에서 하나의 객체로 조립한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>단일 PUT</th>
<th>Multipart upload</th>
</tr>
</thead>
<tbody><tr>
<td>구현 복잡도</td>
<td>낮음</td>
<td>업로드 ID, 파트 번호, 완료와 중단 처리 필요</td>
</tr>
<tr>
<td>실패 복구</td>
<td>파일 전체 재전송</td>
<td>실패한 파트만 재전송 가능</td>
</tr>
<tr>
<td>병렬 전송</td>
<td>보통 사용하지 않음</td>
<td>여러 파트를 병렬 전송 가능</td>
</tr>
<tr>
<td>중단 정리</td>
<td>임시 객체 확인</td>
<td>완료되지 않은 multipart 세션 중단 필요</td>
</tr>
<tr>
<td>어울리는 상황</td>
<td>작은 파일, 안정적인 네트워크</td>
<td>큰 파일, 불안정한 네트워크, 재개와 병렬화 필요</td>
</tr>
</tbody></table>
<p>특정 용량 하나를 모든 서비스의 정답으로 삼기 어렵다. AWS 문서는 100MB 부근부터 multipart를 고려하라고 안내하지만 공급자 제한, 사용자의 네트워크, 브라우저 메모리, 재시도 비용이 다르다. 실제 파일 분포와 실패율을 보고 단일 PUT의 전체 재전송 비용이 감당하기 어려워지는 지점에서 도입하는 편이 낫다.</p>
<p>multipart를 도입하면 presigned URL도 파트마다 발급하거나 필요한 범위만 순차 발급하는 구조가 된다. 완료되지 않은 multipart upload는 저장 비용을 만들 수 있으므로 만료된 업로드를 중단하는 정리 작업도 함께 설계해야 한다.</p>
<h2 id="실패와-고아-파일-정리">실패와 고아 파일 정리</h2>
<p>DB 트랜잭션은 DB 안의 변경은 원자적으로 묶지만 객체 저장소의 PUT과 DELETE까지 같이 롤백해 주지 않는다. 객체 저장에 성공한 뒤 DB 저장이 실패할 수도 있고 DB를 <code>READY</code>로 바꾸기 전에 작업자가 종료될 수도 있다.</p>
<p>두 시스템을 억지로 하나의 트랜잭션처럼 취급하지 않았다. 대신 각 단계를 멱등하게 만들고 상태를 기준으로 다시 실행하는 쪽을 택했다. 실패 뒤 되돌리는 보상 작업과 주기적인 대조 작업이 필요하다.</p>
<pre><code class="language-text">if status == PENDING_UPLOAD and sessionExpired:
    delete temp object if it exists
    expire session

if status == PROCESSING and leaseExpired:
    retry processing or mark FAILED

if status == FAILED and retentionExpired:
    delete temp and partial final objects

if status == DELETE_PENDING:
    retry object deletion
    finalize database deletion only after confirmation</code></pre>
<p>정리 작업은 삭제 요청이 여러 번 실행돼도 안전해야 한다. 객체가 이미 없다는 응답을 성공으로 취급할지, DB 상태를 어떤 조건으로 바꿀지 정해 두면 재시도가 쉬워진다. 반대로 버킷 전체를 무조건 순회하는 방식은 객체 수가 늘면 비싸질 수 있으므로 DB 상태와 만료 인덱스를 기준으로 대상을 좁히고 필요할 때 저장소 목록과 대조하는 편이 낫다.</p>
<p>처리 작업자가 동시에 같은 미디어를 잡지 않도록 상태 조건, 작업 lease, idempotency key 중 하나 이상을 사용할 수 있다. 어떤 방법이든 작업이 영원히 <code>PROCESSING</code>에 머물지 않게 시간 제한과 복구 경로를 둬야 한다.</p>
<h2 id="직접-구현하며-헷갈렸던-점">직접 구현하며 헷갈렸던 점</h2>
<p>가장 먼저 헷갈린 것은 저장 키를 숨기면 보안이 된다는 생각이었다. 키는 객체의 주소에 가깝다. 실제 접근 가능 여부는 버킷 공개 설정, 정책, 자격 증명, 서명된 요청이 결정한다. 키를 예측하기 어렵게 만드는 것은 보조 수단일 수 있지만 권한 검사를 대신하지 않는다.</p>
<p>Presigned URL도 일반 URL처럼 로그에 남겨도 된다고 생각하기 쉽다. 실제로는 만료 전까지 서명된 작업을 실행할 수 있으므로 비밀번호처럼 영구 비밀은 아니어도 임시 접근권으로 취급해야 한다. 서버 로그에서는 쿼리 문자열을 제거하거나 마스킹하고 클라이언트 오류 수집 도구가 전체 URL을 보내지 않는지 확인해야 한다.</p>
<p><code>Content-Type</code> 검사를 파일 검증으로 착각한 적도 있다. 브라우저가 보낸 값과 확장자는 힌트일 뿐이다. FFprobe로 컨테이너와 스트림을 확인하고 서비스가 허용하는 코덱과 제한을 별도로 적용해야 한다.</p>
<p>remux를 변환이라고 부르면서 코덱까지 바뀐다고 오해하기도 했다. stream copy는 인코딩된 스트림을 그대로 옮긴다. 브라우저가 코덱을 지원하지 않으면 재인코딩 없이는 호환성을 만들 수 없다.</p>
<p>마지막으로 ETag를 항상 파일 해시라고 생각하면 안 된다. 특히 multipart upload에서는 전체 파일 MD5와 다를 수 있다. 무결성 요구가 있다면 저장소가 공식 지원하는 체크섬 기능과 알고리즘을 명시적으로 선택해야 한다.</p>
<h2 id="처음-설계한다면-확인할-췤-리스트">처음 설계한다면 확인할 췤 리스트</h2>
<p><strong>업로드 정책</strong></p>
<ul>
<li>허용할 컨테이너와 코덱, 크기, 길이, 해상도 제한을 정했는가?</li>
<li>작은 파일과 큰 파일의 업로드 경로를 나눌 기준이 있는가?</li>
<li>완료 통지 뒤 객체 존재, 크기, 체크섬을 서버가 다시 확인하는가?</li>
</ul>
<p><strong>권한과 URL</strong></p>
<ul>
<li>직접 업로드용 CORS와 서명 헤더가 정확히 맞는가?</li>
<li>presigned URL의 만료와 로그 마스킹 정책이 있는가?</li>
<li>저장 키 대신 <code>mediaId</code>로 권한과 상태를 조회하는가?</li>
</ul>
<p><strong>처리와 재생</strong></p>
<ul>
<li>FFprobe 실패, remux 실패, 작업자 종료를 각각 어떤 상태로 기록하는가?</li>
<li>Range GET과 presigned GET URL 재발급이 실제 브라우저에서 동작하는가?</li>
</ul>
<p><strong>복구와 정리</strong></p>
<ul>
<li>중복 완료 요청과 중복 작업 실행이 안전한가?</li>
<li>오래된 중간 상태와 미완료 multipart upload를 정리하는가?</li>
<li>DB와 객체 저장소 중 한쪽만 성공했을 때 복구할 수 있는가?</li>
</ul>
<h2 id="마무리하며-배운-점">마무리하며 배운 점</h2>
<p>대용량 미디어 업로드를 구현하며 가장 크게 바뀐 생각은 업로드가 파일 전송 한 번으로 끝나는 기능이 아니라는 것이었다. 권한을 확인해 임시 접근권을 만들고 신뢰하지 않는 원본을 격리한 뒤 검증과 처리를 거쳐 서비스가 사용할 수 있는 상태로 승격하는 작업에 가까웠다.</p>
<p>객체 저장소와 DB는 잘하는 일이 다르고 하나의 트랜잭션으로 묶이지 않는다. 그래서 상태를 남기고 각 단계를 다시 실행할 수 있게 만들며 실패한 흔적을 정리하는 과정이 정상 흐름만큼 중요했다.</p>
<p>처음부터 모든 동영상을 multipart upload와 재인코딩으로 처리할 필요는 없다. 작은 단일 PUT으로 시작할 수 있고 코덱이 맞으면 stream-copy remux만으로 충분할 수도 있다. 대신 어느 조건에서 다음 단계로 넘어갈지 측정 기준과 실패 복구 경로를 함께 만들어 두는 편이 이후 변경을 훨씬 수월하게 했다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingObjects.html">Amazon S3 객체 개요</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html">Amazon S3 presigned URL로 업로드와 다운로드하기</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html">Amazon S3 multipart upload 개요</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity-upload.html">Amazon S3 객체 무결성 확인</a></li>
<li><a href="https://ffmpeg.org/ffprobe.html">FFprobe 공식 문서</a></li>
<li><a href="https://ffmpeg.org/ffmpeg.html#Streamcopy">FFmpeg streamcopy와 transcoding</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests">MDN HTTP Range 요청</a></li>
<li><a href="https://developers.cloudflare.com/r2/api/s3/">Cloudflare R2 S3 API 호환 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL 기본 문법 정리: DDL, DML, JOIN부터 집계 함수]]></title>
            <link>https://velog.io/@yujin_1219/SQL-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-DDL-DML-JOIN%EB%B6%80%ED%84%B0-%EC%A7%91%EA%B3%84-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@yujin_1219/SQL-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC-DDL-DML-JOIN%EB%B6%80%ED%84%B0-%EC%A7%91%EA%B3%84-%ED%95%A8%EC%88%98</guid>
            <pubDate>Sun, 22 Jun 2025 10:47:58 GMT</pubDate>
            <description><![CDATA[<h2 id="💡-sqlstructured-query-language">💡 SQL(Structured Query Language)</h2>
<ul>
<li>Sequel이란 언어의 시초</li>
<li>표준 질의어로 채택되어 널리 쓰이는 관계형 질의언어</li>
<li>관계대수나 관계해석은 확실한 이론적 배경을 제공하나 상용으로 쓰이기에는 어렵고 적절 X → SQL 사용</li>
</ul>
<hr>
<h2 id="sql-의-구성--ddl--dml">SQL 의 구성 : DDL &amp; DML</h2>
<blockquote>
<p><strong>DDL(데이터 정의(Definition) 언어)</strong></p>
</blockquote>
<ul>
<li>데이터 저장 구조를 명시</li>
<li>테이블 스키마의 생성, 기본키 외래키 설정,정의, 수정, 삭제</li>
</ul>
<blockquote>
<p><strong>DML(데이터 조작(Manipulation) 언어)</strong></p>
</blockquote>
<ul>
<li>사용자가 데이터를 접근하고 조작할 수 있게 하는 언어</li>
<li>레코드의 검색, 삽입, 삭제, 수정</li>
</ul>
<hr>
<h2 id="ddl-데이터-정의-언어">DDL (데이터 정의 언어)</h2>
<h3 id="테이블-생성">테이블 생성</h3>
<pre><code class="language-sql">create table department
(
    dept_id     varchar2(10),
    dept_name   varchar2(20) not null,
    office      varchar2(20),
    constraint pk_department primary key(dept_id)
);

//constraint pk_department primary key(dept_id)
//제약조건을 지정.
//이 테이블의 **기본 키(primary key)**는 dept_id 컬럼.
//pk_department라는 제약조건 이름을 부여함.
//나중에 이 제약조건을 수정하거나 삭제할 때 이 이름을 사용 가능.</code></pre>
<pre><code class="language-sql">create table student
(
    stu_id       varchar2(10),
    resident_id  varchar2(14) not null,
    name         varchar2(10) not null,
    year         int,
    address      varchar2(10),
    dept_id      varchar2(10),
    constraint pk_student primary key(stu_id),
    constraint fk_student foreign key(dept_id) references department(dept_id)
);

// constraint fk_student foreign key(dept_id) references department(dept_id)
- dept_id는 외래 키(foreign key).
- 외래 키는 다른 테이블의 기본 키를 참조함.
- 여기서는 department 테이블의 dept_id를 참조.
- 즉, student 테이블의 dept_id 값은 반드시 department 테이블에 존재하는 값이어야 함.
- 제약조건 이름: fk_student
</code></pre>
<h3 id="테이블-삭제">테이블 삭제</h3>
<pre><code class="language-sql">drop table &lt;테이블 이름&gt;</code></pre>
<ul>
<li>다른 테이블에서 외래키로 참조되는 경우 삭제할 수 없음</li>
</ul>
<h3 id="테이블-수정">테이블 수정</h3>
<pre><code class="language-sql">// 필드 추가
alter table &lt;테이블 이름&gt; add &lt;추가할 필드&gt;
alter table student add age int

// 필드 삭제
alter table &lt;테이블 이름&gt; drop column &lt;삭제할 필드&gt;
alter table student drop column age</code></pre>
<h3 id="표준-sql과-오라클의-데이터-타입">표준 SQL과 오라클의 데이터 타입</h3>
<table>
<thead>
<tr>
<th><strong>분류</strong></th>
<th><strong>표준 SQL</strong></th>
<th><strong>오라클</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>문자</strong></td>
<td><code>char(n)</code></td>
<td><code>char(n)</code></td>
<td>길이가 n byte인 고정길이 문자열<br>오라클: 최대 2000byte까지 지정 가능</td>
</tr>
<tr>
<td></td>
<td><code>varchar(n)</code></td>
<td><code>varchar2(n)</code></td>
<td>최대 길이 n byte의 가변길이 문자열<br>오라클: 최대 4000byte까지 지정 가능</td>
</tr>
<tr>
<td><strong>숫자</strong></td>
<td><code>int</code></td>
<td><code>int</code></td>
<td>정수형</td>
</tr>
<tr>
<td></td>
<td><code>float</code></td>
<td><code>float</code></td>
<td>부동 소수형</td>
</tr>
<tr>
<td><strong>날짜</strong></td>
<td><code>date</code></td>
<td><code>date</code></td>
<td>년, 월, 일을 갖는 날짜형<br>오라클의 기본 형식: <code>&#39;yy/mm/dd&#39;</code></td>
</tr>
<tr>
<td><strong>시간</strong></td>
<td><code>time</code></td>
<td><em>(미지원)</em></td>
<td><em>(표준 SQL의 시간 타입, 오라클에는 직접적 지원 없음)</em></td>
</tr>
<tr>
<td></td>
<td><code>timestamp</code></td>
<td><code>timestamp</code></td>
<td>년, 월, 일, 시, 분, 초를 갖는 날짜시간형</td>
</tr>
</tbody></table>
<hr>
<h2 id="dml-데이터-조작-언어">DML (데이터 조작 언어)</h2>
<h3 id="레코드-삽입">레코드 삽입</h3>
<pre><code class="language-sql">insert into department(dept_it, dept_name, office)
values(&#39;920&#39;, &#39;컴퓨터공학과&#39;, &#39;201호&#39;)

insert into department 
values (&#39;923&#39;, &#39;산업공학과&#39;, &#39;207호&#39;)</code></pre>
<ul>
<li>필드 이름을 명령문에 나열할 경우, 테이블 생성할 때 지정한 순서와 달라도 됨</li>
</ul>
<h3 id="레코드-수정">레코드 수정</h3>
<ul>
<li>모든 레코드에 대한 수정 적용시 <code>where 절</code> 생략 가능</li>
</ul>
<pre><code class="language-sql">update &lt;테이블 이름&gt; set &lt;수정 내역&gt; where &lt;조건&gt; 

// student 테이블의 모든 학생들의 학년을 하나씩 증가
update student set year = year + 1 

update professor set 지위 =&#39;교수&#39;, number = &#39;923&#39; where name = &#39;고희석&#39;
</code></pre>
<h3 id="레코드-삭제">레코드 삭제</h3>
<ul>
<li>where절에 지정된 조건을 만족하는 레코드들 삭제</li>
<li>where 절 생략 시 모든 레코드 삭제</li>
<li>모든 레코드들이 삭제되어도 테이블은 삭제 X</li>
<li>외래키로 사용되는 필드에 대해 데이터를 삽입할 때</li>
</ul>
<pre><code class="language-sql">delete from &lt;테이블 이름&gt; where &lt;조건&gt;

delete from professor where name=&#39;김태석&#39;</code></pre>
<h3 id="레코드-검색">레코드 검색</h3>
<pre><code class="language-sql">select &lt;필드리스트&gt;
from &lt;테이블리스트&gt;
where &lt;조건&gt;

//중복된 레코드를 제거 -&gt; distinct
select distinct address from student

//select 절에 필드이름 외 산술식이나 상수 사용 가능
select name, 2012-year_emp
from professor

//컴퓨터공학과 3학년 학생들의 학번 검색
select student.stu_id
from student, department
where student.dept_id = department.dept_id and
            student.year = 3 and
            department.dept_name=&#39;컴퓨터공학과&#39;</code></pre>
<hr>
<h2 id="commit">COMMIT</h2>
<ul>
<li>대부분의 DBMS는 명령어를 실행하면 변경 결과를 즉시 데이터베이스에 반영하지 않고 가지고 있다가 <code>COMMIT</code> 명령을 내리면 그때 영구적으로 데이터베이스에 반영</li>
<li>취소 불가능</li>
<li><code>COMMIT</code> 전에는 <code>ROLLBACK</code>으로 작업 취소가능</li>
<li><code>set autocommit on</code> → 자동 커밋</li>
</ul>
<hr>
<h2 id="집계함수">집계함수</h2>
<ul>
<li>count, sum, avg, max, min</li>
<li><strong><code>select</code>절과 <code>having</code>절에서만 가능</strong></li>
<li>sum, avg는 숫자형 데이터 타입을 갖는 필드에서만 가능</li>
</ul>
<hr>
<h3 id="count">COUNT</h3>
<ul>
<li><code>NULL</code> 은 계산에서 제외</li>
<li>단, <code>count *</code> → 레코드 수 계산</li>
</ul>
<pre><code class="language-sql">count(distinct&lt;필드이름&gt;) // 서로 구별되는 값의 개수
</code></pre>
<hr>
<h2 id="order-by">ORDER BY</h2>
<ul>
<li>정렬</li>
<li><code>SELECT</code>문 맨 마지막에</li>
<li>asc, desc</li>
</ul>
<pre><code class="language-sql">SELECT name, student_id
from student
where year=3 or year=4
order by name,stu_id // 학심 이름으로 오름차순 정렬, 같은 이름은 stu_id로 정렬</code></pre>
<hr>
<h2 id="group-by">GROUP BY</h2>
<ul>
<li>select 절에 집계함수가 사용될 경우 다른 필드는 select 절에 사용할 수가 없음</li>
<li>⇒ <code>GROUP BY</code> 사용해야함 !!</li>
<li>Null속성도 그룹화</li>
</ul>
<pre><code class="language-sql">select ename, max(sal) from emp // ⚠️에러발생 

// 부서별 최고 금여
select deptno, max(sal) from emp group by deptno

//deptno별 레코드 개수 출력
select deptno, count(*)
from student
group by deptno 

select dname, count(*), avg(sal), max(sal), min(sal)
from emp e, delt d
where e.deptno = d.deptno
group by dname</code></pre>
<hr>
<h2 id="having">Having</h2>
<ul>
<li>그룹에 대한 조건 명시</li>
<li>group 에 대한 조건은 where 절에 사용할 수 없음 ! → having 절 이용</li>
</ul>
<pre><code class="language-sql">select dept_name, count(*), avg(2012-year_emp), max(2012-year_emp)
from professor p, department d
where d.dept_id = p.dept_id and avg(2012-year_emp)&gt;=10 //⚠️에러발생 
group by dept_name

//올바른 표현식
select dept_name, count(*), avg(2012-year_emp), max(2012-year_emp)
from professor p, department d
where d.dept_id = p.dept_id 
group by dept_name
having avg(2012-year_emp)&gt;=10</code></pre>
<h3 id="💡sql-실행-순서">💡SQL 실행 순서</h3>
<p>from → where → group by → having → select → distinct → order by → rownum</p>
<blockquote>
<p><strong>where, having, group by 절을 모두 함께 사용할 경우</strong></p>
</blockquote>
<ol>
<li>where 절에 명시된 조건을 만족하는 레코드들 검색</li>
<li>group by 절에 명시된 필드값이 서로 일치하는 레코드들끼리 그룹지어 집계함수 적용</li>
<li>집계함수 적용한 결과들 중 having절을 만족하는 결과만 출력</li>
</ol>
<hr>
<h2 id="like-연산자">Like 연산자</h2>
<ul>
<li>문자열에 대해서는 일부분만 일치하는 경우를 찾아야할 때 사용</li>
<li>= 대신 <code>like</code> 연산자 사용</li>
</ul>
<pre><code class="language-sql">//student 테이블에서 김씨 성을 가진 학생들을 찾아라
select * from student where name like &#39;김%&#39;</code></pre>
<hr>
<h2 id="재명명-연산">재명명 연산</h2>
<ul>
<li><code>&lt;원래 이름&gt; as &lt; 새로운 이름&gt;</code></li>
<li>as 생략 가능</li>
<li>Oracle에서 컬럼(속성) 은 as 사용가능, 테이블 별칭은 as키워드 사용X</li>
<li>실제 테이블 이름이 수정되거나 필드 이름이 바뀌는 것은 X</li>
<li>질의를 처리하는 과정 동안만 일시적</li>
</ul>
<pre><code class="language-sql">// ♦️ 테이블 재명명
student 테이블과 department 테이블을 조인하여 학생들의 이름과 소속 학과 이름 검색
select s.name, d.dept_name
from student s, department d
where s.dept_id = d.dept_id

// ♦️ 필드의 재명명
student name 이름, position 직위, 2012-year_emp 재직연수 from professor</code></pre>
<hr>
<h2 id="null-처리">NULL 처리</h2>
<ul>
<li><code>is null,</code> <code>is not null</code></li>
<li><code>산술 표현식(+, - ,* ,/)</code>의 결과는 입력값중 하나라도 null인 경우 null</li>
<li><code>그룹함수(sum, avg, min, max)</code>는 NULL 무시</li>
<li><code>count</code>는 일반적으로 NULL이 아닌 값을 세지만, <code>COUNT(*)</code> 는 NULL 상관없이 전체 행의 수를 리턴</li>
</ul>
<pre><code class="language-sql">SELECT stu_id
FROM takes
WHERE grade &lt;&gt; &#39;A+&#39;; // 👉🏻&lt;&gt; : 오라클에서의 같지 않다

-&gt; grade 의 필드 값이 널인 레코드에 대해서는 질의 결과에 포함 X</code></pre>
<hr>
<h2 id="inner-join">Inner Join</h2>
<ul>
<li>일반 조인(equi-join)과 의미가 동일</li>
</ul>
<pre><code class="language-sql">//형식
select &lt;컬럼명&gt;
from &lt;테이블1&gt; (inner) join &lt;테이블2&gt;
         on table1.컬렴명 = table2.컬럼명

select &lt;컬럼명&gt;
from &lt;테이블1&gt; (inner) join &lt;테이블2&gt;
       using 같은 컬럼명</code></pre>
<hr>
<h2 id="natural-join자연조인">Natural Join(자연조인)</h2>
<ul>
<li>두 테이블 간의 동일한 이름을 갖는 모든 칼럼들에 대해 equi(=) join</li>
<li>두 테이블에 동일한 이름의 컬럼이 있을 때만 동작 !</li>
</ul>
<pre><code class="language-sql">select name, stu_id, dept_name
from student natural join department</code></pre>
<h2 id="outer-join">Outer Join</h2>
<pre><code class="language-sql">select title, credit, year, semester
from course left outer join class
using (course_id)

select title, credit, year, semester
from course left outer join class
on (course.course_id = class.course_id)

select title, credit, year, semester
from course, class
where course.course_id = class.course_id (+) //left outer join

select title, credit, year, semester
from course, class
where course.course_id (+) = class.course_id //right outer join</code></pre>
<ul>
<li>where 절에서 기준이 되는 테이블 반대 테이블 조건 컬럼 뒤에 (+)를 붙힘<ul>
<li>Left outer : 오른쪽</li>
<li>Right outer : 왼쪽</li>
</ul>
</li>
<li>full outer join : 양쪽 테이블에서 서로 일치하는 레코드가 없을 경우, 해당 레코드들도 결과 테이블에 포함시키며 나머지 필드에 대해서는 모두 <code>NULL</code>삽입</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/3f7198f9-bf7c-4f01-a025-59a2a0cdfb0d/image.png" alt=""></p>
<h3 id="카티션-프로덕트">카티션 프로덕트</h3>
<ul>
<li>FROM 테이블1,테이블2</li>
<li>FROM 테이블1 <strong>CROSS JOIN</strong> 테이블2</li>
</ul>
<hr>
<h2 id="무결성">무결성</h2>
<ul>
<li>Intefrity constraints (무결성 제약 조건)</li>
<li>not null, unique,…</li>
</ul>
<h3 id="unique">UNIQUE</h3>
<ul>
<li>기본키와 UNIQUE 모두 특정 속성에 중복된 값이 저장되는 것을 방지</li>
<li>차이점은 기본키 : NULL 값 X , UNIQUE : NULL 값 가능</li>
<li>기본키 = NOT NULL + UNIQUE</li>
</ul>
<pre><code class="language-sql">create table univ (
univ_name varchar(20),
tel char(12),
UNIQUE(univ_name)</code></pre>
<hr>
<h2 id="string-operation">String operation</h2>
<ul>
<li><code>concat()</code> : 문자열 연결<ul>
<li>오라클에서는 오직 두개의 인수만 가능</li>
</ul>
</li>
<li><code>||</code> : concat 연산자<ul>
<li><code>select name || ‘,’ || dept_name</code> → name,dept_name 출력</li>
</ul>
</li>
<li><code>upper()</code>, <code>lower()</code> : 대문자, 소문자로 변환</li>
<li><code>length()</code> : 문자열 길이</li>
<li><code>substr(문자열, 1, 5)</code> : 1부터 5번째 글자의 문자열 추출</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[정렬 알고리즘] 선택정렬, 삽입정렬, 버블정렬, 쉘정렬, 합병정렬, 퀵정렬, 기수정렬]]></title>
            <link>https://velog.io/@yujin_1219/%EC%A0%95%EB%A0%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%84%A0%ED%83%9D%EC%A0%95%EB%A0%AC-%EC%82%BD%EC%9E%85%EC%A0%95%EB%A0%AC-%EB%B2%84%EB%B8%94%EC%A0%95%EB%A0%AC-%EC%89%98%EC%A0%95%EB%A0%AC-%ED%95%A9%EB%B3%91%EC%A0%95%EB%A0%AC-%ED%80%B5%EC%A0%95%EB%A0%AC-%EA%B8%B0%EC%88%98%EC%A0%95%EB%A0%AC</link>
            <guid>https://velog.io/@yujin_1219/%EC%A0%95%EB%A0%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%84%A0%ED%83%9D%EC%A0%95%EB%A0%AC-%EC%82%BD%EC%9E%85%EC%A0%95%EB%A0%AC-%EB%B2%84%EB%B8%94%EC%A0%95%EB%A0%AC-%EC%89%98%EC%A0%95%EB%A0%AC-%ED%95%A9%EB%B3%91%EC%A0%95%EB%A0%AC-%ED%80%B5%EC%A0%95%EB%A0%AC-%EA%B8%B0%EC%88%98%EC%A0%95%EB%A0%AC</guid>
            <pubDate>Fri, 20 Jun 2025 10:47:31 GMT</pubDate>
            <description><![CDATA[<h2 id="💡정렬이란">💡정렬이란?</h2>
<ul>
<li>정렬시켜야할 대상은 <strong>레코드(record) —&gt; 행</strong>에 해당</li>
<li>레코드는 <strong>필드(field) 로 구성 —&gt; 열</strong>에 해당</li>
<li>키필드로 레코드와 레코드를 구분
<img src="https://velog.velcdn.com/images/yujin_1219/post/1203ad1c-4a28-4f41-8e22-fa3c5b924812/image.png
" width="80%" height="60%"></li>
</ul>
<ul>
<li><p>모든 경우에 최적의 정렬 알고리즘은 X</p>
<p>  → 각 응용분야에 <strong>적합한 정렬 방법</strong>을 사용해야 함</p>
<p>  <strong>구분 요소</strong></p>
<ul>
<li>레코드 수</li>
<li>레코드 크기</li>
<li>Key의 특성(문자, 정수, 실수 등)</li>
<li>메모리 내부/외부 정렬<ul>
<li>내부정렬: 모든 데이터가 주기억장치에 저장</li>
<li>외부정렬: 외부기억장치에 대부분의 데이터 일부만 내</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="정렬-알고리즘-평가-기준">정렬 알고리즘 평가 기준</h3>
<ul>
<li>key 값 비교 횟수의 많고 적음</li>
<li>data의 이동 횟수의 많고 적음</li>
</ul>
<pre><code>**이동횟수와 비교 횟수는 비례하지 않음(하나는 적고 하나는 클 수 있다)</code></pre><p><strong>단순하지만 비효율적(O(n^2))</strong></p>
<ul>
<li>삽입,선택,버블 정렬</li>
</ul>
<p><strong>복잡하지만 효율적(O(nlogn))</strong></p>
<ul>
<li>퀵, 히프, 합병, 기수</li>
</ul>
<p>정렬 알고리즘의 안정성→ 동일한 키 값을 갖는 레코드들이 정렬 후에도 바뀌지 않음(삽입, 버블, 합병 정렬)</p>
<table>
<thead>
<tr>
<th><strong>알고리즘 종류</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>퀵 정렬 (Quick Sort)</strong></td>
<td>분할 정복 방식을 이용하여 배열을 빠르게 정렬하는 알고리즘</td>
</tr>
<tr>
<td><strong>Arrays.sort()</strong></td>
<td>퀵 정렬을 사용하여 배열을 정렬하는데 사용되며 기본 타입 배열과 객체 타입 배열 모두에 대해 사용할 수 있음</td>
</tr>
<tr>
<td><strong>Collections.sort()</strong></td>
<td>퀵 정렬을 사용하여 객체를 정렬하는데 사용되며 List, Set, Queue 등의 컬렉션 프레임워크에 대해 사용할 수 있음</td>
</tr>
<tr>
<td><strong>버블 정렬 (Bubble Sort)</strong></td>
<td>인접한 두 원소를 비교하여 큰 값을 오른쪽으로 이동시키는 방식으로 정렬하는 알고리즘</td>
</tr>
<tr>
<td><strong>선택 정렬 (Selection Sort)</strong></td>
<td>주어진 배열에서 최소값을 찾아 맨 앞 원소와 교환하는 방식으로 정렬하는 알고리즘</td>
</tr>
<tr>
<td><strong>삽입 정렬 (Insertion Sort)</strong></td>
<td>정렬되어 있는 부분집합 내에서 자신이 들어갈 위치를 찾아 삽입하는 방식으로 정렬하는 알고리즘</td>
</tr>
<tr>
<td><strong>병합 정렬 (Merge Sort)</strong></td>
<td>분할 정복 방식을 이용하여 배열을 정렬하는 알고리즘</td>
</tr>
<tr>
<td><strong>힙 정렬 (Heap Sort)</strong></td>
<td>힙이라는 자료구조를 이용하여 정렬하는 알고리즘</td>
</tr>
<tr>
<td><strong>기수 정렬 (Radix Sort)</strong></td>
<td>각 자리의 숫자를 비교하여 정렬하는 알고리즘</td>
</tr>
<tr>
<td>---</td>
<td></td>
</tr>
<tr>
<td>## 📌선택정렬</td>
<td></td>
</tr>
</tbody></table>
<p>: 가장 작은 것을 선택해서 제일 앞으로 보내는 알고리즘</p>
<p><strong>→ 제자리정렬</strong>
<img src="https://velog.velcdn.com/images/yujin_1219/post/cd639308-0b89-4727-ac6e-0f8202b8591d/image.png
" width="60%" height="50%"></p>
<ul>
<li><p>(n-1)번 인덱스의 값은 정렬할 필요 없음</p>
</li>
<li><p>(n-2)번 인덱스 까지만 교환(정렬)하면 됨</p>
</li>
<li><p><strong>비교횟수</strong> = n(n-1)/2 = <strong>O(n^2)</strong></p>
<p>  : 외부 for문: n-1 , 내부 for문: i+1</p>
</li>
<li><p><strong>이동횟수</strong> = <strong>3(n-1) = O(n)</strong></p>
</li>
<li><p>전체적인 시간복잡도: O(n^2)</p>
</li>
<li><p><strong>안정성 만족X</strong></p>
</li>
</ul>
<hr>
<h2 id="📌삽입정렬">📌삽입정렬</h2>
<ul>
<li><p>필요할때에만 위치를 바꿈</p>
</li>
<li><p>앞에있는 원소들이 이미 정렬이 되어있다고 가정
<img src="https://velog.velcdn.com/images/yujin_1219/post/52d9de26-cd81-4028-b412-005bdffa6f4b/image.png
" width="70%">
<img src="https://velog.velcdn.com/images/yujin_1219/post/f713585d-176a-47bd-8245-a059b69e9373/image.png" alt=""></p>
</li>
<li><p>최선의 경우 O(n) : 이미 정렬이 되어있는 경우→ 가장 효율</p>
<ul>
<li>비교: n-1 번</li>
</ul>
</li>
<li><p>최악의 경우 O(n^2): 역순으로 정렬되어있는 경우</p>
<ul>
<li>모든 단계에서 앞에 놓인 자료 전부 이동</li>
<li>비교 = n(n-1)/2 = O(n^2)</li>
<li>이동 = n(n-1)/2 + 2(n-1) = O(n^2)</li>
</ul>
</li>
<li><p>시간복잡도 O(n^2)</p>
</li>
<li><p>안정된 정렬방법</p>
</li>
</ul>
<hr>
<h2 id="📌버블정렬">📌버블정렬</h2>
<ul>
<li>효율성이 가장 떨어짐</li>
<li>뒤에서부터 정렬됨(선택정렬과 반대)</li>
<li>바로 옆에있는 값과 비교하여 정렬</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/a29045a4-5ed4-44a6-919e-66e973d813cf/image.png" alt=""></p>
<ul>
<li>비교횟수 (최상, 평균, 최악 모두 일정) : <strong>O(n^2)</strong></li>
<li>이동 횟수<ul>
<li>최악의 경우(역순으로 정렬): 비교횟수 *3 → temp,i,j 3번 교환</li>
<li>최선의 경우(이미 정렬): 0</li>
<li><strong>평균: O(n^2)</strong></li>
</ul>
</li>
<li>이동연산은 비교연산보다 더 많은 시간이 소요</li>
</ul>
<hr>
<h2 id="📌쉘정렬">📌쉘정렬</h2>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/b3048174-50a9-40fa-af3f-a863dbf670d9/image.png" alt=""></p>
<ul>
<li>삽입정렬을 보완한 정렬</li>
<li>불연속적인 부분 리스트에서 멀리 떨어진 자료 이동으로 보다 적은 위치교환</li>
<li>부분 리스트가 5→3→1 로 갈수록 점진적으로 정렬된 상태가 되므로 삽입정렬 속도 증가</li>
<li><strong>시간복잡도</strong><ul>
<li><strong>최악의 경우 O(n^2)</strong></li>
<li><strong>평균 O(n^1.5)</strong></li>
</ul>
</li>
</ul>
<hr>
<h2 id="📌합병정렬">📌합병정렬</h2>
<ul>
<li>분할 → 정복 → 결합<ul>
<li>분할: 리스트를 두개의 <strong>균등한</strong> 크기로 분할, 하나가 될때까지 계속 분할 4→2→1</li>
<li>정복: 부분 배열을 정렬</li>
<li>결합: 정렬된 부분배열을 하나의 배열에 통합</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/968e4e8e-1ea2-4966-be87-880f4eea42eb/image.png" alt=""></p>
<p><strong>분할</strong></p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/32ad48fc-afac-41e8-a612-f7d721532a46/image.png" alt=""></p>
<p><strong>합병 및 정렬</strong> </p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/31fbbe21-cbfd-4e4b-9a31-9a9aeb530c76/image.png" alt=""></p>
<p>왼쪽(2,3,4,7)이 i 오른쪽(1,9,11,15)이 j 라 해보자</p>
<p>먼저 2와 1을 비교해 1이 더 작으므로 1을 정렬한다</p>
<p>그리고 j+1 을 하여 9와 2를 비교한다. 2가 더 작으므로 2를 정렬</p>
<p>i+1을 하여 3과 9를 비교… 이런 식</p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/ea2ec8f2-7bd4-4d27-a1f0-47032a4da0ac/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/90993abc-85c8-4536-a038-4e5807ee3025/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/3999d431-4f5e-44cf-bffd-58db10fd002e/image.png" alt=""></p>
<ul>
<li>비교횟수<ul>
<li>크기가 n인 리스트를 정확히 균등분배하므로 log(n) 개의 패스</li>
<li>각 패스에서 리스트의 모든 레코드를 n개 비교하므로 n번의 비교연산</li>
</ul>
</li>
<li>이동횟수<ul>
<li>레코드의 이동이 각 패스에서 2n번 발생하므로 전체 레코드의 이동은 2n*log(n)번 발생</li>
<li>레코드를 연결리스트로 구성하여 합병 정렬할 경우 효율적</li>
</ul>
</li>
<li><strong>최적, 평균, 최악의 경우 큰 차이 없이 O(n*log(n))의 시간복잡도</strong></li>
<li>안정적이며 데이터의 초기분산 순서에 영향을 덜 받음</li>
</ul>
<hr>
<h2 id="📌퀵정렬">📌퀵정렬</h2>
<ul>
<li>평균적으로 가장 빠른 정렬</li>
<li><strong>비균등</strong> 분할</li>
<li>재귀호출</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/20d7087f-bac2-4097-b116-370bb484bedc/image.png" alt=""></p>
<p>pivot 값보다 큰 값을 low 가 찾고 pivot 값보다 작은 값을 high가 찾는다 → 교환</p>
<p>만약 low 값이 더 작고 high 값이 더 클 시 엇갈린 것 → low 값과 pivot 값을 바꿈</p>
<ul>
<li><strong>시간복잡도 n*log(n)</strong></li>
</ul>
<hr>
<h2 id="📌기수정렬">📌기수정렬</h2>
<p> <strong>: 낮은 자리수부터 비교하여 정렬해 간다는 것을 기본 개념으로 하는 정렬 알고리즘</strong></p>
<ul>
<li>레코드를 비교하지 않고 정렬 수행 ( 대부분의 정렬방법은 레코드를 비교)</li>
<li>시간복잡도 : <strong>O(dn)</strong> (n: 데이터의 개수, d : 데이터들의 최대 자리수, 대부분 d&lt;10 이하)</li>
<li>자리수가 고정되어 있는 안정적인 정렬</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/59d425e8-b337-43c0-a555-bdb40eadaa20/image.png" alt=""></p>
<p> ex) 두자리수의 기수정렬</p>
<p> : 일의 자리수 오름차순으로 정렬 → 데이터 정렬 완료 → 다시 십의 자리수로 오름차순으로 정렬</p>
<hr>
<h2 id="💡-알고리즘별-시간복잡도">💡 알고리즘별 시간복잡도</h2>
<p><img src="https://velog.velcdn.com/images/yujin_1219/post/35d6ac50-21ec-43db-8406-24cc0bbbd808/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[탐색 알고리즘] 그래프, DFS, BFS]]></title>
            <link>https://velog.io/@yujin_1219/%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B7%B8%EB%9E%98%ED%94%84-DFS-BFS</link>
            <guid>https://velog.io/@yujin_1219/%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B7%B8%EB%9E%98%ED%94%84-DFS-BFS</guid>
            <pubDate>Fri, 20 Jun 2025 10:09:35 GMT</pubDate>
            <description><![CDATA[<h2 id="💡그래프란">💡그래프란?</h2>
<p><strong>정점(node)</strong> 와 그 정점을 연결하는 <strong>간선( edge)</strong>로 이루어진 자료구조</p>
<blockquote>
<p>그래프 탐색의 종류 2가지 :  <strong>DFS, BFS</strong></p>
</blockquote>
<hr>
<h2 id="💡깊이-우선-탐색dfs">💡깊이 우선 탐색(DFS)</h2>
<img src="https://velog.velcdn.com/images/yujin_1219/post/e1487589-f0b5-4790-99b1-ed40d0b92ca0/image.png" width="70%" height="50%">


<h3 id="📌-개념">📌 개념</h3>
<ul>
<li><p>그래프의 시작 노드에서 출발하여 탐색할 한 쪽 분기를 정하여 <strong>최대깊이</strong> 까지 탐색을 마친 후 다른 쪽 분기로 이동하여 다시 탐색하는 알고리즘</p>
</li>
<li><p>완전 탐색 기법 중 하나</p>
</li>
<li><p><strong>완전 탐색 기법</strong></p>
<ul>
<li>특징<ul>
<li>재귀함수로 구현</li>
<li>스택 자료구조 이용</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="📌-특징">📌 특징</h3>
<ul>
<li><strong>모든 노드를 방문</strong>하고자 하는 경우 이 방법을 사용</li>
<li>너비 우선 탐색 보다 더 간결함</li>
<li>검색 속도는 너비 우선 탐색에 비해 느리다</li>
</ul>
<h3 id="📌-dfs-템플릿-코드">📌 DFS 템플릿 코드</h3>
<pre><code class="language-java">static Set&lt;String&gt; visited = new HashSet&lt;&gt;();

static void dfs(Map&lt;String, List&lt;String&gt;&gt; graph, String current) {
    // *---------------------------------------*
        // 그래프를 방문하며 처리해야할 일을 여기서 한다
        // (예시)
      // if (current == value) {
        //     &lt;&lt; 현재 노드가 특정 값을 만족할 때 해야할 일 &gt;&gt;
        //     return current;
        // }
    // *---------------------------------------*
    visited.add(current);
    for (String v : graph.get(current)) {
        if (!visited.contains(v)) {
            dfs(graph, v);
        }
    }
}</code></pre>
<hr>
<h2 id="💡너비-우선-탐색bfs">💡너비 우선 탐색(BFS)</h2>
<img src="https://velog.velcdn.com/images/yujin_1219/post/8f2781eb-10da-4b3b-8b0f-0738dabb9c44/image.png" width="70%" height="50%">
### 📌 개념

<ul>
<li>완전 탐색 기법</li>
<li>시작 노드에서 인접한 노드를 먼저 탐색하고, 그 다음 인접한 노드를 탐색하는 알고리즘</li>
</ul>
<h3 id="📌-특징-1">📌 특징</h3>
<ul>
<li>완전 탐색 기법</li>
<li>큐 자료구조 이용</li>
<li><strong>최단 경로</strong>를 찾을 때 유용</li>
</ul>
<h3 id="📌-bfs-템플릿-코드">📌 BFS 템플릿 코드</h3>
<pre><code class="language-java">public static void makeGraph() {
    graph.put(0, Arrays.asList(1, 3, 6));
    graph.put(1, Arrays.asList(0, 3));
    graph.put(2, Arrays.asList(3));
    graph.put(3, Arrays.asList(0, 1, 2, 7));
    graph.put(4, Arrays.asList(5));
    graph.put(5, Arrays.asList(4, 6, 7));
    graph.put(6, Arrays.asList(0, 5));
    graph.put(7, Arrays.asList(3, 5));
}</code></pre>
<pre><code class="language-java">Set&lt;String&gt; bfs(Map&lt;String, List&lt;String&gt;&gt; graph, String start) {
    Set&lt;String&gt; visited = new HashSet&lt;&gt;();
    visited.add(start);
    Queue&lt;String&gt; queue = new ArrayDeque&lt;&gt;();
    queue.add(start);

    while (!queue.isEmpty()) {
        String current = queue.remove();
            // *---------------------------------------*
                // 그래프를 방문하며 처리해야할 일을 여기서 한다
                // (예시)
              // if (current == value) {
                //     &lt;&lt; 현재 노드가 특정 값을 만족할 때 해야할 일 &gt;&gt;
                //     return current;
                // }
            // *---------------------------------------*
        for (String v : graph.get(current)) {
            if (!visited.contains(v)) {
                visited.add(v);
                queue.add(v);
            }
        }
    }
    return visited;
}</code></pre>
<hr>
<h2 id="💡이진탐색binary-search">💡이진탐색(Binary Search)</h2>
<img src="https://velog.velcdn.com/images/yujin_1219/post/2d9f7dd3-0238-4c83-9e16-683b0f962771/image.png" width="90%" height="50%">

<h3 id="📌-개념-1">📌 개념</h3>
<ul>
<li>‘정렬된 배열’에서 ‘특정 값’을 찾는 알고리즘</li>
<li>‘탐색 범위를 절반씩 줄여’나가기 때문에 선형탐색에 비해 빠른 속도를 보장</li>
<li>하지만 ‘배열이 정렬되어 있어야 한다는 조건’이 필요하기 때문에 배열이 정렬되어 있지 않은 경우에는 정렬 작업이 필요</li>
</ul>
<h2 id="📌-특징-2">📌 특징</h2>
<ul>
<li>반드시 정렬되어 있는 상태에서만 사용 가능</li>
<li>배열 또는 이진트리를 이용해 구현 가능</li>
<li>시간복잡도 Olog(n)</li>
</ul>
<h3 id="📌-이진탐색트리란">📌 이진탐색트리란?</h3>
<ul>
<li>이진 탐색을 위한 이진트리</li>
<li>왼쪽 서브트리 &lt; 루트</li>
<li>오른쪽 서브트리 &gt; 루트</li>
</ul>
<img src="https://velog.velcdn.com/images/yujin_1219/post/88042f13-ca0c-4fa5-93c7-d9ca494112b1/image.png" width="70%" height="50%">

<h3 id="📌-반복을-이용한-이진탐색-구현-★★">📌 반복을 이용한 이진탐색 구현 ★★</h3>
<pre><code class="language-sql">int binarySearch2(int key, int low, int high) {
    int mid;

    while(low &lt;= high) {
        mid = (low + high) / 2;

        if(key == arr[mid]) {
            return mid;
        } else if(key &lt; arr[mid]) {
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }

    return -1; // 탐색 실패 
}</code></pre>
<h3 id="📌-순환호출을-이용한-이진탐색-구현">📌 순환호출을 이용한 이진탐색 구현</h3>
<pre><code class="language-sql">int binarySearch1(int key, int low, int high) {
    int mid;

    if(low &lt;= high) {
        mid = (low + high) / 2;

        if(key == arr[mid]) { // 탐색 성공 
            return mid;
        } else if(key &lt; arr[mid]) {
            // 왼쪽 부분 arr[0]부터 arr[mid-1]에서의 탐색 
            return binarySearch1(key ,low, mid-1);  
        } else {
            // 오른쪽 부분 - arr[mid+1]부터 arr[high]에서의 탐색 
            return binarySearch1(key, mid+1, high); 
        }
    }

    return -1; // 탐색 실패 
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Oracle(오라클)]]></title>
            <link>https://velog.io/@yujin_1219/Oracle%EC%98%A4%EB%9D%BC%ED%81%B4-%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@yujin_1219/Oracle%EC%98%A4%EB%9D%BC%ED%81%B4-%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Fri, 20 Jun 2025 08:05:43 GMT</pubDate>
            <description><![CDATA[<h3 id="오라클oracle-역사">오라클(Oracle) 역사</h3>
<ul>
<li>1978년 :로렌스 J. 엘리슨(현 회장)이 관계형 DBMS인 오라클 첫 번째 버전(Version 1)을 개발</li>
<li>1979년 : 회사명을 RSI(Relational Software Inc.)로 바꾸고 첫 번째 상용 DBMS인 오라클 두 번째 버전(Version 2)을 개발</li>
<li>1983년 : 회사 이름을 지금의 오라클로 바꾸고 C언어로 개발된 오라클 세 번째 버전(Version3)을 출시</li>
<li>1999년 : 오라클 8i 출시 (i는 인터넷의 약자)</li>
<li>2011년 : 오라클 11g 출시</li>
<li>현재 : 오라클 21c (최신)</li>
</ul>
<hr>
<h3 id="오라클의-특징">오라클의 특징</h3>
<ul>
<li>네트워크 환경 지원</li>
<li>다양한 운영체제 지원</li>
<li>대용량 데이터 처리 지원</li>
<li>여러 사용자의 동시 접속 지원</li>
<li>신뢰성 높은 보안 기능 지원</li>
<li>오류 및 장애에 대한 대비책 지원</li>
</ul>
<hr>
<h3 id="sqlplus-기본-명령어">SQL*Plus 기본 명령어</h3>
<p><strong>SQL*Plus 접속 및 종료</strong></p>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>sqlplus /nolog</code></td>
<td>SQL*Plus 실행 (바로 DB 연결 안 함)</td>
</tr>
<tr>
<td><code>conn sys as sysdba</code></td>
<td><strong>SYSDBA 권한</strong>으로 관리자 계정 접속</td>
</tr>
<tr>
<td><code>conn 사용자명/비밀번호</code></td>
<td>일반 사용자로 데이터베이스 접속</td>
</tr>
<tr>
<td><code>conn C##HANSUNG</code></td>
<td>비밀번호 입력 후 HANSUNG 계정으로 접속</td>
</tr>
<tr>
<td><code>disconnect</code> 또는 <code>disconn</code></td>
<td>현재 연결 종료</td>
</tr>
<tr>
<td><code>exit</code> 또는 <code>quit</code></td>
<td>SQL*Plus 종료</td>
</tr>
</tbody></table>
<p><strong>화면 및 출력 관리</strong></p>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>clear screen</code> 또는 <code>cl scr</code></td>
<td>SQL*Plus 화면 초기화</td>
</tr>
<tr>
<td><code>set linesize 100</code></td>
<td>출력 줄의 길이를 100자로 설정</td>
</tr>
<tr>
<td><code>set pagesize 50</code></td>
<td>한 페이지에 출력할 최대 행 수 50으로 설정</td>
</tr>
<tr>
<td><code>column 열이름 format</code></td>
<td>열의 출력 포맷 지정예: <code>column 주소 format a20</code></td>
</tr>
</tbody></table>
<p><strong>테이블 목록 조회</strong></p>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>SELECT table_name FROM user_tables;</code></td>
<td>현재 사용자 소유의 모든 테이블 목록 조회</td>
</tr>
<tr>
<td><code>SELECT table_name FROM all_tables;</code></td>
<td>접근 권한 있는 모든 테이블 목록 조회</td>
</tr>
</tbody></table>
<p><strong>자동 커밋 설정</strong></p>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>SHOW AUTOCOMMIT;</code></td>
<td>현재 자동 커밋 상태 확인</td>
</tr>
<tr>
<td><code>SET AUTOCOMMIT OFF;</code></td>
<td>자동 커밋 <strong>비활성화</strong> (수동으로 COMMIT 필요)</td>
</tr>
<tr>
<td><code>SET AUTOCOMMIT ON;</code></td>
<td>자동 커밋 <strong>활성화</strong> (명령어 실행 즉시 반영)</td>
</tr>
</tbody></table>
<pre><code class="language-sql">//SQL*Plus 실행
sqlplus/nolog
sqlplus/nolog
//시스템 관리자인 sys 계정으로 접속
connect sys as sysdba
connect sys as sysdba
//둘이 합쳐서 이렇게도 가능
sqlplus sys as sysdba
sqlplus sys as sysdba
//현재 데이터베이스에 존재하는 모든 사용자 계정 정보를 조회
select * from all_users;

//기본적 생성 명령
create user &lt;사용자 계정&gt; identified by &lt;비밀번호&gt;;
create user C##Yujin identified by 1234
//계정 잠금 해제
alter user &lt;사용자 계정&gt; account unlock;
alter user C#Yujin account unlock
//로그인 및 자원 사용 권한 부여
grant connect,resource to &lt;사용자계정&gt;
grant connect, resource to 
//새로운 사용자계정으로 로그인
connect &lt;사용자 계정&gt;</code></pre>
]]></description>
        </item>
    </channel>
</rss>