<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dohn_1203.log</title>
        <link>https://velog.io/</link>
        <description>DoHn's dev log</description>
        <lastBuildDate>Sun, 27 Jul 2025 07:43:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dohn_1203.log</title>
            <url>https://velog.velcdn.com/images/dohn_1203/profile/155dce59-f08d-4615-b74b-b229b9b46d68/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dohn_1203.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dohn_1203" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[디자인 패턴: 저장소 패턴(Repository Pattern)]]></title>
            <link>https://velog.io/@dohn_1203/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%A0%80%EC%9E%A5%EC%86%8C-%ED%8C%A8%ED%84%B4Repository-Pattern</link>
            <guid>https://velog.io/@dohn_1203/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%A0%80%EC%9E%A5%EC%86%8C-%ED%8C%A8%ED%84%B4Repository-Pattern</guid>
            <pubDate>Sun, 27 Jul 2025 07:43:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>도메인 로직과 데이터 접근 로직을 분리하기 위해 자주 사용되는 소프트웨어 디자인 패턴.
주로 DDD(Domain-Driven Design)에서 강조되지만, MVC나 계층형 아키텍처에서도 널리 사용된다.</p>
</blockquote>
<p>저장소 패턴은 GoF 디자인 패턴 23개에 포함되어 있지 않다. 하지만 실무 백엔드에서 정말 자주 쓰이는 실용적인 설계 패턴이라고 생각한다.</p>
<br/>

<h2 id="저장소-패턴repository-pattern이란">저장소 패턴(Repository Pattern)이란?</h2>
<p><strong>저장소(Repository)</strong>는 도메인 객체의 컬렉션처럼 작동하는 추상 계층으로, 데이터베이스, 외부 API, 캐시 등 다양한 저장소에서 데이터를 가져오는 복잡한 로직을 캡슐화하여 일관된 인터페이스로 제공하는 역할을 한다.</p>
<p>즉, Service -&gt; Repository -&gt; Data Source(DB, API) 구조로, 비즈니스 로직은 Repository에만 의존하며, 구체적인 구현은 숨겨진다.</p>
<br/>

<h2 id="저장소-패턴을-사용하는-이유">저장소 패턴을 사용하는 이유</h2>
<p>일반적인 Service-Controller 구조에서는 비즈니스 로직과 DB 접근 로직이 강하게 결합되어 있다. 이는 장기적으로 보았을 때 코드의 복잡도를 높이고 가독성을 떨어뜨리며, 유지보수에 어려움을 겪을 수 있다.
저장소 패턴은 비즈니스 로직에서 데이터 접근 로직을 분리하여, DB나 외부 API 구조가 바뀌어도 Repository만 수정하면 되기 때문에 유지보수에 용이하며, 실제 DB 없이 Repository를 mocking하여 테스트가 가능하다. 또한 비즈니스 로직에서 복잡한 쿼리나 조건이 Repository로 숨겨지기 때문에 깔끔한 인터페이스를 제공할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/baf96764-53ef-406e-9b9a-0fee6f4f99dd/image.png" alt=""></p>
<br/>

<h2 id="저장소-패턴-예시-fastapi--sqlalchemy">저장소 패턴 예시 (FastAPI + SQLAlchemy)</h2>
<h3 id="as-is-로그인-기능">AS-IS: 로그인 기능</h3>
<p>라우터에서 요청 파라미터를 받아서, 비즈니스 로직으로 넘기게 된다.</p>
<pre><code class="language-python"># ❌ 비즈니스 로직과 데이터 접근 로직이 강하게 결합된 형태
class AuthService:
    def login(self, request, db: Session) -&gt; tuple[str, str]:
        # DB 접근 로직 (User 조회)
        user = db.query(User).filter(User.email == request.email).first()

        # 비즈니스 로직 (비밀번호 검증, 토큰 생성)
        ...</code></pre>
<p><code>AuthService</code>에서 처리 후 결과값을 <code>router</code>에게 다시 넘겨주게 된다.</p>
<h3 id="to-be-로그인-기능">TO-BE: 로그인 기능</h3>
<p>위 과정에서 비즈니스 로직에 DB 접근 로직이 포함되어 있다. 이를 Repository로 분리해주는 것이다.</p>
<pre><code class="language-python">class UserRepository:
    def __init__(self, db: Session):
        self.db = db

    def get_by_email(self, email: str) -&gt; Optional[User]:
        # ✅ DB 쿼리 세부 구현은 여기서만 관리
        return self.db.query(User).filter(User.email == email).first()</code></pre>
<pre><code class="language-python">class AuthService:
    def __init__(self, user_repo: UserRepository):
        self.user_repo = user_repo

    def login(self, request) -&gt; tuple[str, str]:
        user = self.user_repo.get_by_email(request.email)
        if not user or not auth.verify_password(request.password, user.password):
            raise HTTPException(...)

        # ✅ 비즈니스 로직만 남음
        ...</code></pre>
<p>이후 <code>router</code>에서 Repository 클래스와 <code>AuthService</code> 클래스를 생성해 넣어준다.
위와 같은 방식으로 구현하게 되면 각 계층이 단일 책임 원칙(SRP)을 따르기 때문에 테스트, 유지보수, 확장이 쉬워진다.</p>
<br/>

<h2 id="저장소-패턴의-장단점">저장소 패턴의 장단점</h2>
<h3 id="장점">장점</h3>
<ul>
<li>비즈니스 로직과 인프라(데이터 저장소)를 분리하여 유지보수성 향상</li>
<li>다양한 저장소(DB, Redis, API 등)로 교체 가능</li>
<li>Mock Repository를 사용하여 테스트하기 쉬움</li>
<li>코드 일관성과 확장성 증가</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>초기 설계 시 복잡도 증가 (작은 프로젝트에 적용할 경우 오버엔지니어링)</li>
<li>Repository가 너무 비대해지면 SRP(Single Responsibility Principle) 위반</li>
<li>Interface/Implementation의 중복 코드 가능성</li>
</ul>
<br/>

<p>규모가 작은 프로젝트의 경우 오버엔지니어링일 수 있으나, 규모가 커질 수 있는 프로젝트라면 나중을 대비해 저장소 패턴을 활용하여 API 로직을 설계하는 것도 좋은 방안이 될 수 있을 것 같다.</p>
<hr>
<p><strong>Reference</strong>
<a href="https://daco2020.tistory.com/439">저장소 패턴(Repository Pattern) 도입기, by daco2020</a>
<a href="https://ttoogi.tistory.com/149">[디자인패턴] Repository Pattern 레포지토리 패턴, by ttoogi</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스: Redis (0) - Redis란?]]></title>
            <link>https://velog.io/@dohn_1203/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-Redis-0-Redis%EB%9E%80</link>
            <guid>https://velog.io/@dohn_1203/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-Redis-0-Redis%EB%9E%80</guid>
            <pubDate>Wed, 02 Jul 2025 08:04:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Redis는 극한의 속도를 자랑하는 인메모리 데이터 저장소로, 캐시부터 실시간 데이터 처리까지 모든 퍼포먼스를 가속시켜준다.</p>
</blockquote>
<p>우리는 다양한 프로젝트를 진행하다 보면 종종 다음과 같은 고민을 하게 된다.</p>
<ul>
<li>DB 조회가 느리다... 캐싱을 해야 하나?</li>
<li>트래픽이 몰릴 때 응답이 버벅거린다</li>
<li>실시간 랭킹이나 세션 저장을 어떻게 하지?</li>
</ul>
<p>이런 문제를 해결해주는 빠르고 가벼운 저장소가 바로 <strong>Redis</strong>이다.</p>
<br/>

<h2 id="redis란">Redis란?</h2>
<p>Redis는 <strong>Remote Dictionary Service</strong>의 약자로, 오픈 소스 기반의 <strong>Key-Value 형태 In-Memory 데이터 저장소</strong>이다.<br>Key-Value 기반이기 때문에 쿼리를 직접 보낼 필요 없이 <strong>$O(1)$의 시간 복잡도</strong>로 값을 바로 조회할 수 있다.</p>
<p>또한 디스크가 아닌 메모리에서 데이터를 처리하므로 속도가 매우 빠르고,<br><strong>NoSQL</strong> 구조이며 <strong>List, Hash, Set, Sorted Set 등 다양한 자료구조</strong>도 지원한다.</p>
<br/>

<h2 id="redis가-빠른-이유">Redis가 빠른 이유</h2>
<p>Redis는 내부적으로 <strong>해시 테이블(dict)</strong> 구조를 기반으로 동작한다.</p>
<pre><code>SET user:1001 &quot;Alice&quot;
GET user:1001 # -&gt; &quot;Alice&quot;</code></pre><h3 id="왜-빠를까">왜 빠를까?</h3>
<p>Redis는 key를 해시 처리한 후, <strong>메모리 주소에서 즉시 조회</strong>한다. 디스크 접근이 필요 없기 때문에, 일반 RDBMS보다 <strong>수백 배 이상 빠르다</strong>.</p>
<blockquote>
<p>Redis는 <code>&quot;Key → Value&quot;</code>를 메모리에서 바로 찾아내므로,<br><strong>쿼리가 아닌 &quot;주소 접근&quot;처럼 작동</strong>한다.</p>
</blockquote>
<br/>

<h2 id="인메모리in-memory-방식">인메모리(In-Memory) 방식</h2>
<p><strong>인메모리 방식</strong>이란, 데이터를 하드디스크가 아닌 <strong>RAM에 올려서 직접 처리하는 방식</strong>이다.<br>메모리 내부에서 모든 연산이 이루어지기 때문에 저장과 조회 속도가 매우 빠르다.</p>
<p>하지만 한계도 있다:</p>
<ul>
<li><strong>RAM 용량</strong>을 초과하면 더 이상 저장할 수 없고,</li>
<li>별도로 영속성 설정을 하지 않으면 <strong>Redis 재시작 시 데이터가 유실될 수 있다.</strong></li>
</ul>
<br/>

<h2 id="redis는-언제-쓸까">Redis는 언제 쓸까?</h2>
<table>
<thead>
<tr>
<th align="left">용도</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">캐시</td>
<td align="left">DB 쿼리 결과, API 응답 등을 임시 저장해 빠른 응답 제공 가능</td>
</tr>
<tr>
<td align="left">세션 저장소</td>
<td align="left">JWT나 로그인 세션 등 사용자 인증 데이터 저장</td>
</tr>
<tr>
<td align="left">실시간 랭킹</td>
<td align="left">Sorted Set으로 실시간 점수 정렬 처리</td>
</tr>
<tr>
<td align="left">메시지 큐</td>
<td align="left">Pub/Sub 또는 Stream 구조로 간단한 비동기 처리</td>
</tr>
<tr>
<td align="left">토큰 저장/Rate Limit</td>
<td align="left">인증/요청 제한 제어</td>
</tr>
</tbody></table>
<br/>

<h2 id="mysql과-redis를-같이-쓰는-이유">MySQL과 Redis를 같이 쓰는 이유</h2>
<p>Redis는 MySQL을 대체하지 않고, <strong>보완</strong>하는 용도로 많이 사용된다.</p>
<ul>
<li><strong>MySQL</strong>: 영속적인 데이터 저장</li>
<li><strong>Redis</strong>: 빠른 조회/임시 데이터 저장</li>
</ul>
<h3 id="예시-게시글-조회수-처리">예시: 게시글 조회수 처리</h3>
<ol>
<li>사용자들이 게시글을 조회할 때 Redis에서 조회 수를 증가시킨다.<br>→ <code>INCR post:123:views</code></li>
<li>일정 주기마다 MySQL에 반영 (배치 업데이트)</li>
</ol>
<p>이렇게 하면 <strong>DB 부하를 줄이면서도 실시간 응답이 가능</strong>해진다.</p>
<br/>

<h2 id="redis-사용-시-주의점">Redis 사용 시 주의점</h2>
<ul>
<li><strong>메모리 용량 한계</strong>: In-Memory 기반이므로 용량 초과 시 문제가 발생할 수 있다.</li>
<li><strong>데이터 유실 가능성</strong>: 기본 설정에선 재시작 시 데이터가 날아갈 수 있다.<br>→ <code>RDB</code>, <code>AOF</code> 설정 필요</li>
<li><strong>키 설계 중요</strong>: 무분별한 키 생성은 메모리 낭비와 관리 어려움을 초래할 수 있다.</li>
</ul>
<hr>
<p>다음 포스팅에서는 <strong>실전에서 Redis를 어떻게 사용하는지</strong>를 다뤄볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이썬: uv - 패키지 관리 도구]]></title>
            <link>https://velog.io/@dohn_1203/%ED%8C%8C%EC%9D%B4%EC%8D%AC-uv-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B4%80%EB%A6%AC-%EB%8F%84%EA%B5%AC</link>
            <guid>https://velog.io/@dohn_1203/%ED%8C%8C%EC%9D%B4%EC%8D%AC-uv-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B4%80%EB%A6%AC-%EB%8F%84%EA%B5%AC</guid>
            <pubDate>Sun, 20 Apr 2025 08:41:52 GMT</pubDate>
            <description><![CDATA[<h2 id="🚀-uv란-무엇인가">🚀 uv란 무엇인가?</h2>
<blockquote>
<p>파이썬 패키지를 설치하고 관리하기 위한 만능 패키지 관리 도구.
Rust로 구현되어 있으며, 기존 pip보다 <strong>훨씬 빠른 설치 속도</strong>와
<strong>더 안전한 의존성 관리</strong>를 목표로 개발됨.</p>
</blockquote>
<p>uv는 <code>pip</code>, <code>pip-tools</code>, <code>pipenv</code>를 모두 대체할 수 있도록 설계된 패키지 매니저이다.</p>
<p>최근에는 MCP(Model Context Protocol) 개발 등에 많이 활용되면서 큰 관심을 받고 있다.</p>
<br/>

<h2 id="❓-왜-uv를-사용할까">❓ 왜 uv를 사용할까?</h2>
<p>기존엔 파이썬 프로젝트를 관리할 때는 보통 다음 과정을 거쳤다.</p>
<pre><code class="language-bash">python -m venv venv
source venv/Scripts/activate
pip install pandas</code></pre>
<ul>
<li><code>python -m venv</code>로 가상환경을 만들고,</li>
<li><code>source</code>로 가상환경을 활성화한 뒤,</li>
<li>pip로 필요한 패키지를 설치하는 방식이다.</li>
</ul>
<p>하지만 이 방법은 관리가 번거롭고, 패키지를 설치할 때마다 시간이 오래 걸리는 문제가 있었다.</p>
<p>또한, pip만으로는 의존성 충돌 관리나 lockfile 생성을 자동화하기 어렵고, poetry 같은 도구는 설정이 복잡하고 무겁게 느껴질 수 있다.</p>
<p>하지만, uv를 사용하면 상황이 완전히 달라진다.</p>
<p>uv를 사용하면, pip나 poetry 대비 10배에서 최대 100배까지 빠르게 패키지를 설치할 수 있다. Rust 기반 최적화 덕분이다.</p>
<p>또한, 프로젝트 관리 역시 훨씬 간단해진다.</p>
<pre><code class="language-bash">uv init
uv add pandas</code></pre>
<ul>
<li><code>uv init</code>으로 프로젝트를 초기화하고,</li>
<li><code>uv add</code> 명령어로 필요한 패키지를 간편하게 추가할 수 있다.</li>
</ul>
<p>추가로, uv는 가상환경을 직접 관리하기 때문에 <code>python -m venv</code>나 <code>pyenv</code>, <code>virtualenv</code>같은 명령어를 따로 신경 쓸 필요가 없다.</p>
<p>모든 패키지 의존성은 <code>uv.lock</code> 파일로 명확하게 기록되기 때문에, 다른 환경에서도 동일한 설치 결과를 재현할 수 있다.</p>
<br/>

<h2 id="✨-uv의-특징">✨ uv의 특징</h2>
<table>
<thead>
<tr>
<th align="left">항목</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">🚀 설치 속도</td>
<td align="left">Rust 기반, 기존 <code>pip</code> 대비 압도적으로 빠른 설치</td>
</tr>
<tr>
<td align="left">🛠️ 의존성 관리</td>
<td align="left"><code>lockfile</code>을 사용해 설치 환경을 완벽히 재현</td>
</tr>
<tr>
<td align="left">🔗 CLI 통합</td>
<td align="left"><code>pip</code>, <code>pip-tools</code>, <code>pipenv</code> 기능을 하나로 통합</td>
</tr>
<tr>
<td align="left">🔄 호환성</td>
<td align="left">기존 파이썬 프로젝트와 호환성 유지</td>
</tr>
<tr>
<td align="left">⚡ 로컬 캐싱</td>
<td align="left">설치한 패키지 캐싱으로 재설치 시 속도 최적화</td>
</tr>
</tbody></table>
<br/>

<h2 id="🛠️-uv-설치-방법">🛠️ uv 설치 방법</h2>
<p>Linux 및 macOS에서 uv는 다음과 같이 간단하게 설치할 수 있다. <a href="https://docs.astral.sh/uv/getting-started/installation/">설치 방법 참고</a></p>
<pre><code class="language-bash"># curl 사용하기
curl -LsSf https://astral.sh/uv/install.sh | sh
# wget 사용하기
wget -qO- https://astral.sh/uv/install.sh | sh
# 특정 버전 설치하기
curl -LsSf https://astral.sh/uv/0.6.14/install.sh | sh</code></pre>
<p>혹은 기존 패키지 관리 도구로 설치 가능하다.</p>
<pre><code class="language-bash">pip install uv</code></pre>
<p>설치가 완료되었으면, 다음 명령어로 설치 여부를 확인할 수 있다.</p>
<pre><code class="language-bash">uv --version</code></pre>
<br/>

<h2 id="🧪-uv-사용해보기">🧪 uv 사용해보기</h2>
<h3 id="🏗️-프로젝트-생성">🏗️ 프로젝트 생성</h3>
<pre><code class="language-bash">uv init test
cd test</code></pre>
<p><code>uv init</code> 명령어를 사용하면,
현재 디렉토리를 uv 프로젝트로 초기화하거나,
새로운 프로젝트 폴더를 생성할 수 있다.</p>
<p>위 예시에서는 test라는 이름의 uv 프로젝트를 생성하였다.</p>
<p>생성된 test 폴더의 구조는 다음과 같다.</p>
<pre><code class="language-bash">test/
├── .git
├── .gitignore
├── .python-version
├── README.md
├── main.py
└── pyproject.toml</code></pre>
<ul>
<li><code>.python-version</code>을 통해 파이썬 버전이 고정된다.</li>
<li><code>pyproject.toml</code>은 의존성 및 프로젝트 메타데이터를 정의하는 핵심 파일이다.</li>
<li><code>.venv</code> 폴더는 의존성을 관리하기 위한 폴더이다.</li>
</ul>
<p><code>.venv</code> 폴더는 실제로 패키지를 설치하거나 <code>uv sync</code>를 실행할 때 생성된다.</p>
<br/>

<h3 id="➕-패키지-추가">➕ 패키지 추가</h3>
<p>패키지를 추가하는 것도 매우 간단하다.</p>
<pre><code class="language-bash">uv add pandas</code></pre>
<ul>
<li><code>uv add</code> 명령어를 사용하면 패키지를 설치하고,</li>
<li><code>pyproject.toml</code>과 <code>uv.lock</code>에 의존성이 자동으로 기록된다.</li>
</ul>
<p>추가적으로 로컬 캐시를 활용하여, 설치 속도가 pip에 비해 월등히 빠르다.</p>
<br/>

<h3 id="📋-설치된-패키지-확인">📋 설치된 패키지 확인</h3>
<p>현재 설치된 패키지 목록을 확인하려면</p>
<pre><code class="language-bash">uv pip list</code></pre>
<p>또는,</p>
<pre><code class="language-bash">uv pip freeze</code></pre>
<p>를 사용할 수 있다.
pip 명령어처럼 사용할 수 있다는 점이 매우 직관적이다.</p>
<br/>

<h3 id="🔄-의존성-동기화">🔄 의존성 동기화</h3>
<p>만약 다른 개발자가 이 프로젝트를 클론했다면, 다음 명령어 하나로 모든 환경을 재현할 수 있다.</p>
<pre><code class="language-bash">uv sync</code></pre>
<ul>
<li><code>uv.lock</code> 파일을 기준으로 정확히 동일한 패키지 버전이 설치된다.</li>
<li>프로젝트 환경 복원이 완벽하게 보장된다.</li>
</ul>
<br/>

<h3 id="▶️-실행-테스트">▶️ 실행 테스트</h3>
<p>초기화 시 생성된 <code>main.py</code> 파일을 실행해보자.</p>
<pre><code class="language-bash">uv run main.py</code></pre>
<p>기본적으로 <code>Hello, world!</code>가 출력된다.</p>
<p>필요에 따라 <code>main.py</code>에 코드를 추가해서 본격적인 개발을 진행하면 된다.</p>
<br/>

<hr>
<p>uv는 빠르고 직관적이며, 가상환경과 의존성 관리를 통합한 덕분에 파이썬 프로젝트를 훨씬 효율적으로 관리할 수 있다. </p>
<p>아마 이제 아나콘다처럼 무겁고 복잡한 환경은 점점 필요 없어지지 않을까 싶다.</p>
<br/>

<h3 id="📚-references">📚 References</h3>
<p><a href="https://docs.astral.sh/uv/getting-started/">uv 공식 문서 - Getting Started</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘: 이분 탐색(Binary Search)]]></title>
            <link>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%9D%B4%EB%B6%84-%ED%83%90%EC%83%89Binary-Search</link>
            <guid>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%9D%B4%EB%B6%84-%ED%83%90%EC%83%89Binary-Search</guid>
            <pubDate>Sun, 20 Apr 2025 06:04:02 GMT</pubDate>
            <description><![CDATA[<h2 id="이분-탐색이란">이분 탐색이란?</h2>
<blockquote>
<p>정렬되어 있는 데이터 집합에서, 원하는 값을 빠르게 찾는 알고리즘.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/9a46e371-4a1c-40ab-acd9-15d835b3ae65/image.png" alt="" title="출처: https://blog.penjee.com/binary-vs-linear-search-animated-gifs"></p>
<div style="text-align: center; margin-top: -40px; color: gray;">출처: https://blog.penjee.com/binary-vs-linear-search-animated-gifs</div>



<p>전체를 순차적으로 탐색하는 대신, <strong>항상 탐색 범위를 절반씩 줄여나가면서</strong> 원하는 값을 찾아나간다.</p>
<p>시간 복잡도는 $O(\log n)$으로 매우 빠른 탐색을 할 수 있다.</p>
<br/>

<h2 id="전제-조건">전제 조건</h2>
<ul>
<li>데이터가 <strong>정렬</strong>되어 있어야 한다.</li>
<li>데이터가 오름차순/내림차순 등으로 이미 정리되어 있지 않다면, 먼저 정렬해야 한다.</li>
</ul>
<br/>

<h2 id="동작-과정">동작 과정</h2>
<ol>
<li>탐색 범위의 중간값 <code>mid</code>를 찾는다.</li>
<li>중간값이 찾는 값이면 종료한다.</li>
<li>중간값보다 찾는 값이 작으면 왼쪽 구간만 탐색한다.</li>
<li>중간값보다 찾는 값이 크면 오른쪽 구간만 탐색한다.</li>
<li>탐색 구간이 없을 때까지 반복한다.</li>
</ol>
<p>항상 절반씩 범위를 좁히기 때문에, $O(\log n)$이라는 빠른 속도를 가진다.</p>
<br/>

<h2 id="구현">구현</h2>
<p>이분 탐색으로 풀 수 있는 문제인 <a href="https://www.acmicpc.net/problem/1920">#1920. 수 찾기</a>를 예시로 들어보자.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/1138df1b-a46b-4e95-9115-6226d5ae759e/image.png" alt=""></p>
<p>주어진 수열에서 특정 수가 존재하는지 아닌지만 판별하는 문제이다.</p>
<p>처음 떠올릴 수 있는 가장 직관적인 방법은, 단순히 모든 원소를 순회하면서 하나씩 비교하는 방식이다.</p>
<pre><code class="language-python">def find(target: int, seq: list) -&gt; bool:
    for n in seq:
        if n == target: return True
    return False</code></pre>
<p>하지만 이 방법은 최악의 경우 <strong>모든 원소를 다 비교해야 하므로</strong> $O(n)$이 걸린다.</p>
<p>특히 데이터가 수십만 개 이상일 경우, 시간 초과가 발생할 수 있다.</p>
<p>따라서 더 효율적인 방법인 <strong>이분 탐색</strong>을 사용해 문제를 해결할 수 있다.</p>
<pre><code class="language-python">def find(target: int, seq: list) -&gt; bool:
    low = 0
    high = len(seq) - 1
    while low &lt;= high:
        mid = (low + high) // 2
        if seq[mid] == target: return True
        elif seq[mid] &lt; target: low = mid + 1
        else: high = mid - 1
    return False</code></pre>
<p>여기서 seq는 반드시 정렬되어 있어야 한다. 정렬되어 있지 않으면, 이분 탐색은 올바르게 동작하지 않는다.</p>
<p>이처럼 탐색 문제에서 단순 반복문 대신 정렬 + 이분 탐색 패턴을 적용하면 시간복잡도를 대폭 줄여 효율적인 해결이 가능하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[BOJ: #1865. 웜홀]]></title>
            <link>https://velog.io/@dohn_1203/BOJ-1865.-%EC%9B%9C%ED%99%80</link>
            <guid>https://velog.io/@dohn_1203/BOJ-1865.-%EC%9B%9C%ED%99%80</guid>
            <pubDate>Fri, 18 Apr 2025 04:49:50 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="">#1865. 웜홀</a></p>
<p>때는 2020년, 백준이는 월드나라의 한 국민이다. 월드나라에는 $N$개의 지점이 있고 N개의 지점 사이에는 $M$개의 도로와 $W$개의 웜홀이 있다. <strong>(단 도로는 방향이 없으며 웜홀은 방향이 있다.)</strong> 웜홀은 시작 위치에서 도착 위치로 가는 하나의 경로인데, 특이하게도 도착을 하게 되면 시작을 하였을 때보다 시간이 뒤로 가게 된다. 웜홀 내에서는 시계가 거꾸로 간다고 생각하여도 좋다.</p>
<p>시간 여행을 매우 좋아하는 백준이는 한 가지 궁금증에 빠졌다. 한 지점에서 출발을 하여서 시간여행을 하기 시작하여 다시 출발을 하였던 위치로 돌아왔을 때, 출발을 하였을 때보다 시간이 되돌아가 있는 경우가 있는지 없는지 궁금해졌다. 여러분은 백준이를 도와 이런 일이 가능한지 불가능한지 구하는 프로그램을 작성하여라.</p>
<hr>
<h2 id="접근-방식">접근 방식</h2>
<p>웜홀이 음수 가중치를 갖기 때문에, <strong>그래프 내 음수 사이클 존재 여부</strong>를 확인하는 문제로 해석할 수 있다.
따라서, 벨만-포드 알고리즘을 적용하는 방향으로 접근하였다.</p>
<pre><code class="language-python">def solution(n: int, edges: list) -&gt; bool:
    dist = [float(&quot;inf&quot;)] * (n + 1)
    dist[1] = 0
    for i in range(1, n + 1):
        for s, e, cost in edges:
            if dist[s] != float(&quot;inf&quot;) and dist[e] &gt; dist[s] + cost:
                dist[e] = dist[s] + cost
                if i == n: return True
    return False</code></pre>
<p>위는 일반적인 벨만-포드 알고리즘 구현 방식이다. <code>n</code>번 반복한 뒤, 마지막 반복에서 거리 갱신이 일어나면 음수 사이클이 존재하는 것이므로 <code>True</code>를 리턴하고, 그렇지 않다면 <code>False</code>를 리턴해 출발하였을 때보다 시간이 되돌아가 있는 경우가 있는지를 확인할 수 있다.</p>
<br/>

<h2 id="주의해야-할-점">주의해야 할 점</h2>
<p>여기서 중요하게 짚고 넘어가야 하는 부분이 있다.</p>
<p><strong>1. 백준이가 출발을 어디서 하는가?</strong></p>
<p>백준이는 아무데서나 출발할 수 있다. 따라서 음수 가중치가 존재하기만 한다면, 출발했을 때보다 시간이 되돌아가 있는 경우가 있다는 의미이다.</p>
<p><strong>2. 출발 지점을 어디로 설정해야 하는가?</strong></p>
<p>모든 지점이 연결되어 있다면, 시작 지점이 1이어도 상관없지만(어차피 모든 지점을 모두 지나가게 됨), 1 지점과 연결되어 있지 않은 지점이 존재한다면, 해당 지점들 사이에서 음수 사이클이 발생하는지 확인할 수 없기 때문이다.</p>
<p>이 문제를 해결하기 위해 0이라는 가상 지점을 만든 후 모든 지점과 연결만 시켜준다면, 기존 알고리즘으로도 무리 없이 음수 사이클을 발견할 수 있다.</p>
<p>혹은 if문의 <code>dist[s] != float(&quot;inf&quot;)</code> 부분을 제거하여 방문하지 않은 노드더라도 일단 탐색을 하는 방식이 있다.</p>
<br/>

<h3 id="코드-1가상-지점-만들기">코드 1(가상 지점 만들기)</h3>
<pre><code class="language-python">def solution(n: int, edges: list) -&gt; bool:
    new_edges = edges + [(0, i, 0) for i in range(1, n + 1)]
    dist = [float(&quot;inf&quot;)] * (n + 1)
    dist[0] = 0
    for i in range(n + 1):
        for s, e, cost in new_edges:
            if dist[s] != float(&quot;inf&quot;) and dist[e] &gt; dist[s] + cost:
                dist[e] = dist[s] + cost
                if i == n: return True
    return False</code></pre>
<p>0과 모든 지점간 비용이 0으로 연결되어있는 간선을 만든 후, 시작 지점을 0으로 둔다.</p>
<p>이후 n번 반복한 다음, 마지막 반복에서 갱신이 일어나면 음수 사이클이 있는것이므로 <code>True</code>를 리턴한다.</p>
<br/>

<h3 id="코드-2방문-여부-체크-x">코드 2(방문 여부 체크 X)</h3>
<pre><code class="language-python">def solution(n: int, edges: list) -&gt; bool:
    dist = [100_000_000] * (n + 1)
    dist[1] = 0
    for i in range(n + 1):
        for s, e, cost in edges:
            if dist[e] &gt; dist[s] + cost:
                dist[e] = dist[s] + cost
                if i == n: return True
    return False</code></pre>
<p>위 코드에서는 시작 지점을 1로 설정해주었지만, 시작 지점을 설정해주지 않아도 되고, 모든 지점에서 동시에 출발해도 상관 없다(dist 리스트의 모든 값을 0으로 초기화해도 상관 없음).</p>
<p>위 방식에서 중요한 점은 절대 dist 리스트를 <code>float(&quot;inf&quot;)</code>로 초기화하면 안된다는 점인데, 무한에 음수를 더해도 여전히 무한이기 때문에 거리 갱신이 이루어지지 않기 때문이다.</p>
<br/>

<h2 id="그럼에도-불구하고-내가-틀린-이유">그럼에도 불구하고 내가 틀린 이유</h2>
<p>두 가지 방식을 모두 적용해보았지만 2%에서 계속 틀려서 다른 사람들의 코드를 참고했다. 하지만 내 코드와의 차이점을 찾을 수 없었다..</p>
<p>그 해답을 <a href="https://www.acmicpc.net/board/view/89941">질문 게시판</a>에서 찾을 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/191a3440-a110-4150-8e9f-6d04b51408dc/image.png" alt=""></p>
<p>그동안 도로의 간선도 웜홀과 동일하게 방향 그래프로 넣어주었다.. 즉, <code>solution()</code> 함수에 문제가 있는게 아니라 그래프 자체에 문제가 있었던 것이다.</p>
<pre><code class="language-python"># 원래 이렇게 넣어주고 있었음
edges.append(tuple(map(int, input().split())))</code></pre>
<pre><code class="language-python"># 도로는 무방향 -&gt; 양쪽 모두 추가해야 함
s, e, c = map(int, input().split())
edges.append((s, e, c))
edges.append((e, s, c))</code></pre>
<p>바꿔주니까 잘 작동하는 것을 확인할 수 있었다..</p>
<hr>
<p>이 문제는 최단 거리를 묻는 문제가 아니라서, 벨만 포드 외에도 다양한 풀이법이 존재하는 것 같다. 나중에 다른 방법도 시도해보는게 좋을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘: 벨만 포드(Bellman-Ford) - 최단 경로 알고리즘]]></title>
            <link>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%B2%A8%EB%A7%8C-%ED%8F%AC%EB%93%9CBellman-Ford-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%B2%A8%EB%A7%8C-%ED%8F%AC%EB%93%9CBellman-Ford-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Thu, 17 Apr 2025 07:39:06 GMT</pubDate>
            <description><![CDATA[<h2 id="벨만-포드란">벨만 포드란?</h2>
<blockquote>
<p>음수 가중치가 있는 그래프에서도
단일 출발점에서 모든 정점까지의 최단 경로를 구할 수 있는 알고리즘.</p>
</blockquote>
<p>다익스트라 알고리즘은 <strong>음수 가중치를 허용하지 않지만, 벨만 포드는 이를 허용</strong>하며, <strong>음수 사이클 존재 여부도 판별할 수 있다.</strong></p>
<br/>

<h2 id="전제-조건">전제 조건</h2>
<table>
<thead>
<tr>
<th align="left">조건</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">그래프 종류</td>
<td align="left">방향 그래프</td>
</tr>
<tr>
<td align="left">간선 가중치</td>
<td align="left">음수 허용, 단 <strong>음수 사이클 탐지 필요</strong></td>
</tr>
<tr>
<td align="left">목적</td>
<td align="left">한 정점에서 모든 정점까지의 최단 거리 <strong>(One-To-All)</strong></td>
</tr>
</tbody></table>
<br/>

<h2 id="동작-원리">동작 원리</h2>
<p>벨만 포드는 다음과 같은 방식으로 진행된다.</p>
<ol>
<li>모든 간선 <code>(u -&gt; v)</code>에 대해 다음을 <code>V-1</code>번 반복한다.</li>
</ol>
<pre><code class="language-python">if dist[v] &gt; dist[u] + cost:
    dist[v] = dist[u] + cost</code></pre>
<ol start="2">
<li><p>위 작업을 정점 개수(<code>V</code>)보다 하나 적은 횟수만큼 반복하면 <strong>모든 최단거리가 안정적으로 수렴</strong>하게 된다.</p>
</li>
<li><p>이후 한 번 더 반복했을 때 값이 갱신되면, 음수 사이클이 존재하는 것이다.</p>
</li>
</ol>
<p>시간 복잡도는 $O(V \times E)$이다.</p>
<p>간선을 $V-1$번 반복하면서 모두 확인하므로, 간선 수가 많거나 정점이 많을 경우 느릴 수 있다.</p>
<p>즉, 음수 가중치가 존재하지 않는다면 다익스트라 알고리즘을 사용하는게 좋고(더 빠름), 음수 가중치가 존재할 때 사용하기 좋다.</p>
<br/>

<h2 id="구현">구현</h2>
<p>음수 가중치가 있는 그래프 문제인 <a href="https://www.acmicpc.net/problem/11657">#11657. 타임머신</a>을 봐보자.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/58c3877b-859a-4ec2-b709-bebfa1f1650a/image.png" alt=""></p>
<p>$N$개의 도시와 $M$개의 버스가 있으며, 버스를 타고 이동하는 시간이 0이면 순간이동, 0보다 작으면 타임머신을 이용해 시간을 되돌아가는 상황이다. <strong>(음수 가중치)</strong></p>
<p>앞서 설명한 벨만-포드 알고리즘을 적용하여 구현해보자.</p>
<pre><code class="language-python">def bellman_ford(n: int, edges: list) -&gt; list:
    dist = [float(&quot;inf&quot;)] * (n + 1)
    dist[1] = 0
    for i in range(1, n + 1):
        for s, e, cost in edges:
            if dist[s] != float(&quot;inf&quot;) and dist[e] &gt; dist[s] + cost:
                dist[e] = dist[s] + cost
                if i == n: return [-1]    # n번째 반복에서도 갱신된다면 음수 사이클 존재
    # INF의 경우 도달 불가능한 도시이므로 -1
    for i in range(2, len(dist)):
        if dist[i] == float(&quot;inf&quot;): dist[i] = -1
    return dist[2:]</code></pre>
<p>시작 노드는 1번으로 설정하여 거리를 0으로 초기화하고, 도시 수($N$)만큼 반복하며 최단 거리를 갱신한다.</p>
<p>모든 간선에 대해 현재 노드에서 다음 노드로 가는 비용이 더 적으면 갱신하고, 이 과정을 반복한다.</p>
<p>만약 마지막($N$번째) 반복에서도 거리 갱신이 발생하면, 이는 음수 사이클이 존재하는 것이므로 -1을 반환한다.</p>
<p>최종적으로 각 도시별 최단 거리를 계산하고, 도달할 수 없는 도시는 -1로 표시한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[BOJ: #3344. N-Queen]]></title>
            <link>https://velog.io/@dohn_1203/BOJ-3344.-N-Queen</link>
            <guid>https://velog.io/@dohn_1203/BOJ-3344.-N-Queen</guid>
            <pubDate>Tue, 15 Apr 2025 09:55:57 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/3344">#3344. N-Queen</a></p>
<p>$N$이 주어졌을 때, $N \times N$ 보드에 $N$개의 퀸을 서로 다른 두 퀸이 공격하지 못하게 놓는 한 가지 경우를 출력하는 프로그램을 작성하시오.</p>
<p>첫째 줄에 $N$이 주어진다. $N$은 $8,26,213,2012,99991,99999$ 중 하나이다.</p>
<hr>
<h2 id="접근-방식">접근 방식</h2>
<p>먼저, 또 다른 N-Queen 문제(<a href="https://www.acmicpc.net/problem/9663">#9663</a>)에서는 백트래킹을 이용해 $N$일 때 나올 수 있는 해의 모든 경우의 수를 출력하였다. 해당 코드에서 첫 해를 구한 직후 바로 탈출하면 되지 않을까?</p>
<pre><code class="language-python">def solution(n: int, check: dict, row: int = 0) -&gt; int:
    if n == row: return True
    for col in range(n):
        # 놓을 수 있는 공간에 말 놓기
        if check[&quot;c&quot;][col] and check[&quot;u&quot;][row+col] and check[&quot;d&quot;][row-col+n-1]:
            check[&quot;c&quot;][col] = check[&quot;u&quot;][row+col] = check[&quot;d&quot;][row-col+n-1] = False
            if solution(n, check, row + 1):
                print(col + 1)
                return True
            check[&quot;c&quot;][col] = check[&quot;u&quot;][row+col] = check[&quot;d&quot;][row-col+n-1] = True
    return False

if __name__ == &quot;__main__&quot;:
    N = 8
    check = {
        &quot;c&quot;: [True]*N,
        &quot;u&quot;: [True]*(N*2),
        &quot;d&quot;: [True]*(N*2),
    }
    solution(N, check)</code></pre>
<p>작은 숫자의 경우 빠르게 출력되는 반면, $216$과 같은 큰 숫자는 출력되는데 오랜 시간이 소요된다.</p>
<p>따라서 해당 접근 방식은 잘못되었다.</p>
<br/>

<h2 id="규칙-찾기">규칙 찾기</h2>
<p>퀸을 어떤식으로 두어야 서로 공격할 수 없을까? $N=4$인 경우부터 확인해보자.</p>
<h3 id="1-n이-4일-때-짝수일-때">1. N이 4일 때 (짝수일 때)</h3>
<p>$N=4$일 때 둘 수 있는 배치는 총 두 가지이다.</p>
<table>
<thead>
<tr>
<th align="center">□&ensp;□&ensp;■&ensp;□<br/>■&ensp;□&ensp;□&ensp;□<br/>□&ensp;□&ensp;□&ensp;■<br/>□&ensp;■&ensp;□&ensp;□</th>
<th align="center">□&ensp;■&ensp;□&ensp;□<br/>□&ensp;□&ensp;□&ensp;■<br/>■&ensp;□&ensp;□&ensp;□<br/>□&ensp;□&ensp;■&ensp;□</th>
</tr>
</thead>
</table>
<p>여기서 두 번째 배치를 보면, 첫 번째 행부터 $2,4$, 즉 짝수 칸에 배치하고, 그 이후부터는 $1$부터 홀수 칸에 배치한다.</p>
<p>아래 검증 코드를 통해 확인해보자.</p>
<pre><code class="language-python"># 검증 코드
def check_queen(board: list):
    N = len(board)
    check = {
        &quot;c&quot;: [True]*N,
        &quot;u&quot;: [True]*(N*2),
        &quot;d&quot;: [True]*(N*2),
    }
    for row in range(len(board)):
        if check[&quot;c&quot;][board[row]-1] and check[&quot;u&quot;][row+board[row]-1] and check[&quot;d&quot;][row-board[row]+N-2]:
            check[&quot;c&quot;][board[row]-1] = check[&quot;u&quot;][row+board[row]-1] = check[&quot;d&quot;][row-board[row]+N-2] = False
        else:
            return False
    return True</code></pre>
<pre><code class="language-python">def solution(n: int) -&gt; int:
    board = []
    # 위쪽부터 짝수로 채우기
    for i in range(2, n + 1, 2):
        board.append(i)
    # 그 이후 홀수로 채우기
    for i in range(1, n + 1, 2):
        board.append(i)
    return board

for n in [4, 5, 6, 7, 8, 9, 10]:
    print(f&quot;{n}의 경우:&quot;, solution(n), &quot;✅&quot; if check_queen(solution(n)) else &quot;❌&quot;)</code></pre>
<pre><code>Output:
4의 경우: [2, 4, 1, 3] ✅
5의 경우: [2, 4, 1, 3, 5] ✅
6의 경우: [2, 4, 6, 1, 3, 5] ✅
7의 경우: [2, 4, 6, 1, 3, 5, 7] ✅
8의 경우: [2, 4, 6, 8, 1, 3, 5, 7] ❌
9의 경우: [2, 4, 6, 8, 1, 3, 5, 7, 9] ❌
10의 경우: [2, 4, 6, 8, 10, 1, 3, 5, 7, 9] ✅</code></pre><p>대부분의 짝수, 홀수에서는 정상적으로 동작하지만, $8,9,14,15,26,27$ 등의 숫자에서 반례가 등장한다.</p>
<p>위 숫자들의 공통점은 $6k+2, 6k+3$으로 표현된다는 것이다.</p>
<p>해당 숫자들에 대한 규칙도 찾아보도록 하자.</p>
<br/>

<h3 id="2-n이-6k2일-때">2. N이 6k+2일 때</h3>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/aa609d8b-589f-4367-8c38-e4b4c5e58731/image.png" alt=""></p>
<p>위 그림을 보면 알 수 있듯이, 체스판 절반 아래쪽 부분이 모두 위쪽 퀸에게 공격받는 모습을 볼 수 있다.</p>
<p>이를 해결하기 위해서, 표시된 빨간색 및 파란색 영역을 제외하고 퀸을 배치했을 때, 서로 공격하지 않도록 해야한다.</p>
<p>위쪽 퀸들의 공격 경로를 빨간색과 파란색 칸으로 표시해 나타내면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/59ba07fc-ad7d-4b8c-8050-b3072580e01c/image.png" alt=""></p>
<p>빨간색 네모칸은 대각선 왼쪽으로 공격하는 경로, 파란색 네모칸은 대각선 오른쪽으로 공격하는 경로, 빨간색 X 표시는 직선 경로를 뜻한다.</p>
<p>여기서 가장 경우의 수가 적은 7번 열부터 시작해본다.</p>
<table>
<thead>
<tr>
<th align="center">첫 번째 퀸</th>
<th align="center">두 번째 퀸</th>
<th align="center">세 번째 퀸</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="blob:https://velog.io/8debda0f-e54b-4718-afc7-026e3c67476d" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/f2e79ea0-c847-43dd-b53a-7154d089f613/image.png" alt=""></td>
<td align="center">3열에 퀸을 놓을 수 없으므로 불가능</td>
</tr>
</tbody></table>
<p>$(8,7)$에는 퀸을 둘 수 없다.</p>
<p>다음으로, $(7,7)$을 확인해보겠다.</p>
<table>
<thead>
<tr>
<th align="center">첫 번째 퀸</th>
<th align="center">두 번째 퀸</th>
<th align="center">세 번째 퀸</th>
<th align="center">네 번째 퀸</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/15481101-f159-48f8-ad83-a65d08d6b1c4/image.png" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/af5a14f6-3a77-41fb-af2f-5d9bc7f3406d/image.png" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/ef5d1fd0-0b63-4e7d-b244-e84e414184a6/image.png" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/41f6a2e9-e420-44f7-8e85-a6c44218c9c8/image.png" alt=""></td>
</tr>
</tbody></table>
<p>$(7,7)$에 퀸을 둘 경우, 위와 같이 배치가 가능하다. 이제 위 배치에서 규칙성을 찾아보자.</p>
<ul>
<li>$5$는 $3$</li>
<li>$6$은 $1$</li>
<li>$7$은 $7$</li>
<li>$8$은 $5$</li>
</ul>
<p>사실 이거 하나만 봐서는 규칙성을 볼 수가 없다. $N=14$인 경우도 봐보자.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/9c120bed-462d-4912-857e-327cc964216c/image.png" alt=""></p>
<p>여기서 규칙성을 찾을 수 있다. 위쪽 행을 <code>[2, 4, 6, 8, 10, 12, 14]</code>와 같이 짝수 칸으로 채운 후 아래쪽 행을 채울 때, <code>[3, 1]</code> 배열로 시작한다는 것을 알 수 있다. 이후 7부터 홀수 열을 순서대로 채우게 되고, 맨 마지막 행은 5열에 퀸이 놓이게 된다.</p>
<p>이를 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-python">def solution(n: int) -&gt; list:
    board = []
    for i in range(2, n + 1, 2):
        board.append(i)
    if n % 6 == 2:
        board.append(3)
        board.append(1)
        for i in range(7, n + 1, 2):
            board.append(i)
        board.append(5)
    else:
        for i in range(1, n + 1, 2):
            board.append(i)
    return board

for n in [8, 14, 20, 26]:
    print(f&quot;{n:2d}의 경우:&quot;, solution(n), &quot;✅&quot; if check_queen(solution(n)) else &quot;❌&quot;)</code></pre>
<pre><code>Output:
 8의 경우: [2, 4, 6, 8, 3, 1, 7, 5] ✅
14의 경우: [2, 4, 6, 8, 10, 12, 14, 3, 1, 7, 9, 11, 13, 5] ✅
20의 경우: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 3, 1, 7, 9, 11, 13, 15, 17, 19, 5] ✅
26의 경우: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 3, 1, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 5] ✅</code></pre><p>완벽하다.</p>
<br/>

<h3 id="3-n이-6k3일-때">3. N이 6k+3일 때</h3>
<p>이제 $6k+3$일 때의 규칙을 찾아보자. 이거는 도무지 머리로 규칙이 안떠올라서 전체 경우의 수를 다 출력해두고 괜찮아 보이는 배열을 선택하였다..</p>
<p>위쪽 행은 짝수 열에만, 아래쪽 행은 홀수 열에만 배치한다는 점은 동일하다.</p>
<table>
<thead>
<tr>
<th align="center">□&ensp;□&ensp;□&ensp;■&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□<br/>□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;■&ensp;□&ensp;□&ensp;□<br/>□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;■&ensp;□<br/>□&ensp;■&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□<br/>□&ensp;□&ensp;□&ensp;□&ensp;■&ensp;□&ensp;□&ensp;□&ensp;□<br/>□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;■&ensp;□&ensp;□<br/>□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;■<br/>■&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□<br/>□&ensp;□&ensp;■&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□&ensp;□</th>
</tr>
</thead>
</table>
<p>위 배치를 보면, $4$부터 시작해서 두 칸씩 건너뛰어 $2$까지, 그리고 아래쪽은 $5$부터 시작해서 두 칸씩 건너뛰어 $3$까지 순서대로 배치하는 것을 알 수 있다.</p>
<pre><code class="language-python">def solution(n: int) -&gt; list:
    board = []
    if n % 6 == 3:
        for i in range(4, n + 1, 2):
            board.append(i)
        board.append(2)
        for i in range(5, n + 1, 2):
            board.append(i)
        board.append(1)
        board.append(3) 
    else:
        for i in range(2, n + 1, 2):
            board.append(i)
        if n % 6 == 2:
            board.append(3)
            board.append(1)
            for i in range(7, n + 1, 2):
                board.append(i)
            board.append(5)
        else:
            for i in range(1, n + 1, 2):
                board.append(i)
    return board

for n in [9, 15, 21, 27]:
    print(f&quot;{n:2d}의 경우:&quot;, solution(n), &quot;✅&quot; if check_queen(solution(n)) else &quot;❌&quot;)</code></pre>
<pre><code>Output:
 9의 경우: [4, 6, 8, 2, 5, 7, 9, 1, 3] ✅
15의 경우: [4, 6, 8, 10, 12, 14, 2, 5, 7, 9, 11, 13, 15, 1, 3] ✅
21의 경우: [4, 6, 8, 10, 12, 14, 16, 18, 20, 2, 5, 7, 9, 11, 13, 15, 17, 19, 21, 1, 3] ✅
27의 경우: [4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 2, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 1, 3] ✅</code></pre><br/>

<h2 id="번외-21133-n-queen-2">번외: #21133. N-Queen 2</h2>
<p><a href="https://www.acmicpc.net/problem/21133">#21133. N-Queen 2</a></p>
<p>위 문제는 <a href="https://www.acmicpc.net/problem/3344">#3344</a>랑 크게 다를 것 없는 문제이다. 하지만 그대로 제출하면 시간 초과가 뜨게 된다. <del>나는 그랬다.</del></p>
<p>실행 시간을 줄이기 위해 <code>input()</code>은 <code>sys.stdin.readline()</code>, <code>print()</code>는 <code>sys.stdout.write()</code>로 바꿔주어야 한다.</p>
<br/>

<hr>
<p>이 문제는 규칙만 찾을 수 있다면 구현 자체는 매우 쉬운 문제이다. 하지만 그 규칙을 찾는게 쉽지 않았다.</p>
<p>내가 생각한 규칙보다 더 간단하고 쉬운 규칙이 존재할 수도 있다. 그래도 여러 경우의 수를 다 따져보며 규칙을 찾아 나가는 과정이 나름 재미있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘: 피보나치 수열 구현하기]]></title>
            <link>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98-%EC%88%98%EC%97%B4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98-%EC%88%98%EC%97%B4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 15 Apr 2025 08:11:02 GMT</pubDate>
            <description><![CDATA[<h2 id="피보나치-수열">피보나치 수열</h2>
<p>피보나치 수열은 다음과 같은 점화식으로 정의되는 수열이다.</p>
<p>$$
F_{0} = 0 \
F_{1} = 1 \
F_{n+2} = F_{n+1} + F_{n}
$$</p>
<p>피보나치 수열을 제 15항까지 나타낸다면 다음과 같다.</p>
<table>
<thead>
<tr>
<th align="center">$n$</th>
<th align="center">$0$</th>
<th align="center">$1$</th>
<th align="center">$2$</th>
<th align="center">$3$</th>
<th align="center">$4$</th>
<th align="center">$5$</th>
<th align="center">$6$</th>
<th align="center">$7$</th>
<th align="center">$8$</th>
<th align="center">$9$</th>
<th align="center">$10$</th>
<th align="center">$11$</th>
<th align="center">$12$</th>
<th align="center">$13$</th>
<th align="center">$14$</th>
<th align="center">$15$</th>
</tr>
</thead>
<tbody><tr>
<td align="center">$F_{n}$</td>
<td align="center">$0$</td>
<td align="center">$1$</td>
<td align="center">$1$</td>
<td align="center">$2$</td>
<td align="center">$3$</td>
<td align="center">$5$</td>
<td align="center">$8$</td>
<td align="center">$13$</td>
<td align="center">$21$</td>
<td align="center">$34$</td>
<td align="center">$55$</td>
<td align="center">$89$</td>
<td align="center">$144$</td>
<td align="center">$233$</td>
<td align="center">$377$</td>
<td align="center">$610$</td>
</tr>
</tbody></table>
<br/>

<h2 id="피보나치-수열의-구현">피보나치 수열의 구현</h2>
<p>피보나치 수열은 재귀 함수를 통해 구현 가능하다.</p>
<pre><code class="language-python">def fib(n: int) -&gt; int:
    if n == 0 or n == 1: return n
    return fib(n - 1) + fib(n - 2)

fib(17)</code></pre>
<p>위 코드를 직접 돌려보면, $n$을 $100$과 같이 큰 숫자를 넣기만 해도 굉장히 오래 걸리는 모습을 볼 수 있다. 
어디서 오랜 시간이 소요되는걸까?</p>
<p><code>fib(5)</code>를 구하기 위한 과정은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/9f080faf-c94c-4068-86e5-1f6404a7d35d/image.png" alt=""></p>
<p>위 과정을 보면 <code>fib(5)</code>를 계산하기 위해 <code>fib(3)</code>은 두 번 호출되고, <code>fib(2)</code>는 세 번 호출된다.</p>
<p>즉, <strong>하나의 노드 당 두 개의 자식 노드가 생성되고 있음</strong>을 나타낸다. 이를 시간 복잡도로 표현하자면, $O(2^{n})$이 된다.</p>
<br/>

<h2 id="다이나믹-프로그래밍">다이나믹 프로그래밍</h2>
<h3 id="캐싱-방식-적용하기">캐싱 방식 적용하기</h3>
<p>기존의 재귀적 문제 풀이 방식에서의 가장 큰 문제는 <strong>같은 계산을 여러 번 하는 것</strong>이었다. 이러한 문제를 해결하기 위해 <strong>캐싱</strong> 방식을 사용하면 되지 않을까?</p>
<pre><code class="language-python">def fib(n):
    cache = {0: 0, 1: 1}
    def calc(n):
        if n in cache:
            return cache[n]
        else:
            cache[n] = calc(n-1) + calc(n-2)
            return cache[n]
    return calc(n)

fib(100)</code></pre>
<p>캐싱을 하니 $n = 100$에 대한 값도 금방 나오는 것을 확인할 수 있다. 딕셔너리에 모든 값들을 캐싱하는 것이기 때문에, 나중에 값이 매우 커졌을 때 공간 복잡도에 문제가 발생하지 않을까 싶다.</p>
<p>시간복잡도를 보면 캐싱으로 인해 각 $n$에 대해 한 번만 계산을 수행하기 때문에 $O(n)$의 시간 복잡도를 얻을 수 있다.</p>
<p>그렇다면, 공간 복잡도를 줄이면서 $O(n)$의 시간 복잡도를 가질 순 없을까?</p>
<br/>

<h3 id="상향식으로-접근하기">상향식으로 접근하기</h3>
<p>위 방법 모두 $F_{n}$을 구하기 위해 $F_{n-1}$부터 $F_{0}$까지 $n$을 감소시켜가며 접근하였다.
반대로, $n$을 늘려가며 접근하는 방식은 어떨까?</p>
<pre><code class="language-python">def fib(n):
    if n == 0: return 0
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

fib(100)</code></pre>
<p>위 방식을 보면, <code>a</code>와 <code>b</code>를 각각 $0$, $1$로 두고, $2$부터 $n+1$까지 올라가며 계산을 진행한다.</p>
<p>하나의 반복문으로 계산을 진행하기 때문에 $O(n)$의 시간 복잡도를 갖는다.</p>
<br/>

<h2 id="행렬의-거듭제곱">행렬의 거듭제곱</h2>
<p>백준 <a href="https://www.acmicpc.net/problem/11444">#11444. 피보나치 수 6</a>를 풀면서 새롭게 알게 된 개념이다. 피보나치 수열은 <strong>행렬의 거듭제곱 형태로 표현이 가능</strong>하다.</p>
<p>어떻게 가능할까?</p>
<p>먼저, 피보나치 수열은 앞서 말했듯이, 다음과 같은 점화식을 나타낸다.</p>
<p>$$F_{n+1} = F_{n} + F_{n-1}$$</p>
<p>이를 행렬 형태로 나타내본다.</p>
<p>$$
\begin{pmatrix}
    F_{n+1} \
    F_{n}
\end{pmatrix}</p>
<p>=</p>
<p>A \begin{pmatrix}
    F_{n} \
    F_{n-1}
\end{pmatrix}
\tag{1}
$$</p>
<p>여기서 행렬곱의 결과가 좌변과 같아지려면, $A$는 다음과 같아야 한다.</p>
<p>$$
\begin{pmatrix}
    F_{n+1} \
    F_{n}
\end{pmatrix}</p>
<p>=</p>
<p>\begin{pmatrix}
    1 &amp; 1 \
    1 &amp; 0
\end{pmatrix}</p>
<p>\begin{pmatrix}
    F_{n} \
    F_{n-1}
\end{pmatrix}
\tag{2}
$$</p>
<p>여기서, 우변의 $\begin{pmatrix}F_n \F_{n-1}\end{pmatrix}$ 또한 다음과 같이 나타낼 수 있다.</p>
<p>$$
\begin{pmatrix}
    F_{n} \
    F_{n-1}
\end{pmatrix}</p>
<p>=</p>
<p>\begin{pmatrix}
    1 &amp; 1 \
    1 &amp; 0
\end{pmatrix}</p>
<p>\begin{pmatrix}
    F_{n-1} \
    F_{n-2}
\end{pmatrix}
\tag{3}
$$</p>
<p>$(2)$번 수식과 $(3)$번 수식을 합치면 다음과 같이 거듭제곱으로 표현된다.</p>
<p>$$
\begin{pmatrix}
    F_{n+1} \
    F_{n}
\end{pmatrix}</p>
<p>=</p>
<p>\begin{pmatrix}
    1 &amp; 1 \
    1 &amp; 0
\end{pmatrix}^{2}</p>
<p>\begin{pmatrix}
    F_{n-1} \
    F_{n-2}
\end{pmatrix}
\tag{4}
$$</p>
<p>위 과정을 계속 진행하다 보면, 최종적으로 다음과 같은 수식이 나오게 된다.</p>
<p>$$
\begin{pmatrix}
    F_{n} \
    F_{n-1}
\end{pmatrix}</p>
<p>=</p>
<p>\begin{pmatrix}
    1 &amp; 1 \
    1 &amp; 0
\end{pmatrix}^{n}</p>
<p>\begin{pmatrix}
    F_{1} \
    F_{0}
\end{pmatrix}</p>
<p>=</p>
<p>\begin{pmatrix}
    1 &amp; 1 \
    1 &amp; 0
\end{pmatrix}^{n}</p>
<p>\begin{pmatrix}
    1 \
    0
\end{pmatrix}
\tag{5}
$$</p>
<p>$\begin{pmatrix}1 &amp; 1 \1 &amp; 0\end{pmatrix}^{n}$ 행렬의 제곱을 풀어보면 다음과 같은 관계식을 갖는 것을 알 수 있다.</p>
<p>$$
\begin{pmatrix}
    1 &amp; 1 \
    1 &amp; 0
\end{pmatrix}^{n-1}</p>
<p>=</p>
<p>\begin{pmatrix}
    F_{n} &amp; F_{n-1} \
    F_{n-1} &amp; F_{n-2}
\end{pmatrix}
\tag{6}
$$</p>
<p>즉, 행렬의 첫 번째 행 첫 번째 열에 있는 값이 피보나치 수열의 $n$번째 값이 된다.</p>
<p>이를 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-python">def multi(A: list, B: list) -&gt; list:
    a11 = A[0][0]*B[0][0] + A[1][0]*B[0][1]
    a12 = A[0][0]*B[1][0] + A[1][0]*B[1][1]
    a21 = A[0][1]*B[0][0] + A[1][1]*B[0][1]
    a22 = A[0][1]*B[1][0] + A[1][1]*B[1][1]
    return [[a11, a12], [a21, a22]]

def power(F: list, n: int) -&gt; list:
    result = F
    for _ in range(n - 2):
        result = multi(result, F)
    return result

def fib(n: int) -&gt; int:
    F = [[1, 1], [1, 0]]
    return power(F, n)[0][0]

fib(100)</code></pre>
<p>위 방식으르 활용하면 $O(n)$의 시간 복잡도를 얻을 수 있다.</p>
<p>여기서 지수의 특성을 활용해 분할 정복을 하게 되면 $O(\log{n})$까지 시간 복잡도를 줄여볼 수 있다.</p>
<pre><code class="language-python">def multi(A: list, B: list) -&gt; list:
    a11 = A[0][0]*B[0][0] + A[1][0]*B[0][1]
    a12 = A[0][0]*B[1][0] + A[1][0]*B[1][1]
    a21 = A[0][1]*B[0][0] + A[1][1]*B[0][1]
    a22 = A[0][1]*B[1][0] + A[1][1]*B[1][1]
    return [[a11, a12], [a21, a22]]

def power(F: list, n: int):
    # 분할 정복을 활용한 거듭제곱
    if n == 0:
        return [[1, 0], [0, 1]]
    temp = power(F, n//2)
    result = multi(temp, temp)
    if n % 2 == 1:
        return multi(F, result)
    else:
        return result

def fib(n: int) -&gt; int:
    F = [[1, 1], [1, 0]]
    return power(F, n-1)[0][0]

fib(100)</code></pre>
<hr>
<h3 id="문제-풀어보기">문제 풀어보기</h3>
<p><a href="https://www.acmicpc.net/problem/2747">#2747. 피보나치 수</a>: 브론즈 문제임에도 불구하고 재귀를 이용한 단순 무식한 방법으로는 문제를 풀 수 없게 되어있다.</p>
<p><a href="https://www.acmicpc.net/problem/11444">#11444. 피보나치 수 6</a>: 일반적인 방식으로는 시간 초과가 뜨기 때문에, 모듈러 연산과 행렬의 거듭제곱 방식을 활용하여 문제를 해결해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘: 플로이드 와샬(Floyd-Warshall) -  모든 정점 쌍 최단 거리 구하기]]></title>
            <link>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%94%8C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%99%80%EC%83%ACFloyd-Warshall-%EB%AA%A8%EB%93%A0-%EC%A0%95%EC%A0%90-%EC%8C%8D-%EC%B5%9C%EB%8B%A8-%EA%B1%B0%EB%A6%AC-%EA%B5%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%94%8C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%99%80%EC%83%ACFloyd-Warshall-%EB%AA%A8%EB%93%A0-%EC%A0%95%EC%A0%90-%EC%8C%8D-%EC%B5%9C%EB%8B%A8-%EA%B1%B0%EB%A6%AC-%EA%B5%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 14 Apr 2025 17:53:13 GMT</pubDate>
            <description><![CDATA[<h2 id="플로이드-와샬이란">플로이드 와샬이란?</h2>
<blockquote>
<p>그래프 내 모든 정점 쌍 <code>(i, j)</code>에 대한
최단 거리를 구하는 알고리즘.</p>
</blockquote>
<p>한 정점에서 출발해서 다른 모든 정점으로 가는 것 뿐만 아니라, 모든 정점 간의 최단 경로를 한 번에 구할 수 있다.</p>
<p>플로이드-와샬 알고리즘은 3중 for문을 사용한 DP를 통해 구현된다. ($O(n^{3})$)</p>
<br/>

<h2 id="전제-조건">전제 조건</h2>
<table>
<thead>
<tr>
<th align="left">조건</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">그래프 종류</td>
<td align="left">방향 / 무방향 모두 가능</td>
</tr>
<tr>
<td align="left">간선 가중치</td>
<td align="left"><strong>음수 허용</strong>, 단 <strong>음수 사이클은 불가능</strong></td>
</tr>
<tr>
<td align="left">목적</td>
<td align="left">모든 정점 쌍 <code>(i, j)</code>간의 최단 거리 <strong>(All-To-All)</strong></td>
</tr>
</tbody></table>
<br/>

<h2 id="동작-과정">동작 과정</h2>
<p>플로이드 와샬 알고리즘은 다음과 같은 방식으로 진행된다.</p>
<ol>
<li><p>초기화
모든 정점 간의 거리를 다음과 같이 초기화한다.</p>
<ul>
<li><code>dist[i][j] = 0</code> (i == j)</li>
<li><code>dist[i][j] = w</code> (i에서 j로 가는 간선의 가중치 w가 존재할 때)</li>
<li><code>dist[i][j] = INF</code> (그 외의 경우)</li>
</ul>
</li>
<li><p>중간 노드 k에 대해 모든 경로를 갱신</p>
</li>
</ol>
<p>모든 정점 쌍 <code>(i, j)</code>에 대해, <strong>i -&gt; j로 가는 경로가 i -&gt; k -&gt; j로 우회했을 때 더 짧은지 확인</strong>한다.</p>
<pre><code class="language-python">for k in range(n):
    for i in range(n):
        for j in range(n):
            dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])</code></pre>
<p>즉, 각 단계에서는 <strong>경로의 중간에 정점 k를 추가했을 때 더 짧은지</strong>를 확인하고 갱신한다.</p>
<br/>

<h2 id="음수-간선이-존재할-경우">음수 간선이 존재할 경우</h2>
<p>음수 간선이 존재할 경우에도 다익스트라와 다르게, 동작 가능하다.</p>
<p>하지만, 음수 사이클이 존재할 경우 올바른 최단 거리를 구할 수 없다. 이 경우, <code>dist[i][j] &lt; 0</code>인 정점이 생긴다.</p>
<br/>

<h2 id="구현">구현</h2>
<p>대표적인 플로이드 문제인 <a href="https://www.acmicpc.net/problem/11404">#11404. 플로이드</a>를 봐보자.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/ae1ed5fb-164e-416a-969d-1be69c10c6f0/image.png" alt=""></p>
<p>각 정점간의 최단 거리를 모두 출력해야하는 문제이다. 앞서 설명한 동작 과정을 생각하면서 구현해보자.</p>
<pre><code class="language-python">def floyd_warshall(n: int, edges: list) -&gt; list:
    dist = [[float(&quot;inf&quot;)] * n for _ in range(n)]
    for i in range(n): dist[i][i] = 0
    for s, e, c in edges: dist[s - 1][e - 1] = min(dist[s - 1][e - 1], c)
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if dist[i - 1][j - 1] &gt; dist[i - 1][k - 1] + dist[k - 1][j - 1]:
                    dist[i - 1][j - 1] = dist[i - 1][k - 1] + dist[k - 1][j - 1]
    return dist</code></pre>
<p>간선 정보를 바탕으로 비용을 초기화하고, 3중 for문을 통해 i에서 j로 가는 경로 중 k를 경유하는 경우가 더 짧다면 갱신하는 방식으로 동작하게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[BOJ: #13549. 숨바꼭질 3]]></title>
            <link>https://velog.io/@dohn_1203/BOJ-13549.-%EC%88%A8%EB%B0%94%EA%BC%AD%EC%A7%88-3</link>
            <guid>https://velog.io/@dohn_1203/BOJ-13549.-%EC%88%A8%EB%B0%94%EA%BC%AD%EC%A7%88-3</guid>
            <pubDate>Sun, 13 Apr 2025 00:10:12 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/13549">#13549. 숨바꼭질 3</a></p>
<p>수빈이는 동생과 숨바꼭질을 하고 있다. 수빈이는 현재 점 $N(0 \leq N \leq 100,000)$에 있고, 동생은 점 $K(0 \leq K \leq 100,000)$에 있다. 수빈이는 걷거나 순간이동을 할 수 있다. 만약, 수빈이의 위치가 $X$일 때 걷는다면 1초 후에 $X-1$ 또는 $X+1$로 이동하게 된다. 순간이동을 하는 경우에는 0초 후에 $2\times X$의 위치로 이동하게 된다.</p>
<p>수빈이와 동생의 위치가 주어졌을 때, 수빈이가 동생을 찾을 수 있는 가장 빠른 시간이 몇 초 후인지 구하는 프로그램을 작성하시오.</p>
<hr>
<h2 id="접근-방식">접근 방식</h2>
<p>실버 1 문제인 <a href="https://www.acmicpc.net/problem/1697">#1697. 숨바꼭질</a>과의 차이점은 수빈이가 순간이동을 할 때 걸리는 시간이 1초에서 0초로 바뀌었다는 것이다.
즉, 한 번의 행동에 동일한 시간이 소요되는것이 아니다.</p>
<p>따라서 해당 문제는 다익스트라 알고리즘으로 접근하였다.</p>
<pre><code class="language-python">def solution(subin_x: int, sister_x: int) -&gt; int:
    dist = [float(&quot;inf&quot;)] * 100_001
    dist[subin_x] = 0
    pq = [(0, subin_x)]
    while pq:
        d, x = heappop(pq)
        if x == sister_x: return dist[sister_x]
        if d &gt; dist[x]: continue
        for nx, cost in [(x - 1, 1), (x + 1, 1), (x * 2, 0)]:
            if nx &gt; 100_000 or nx &lt; 0: continue
            nd = d + cost
            if nd &lt; dist[nx]:
                dist[nx] = nd
                heappush(pq, (nd, nx))
    return dist[sister_x]</code></pre>
<p><code>heapq</code>를 사용하여 최소힙에 위치 및 이동 횟수 정보를 넣어주었고, 위치가 100,000을 넘어가게 되면 <code>continue</code> 하였다.
그 후 거리 배열(이동 횟수 배열)에서 동생의 위치에 해당하는 인덱스의 값을 리턴해주었다.</p>
<br/>

<h2 id="다른-방식">다른 방식</h2>
<p>문제을 다 푼 다음 알고리즘 분류를 확인해보니 <strong>0-1 너비 우선 탐색</strong>이라는 항목이 보였다. 그래서 너비 우선 탐색을 활용하여 풀어보기로 했다.</p>
<h3 id="0-1-너비-우선-탐색">0-1 너비 우선 탐색</h3>
<blockquote>
<p>간선 가중치가 0 또는 1인 그래프에서 최단거리를 $O(V+E)$에 구하는 알고리즘.
다익스트라보다 더 빠르고, 구현도 단순하다.</p>
</blockquote>
<p>0-1 너비 우선 탐색에서 0과 1은 각각 다음과 같이 해석이 가능하다.</p>
<ul>
<li><code>0</code>: 우선순위 높음</li>
<li><code>1</code>: 우선순위 낮음</li>
</ul>
<p>일반적인 BFS는 큐를 하나만 쓰는데 어떻게 다익스트라처럼 동작할까?</p>
<p>0-1 BFS에서는 덱(deque)을 사용한다.</p>
<ul>
<li>간선 가중치가 <code>0</code> -&gt; deque의 앞쪽에 넣음 (빨리 탐색)</li>
<li>간선 가중치가 <code>1</code> -&gt; deque의 뒤쪽에 넣음 (나중에 탐색)</li>
</ul>
<p>위 아이디어를 가지고 코드를 구현해보면 다음과 같다.</p>
<pre><code class="language-python">def solution_with_BFS(subin_x: int, sister_x: int) -&gt; int:
    dist = [float(&quot;inf&quot;)] * 100_001
    dist[subin_x] = 0
    queue = deque([(subin_x, 0)])
    while queue:
        x, d = queue.popleft()
        if x == sister_x: return d
        for nx, cost in [(x - 1, 1), (x + 1, 1), (x * 2, 0)]:
            if nx &gt; 100_000 or nx &lt; 0: continue
            if dist[x] + cost &lt; dist[nx]:
                if cost == 0: queue.appendleft((nx, d))
                else: queue.append((nx, d + 1))
                dist[nx] = dist[x] + cost</code></pre>
<p>만약 가중치(<code>cost</code>)가 0이라면 <code>appendleft()</code>를 통해 deque의 앞쪽에 넣고, 그렇지 않다면 원래 BFS 방식대로 <code>append()</code>를 통해 뒤쪽에 넣게 된다.
위 방식으로 우선순위를 정해줄 수 있다.</p>
<table>
<thead>
<tr>
<th align="left">알고리즘</th>
<th align="left">메모리</th>
<th align="left">시간</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Dijkstra</td>
<td align="left">113680KB</td>
<td align="left">184ms</td>
</tr>
<tr>
<td align="left">0-1 BFS</td>
<td align="left">113548KB</td>
<td align="left">124ms</td>
</tr>
</tbody></table>
<p>대략 60ms 차이가 나는 것을 알 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘: 다익스트라(Dijkstra) - 최단 경로 구하기]]></title>
            <link>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%8B%A4%EC%9D%B5%EC%8A%A4%ED%8A%B8%EB%9D%BCDijkstra-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C-%EA%B5%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dohn_1203/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%8B%A4%EC%9D%B5%EC%8A%A4%ED%8A%B8%EB%9D%BCDijkstra-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C-%EA%B5%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 12 Apr 2025 23:30:01 GMT</pubDate>
            <description><![CDATA[<h2 id="다익스트라란">다익스트라란?</h2>
<blockquote>
<p>가중치가 양수인 그래프에서, 
한 정점으로부터 모든 정점까지의 최단 거리를 구하는 알고리즘.</p>
</blockquote>
<p>그래프에서 경로를 탐색하면서, <strong>비용이 최소인 경로부터 차례로 확정</strong>해나가는 방식.
우선순위 큐(힙)를 사용하면 매우 효율적으로 구현이 가능하다.</p>
<br/>

<h2 id="전제-조건">전제 조건</h2>
<table>
<thead>
<tr>
<th align="left">조건</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">그래프 종류</td>
<td align="left">방향 / 무방향 모두 가능</td>
</tr>
<tr>
<td align="left">간선 가중치</td>
<td align="left">음수를 제외한 모든 가중치 가능</td>
</tr>
<tr>
<td align="left">목적</td>
<td align="left">단일 시작점에서부터 모든 노드까지의 최단 거리 <strong>(One-To-All)</strong></td>
</tr>
</tbody></table>
<p>우선순위 큐가 아닌 배열 기반으로 코드를 구성하면 $O(V^{2})$로 매우 느리지만, 우선순위 큐를 기반으로 코드를 구성하면 $O(E\log{V})$로 빠르다.</p>
<p>간선에 음수 가중치가 있다면 다익스트라는 사용할 수 없다.</p>
<br/>

<h2 id="동작-과정">동작 과정</h2>
<p>다익스트라는 다음 과정을 반복하며 최단 거리를 확정한다.</p>
<ol>
<li>시작 노드의 거리를 0으로 설정하고, 나머지는 무한대로 초기화한다.</li>
<li>우선순위 큐에 <code>(거리, 노드)</code> 형태로 삽입한다.</li>
<li>큐에서 거리가 가장 짧은 노드를 꺼낸다.</li>
<li>해당 노드에서 인접한 노드를 탐색하며, 더 짧은 거리로 도달 가능한 경우, 거리 배열을 갱신하고 큐에 넣는다.</li>
<li>모든 노드가 처리될 때까지 반복한다.</li>
</ol>
<p>그림으로 보면 다음과 같다.</p>
<table>
<thead>
<tr>
<th align="center">순서</th>
<th align="center">그래프 이미지</th>
</tr>
</thead>
<tbody><tr>
<td align="center">1</td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/c0f7a2e2-7d71-4ca0-827e-3daf1fe9c774/image.png" alt=""></td>
</tr>
<tr>
<td align="center">2</td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/675eebb6-d94d-499e-afa0-c191b7ed5eb2/image.png" alt=""></td>
</tr>
<tr>
<td align="center">3</td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/562efb8c-1fa3-48e6-a721-3502e77273c8/image.png" alt=""></td>
</tr>
<tr>
<td align="center">4</td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/08f8ee41-df83-4e1f-b155-ecf125401e11/image.png" alt=""></td>
</tr>
<tr>
<td align="center">5</td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/2a7ce546-c21b-4d5f-b48e-491ae0a436a1/image.png" alt=""></td>
</tr>
<tr>
<td align="center">6</td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/1bb4b96e-214a-4585-b235-1f5cd15f44a8/image.png" alt=""></td>
</tr>
<tr>
<td align="center">7</td>
<td align="center"><img src="https://velog.velcdn.com/images/dohn_1203/post/51ae1d78-039d-47de-b370-7248be444825/image.png" alt=""></td>
</tr>
</tbody></table>
<br/>

<h2 id="음수-가중치가-있는-경우">음수 가중치가 있는 경우</h2>
<p>다익스트라는 &quot;먼저 방문한 경로가 최단 거리&quot;라는 가정을 전제로 한다.
하지만 <strong>음수 가중치가 있는 경우</strong>, 이 가정이 깨질 수 있다.</p>
<blockquote>
<p>단순히 음수 가중치가 있다고 동작하지 않는것은 아니다.
하지만, 음수 간선 자체가 <strong>더 짧은 경로가 나중에 등장</strong>하는 상황을 만들수도 있다.</p>
</blockquote>
<p>-&gt; 따라서 음수 가중치가 있는 경우엔 <strong>벨만-포드 알고리즘</strong>을 사용해야 한다.</p>
<blockquote>
<p>추가로, 그래프 내에 <strong>음수 사이클</strong>이 존재한다면 거리가 무한히 작아져 다른 알고리즘도 계산이 불가능하다.</p>
</blockquote>
<br/>

<h2 id="구현">구현</h2>
<p>위 과정을 생각하며 코드를 구현해보자. 
대표적인 다익스트라 문제인 <a href="https://www.acmicpc.net/problem/1753">#1753. 최단경로</a>를 봐보자.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/579d0594-59d1-4f50-9f90-f6c287e55a98/image.png" alt=""></p>
<p>시작점에서 다른 모든 정점으로의 최단 경로를 구하는 전형적인 다익스트라 알고리즘 문제이다.
하나씩 구현을 해보겠다.</p>
<pre><code class="language-python">from heapq import heappop, heappush

def dijkstra(graph: dict, start: int) -&gt; list:
    # 1. 시작 노드의 거리를 0으로 설정하고, 나머지는 무한대로 초기화한다.
    dist = [float(&quot;inf&quot;)] * 6        # 노드 번호가 1번부터 시작하므로 0번 인덱스는 사용하지 않음
    dist[start] = 0
    # 2. 우선순위 큐에 (거리, 노드) 형태로 삽입한다.
    pq = [(dist[start], start)]
    # 5. 모든 노드가 처리될 때까지 반복한다.
    while pq:
        print(&quot;heapq (거리, 노드):&quot;, pq)
        # 3. 큐에서 거리가 가장 짧은 노드를 꺼낸다.
        d, n = heappop(pq)
        if d &gt; dist[n]: continue    # 현재 거리가 거리 배열에 저장된 거리보다 크다면 탐색할 필요가 없음.
        # 4. 해당 노드에서 인접한 노드를 탐색하며, 더 짧은 거리로 도달 가능한 경우, 거리 배열을 갱신하고 큐에 넣는다.
        for nn, cost in graph[n]:
            nd = d + cost
            if nd &lt; dist[nn]:
                dist[nn] = nd
                heappush(pq, (nd, nn))
    return dist</code></pre>
<p><code>heapq</code> 라이브러리를 사용하여 <code>pq</code> 리스트에 <strong>최소힙</strong> 형태로 저장한다.</p>
<p>이후 큐가 빌때까지 위 과정을 무한반복하게 된다.</p>
<p>이후 반환되는 거리 배열을 출력해보면 테스트 케이스에 나와있는대로 값이 나오는 것을 알 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu, Apache: 하나의 서버에서 여러 개의 웹 서비스 구동하기]]></title>
            <link>https://velog.io/@dohn_1203/Ubuntu-Apache-%ED%95%98%EB%82%98%EC%9D%98-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B5%AC%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dohn_1203/Ubuntu-Apache-%ED%95%98%EB%82%98%EC%9D%98-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B5%AC%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 30 Jan 2025 19:40:24 GMT</pubDate>
            <description><![CDATA[<p>토지 가격 예측 웹 서비스를 배포해야 하는 과정에서 개인 서버에 배포하려다 보니, 기존에 만들어두었던 웹 서비스가 동작하지 않았다. 웹이 안뜨는건 당연하기 때문에 깃허브로 옮겨두었지만, php도 아파치에서 돌아간다는 사실을 간과하고 있었다. 이제와서 파이썬으로 백엔드를 돌리게 만들기도 이미 늦어버렸고, 데이터베이스 텀프로젝트이기 때문에 이미 끝난 프로젝트에 시간을 쏟기 싫어 방법을 찾아보게 되었다.</p>
<hr>
<h3 id="가상-호스트-파일-만들기">가상 호스트 파일 만들기</h3>
<p>아파치에서는 가상 호스트를 설정해서 하나의 서버에 여러 도메인 서비스를 제공할 수 있다. 하지만 도메인을 각각 부여하기에는 무리가 있어서, 포트별로 구분을 해주었다.
일단 musicApp.conf라는 이름의 파일을 새로 만들어준다.</p>
<pre><code class="language-bash">sudo vi /etc/apache2/site-available/musicApp.conf</code></pre>
<p>그 다음 아래와 같이 내용을 작성해주었다.</p>
<pre><code class="language-shell">&lt;VirtualHost *:5050&gt;
        ServerName mydomain
        ServerAlias mydomain
        DocumentRoot /var/www/html/music-app/php
        &lt;Directory /var/www/html/music-app/php&gt;
                Options Indexes FollowSymLinks
                AllowOverride None
                Require all granted
        &lt;/Directory&gt;

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

        SSLEngine On
        SSLCertificateFile /etc/letsencrypt/live/mydomain/fullchain.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/mydomain/privkey.pem
&lt;/VirtualHost&gt;
&lt;VirtualHost *:500&gt;
        ServerName mydomain
        ServerAlias mydomain
        DocumentRoot /var/www/html/music-app/music-app/build
        &lt;Directory /var/www/html/music-app/music-app/build&gt;
                Options Indexes FollowSymLinks
                AllowOverride None
                Require all granted
        &lt;/Directory&gt;

        SSLEngine On
        SSLCertificateFile /etc/letsencrypt/live/mydomain/fullchain.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/mydomain/privkey.pem
&lt;/VirtualHost&gt;</code></pre>
<p>개인 서버에 SSL 인증서가 있어서 SSL 관련 설정도 추가해주었다. 그리고 가상 호스트를 활성화해준다.</p>
<pre><code class="language-bash">sudo a2ensite musicApp.conf</code></pre>
<p>이후 아파치를 재시작해준다.</p>
<pre><code class="language-bash">sudo service apache2 restart</code></pre>
<p>이제 js코드 내에 있는 php 통신 경로에 <code>:5050</code>을 붙여주고, mydomain:500으로 접근하면 정상적으로 뜰 것 같지만, 아직 안된다. 아파치에서 해당 포트에 관한 리스닝이 안되어있기 때문이다. 리스닝 설정을 해주자.</p>
<h3 id="portsconf-파일-수정하기">ports.conf 파일 수정하기</h3>
<p><code>ports.conf</code> 파일은 아파치 가상 호스트에서 사용하는 포트를 관리하는 파일이다.</p>
<pre><code class="language-shell">sudo vi /etc/apache2/ports.conf

# If you just change the port or add more ports here, you will likely also
# have to change the VirtualHost statement in
# /etc/apache2/sites-enabled/000-default.conf

Listen 80

&lt;IfModule ssl_module&gt;
        Listen 443
&lt;/IfModule&gt;

&lt;IfModule mod_gnutls.c&gt;
        Listen 443
&lt;/IfModule&gt;</code></pre>
<p>기본 구성은 다음과 같다. 80번 포트와 443번 포트에 대해서 대기하도록 설정되어 있다는 뜻이고, 모듈이 ssl_module 또는 mod_gnutls.c일 경우 443 포트에서 대기한다는 뜻이다. 80번 포트는 HTTP 트래픽, 443번 포트는 HTTPS 트래픽을 위한 포트이다. 이제 <code>Listen 80</code> 아랫줄에 원하는 포트 번호를 추가해주면 된다.</p>
<pre><code class="language-shell">Listen 80
Listen 500
Listen 5050</code></pre>
<p>웹 호스팅용 포트 <code>500</code>과 php 서버용 포트 <code>5050</code>을 열어주었다.</p>
<h3 id="포트-방화벽-열기">포트 방화벽 열기</h3>
<p>만약 서버에 방화벽 설정이 되어있다면, 그냥 접속할 시 오류가 발생할 것이다. 해당 포트가 막혀있기 때문인데, 해당 포트의 방화벽을 열어주면 해결된다.</p>
<pre><code class="language-bash">sudo ufw allow 5050/tcp
sudo ufw allow 500/tcp</code></pre>
<p>tcp통신에서 5050 포트와 500 포트의 접근을 허용해준다.</p>
<p>위 명령어를 실행하게 되면 규칙을 추가했다는 문구가 뜰 것이다. 이제 다시 웹에 접속해보면 정상적으로 호스팅되는 모습을 확인할 수 있다.</p>
<h3 id="마무리">마무리</h3>
<p>의도치않게 가상 호스트를 나눠주는 작업을 하게 되었는데, 알고 있어야 할 유용한 내용인 것 같아 따로 정리하게 되었다. 추후 개인 서버에 토이 프로젝트를 진행하게 된다면 유용하게 사용할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu: public_html 설정]]></title>
            <link>https://velog.io/@dohn_1203/Ubuntu-publichtml-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@dohn_1203/Ubuntu-publichtml-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sun, 08 Dec 2024 16:08:39 GMT</pubDate>
            <description><![CDATA[<h1 id="public_html-설정">public_html 설정</h1>
<h2 id="1-user의-home-폴더에-자동으로-public_html-폴더-생성">1) User의 Home 폴더에 자동으로 public_html 폴더 생성</h2>
<p>adduser을 했을 때 자동으로 public_html이 생성되도록 해야한다. <code>/etc/skel</code> 디렉토리는 리눅스 운영체제에서 새로운 사용자를 생성하였을 경우, 새 사용자를 위한 기본 폴더를 참고하는 디렉토리이다.</p>
<pre><code class="language-bash">$ mkdir /etc/skel/public_html
$ vim /etc/skel/public_html/index.html</code></pre>
<p>index.html에는 아무 내용이나 적어주면 된다.</p>
<pre><code class="language-html">&lt;html&gt;
    &lt;head&gt;&lt;title&gt;This is cslinux2&lt;/title&gt;&lt;/head&gt;
    &lt;body&gt;&lt;p&gt;hello world&lt;/p&gt;&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>아마 위 작업까지 마친 후에도 안된다면 아래 명령어를 차례대로 실행해주면 된다.</p>
<p>그래도 안된다 하면 아래 방법을 시도해보자.</p>
<h2 id="2-symbolic-link-설정">2) Symbolic Link 설정</h2>
<pre><code class="language-bash">$ cd /etc/apache2/mods-enabled
$ sudo ln -s ../mods-available/userdir.conf userdir.conf
$ sudo ln -s ../mods-available/userdir.load userdir.load
$ sudo service apache2 restart</code></pre>
<h2 id="3-home-디렉토리-접근-권한-설정">3) home 디렉토리 접근 권한 설정</h2>
<pre><code class="language-bash">$ sudo vim /etc/apache2/apache2.conf

&lt;Directory /home&gt;
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
&lt;/Directory&gt;</code></pre>
<p>이제 <code>http://mydomain/~id</code>로 들어가주면 성공적으로 뜨는 모습을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu: APM(Apache2, PHP, MySQL) 설치]]></title>
            <link>https://velog.io/@dohn_1203/Ubuntu-APMApache2-PHP-MySQL-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@dohn_1203/Ubuntu-APMApache2-PHP-MySQL-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Sun, 08 Dec 2024 16:06:36 GMT</pubDate>
            <description><![CDATA[<h1 id="apmapache2-php-mysql-설치">APM(Apache2, PHP, MySQL) 설치</h1>
<h2 id="1-apache2-설치">1) Apache2 설치</h2>
<p>아래 명령어를 입력해 apache2를 설치해준다.</p>
<pre><code class="language-bash">$ sudo apt update
$ sudo apt upgrade
$ sudo apt install apache2</code></pre>
<p>아파치 서버를 시작 및 중지, 재시작 하는 명령어는 다음과 같다.</p>
<pre><code class="language-bash">$ sudo service apache2 start
$ sudo service apache2 stop
$ sudo service apache2 restart</code></pre>
<p>Apache2를 설치하고 난 뒤 자신의 서버에 http 프로토콜을 이용해 접속을 하게 되면 브라우저아 다음과 같은 화면이 나온다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/cd40b3b4-1618-495c-abc5-2dc593849851/image.png" alt=""></p>
<h2 id="2-mysql-설치">2) MySQL 설치</h2>
<p>아래 명령어를 입력해 mysql을 설치해준다.</p>
<pre><code class="language-bash">sudo apt install mysql-server</code></pre>
<p>MySQL 서버의 보안 스크립트는 따로 할 필요 없다.</p>
<h2 id="3-php-설치">3) PHP 설치</h2>
<p>Apache2와 MySQL 설치가 정상적으로 끝이 났다면 php 설치를 진행해준다.</p>
<pre><code class="language-bash">$ sudo apt install php libapache2-mod-php php-mysql</code></pre>
<p>위 명령어를 통해 php 설치 및 php에서 apache2와 mysql을 사용할 수 있게 해주는 모듈을 설치할 수 있다.</p>
<p>php 설치가 끝이 나면 <code>/var/www/html</code> 경로로 이동하여 <code>index.php</code> 를 생성하고 다음과 같이 코딩해준다.</p>
<pre><code class="language-php">&lt;?php
    phpinfo();
?&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu: Ubuntu 커널 변경]]></title>
            <link>https://velog.io/@dohn_1203/Ubuntu-Ubuntu-%EC%BB%A4%EB%84%90-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@dohn_1203/Ubuntu-Ubuntu-%EC%BB%A4%EB%84%90-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Thu, 05 Dec 2024 21:20:46 GMT</pubDate>
            <description><![CDATA[<p>특별한 상황이 아니라면 우분투의 커널 버전을 변경할 필요는 없다. 하지만 학과 서버를 백업하기 위한 용도로 시놀로지 나스를 들여오고 난 이후, 백업 연동을 하기 위해 시놀로지 나스 패키지를 우분투 서버에 깔아야 되는데, 이 패키지가 특정 커널 버전까지만 지원을 한다. 즉, 특정 커널보다 버전이 높다면 사용이 불가능 하다는 것 이다.</p>
<p>이러한 문제를 해결하기 위해 우분투의 커널 버전을 변경해주었다.</p>
<h1 id="ubuntu-커널-변경">Ubuntu 커널 변경</h1>
<h2 id="1-mainline를-통해-커널-다운로드">1) Mainline를 통해 커널 다운로드</h2>
<p>우리가 사용하고 있는 백업용 NAS의 최신 지원 커널이 5.9.16이었다. 물론 2년 전의 일이니 현재는 더 많은 커널 버전을 지원할 수도 있지만, 전에 사용하였던 5.9.16 버전 커널을 사용하려고 한다.</p>
<p>먼저 터미널을 열어 ppa를 추가해준다.</p>
<pre><code class="language-bash">$ sudo add-apt-repository ppa:cappelikan/ppa</code></pre>
<p>apt를 업데이트 한 다음 mainline을 설치한다.</p>
<pre><code class="language-bash">$ sudo apt update
$ sudo apt install mainline</code></pre>
<p>이제 Mainline을 실행시킨다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/348036e4-8245-432e-ae4b-d3df42d9907e/image.png" alt=""></p>
<p>Mainline을 실행시키면 설치 가능한 Ubuntu Kernel 버전을 확인 가능하다. 여기서 설치해야 하는 버전(5.9.16)을 선택해 Install 해 준다.</p>
<h2 id="2-grub-설정-변경">2) GRUB 설정 변경</h2>
<p>커널의 설치가 완료된 후 재부팅을 하게 되면 새로 설치한 커널이 아닌 기존의 커널로 진입이 될 것이다. 따라서 새로 설치한 커널의 버전으로 진입하게 설정을 해주어야 하는데, GRUB 부트로더의 설정을 변경해주어야 한다. </p>
<pre><code class="language-bash">$ sudo vim /etc/default/grub</code></pre>
<p>위 명령어를 입력 후 GRUB 부트로더의 설정을 다음과 같이 변경해준다.</p>
<pre><code class="language-bash">GRUB_TIMEOUT_STYLE=menu
GRUB_TIMEOUT=3</code></pre>
<p>수정이 완료되었다면 GRUB 부트로더를 업데이트 해준다.</p>
<pre><code class="language-bash">$ sudo update-grub</code></pre>
<p>이후 재부팅하면 GRUB 부트로더 화면이 3초간 뜨게 된다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/4af065d9-0c7b-41b0-807f-6579d75f1eb8/image.png" alt=""></p>
<p>위 아래 방향키를 이용해 [Advanced options for Ubuntu] 를 선택해준 다음 엔터를 누르게 되면 현재 설치되어 있는 커널 목록이 표시된다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/00063d24-9434-497d-b4a6-e9bc6e7da3b2/image.png" alt=""></p>
<p>우리는 5.9.16 버전을 사용할 것이기 때문에 5.9.16 버전이 위에서 몇 번째에 있는지 확인해준다. (0부터 넘버링)</p>
<p>위 예시이미지에서 만약 5.13.0-25-generic으로 진입하고 싶다면 ‘2’를 기억하고 있으면 된다.</p>
<p>다시 grub 부트로더 파일을 수정해준다.</p>
<pre><code class="language-bash">$ sudo vim /etc/default/grub

GRUB_DEFAULT=&quot;1&gt;해당하는번호&quot;</code></pre>
<p>위와 같이 입력해주면 된다. 예시 이미지를 예로 들면 <code>GRUB_DEFAULT=&quot;1&gt;2&quot;</code>로 수정해주면 된다.</p>
<p>다시 <code>sudo update-grub</code> 을 해준 후에 재부팅하여 해당 커널에 잘 진입하는지 확인해보면 된다.</p>
<p>커널 버전을 확인하는 명령어는 다음과 같다.</p>
<pre><code class="language-bash">$ uname -r
5.9.16-050916-generic</code></pre>
<hr>
<h3 id="references">References</h3>
<p><a href="https://pstudio411.tistory.com/entry/Ubuntu-2004-LTSKernal-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0">[Ubuntu 20.04-LTS]Kernel 설치 및 변경하기, by 평범한 이야기</a>
<a href="https://velog.io/@markyang92/boot-loader-grub">2-1. GRUB 설치, 수정, recovery mode, by markyang92</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu: SSH 설치 및 포트 변경]]></title>
            <link>https://velog.io/@dohn_1203/Ubuntu-SSH-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%8F%AC%ED%8A%B8-%EB%B3%80</link>
            <guid>https://velog.io/@dohn_1203/Ubuntu-SSH-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%8F%AC%ED%8A%B8-%EB%B3%80</guid>
            <pubDate>Thu, 05 Dec 2024 21:09:26 GMT</pubDate>
            <description><![CDATA[<h1 id="ssh-설치">SSH 설치</h1>
<h2 id="1-open-ssh-server-설치">1) Open SSH Server 설치</h2>
<p>터미널에서 Open SSH Server를 설치한다.</p>
<pre><code class="language-bash">$ sudo apt update
$ sudo apt install openssh-server</code></pre>
<h2 id="2-ssh-server-실행">2) SSH Server 실행</h2>
<p>SSH를 설치하면 자동으로 실행된다. 다음 명령어로 SSH가 실행 중인지 확인할 수 있다. 로그에서 <code>active (running)</code>이 보이면 실행 중인 상태이다.</p>
<pre><code class="language-bash">$ sudo systemctl status ssh
● ssh.service - OpenBSD Secure Shell server
     Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2023-01-12 17:27:49 KST; 1 day 21h ago
       Docs: man:sshd(8)
             man:sshd_config(5)
   Main PID: 837 (sshd)
      Tasks: 1 (limit: 76964)
     Memory: 4.9M
     CGroup: /system.slice/ssh.service
             └─837 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
</code></pre>
<p>실행 중이 아니라면 다음 명령어를 통해 실행시킬 수 있다.</p>
<pre><code class="language-bash">$ sudo systemctl enable ssh
$ sudo systemctl start ssh</code></pre>
<br/>

<h2 id="ssh-server-port-변경">SSH Server Port 변경</h2>
<p>ssh 접속 포트는 22이다. 이를 특정 포트 번호로 바꿔줄 수 있다. 해당 포스트에서는 1022로 변경해 줄 것이다.</p>
<p>Ubuntu에는 기본적으로 SSH Client가 설치되어 있지만, 만일 없다면 다음 명령어를 통해 설치해 볼 수 있다.</p>
<pre><code class="language-bash">$ sudo apt-get install openssh-client</code></pre>
<p>접속 포트는 sshd_config 파일을 수정해 변경해 볼 수 있다.</p>
<pre><code class="language-bash">$ sudo vim /etc/ssh/sshd_config</code></pre>
<p>참고로 vim이 없다면 파일 수정이 불가능하니 그 전에 vim을 깔아주도록 하자.</p>
<p>vim이 열렸다면 <code>i</code>를 눌러 수정 모드로 변경한다. Port 부분의 주석을 해제하고 22를 1022로 변경해주면 된다.</p>
<pre><code class="language-bash">Include /etc/ssh/sshd_config.d/*.conf

Port 1022
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::</code></pre>
<p>수정이 완료되었다면 esc를 누른 후 <code>:wq</code> 를 이용해 저장해주면 된다. 변경한 포트를 적용하기 위해서는 ssh 서비스를 재시작해야 한다.</p>
<pre><code class="language-bash">$ sudo service ssh restart</code></pre>
<p>서비스를 재시작하였으면 <code>netstat</code> 명령어를 이용해 ssh가 변경된 포트에서 돌아가고 있는지 확인해준다. netstat은 <code>net-tools</code>를 설치하여 사용할 수 있다.</p>
<pre><code class="language-bash">$ sudo apt install net-tools
$ netstat -tnlp</code></pre>
<p>이제 포트 1022로 ssh 접속을 하게 되면 성공적으로 접속이 되는 것을 확인할 수 있다.</p>
<pre><code class="language-bash">$ ssh myserver@myserver.domain.com -p 1022</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu: Ubuntu 20.04LTS 설치]]></title>
            <link>https://velog.io/@dohn_1203/Ubuntu-%EC%84%9C%EB%B2%84-%EC%84%B8%ED%8C%85-Ubuntu-20.04LTS-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@dohn_1203/Ubuntu-%EC%84%9C%EB%B2%84-%EC%84%B8%ED%8C%85-Ubuntu-20.04LTS-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Thu, 05 Dec 2024 20:43:00 GMT</pubDate>
            <description><![CDATA[<p>학과 연구실을 들어오게 되면서 학과 서버도 함께 관리하게 되었는데, 우리 학과가 우분투를 사용하기 때문에 벨로그 포스트로 글을 남겨본다. CentOS, Ubuntu 등을 사용해 보았는데, 아직까진 우분투가 제일 편한 것 같다.</p>
<h2 id="ubuntu-2004lts-설치">Ubuntu 20.04LTS 설치</h2>
<h3 id="1-ubuntu-2004lts-설치">1) Ubuntu 20.04LTS 설치</h3>
<p>이 글을 작성한 현재 시점에서는 20.04보단 22.04를 주로 사용하긴 한다. 버전이 바뀐다고 설치 방법이 달라지는 것은 아니기 때문에 20.04 버전을 기준으로 작성하였다.</p>
<p>먼저 아래 사이트에 접속해서 Alternative downloads &gt; Ubuntu Server 20.04LTS를 다운받는다.</p>
<p><a href="https://ubuntu.com/download/server">https://ubuntu.com/download/server</a></p>
<h3 id="2-ubuntu-server-2004-부팅-usb-생성">2) Ubuntu Server 20.04 부팅 USB 생성</h3>
<p>ISO 파일을 다운받는 동안 아래 사이트에서 <code>balenaEtcher</code>를 다운받는다.</p>
<p><a href="https://www.balena.io/etcher/">https://www.balena.io/etcher/</a></p>
<p><code>balenaEtcher</code>는 이미지를 USB 드라이브로 Flash하는 무료 오픈 소스 유틸리티이며 Windows, MacOS 및 Linux를 지원한다.
설치 파일이 다운로드되면 해당 파일을 클릭하여 설치 마법사를 시작한다. 설치 마법사의 단계에 따라 Etcher를 설치한다.</p>
<p>balenaEtcher와 ISO 파일 설치가 완료되었으면 balenaEtcher를 열어준다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/d4009542-6bb8-4b34-a143-6fe6dd9df10a/image.png" alt=""></p>
<p>[Flash from file]을 클릭한 다음 전에 다운받았던 ISO 파일을 선택한다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/6121822a-7f97-4849-886c-6751fdeab9f7/image.png" alt=""></p>
<p>ISO를 설치할 USB를 연결한 다음 [Select target]을 클릭해 USB Device를 선택해준다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/270c304a-9491-4f58-9220-e05c19e98002/image.png" alt=""></p>
<p>[Flash!] 버튼을 누르고 완료되기를 기다리면 된다.</p>
<p>Flash가 완료되면, 서버의 USB 포트에 USB를 연결하고 바이오스에 진입해 부팅 디스크를 USB로 바꿔준다. 바이오스에 진입하는 키는 메인보드 제조사마다 다르며, 보통 <code>F2</code> 혹은 <code>F12</code>, <code>Del</code>이다. 부팅 디스크를 변경해 준 후에 재부팅을 하게 되면 Ubuntu를 설치할 수 있게 된다.</p>
<h3 id="3-ubuntu-2004-설치">3) Ubuntu 20.04 설치</h3>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/016e531e-f42b-4b8c-b168-a59c3f23bd0a/image.png" alt=""></p>
<p>언어를 한국어로 변경한 후 [Ubuntu 설치]를 눌러준다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/925023d6-fb91-4166-b5d2-77564ec5a315/image.png" alt=""></p>
<p>키보드 레이아웃을 정하고 [계속하기]를 누른다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/6326bea8-060a-404c-a326-0d9af0f3748d/image.png" alt=""></p>
<p>[일반 설치]와 [그래픽과 Wi-Fi 하드웨어 그리고 추가 미디어 포맷을 위한 서드파티 소프트웨어 설치]에 체크를 해준다. [Ubuntu 설치 중 업데이트 다운로드]는 현재 이더넷 연결을 하지 않았기 때문에 체크가 불가능하다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/17df1174-1e6c-485e-a684-af2a61b9b0fd/image.png" alt=""></p>
<p>[디스크를 지우고 Ubuntu 설치]를 선택하고 [지금 설치]를 클릭해준다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/1b32a0cb-d0a8-49c5-9dcc-bb6160efb634/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/c53681c6-470c-4fe9-b423-fad499430465/image.png" alt=""></p>
<p>이름과 컴퓨터 이름, 사용자 이름 모두 원하는 사용자 명으로 변경해준다. 암호는 서버실에서 사용하는 공용 암호로 지정해주었다.</p>
<p>[로그인할 때 암호 입력]에 체크를 해준다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/02d08ac7-99bf-4ae7-9919-d95b52fd943c/image.png" alt=""></p>
<p>설치에 다소 시간이 소요된다. 기다려주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/bfb576bc-9656-4656-b1fb-0c0059ee7b6d/image.png" alt=""></p>
<p>설치가 완료되면 위와 같은 팝업창이 뜬다. [지금 다시 시작]을 눌러주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/7fd4b401-7c62-478e-94de-c62bc4aa50b3/image.png" alt=""></p>
<p>사용자 계정 클릭 후 암호를 입력하여 로그인한다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/426cda77-1217-421a-8c23-9199ebbfb386/image.png" alt=""></p>
<h3 id="4-ubuntu-server-이더넷-연결">4) Ubuntu Server 이더넷 연결</h3>
<p>이제 인터넷을 연결해주어야 한다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/eac329e4-15e7-4dbc-b7d2-94e694bf4b76/image.png" alt=""></p>
<p>상단의 아이콘을 눌러 인터넷 설정으로 진입한다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/2960340f-b333-4e7b-993e-ccf4bf88a6e9/image.png" alt=""></p>
<p>톱니바퀴 모양의 아이콘을 눌러 설정창으로 진입한다.</p>
<p><img src="https://velog.velcdn.com/images/dohn_1203/post/08f87381-e6c6-48de-a03a-c885e1fd11f2/image.png" alt=""></p>
<p>IPv4로 들어가서 IPv4 Method를 Manual로 설정해 준 다음 Address, Netmask, Gateway, DNS 주소를 입력해준다.
서버마다 고정된 IP 주소가 있으니, 그대로 입력해주기만 하면 끝이 난다.</p>
<p>인터넷 연결이 되었는지에 대한 확인은 Firefox를 열어 유튜브에 들어가 확인해본다.</p>
<hr>
<h3 id="references">References</h3>
<p><a href="https://virusheo.blogspot.com/2021/09/210908.html">[ Ubuntu ] 우분투 설치 USB 만들기 - balenaEtcher, by VirusHeo</a>
<a href="https://www.manualfactory.net/12619">Ubuntu 20.04 Desktop / 설치하기, by MANUAL FACTORY</a></p>
]]></description>
        </item>
    </channel>
</rss>