<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>poor-food.log</title>
        <link>https://velog.io/</link>
        <description>Wandering wondering. </description>
        <lastBuildDate>Tue, 11 Jun 2024 12:07:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>poor-food.log</title>
            <url>https://velog.velcdn.com/images/poor-food/profile/d82ea732-cf34-4d34-b0f1-fb198b86be99/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. poor-food.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/poor-food" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[투명하게 커뮤니케이션하기]]></title>
            <link>https://velog.io/@poor-food/%ED%88%AC%EB%AA%85%ED%95%98%EA%B2%8C-%EC%BB%A4%EB%AE%A4%EB%8B%88%EC%BC%80%EC%9D%B4%EC%85%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@poor-food/%ED%88%AC%EB%AA%85%ED%95%98%EA%B2%8C-%EC%BB%A4%EB%AE%A4%EB%8B%88%EC%BC%80%EC%9D%B4%EC%85%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 11 Jun 2024 12:07:54 GMT</pubDate>
            <description><![CDATA[<p>나에게는 아주 작은 오디오 취미가 있다.</p>
<p>보통 오디오에 있어서 투명한 음색이라고 하면 기이하게도 중고역대가 살짝 강조되었다는 의미를 가지게 된다. 투명 -&gt; 유리 -&gt; 유리병을 불 때 나는 맑고 고운 소리 -&gt; 고음으로 연상이 되는 걸까? (clear)</p>
<p>문자 그대로의 의미를 가지고 투명한(transparent) 음색을 해석하자면 작곡가 또는 오디오 엔지니어가 의도한 소리를 그대로 들려준다는 의미가 될 것이다. 보통 이런 것을 Hi-Fi 하다고 하며, 많은 오디오 매니아들의 지향점이기도 하다. 이렇게 유감스럽게도, 스피커, 헤드폰 소리가 어떻다할 이야기를 할 때에도 커뮤니케이션은 의도가 왜곡되고 만다. 마치 Hi-fi하지 않은 음향기기를 거친 소리가 의도된 것과 다른 소리를 재생하곤 하는 것처럼 말이다. </p>
<p>우리는, 우리의 마음을 있는 그대로 전달할 수 없다. 그래서 노래하는 거겠지. </p>
<h1 id="커뮤니케이션을-잘한다는-것">커뮤니케이션을 잘한다는 것</h1>
<p>개발자, 프로그래머, 소프트웨어 엔지니어, 뭐라고 불러야할 지 모르는 이 IT 관계의 기술 직종은 커뮤니케이션 관련 문제로 고통을 겪는 것으로 유명하다. </p>
<p>주변인들이 답답해하기도 하고 본인들이 답답해하기도 한다. 이러한 문제의 근본은 우리가 현실에 존재하지 않는 추상적인 개념을 구체화 시키는 작업을 하고 있거나, 혹은 그 반대의 작업, 현실의 물건을 개념화시키는 작업을 하는, 복합적인 작업에 종사하고 있기 때문이거나, 혹은 다른 사람들도 다 똑같이 겪는 문제임에도 불구하고 인터넷 세상에서 유난히 모습을 드러내려고 하는 특성 때문에 과도하고 비대하게 모습을 부풀리고 있을지도 모르며, 그냥 주로 이모저모 답답한 사람들이 현실 세상의 모사품으로 컴퓨터와 친해지다보니 답답한 사람들이 개발자가 되는 경향이 있기 때문일지도 모른다. </p>
<p>커뮤니케이션을 잘한다는 것은 대체 무엇일까? </p>
<p>앞서 꺼낸 오디오의 비유를 다시 꺼내보자면 &quot;의도를 왜곡하지 않고 전달하는 것&quot;이 될 것이다. 가능하다면 적은 비용으로. 이렇게 심플하게 정의한다고 해도 그리 틀린 말은 아닐 것이다. 그럼에도, &quot;커뮤니케이션 능력이 뛰어나신 분!&quot;을 찾는 곳은 무척이나 많다. 그만큼 어렵다는 것이겠지. 원칙은 단순해도, 실천하는 방법은 단순하지 않기 때문이다. 그렇기 때문에 커뮤니케이션 &quot;스킬&quot;이라는 말이 있는 것이라고 할 수 있다. </p>
<h1 id="투명한-커뮤니케이션">투명한 커뮤니케이션</h1>
<p>이것들은 내가 하는 [노력들]이다. 커뮤니케이션을 잘 하기 위한 노력들. </p>
<ol>
<li>의도를 먼저 밝히기</li>
</ol>
<p>나는 &quot;안녕하세요. 잠깐 말씀 좀 나눌 수 있으실까요?&quot; 라는 서두를 별로 좋아하지 않는다. 특히 이메일/문자/메신저 등 비동기 통신수단일 경우에 특히 그렇다. 상대의 발화 의도를 정확히 알 수 없기 때문이다. 이런 식의 발화는 상대를 긴장하고 겁나게 한다. 결국 의도가 전달되기 이전에 정보가 불분명한 상태로 대화를 시작해야 한다. 이런 식의 발화가 적절한 경우는 내 생각에는 해고 통지 정도밖에 없다.</p>
<ol start="2">
<li>먼저 연락하기 </li>
</ol>
<p>예를 들어, 내가 어떤 클라이언트에게 업무를 수주해서 진행을 하고 있다고 하자. 어느 시점에 연락하는 게 좋을까? 업무를 완료한 뒤에 연락하는 것은 너무 늦다. 클라이언트가 업무가 진행되는 중간에 업무가 잘 진행되고 있는지 확인하고 싶을 것이라는 것은 명확한 사실이다. 그러니, 업무의 진행이 예상 가능한 시점에 올랐을 때, 예상 기간에 변동이 일어날만한 일이 발생할 때마다 먼저 알려주는 것이 좋다. (실제로 기간에 변동이 일어나지 않았더라고 하더라도) 특히, 엔지니어 직군은 비엔지니어 직군에 비해 비대칭적으로 정보를 보유한 경우가 많다. </p>
<p>이 말은 &quot;어느 시점에 연락을 하는 게 좋은지&quot; 또한 엔지니어 직군이 가지고 있을 가능성이 높다는 것이다. </p>
<ol start="3">
<li>공개된 곳에 남겨놓기</li>
</ol>
<p>내가 존경하는 프로그래머 중 한 분이 있다. 과거에 재직했던 회사에서 10년 전에 재직을 하신 분이다. 그 분은 해당 회사의 핵심이 되는 솔루션의 코드를 대부분 작성하셨다. 추측이지만 당시에는 주니어 레벨이었던 것으로 보이며, 많은 부분에서 고민하신 흔적을 찾을 수 있었다. 나는 어떻게 10년 전에 재직하신 분은 존경할 수 있었을까? 그 분이 나와 비동기적 커뮤니케이션을 하셨기 때문이다. 완벽하진 않지만 고민이 담긴 코드들을 고민만으로 남긴 것이 아니라, 문서, 주석, 위키 등을 통해서 남겨놓으셨고, 10년이 지난 지금도 해당 정보들은 남아서 개발을 돕고 있다. </p>
<p>찾으려고 하는 정보를 공개된 곳에 남겨놓으면 그 정보를 필요로 하는 사람이 찾을 수 있다.</p>
<h1 id="and-">And ...</h1>
<p>[10년 전에 재직했던 개발자]님에 대한 이야기를 했다. 그 분은 날 모르실 것이고, 이 블로그에 [투명한 커뮤니케이션의 예시]로 쓰이신 것도 모르실 것이다. 우연히도 그 분의 블로그를 발견한 적이 있다. 그 분도 오디오에 취미가 있으셨다.</p>
<p>춤이라도 추실까요? </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dedicated Server 에서의 배포 자동화 (지속적 통합) ]]></title>
            <link>https://velog.io/@poor-food/Dedicated-Server-%EC%97%90%EC%84%9C%EC%9D%98-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94-%EC%A7%80%EC%86%8D%EC%A0%81-%ED%86%B5%ED%95%A9</link>
            <guid>https://velog.io/@poor-food/Dedicated-Server-%EC%97%90%EC%84%9C%EC%9D%98-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94-%EC%A7%80%EC%86%8D%EC%A0%81-%ED%86%B5%ED%95%A9</guid>
            <pubDate>Mon, 20 May 2024 12:55:55 GMT</pubDate>
            <description><![CDATA[<h1 id="소개">소개</h1>
<p>배포 자동화는 처음 접하면 까다로운 개념이고 복잡해보이지만 실제로 작업해보면 그리 어렵지 않게 작업할 수 있다. 그리고 배포에 드는 수고를 크게 줄여줄 수 있다. Cloud 서버를 비롯해서 많은 서비스에서 이와 같은 서비스를 제공한다. 이번 글에서는 Github Actions를 이용해 Dedicated Server (Cloud 서비스, 호스팅 서비스가 아닌 서버 하나를 임대 또는 구축하여 사용하는 경우)에서 배포하는 방식에 대해서 소개하겠다. </p>
<p>이전에 다른 글에서 소개했다시피, 나는 토이 서버로 Odroid 를 이용한 서버를 가지고 있다. 간단한 어플리케이션을 구동하는 데에 주로 사용했지만, 이번에는 이것으로 웹 어플리케이션을 배포하게 되었고, 배포 자동화를 구현할 필요성을 느껴서 작업하게 되었다.</p>
<h1 id="개념">개념</h1>
<p>다양한 언어, 프레임워크, 서비스를 통해서 배포 자동화를 구현할 수 있지만, 기본적으로 배포 자동화는 다음과 같은 방식으로 구현된다.</p>
<ol>
<li>커밋 감지 및 코드 클론 </li>
</ol>
<p>어떤 트리거를 감지했을 때, 코드 저장소에서 코드를 모두 가져온다(clone). 보통은 특정 브랜치에 commit 이 이뤄졌을 때가 될 것이다. AWS의 CodeCommit 이 이와 같은 일을 한다.</p>
<ol start="2">
<li>빌드</li>
</ol>
<p>(1)에서 가져온 코드를 빌드용 서버(프로덕션 환경과 동일한 환경을 가져야 한다)로 가져온 뒤, 절차에 따라 빌드를 실행한다. 자동화 테스트가 있다면 테스트도 실행한다. 이 때 언어, 프레임워크마다 빌드를 하는 방식이 다르기 때문에, 빌드용 스크립트를 작성해야 한다. AWS의 CodeBuild가 이 일을 작업한다.</p>
<ol start="2">
<li>배포</li>
</ol>
<p>빌드 결과물을 production 서버로 전달하고 재시작한다. 클라우드 서버에서는 블루/그린(무중단 배포 방식) 배포를 위해서 기존 서버 인스턴스를 유지한 상태로 새 인스턴스를 생성하여 배포를 진행하고, 라우팅을 변경하는 식으로 진행하기도 한다. AWS의 CodeDeploy가 이 작업을 진행해준다. </p>
<h1 id="github-actions">Github Actions</h1>
<p>깃허브에서는 Github Actions 라고 하는 서비스를 제공하는데, 커밋이 실행되었을 때 특정한 순서로 스크립트를 실행할 수 있는 기능이다. 이 때 Github Hosted Runner 라고 하는 클라우드 인스턴스를 제공하고 사용자는 이 인스턴스를 이용해서 자동화 테스트나 빌드를 진행할 수 있다. </p>
<h2 id="이슈---dedicated-server의-보안">이슈 - Dedicated Server의 보안</h2>
<p>위의 코드 클론 / 빌드 / 배포의 세 개의 과정을 실행하기 위해선 Dedicated Server로 코드의 변경 또는 빌드 결과물을 Dedicated Server로 전달할 필요가 있다. 문제는 어떻게 하느냐는 것이다.</p>
<ol>
<li>SSH</li>
</ol>
<p>Github Hosted Runner가 Dedicated Server로 SSH 접속을 해, 코드 클론 / 빌드 / 배포를 실행한다. 이 방법은 가장 단순하고 구현하기도 쉬운 방식이다. 빌드 환경과 프로덕션 환경을 동일한 환경으로 유지하기도 쉬운데, 프로덕션 환경이 곧 빌드 환경이 되기 때문이다. </p>
<p>그러나 이 방법은 사용하지 않기로 했다. 커밋과 동시에 생성되는 Github Hosted Runner의 SSH 접속을 허용한다는 것은 내 네트워크에 임의의 SSH 접속을 허용한다는 이야기가 된다. 네트워크의 폐쇄성을 유지하는 게 보안상 훨씬 유리했다. </p>
<ol start="2">
<li>Jenkins 등의 CI/CD 툴 또는 웹훅</li>
</ol>
<p>커밋이 실행된 시점에서 커밋이 실행됐다는 정보만을 담은 리퀘스트를 실행하고, 해당 리퀘스트를 CI/CD 툴이나 웹훅 클라이언트를 통해서 감지하고 코드 클론 / 빌드 / 배포 순서로 실행하는 방법이 있었다. </p>
<p>그러나 아주 단순한 프로젝트를 진행하는만큼 이는 배보다 배꼽이 더 커질 염려가 있었다. </p>
<ol start="3">
<li>Self-hosted Runner </li>
</ol>
<p>다행히 Github는 Self-hosted Runner 라는 기능을 지원했다. 커밋이 실행됐을 때, 클라우드 인스턴스 대신, 특정한 서버에 설치된 Github Runner 가 대신 작업을 수행하게 하는 기능이다. </p>
<p>Production Server 에 Self-hosted Runner를 설치 후, 스크립트를 Self-hosted Runner 가 실행하게 하는 것으로 위의 문제를 쉽게 해결할 수 있었다. 코드 클론 / 빌드 / 배포 모두 프로덕션 서버 내에서 실행하되, Github Actions 가 커밋이 되었다는 정보만 전달하게 하는 것이다. </p>
<p>다음은 실제로 사용한 github actions 용 스크립트다. </p>
<pre><code class="language-yaml">name: Node.js CI

on:
  push:
    branches: [ &quot;main&quot; ]

jobs:
  build:

    runs-on: odroid # self-hosted runner의 tag 값이 들어간다. 

    strategy:
      matrix:
        node-version: [22.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: &#39;npm&#39;
    - run: npm ci
    - name: build tailwindcss
      run : npm run tailwind-once
    - name: Deploy
      run: 
        cp -r $GITHUB_WORKSPACE/* /home/pjc1991/myfirstnodejs
    - name: Reload node
      run: pm2 restart myfirstnodejs</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[2023년 회고]]></title>
            <link>https://velog.io/@poor-food/2023%EB%85%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@poor-food/2023%EB%85%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 25 Mar 2024 13:10:59 GMT</pubDate>
            <description><![CDATA[<p>바쁘게 진행하던 프로젝트가 일단락 됐다.
좋은 시기다. </p>
<h3 id="무엇을-했는가">무엇을 했는가</h3>
<ul>
<li><p>퇴사 결정 : 좋은 조건의 직장이었다. 그러나 나를 죽이는 곳에 있을 이유는 없다. </p>
</li>
<li><p>프리랜서 : 기술을 비즈니스로 이어주는 게 엔지니어링이다. 지금까진 비즈니스를 몰랐다. </p>
</li>
<li><p>사이드 프로젝트 : 시작이 반이다. 완성이 나머지 반이다. 적어도 새 기술을 배우는 두려움은 없어졌다. </p>
</li>
<li><p>구직 활동 : 친구의 도움을 많이 받았다. 시장은 상황이 안 좋다. 그래도 맘이 맞으면 갈 곳이 있을 것이다. </p>
</li>
</ul>
<h3 id="이제-그만-둘-것들">이제 그만 둘 것들</h3>
<ul>
<li><p>(다른 계획들을 완수하기 전까지) 새로운 계획을 만들기 : 나에겐 계획이 많다. </p>
</li>
<li><p>과장하지 않기 : 그게 상대를 믿는 태도이다. </p>
</li>
</ul>
<h3 id="계속-해나갈-것들">계속 해나갈 것들</h3>
<ul>
<li>사이드 프로젝트 진행 : 애자일, MVP 중요하다. 내 프로젝트에서도 그렇다. </li>
</ul>
<h3 id="새롭게-시작할-것들">새롭게 시작할 것들</h3>
<ul>
<li>책 읽는 버릇 만들기 : 독서는 쉬운 일이다. 독서하는 버릇을 만드는 게 어려운 일이다. </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL, TIMESTAMP, Not Null]]></title>
            <link>https://velog.io/@poor-food/MySQL-TIMESTAMP-Not-Null</link>
            <guid>https://velog.io/@poor-food/MySQL-TIMESTAMP-Not-Null</guid>
            <pubDate>Sat, 04 Nov 2023 01:05:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/poor-food/post/404af853-66dc-497d-9c56-7410d11a929e/image.png" alt=""></p>
<h1 id="tldr">TL;DR</h1>
<p>MySQL 의 Timestamp 타입은 Not Null 로 지정됐을 때 CURRENTTIME을 DEFAULT로 가지게 되고, 그 중에서도 첫번째 Timestamp 칼럼은 ON UPDATE CURRENT_TIMESTAM 속성도 가지게 됩니다. 이 두 가지는 둘 다 수정하지 못하고 강제로 고정됩니다. </p>
<p>해결하기 위해선 NOT NULL 을 해제하고 어플리케이션 차원에서 제어하거나, explicit_defaults_for_timestamp 시스템 변수를 활성화합니다. </p>
<h1 id="intro">Intro</h1>
<p>나는 이렇게 생각하곤 했습니다. </p>
<p>_RDBMS는 SQL 문법도 거의 동일하고, 이마저도 ORM을 통해서 표준화해서 사용하는 것이 보편적인 흐름이 되었으므로, 각 DB 간의 차이는 적거나 크게 신경 쓸 부분이 아닌 것 아닌가? _</p>
<p>그러나 때때로 데이터베이스는 각자 독특한 사양을 지니기도 합니다. 가장 자주 사용되는 DB 중 하나인 MySQL인데도 모르던 사양이 있어 이 부분을 소개합니다. </p>
<h1 id="issue">Issue</h1>
<p>여느 때처럼 데이터베이스를 구성하고 개발을 진행하는데 운영측에서 긴급히 연락이 왔습니다. 어떤 TIMESTAMP 필드가 주기적으로 현재 시간으로 변경된다는 것입니다. 모든 행이 변하는 것은 아닌데 수많은 행에 진행이 되었습니다. </p>
<p>당연하지만 프로그램 코드에는 &quot;주기적으로 데이터베이스의 모든 행을 현재 시간으로 설정하는 코드&quot; 같은 내용은 담겨있지 않습니다. 따라서 저는 이 문제가 &quot;데이터베이스 사양&quot;에서 발생하는 문제라고 어느 정도 확신을 가졌습니다.  </p>
<p>이 때 왜 지금까지 겪지 못했나 싶은 경험을 하게 되는데, 그것은 바로 MySQL의 Timestamp 사양입니다. MySQL의 Timestap 타입은 아주 특별한 특징이 있는데 다음과 같습니다.</p>
<ul>
<li>MySQL 테이블의 TIMETSTAMP 컬럼은, NOT NULL로 설정되었을 때 DEFAULT 값이 CURRENT_TIMESTAMP로 고정이 된다.</li>
<li>한 테이블의 NOT NULL로 설정된 TIMESTAMP 칼럼은 OP UPDATE CURRENT_TIMESTAMP로 고정이된다.</li>
</ul>
<p>이 사실을 모르고 있던 저는 &quot;테이블에 넣은 적도 없는 디폴트 값이 들어가있고 변경하려고 시도하면 롤백되는&quot; 상황을 맞이하게 됩니다. </p>
<p>근데, 그럼 행들은 왜 update 가 된걸까요? 업데이트를 실행한 적은 없는데? </p>
<p>이 문제의 해결을 하기 위해 다음과 같은 방식으로 문제를 추적했습니다.</p>
<ol>
<li><p>ON UPDATE CURRENT_TIMESTAMP 덕분에, 해당 업데이트가 실행된 시각을 알 수 있었습니다. 배포 공지 (슬랙 메시지)와 깃 커밋 시각을 토대로, 해당 업데이트는 &quot;배포 시점&quot;에 다량으로 발생했음을 확인했습니다.</p>
</li>
<li><p>배포 과정에서 데이터베이스에 영향을 줄만한 일은 mvn test (단위 테스트)와 어플리케이션 재실행입니다. 그러나 어플리케이션 재실행은 배포 이외에도 자주 일어나기 때문에 (로컬 실행 등) mvn test 에 의해서 해당 행들이 업데이트가 되었다고 봄이 타당합니다. </p>
</li>
<li><p>JUnit을 이용한 단위 테스트들을 확인해본 결과 Transaction 및 롤백 처리가 되어있습니다. Transaction을 사용할 수 없는 통합 테스트들도 존재했으나, 이 테스트들이 INSERT, UPDATE 하는 행들은 기존의 데이터에 영향을 미치지 않도록 설계 되어있었습니다. (테스트에 필요한 데이터를 생성하고 테스트 이후에 삭제) </p>
</li>
<li><p>문제가 발생한 테이블에 접근하는 서비스 메소드들을 찾아보며 트랜잭션에서 벗어날만한 부분이 있는지 우선적으로 조사했습니다. 조사 결과 서비스 중에 &#39;낙관적 락&#39;을 사용하는 메소드를 발견합니다.</p>
</li>
<li><p>낙관적 락은 엔티티에 VERSION을 부여하여, 동시에 한 행에 대한 여러 개의 접근으로 인한 충돌을 방지합니다. 달리말하면 엔티티를 불러오는 것만으로도 버전 상승으로 인한 UPDATE 쿼리가 실행될 수 있습니다. </p>
</li>
<li><p>즉, 단위 테스트로 인해 SELECT 쿼리가 실행되었고, SELECT 쿼리는 낙관적 락을 트리거해 UPDATE 쿼리를 실행했고, 이 업데이트 쿼리가 데이터베이스의 ON UPDATE 를 트리거했으며, 이 ON_UPDATE는 트랜잭션 외(!) 에서 실행이 되어 데이터에 영향을 미쳤다는 이야기가 됩니다.</p>
</li>
</ol>
<p>jUnit 트랜잭션은 분리가 되어있고 롤백 처리도 되어있을텐데 어째서 ON UPDATE 가 트리거되었는지 아직 정확히 알지 못합니다. 이 부분은 근 시일 내에 테스트를 진행해서 검증해볼까 합니다. </p>
<h1 id="fix">Fix</h1>
<p>단순한 방법으로 해결하기로 합니다. </p>
<p>해당 칼럼에서 Not Null을 해제합니다. Null 여부가 중요한 칼럼일 경우, 어플리케이션 차원에서 이를 체크하도록 합니다. </p>
<p>explicit_defaults_for_timestamp 시스템 변수를 수정해서 해결하는 방법도 있습니다. 이게 더 적절한 방법이고 접근일 수 있으나 다음의 이유로 택하지 않기로 합니다. </p>
<p>어플리케이션은 ORM을 사용하고 있고, 가능하면 코드 이외의 부분에서 데이터베이스 정의를 늘리고 싶지 않습니다. 현재 프로젝트의 메인 프로그래머긴 하지만, 근 시일내에 타 프로그래머에게 인수인계 예정이며 사내에는 별도의 서버 엔지니어도 있습니다. &#39;코드에 작성되지 않은 알아야 하는 부분&#39;을 늘리는 것은 바람직하지 않다고 판단됩니다. </p>
<hr>
<h1 id="reference">REFERENCE</h1>
<p><a href="https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html">https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료 구조 - List]]></title>
            <link>https://velog.io/@poor-food/%EC%9E%90%EB%A3%8C-%EA%B5%AC%EC%A1%B0-List</link>
            <guid>https://velog.io/@poor-food/%EC%9E%90%EB%A3%8C-%EA%B5%AC%EC%A1%B0-List</guid>
            <pubDate>Fri, 29 Sep 2023 12:19:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/poor-food/post/77a5930e-4879-491d-a4cf-50813adcb87d/image.png" alt=""></p>
<p>자바를 사용하는 개발자라면 정말 자주 사용할 인터페이스 중 하나로 List 가 있습니다. 데이터를 담는 컬렉션 중 하나입니다.</p>
<p>대표적으로는 다음과 같은 메소드를 가지고 있습니다. </p>
<ul>
<li>add</li>
<li>get/set</li>
<li>indexOf</li>
<li>isEmpty/size</li>
<li>remove</li>
<li>clear</li>
</ul>
<p>실제로 무척 자주 사용하는 인터페이스이며, 개발자 기술 면접에서도 자주 언급되는 내용입니다. 오늘은 그 구현체인 Array List와 Linked List를 살펴보려고 합니다. </p>
<h2 id="arraylist">ArrayList</h2>
<p>내부를 배열로 구현한 리스트입니다. 컬렉션의 크기 (size)를 필드로 따로 관리하고, 실제 배열은 리스트의 크기와 같거나 그것보다 큽니다. 배열과 size를 필드로 가집니다. </p>
<p>이 때문에 get/set 에 유리합니다. 그러나 새 값을 추가할 때 배열을 새로 만들고 오브젝트를 모두 옮겨야 할 가능성이 있습니다.  이 때문에, 컬렉션의 전체 크기보다 더 큰 배열을 만들어 놓는 것으로 이 문제를 해결합니다. 그러나 리스트의 가장 앞에에 값을 추가하고 싶을 때는 어쩔 수 없이 새롭게 일일히 배치해야 합니다. </p>
<p>또한 배열을 사용하기 때문에, 메모리에 물리적으로 나란히 저장되는 이점이 있습니다. 컴퓨터 하드웨어는 연속된 메모리를 읽어올 때 성능에 이점이 있습니다. </p>
<ul>
<li>add (뒤에 추가) : O(1)</li>
</ul>
<p>배열의 size+1 번째 행에 오브젝트를 추가해줍니다. </p>
<ul>
<li>add (앞에 추가) : O(n)</li>
</ul>
<p>기존의 배열보다 큰 크기의 배열을 생성하고, 추가할 오브젝트를 첫번째 행에 더하고, 기존 배열의 행을 모두 옮깁니다. </p>
<ul>
<li>get : O(1)</li>
</ul>
<p>배열에서 바로 값을 호출합니다. set도 마찬가지로 O(1) 입니다. </p>
<ul>
<li>indexOf : O(n)</li>
</ul>
<p>배열을 순회하면서 equals()를 통해 비교합니다. </p>
<ul>
<li>isEmpty : O(1)</li>
</ul>
<p>필드의 size 값이 0인지를 확인합니다. </p>
<ul>
<li>remove : O(n) </li>
</ul>
<p>새 배열을 만들고, 삭제할 행을 모든 오브젝트를 옮겨오고, 삭제한 행 이후의 값들의 행번호를 하나씩 당깁니다.</p>
<p>가장 마지막 값을 삭제한다면, 가장 마지막의 오브젝트를 지우고 사이즈를 1 줄이기만 하면 됩니다. (이 경우 O(1)) </p>
<ul>
<li>clear : O(n)</li>
</ul>
<p>배열을 순회하면서 모든 값을 비우고, size를 0만 조절해주면 됩니다. </p>
<p>실제 구현체의 코드는 다음 주소에서 확인할 수 있습니다.</p>
<p>[<a href="https://developer.classpath.org/doc/java/util/ArrayList-source.html%5D">https://developer.classpath.org/doc/java/util/ArrayList-source.html]</a></p>
<h2 id="linkedlist-singly">LinkedList (Singly)</h2>
<p>내부를 노드 형태로 연결합니다. 각 노드는 다음 노드의 참조값(next)을 가지고 있습니다. 첫번째 노드(head)와 size를 필드로 가지고 있습니다. </p>
<p>각 노드들을 연결하기 위한 참조를 가지기 때문에, 메모리에서 손해를 볼 수 있습니다. </p>
<ul>
<li>add (뒤에 추가): O(n)</li>
</ul>
<p>노드를 순회하면서 마지막 노드를 찾습니다. 마지막 노드에 새 노드를 연결합니다. </p>
<ul>
<li>add (앞에 추가): O(1)</li>
</ul>
<p>새 노드를 생성합니다.  head 를 새로 만든 새 노드로 생성하고, next를 기존의 head 노드로 변경합니다. </p>
<ul>
<li>get/set : O(n)</li>
</ul>
<p>노드를 순회하면서 원하는 순서의 노드를 찾습니다. 값을 반환하거나 새 노드로 변경합니다. </p>
<ul>
<li>indexOf : O(n)</li>
</ul>
<p>노드를 순회하면서 equals로 확인합니다. </p>
<ul>
<li>isEmpty/size : O(1)</li>
</ul>
<p>size 값이 0 인지 확인하거나 반환합니다. </p>
<ul>
<li>remove : O(n)</li>
</ul>
<p>노드를 순회하면서 삭제할 노드를 찾습니다. 해당 노드를 next로 가지는 이전 노드의 next를 삭제할 노드의 next로 변경해줍니다. </p>
<p>가장 첫번째 값을 지운다면, head 노드를 기존 head 노드의 next 로 변경해주면 됩니다. (이 경우 O(1))</p>
<ul>
<li>clear : O(n)</li>
</ul>
<p>head를 null로, size 를 0으로 바꿔줍니다. 마치 O(1)처럼 보입니다. </p>
<p>그러나 주의해야할 점은, head 에서 연결이 끊어진 n개의 노드들은 언젠가 garbage collector가 일일히 청소해줘야합니다. 이 청소에 걸리는 비용은 노드의 갯수에 비례합니다. 즉 O(n)이 됩니다. </p>
<h2 id="linkedlist-doubly">LinkedList (Doubly)</h2>
<p>Singly Linked List와 동일하게 내부를 노드로 연결합니다. 단, 연결할 때 각 노드는 자신의 다음 노드(next)만이 아니라 자신의 이전 노드(prev)의 참조값도 가지고 있습니다. head, size 말고도 가장 마지막 노드인 tail 을 필드로 가지고 있습니다. </p>
<p>실제 Java의 LinkedList 는 Singly Linked List가 아닌 Doubly Linked List의 코드를 사용합니다. </p>
<ul>
<li>add (뒤에 추가): O(1)</li>
</ul>
<p>새 노드를 만들고 기존의 tail의 next에 새 노드를 연결, 새 노드의 prev에 기존의 tail을 추가합니다. 그리고 tail을 새 노드로 변경합니다. </p>
<ul>
<li>add (앞에 추가): O(1)</li>
</ul>
<p>새 노드를 만들고 기존의 head의 prev에 새 노드를 연결, 새 노드의 next에 기존의 head를 추가합니다. 그리고 head를 새 노드로 변경합니다. </p>
<ul>
<li>get/set : O(n)</li>
</ul>
<p>Singly와 동일합니다. 단, index가 head 와 tail 중 어디에 더 가까운지 알 수 있으므로, 실질적으로는 Singly 보다 더 빠르게 작동합니다. index가 size / 2 보다 크다면 tail에서부터 탐색을, 작다면 head 부터 탐색을 합니다. </p>
<ul>
<li>indexOf : O(n)</li>
</ul>
<p>Singly 와 동일합니다. 노드를 순회하면서 확인합니다. </p>
<ul>
<li>isEmpty/size : O(1)</li>
</ul>
<p>Singly와 동일합니다. size 가 0 인지 확인합니다. </p>
<ul>
<li>remove : O(n)</li>
</ul>
<p>Singly와 동일합니다. 단, 가장 첫번째와 가장 마지막 오브젝트를 제거할 때에는 O(1)로 동작합니다. 실제로 가장 첫번째 요소나 가장 마지막 요소를 삭제하는 경우가 많다면 중요합니다. </p>
<p>가장 첫번째나 마지막 오브젝트를 지우고, 그 노드와 연결된 오브젝트를 새로운 head나 tail로 변경합니다. </p>
<ul>
<li>clear : O(n)</li>
</ul>
<p>Singly 와 동일합니다. </p>
<p>실제 구현체의 코드는 다음 주소에서 확인할 수 있습니다.</p>
<p>[<a href="https://developer.classpath.org/doc/java/util/LinkedList-source.html%5D">https://developer.classpath.org/doc/java/util/LinkedList-source.html]</a></p>
<h2 id="정리">정리</h2>
<ul>
<li>ArrayList 는 get과 set에 유리합니다. 오브젝트의 입출력 자체가 중요하다면 ArrayList가 유리합니다.</li>
<li>LinkedList 는 add와 remove에 유리합니다. 리스트의 요소 추가/삭제가 빈번히 일어난다면 LinkedList가 유리합니다. </li>
<li>Linked List는 Singly와 Doubly가 있으나, Doubly가 (메모리 사용을 제외한다면) Singly에 비해 유리합니다. 자바는 기본적으로 Doubly Linked List 를 사용합니다. </li>
<li>Linked List는 ArrayList보다 종종 더 많은 메모리를 사용합니다. (공간 복잡도에 있어서 불리)</li>
</ul>
<p>유의할 점은, 이런 선택 기준은 어플리케이션의 성능이 자료 구조에 의존하는 형태가 아니라면 큰 의미가 없다는 점입니다. 이를테면, 외부 DB와의 입출력에서 발생하는 IO가 어플리케이션 실행시간의 주요원인이라면, 어플리케이션 내부의 List 선택은 큰 의미가 없을 수 있습니다. </p>
<p>또한, 성능 자체가 어플리케이션의 주요한 관심사가 아닐 수도 있습니다. </p>
<hr>
<p>앨런 B. 다우니의  Think Data Structures를 참고했습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터 생성 시간 입력하기]]></title>
            <link>https://velog.io/@poor-food/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%9D%EC%84%B1-%EC%8B%9C%EA%B0%84-%EC%9E%85%EB%A0%A5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@poor-food/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%9D%EC%84%B1-%EC%8B%9C%EA%B0%84-%EC%9E%85%EB%A0%A5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 13 Sep 2023 12:04:07 GMT</pubDate>
            <description><![CDATA[<h2 id="created_at2023-09-13t200600">&quot;CREATED_AT=2023-09-13T20:06:00&quot;</h2>
<p>많은 데이터베이스의 테이블에는 항상 해당 데이터의 생성시간을 기록한다. 가장 최신부터 조회한다던가하는 등의 목적으로 다양하게 활용이 가능하기 때문이다. PK를 내림차순으로 정렬하는 전략도 많이 쓰이지만, 모든 PK를 숫자로 사용하는 것은 아니다. UUID를 쓰는 경우도 있고, 회원의 아이디같은 경우에는 순수하게 String 을 PK 로 사용해 중복을 방지하는 경우도 있다. </p>
<p>그래서, 많은 어플리케이션이 생성시각, 그리고 더불어 수정 상황을 추적하기 위해 해당 로우의 수정시간을 기록한다. </p>
<h2 id="어디서-입력할-것인가">어디서 입력할 것인가.</h2>
<p>데이터는 입력부터 목적지인 데이터베이스까지 흘러간다. 웹 페이지에서 액트가 실행되어 자바스크립트, 하이퍼링크, 폼이 실행되고, 해당 데이터가 API 서버에 도착하면, 라우팅을 거치고 역직렬화된 뒤 비지니스 로직을 수행하고 데이터베이스에 기록된다. 이 중 한 군데에서라도 생성시간을 입력해준다면, 해당 데이터는 생성시간을 가질 수 있다.</p>
<p>보통 방법은 많을수록 문제가 된다. </p>
<h3 id="웹-페이지에서-생성한다">웹 페이지에서 생성한다.</h3>
<p>웹 페이지에서, 프론트엔드에서 생성해서 보내면 어떨까?</p>
<pre><code class="language-javascript">
function sumbit() {
  fetch(&#39;/api/common/blog/post&#39;, {
        method: &#39;POST&#39;,
        headers: {
          &#39;Content-Type&#39;: &#39;application/json&#39;
        },
        body: JSON.stringify({
          title: this.title,
          description: this.description,
          createdAt: new Date()
        })
      })
      .then(response =&gt; response.json())
      .then(data =&gt; {
        console.log(&#39;전송완료&#39;)
      })
      .catch((error) =&gt; {
        console.error(&#39;Error:&#39;, error);
      });
}
</code></pre>
<p>사실 자세히 다룰 필요도 없이 이런 전략을 취할 이유는 별로 없을 것이다. 위변조의 가능성이 있기 때문이다. 그리고, 시간 타입의 역직렬화는 지독하게 귀찮기 때문에, 하고 싶은 이유도 없을 것이다. </p>
<h3 id="비지니스-로직에서-생성한다">비지니스 로직에서 생성한다.</h3>
<pre><code class="language-java">// PostServiceImpl.java
@Service
@RequiredArgsConstructor
@Transactional
public class PostServiceImpl implements PostService {
    private final PostRepository postRepository

    @Override
    public PostResponse writePost(PostRequest request) {
        Post entity = Post.create(request);
        return postRepository.save(entity);
    }
}

// Post.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String title;

    @Column
    private String description;

    @Column
    private LocalDateTime createdAt;

    @Column
    private LocalDateTime updatedAt;

    public static Post create(PostRequest request) {
        Post entity = new Post();
        entity.title = request.getTitle;
        entity.description = request.getDscription;
        entity.createdAt = LocalDateTime.now();
        entity.updatedAt = LocalDateTime.now();
    }
}
</code></pre>
<p>DDD 기반의 비지니스 로직에서 직접 날짜를 생성해봤다. 심플하게 데이터를 입력하기 전에 넣어주는 것이다. 오류의 가능성도 적고 구현도 단순하다.</p>
<h3 id="자동화-기능을-사용한다">자동화 기능을 사용한다.</h3>
<p>ORM 기능 등에 있는등으로 생성, 수정 날짜를 감시하는 것이다. 대표적으로 JPA Auditing 이나 Spring AOP 등을 활용하는 것이다. 수레바퀴를 새롭게 개발할 필요는 없다는 이야기다.</p>
<pre><code class="language-java">// Post.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post extends BaseTimeEntity { // 추상 클래스 상속 

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String title;

    @Column
    private String description;

    public static Post create(PostRequest request) {
        Post entity = new Post();
        entity.title = request.getTitle;
        entity.description = request.getDscription;
    }
}

// BaseTimeEntity.java
@Entity
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // Auditing 추가
public class Post extends BaseTimeEntity {

    @CreatedDate
       private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}</code></pre>
<p>많이들 사용하는 예시다. 간편하고, 한번 적용해두면 상속만으로 구현이 완료되니 상당히 간편하다. </p>
<h3 id="데이터베이스를-사용한다">데이터베이스를 사용한다.</h3>
<p>어차피 데이터베이스에 입력되는 값이다.</p>
<pre><code class="language-sql">-- MYSQL 기준
    INSERT INTO post
        (
            title
            , description
            , createdAt
            , updatedAt
    VALUES
        (    
            &#39;글 제목&#39;
            , &#39;글 내용입니다.&#39;
            , NOW()
            , NOW() 
         )
</code></pre>
<p>자주 보게 되는 방식이다. 개인적으로는 이 방식은 무척 안 좋아한다. 실제 데이터의 값을 다루는 곳과 데이터가 실제로 생성되는 곳이 너무 멀기 때문이다. 이것 말고도 테이블을 정의할 때 DEFAULT 로 NOW() 를 사용하는 일도 있다. </p>
<h2 id="그래서-어떤-걸-써야하나">그래서 어떤 걸 써야하나?</h2>
<p>웹 페이지에서 넣는 것은 그냥 가능은 하다 정도로 받아들이자. </p>
<p>단, &quot;사용자의 제어권&quot;을 강하게 준다는 의미에서 아주 실용성이 없는 것은 아니다. 근무하면서 &quot;이 글의 작성시간을 3개월 전으로 바꿔주세요&quot; 라는 요청을 받는 일도 자주 있었다. 이 경우엔 생성시간/수정시간이라기보단, 사용자의 입력을 받아들이는 의미가 더 크다. 개발자에겐 &quot;날짜의 위변조&quot;가 데이터의 신뢰성을 깨는 행위일 수 있지만, 공시 의무 게시 기간을 놓친 담당자에겐 중요한 일일 수도 있다. </p>
<p>미리 말했지만 쿼리로 제어하는 방식은 무척 안 좋아한다. ORM 을 사용한다면 일부러 쿼리를 칠 이유가 없을 것이다. </p>
<p>테이블에 정의하는 것은 다소 의미가 있을 수 있다. 이것은 실제로 코드를 제어하는 것에 의미를 가진다기보단 &quot;이 테이블의 이 값이 지니는 의미&quot;를 명시하는 의미가 있기 때문이다. 하지만 명시성을 중요시한다면, 비지니스 로직에서 시간의 입력을 배제할 이유도 없을 것이다. </p>
<p>남는 것은 자동화와 비지니스 로직인데, 사실 나는 <em>비지니스 로직</em>을 사용하는 경우가 대부분의 경우 더 좋은 결과를 가져온다고 생각한다. </p>
<h3 id="데이터의-입력수정시점이-명확하게-드러난다">데이터의 입력/수정시점이 명확하게 드러난다.</h3>
<p>&#39;this.createdAt=&#39; 이나 &#39;this.updatedAt=&#39; 으로 검색하는 것으로 해당 데이터의 변경 시점을 명확히 알 수 있다. DDD로 개발할 경우에는 엔티티 클래스 하나만 확인하면 된다. </p>
<h3 id="데이터의-수정-포매팅이-간편하다">데이터의 수정, 포매팅이 간편하다.</h3>
<p>원하든 원치 않든, 데이터가 수정이 필요한 경우가 있다. 아직도 많은 데이터베이스에서 날짜를 문자열로 저장한다. 유감스러운 일이지만 항상 리팩터링이 가능한 것은 아니다. 여러 개의 서버에서 사용하는 데이터인데 서버마다 타임존이 각각 다른 경우도 있다. 이런 일을 대응하기 위해 라이브러리를 재구현하거나 혹은 어느 부분을 오버라이딩한다거나 하는 수고를 들이는 것보단, 대부분의 경우에는 한 줄 더 치는 게 낫다.</p>
<p>관리자 기능으로 &quot;데이터 수정일&quot;을 변경하고 싶을 때는? 
직접 입력한 값과 Auditing 값 중 어떤 것이 우선되는지는 나중에 확인해보자. </p>
<p>물론 어플리케이션의 규모나, 데이터를 다루는 태도에 따라서 선택 기준이 달라질 수 있다. 나라면 AI 학습을 위한 데이터를 수집할 때라면 비지니스 로직에 의해 값이 수정될 수 있는 방식으로는 구현하지 못하게 할 것이다. 그런 값들은 Object 보단 Raw 데이터에 가깝다. </p>
<h2 id="결론">결론</h2>
<p>MVC 패턴을 지키면 된다. <img src="https://velog.velcdn.com/images/poor-food/post/ccb5f3ad-00f2-4710-9800-cc36c48555b3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot Docker-compose 레시피]]></title>
            <link>https://velog.io/@poor-food/spring-boot-docker-compose-recipe</link>
            <guid>https://velog.io/@poor-food/spring-boot-docker-compose-recipe</guid>
            <pubDate>Fri, 25 Aug 2023 05:38:50 GMT</pubDate>
            <description><![CDATA[<h1 id="summary">Summary</h1>
<p>Docker를 사용하면 쉽게 환경을 구축할 수 있습니다. 로컬 테스트 환경을 구축하기 위해 매번 새로운 환경을 PC에 설치하는 대신에, Docker를 이용해서 가상 컨테이너를 사용합니다. 이 글에서는 Spring Boot 웹 어플리케이션에서 사용하는 전형적인 구성을 Docker-compose를 통해서 구성하는 법에 대해서 다룹니다. </p>
<p>이 글에 사용된 설정을 이용하면 Spring Boot + PostgreSQL + Redis 환경을 구축할 수 있습니다. <img src="https://velog.velcdn.com/images/poor-food/post/fe56b987-79c6-4ef0-9fdb-f25e57b4af10/image.png" alt=""></p>
<h1 id="issue">Issue</h1>
<p>Spring Boot로 만든 웹 어플리케이션을 공유하려고 합니다. 그런데, Spring Boot 는 무척 배포하기 쉬운 것이 특징인데도 다른 사람이 해당 프로젝트를 실행하는 것은 쉽지 않습니다. </p>
<p>대부분의 경우, 데이터베이스를 같이 구성해야 하기 때문입니다. </p>
<p>임베디드 H2 Database 를 같이 사용할 수 있습니다. 하지만 데이터베이스 환경이 완벽히 일치하는 것은 아니고, Database에 특화된 기능을 사용할 경우 호환성을 장담하기 어렵습니다. </p>
<h1 id="solution">Solution</h1>
<p>Docker를 이용해서 환경을 구성합니다. </p>
<p>다음이 설치 되어있어야 합니다.</p>
<ul>
<li>Docker</li>
<li>Docker-Compose</li>
</ul>
<p>Docker의 설치에 대해서는 다루지 않습니다. 다음 문서를 참고하세요.</p>
<p><a href="https://docs.docker.com/engine/install/ubuntu/">도커 공식 문서</a></p>
<ol>
<li>Dockerfile 을 작성합니다.</li>
</ol>
<p>편의상, 프로젝트 루트에 작성합니다. </p>
<pre><code class="language-shell">touch ./Dockerfile
vi Dockerfile</code></pre>
<pre><code class="language-dockerfile">#Dockerfile
# 사용하고자 하는 도커 이미지를 적습니다. jdk17을 사용하기 위해 amazoncorretto:17을 사용합니다.
FROM amazoncorretto:17
WORKDIR /app
COPY . /app
RUN chmod +x ./gradlew
# gradlew의 권한 문제를 해결하기 위해 chmod를 진행합니다. 
RUN ./gradlew build -x test
# 테스트에 필요한 Database가 없으므로 테스트를 제외합니다. 
# 테스트를 포함하고 싶다면, Spring 프로필을 통해서 테스트 전용 환경을 추가합니다. 
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;-Dspring.profiles.active=prod&quot;, &quot;build/libs/app.jar&quot;]
# 실행시킬 때 프로필을 명시할 수 있습니다. </code></pre>
<ol start="2">
<li>docker-compose.yml 파일을 작성합니다. </li>
</ol>
<pre><code class="language-shell">touch ./docker-compose.yml
vi docker-compose.yml</code></pre>
<pre><code class="language-yaml"># docker-compose의 버전을 명시합니다. 
version: &#39;3&#39;
services:
# 필요한 콘테이너를 원하는만큼 구성합니다. 
# 여기서는 Database를 위한 PostgreSQL과 캐시용 Redis를 구성하기로 합니다. 

# PostgreSQL
  db:
    image: &quot;postgres:latest&quot;
    restart: unless-stopped
    # 재시작 옵션입니다. 
    hostname: &quot;db&quot;
    # 호스트네임을 명시하지 않으면 콘테이너명과 동일한 값을 지닙니다. 
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: dontpostyourpasswordonyourblog
      # 환경변수를 사용할 수 있습니다.
    ports:
      - &quot;5432:5432&quot;
      # 개방할 포트를 설정해줍니다. 

# Redis
  redis:
    image: &quot;redis:latest&quot;
    hostname: &quot;redis&quot;
    volumes:
      - ./redis/data:/data
      - ./redis/redis.conf:/usr/local/etc/redis/redis.conf
      # 원한다면 파일을 컨테이너 외부와 연결할 수 있습니다. 
    restart: unless-stopped
    ports:
      - &quot;6379:6379&quot;

# Spring Boot
  app:
    build: .
    # 같은 경로에 있는 Dockerfile을 이용해 빌드합니다. 
    ports:
      - &quot;8080:8080&quot;
    depends_on:
      - db
      - redis
      # 다른 컨테이너에 의존 설정을 해두면, 위의 컨테이너가 실행된 이후에 실행됩니다. 
</code></pre>
<ol start="3">
<li>데이터베이스 연결 설정을 해줍니다.</li>
</ol>
<p>다른 컨테이너의 IP 주소를 알지 않아도, docker-compose의 설정에 의해 호스트네임을 얻을 수 있습니다. </p>
<pre><code class="language-yaml"># datasource 
spring:
    datasource:
        url: jdbc:postgresql://db:5432/postgres
        # docker-compose 에서 설정한 hostname을 사용할 수 있습니다. 
        # (미설정시 container name과 동일) 
        username: postegres
        password: dontpostyourpasswordonyourblog
        driver-cname-name: org.postgresql.Driver

# redis (docker container)

spring:
    data:
        redis:
            host: redis
            # 마찬가지로 hostname 을 사용합니다. 
            port: 6379
</code></pre>
<ol start="4">
<li>docker-compose를 통해 컨테이너를 빌드하고 실행합니다.</li>
</ol>
<pre><code class="language-shell">docker-compose build # 빌드를 진행합니다.
docker-compose up # 컨테이너를 실행합니다. 
# CTRL+C로 종료할 수 있습니다. </code></pre>
<ol start="5">
<li>필요한다면 컨테이너를 내립니다. </li>
</ol>
<pre><code class="language-shell">docker-compose down # 컨테이너를 제거합니다. </code></pre>
<ol start="6">
<li>다시 빌드하고 새로 실행할 경우 --build 옵션을 사용할 수 있습니다. (코드 수정 등에 사용)<pre><code class="language-shell">docker-compose up --build
</code></pre>
</li>
</ol>
<h1 id="아니면-그냥-직접-해도-됩니다">아니면 그냥 직접 해도 됩니다.</h1>
<p>docker-compose build &amp;&amp; 
docker-compose down</p>
<pre><code>
# Result

이제 복잡한 구성의 어플리케이션도 쉽게 공유할 수 있습니다. 클라우드 환경을 사용할 경우, 도커로 DB를 구성할 일은 없겠지만, 로컬 테스트 환경 구축에 편리하겠습니다. 

다른 스택을 사용하는 개발자 등에게 개발 환경을 공유해야 할 일이 있다면 이 방법을 사용해보는 것도 좋겠습니다. 
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[./gradlew: /bin/sh^M: bad interpreter]]></title>
            <link>https://velog.io/@poor-food/bad-interpreter-bad-lines</link>
            <guid>https://velog.io/@poor-food/bad-interpreter-bad-lines</guid>
            <pubDate>Fri, 25 Aug 2023 05:00:14 GMT</pubDate>
            <description><![CDATA[<h1 id="summary">Summary</h1>
<p>어떤 스크립트는 OS에 따라서 실행성이 변경되기도 합니다. 이것은 CRLF/LF 개행 문자 차이로 인한 것입니다. Gradle 에서 다음과 같은 오류가 발생했다면 개행 문자의 차이로 인한 것일 수도 있습니다. 그런 차이를 우리는 평소에는 인지하지 못하고 있을 수 있습니다. 개행 문자는 화면에 표시되지 않기 때문입니다. <img src="https://velog.velcdn.com/images/poor-food/post/c4a093a9-95a8-4e74-a2df-5a95896f9fff/image.png" alt=""></p>
<p>자세한 내용은 아래에 작성합니다. </p>
<h1 id="situation">Situation</h1>
<p>이번에 진행하는 프로젝트는 개발자의 개발 환경이나, 배포가 진행될 서버 이외의 다양한 환경에서 작동할 필요가 있었습니다. 개발 PC에서 Docker-compose 환경을 통해 Spring Boot + PostgreSQL + Redis 를 포함해 Docker 실행이 정상적으로 진행되는 것을 확인하고, 테스트 삼아 개발 PC 이외에도 게이밍 PC, 개인 보유 리눅스 서버 등에서 테스트 배포를 시작했습니다. 문제는 게이밍 PC에서 테스트할 때 발생했습니다. </p>
<h1 id="issue">Issue</h1>
<p>개발 PC에서 정상적으로 작동하던 docker-compose build 에서 문제가 발생했습니다. 커맨드 자체는 도커를 실행한 것이나, 오류 메시지를 확인하면 도커에서 실행한 Gradle Wrapper 에서 오류가 발생한 것입니다. </p>
<blockquote>
<p>Gradle bin/sh: ./gradlew: /bin/sh^M: bad interpreter </p>
</blockquote>
<p>gradlew 파일은 Spring Initializr가 자동으로 생성해준 것입니다. </p>
<h1 id="cause">Cause</h1>
<p>원인은 OS별 개행 문자 차이에 의한 것입니다. Windows OS에서는 CRLF 형식의 개행 문자를 사용하고, Unix 기반 OS(리눅스, MacOS) 에서는 LF를 개행 문자로 사용합니다. </p>
<p>OS 별로 사용하는 개행 문자가 다르다보니 소스코드를 관리하는 Git 역시 이를 관리하는 정책이 복잡한데, 기본적으로는 OS에 맞춰서 자동으로 관리가 됩니다. 즉, 사용자가 Windows OS라면 CRLF 로 코드를 불러오고, Unix 기반의 OS에서는 LF 로 불러오게 됩니다. 사용자는 원한다면 위와 같은 방식에서 다른 정책으로 변경이 가능하지만, 기본값은 &#39;OS에 맞춰서 자동 변환&#39; 이고 제 git 설정 또한 기본값으로 설정 되어있습니다. </p>
<p>제 개발용 PC는 Mac을 사용하고 있으므로 최초에는 LF 상태로 gradlew 가 작성되었을 것입니다. OS에 맞는 스크립트이므로 문제 없이 작동합니다.</p>
<p>제 Windows PC에서 원격 저장소를 git clone 했을 때, LF의 소스코드들은 CRLF 로 변환되었을 것입니다. 따라서 스크립트들은 문제 없이 작동합니다. </p>
<p>Dockerfile 에 맞게 docker-compose build를 실행했을 때, ./gradlew 은 Docker 컨테이너 내부에서 실행됩니다. 사용한 image는 amazoncorretto:17 이고, 이 이미지는 linux 베이스의 이미지입니다. </p>
<p>즉, Windows 에서 정상적으로 실행되던 gradlew 파일을 그대로 docker 내부에서 사용하면 스크립트는 정상적으로 실행되지 않습니다. </p>
<h1 id="solution">Solution</h1>
<p>사용하고 있는 IDE인 IntelliJ에는 개행문자를 변경할 수 있는 기능이 있습니다. 이를 활용해 LF 로 전환을 해주자 문제 없이 작동했습니다.</p>
<p>서버 상에서 문제를 해결해야 할 경우에는 다음과 같은 방식을 사용합니다.</p>
<ol>
<li>vi 로 해당 파일을 연다.</li>
</ol>
<pre><code class="language-shell">vi gradlew</code></pre>
<ol start="2">
<li>명령 모드로 다음 커맨드를 실행한다. </li>
</ol>
<pre><code>:set fileformat=unix</code></pre><ol start="3">
<li>저장한다. </li>
</ol>
<pre><code>!wq</code></pre><h1 id="result">Result</h1>
<p>gradlew 빌드가 정상적으로 실행되는 것을 확인할 수 있습니다. </p>
<hr>
<h1 id="reference">Reference</h1>
<p>[Stackoverflow : /bin/sh^M: bad interpreter: No such file or directory]
(<a href="https://stackoverflow.com/questions/23097528/bin-shm-bad-interpreter-no-such-file-or-directory">https://stackoverflow.com/questions/23097528/bin-shm-bad-interpreter-no-such-file-or-directory</a>)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 와 DRF 같이 쓰기. ]]></title>
            <link>https://velog.io/@poor-food/Django-DRF-%EA%B0%99%EC%9D%B4-%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@poor-food/Django-DRF-%EA%B0%99%EC%9D%B4-%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Tue, 15 Aug 2023 12:20:30 GMT</pubDate>
            <description><![CDATA[<h1 id="situation">Situation</h1>
<p>django 웹 어플리케이션을 구성할 때, API 를 쉽게 개발하기 위해서 DRF(Django RESTful Framework)를 사용합니다. django + DRF 를 백엔드 API 서버로만 사용하기 위해 단독 구성할 수도 있지만, django 는 기본적으로 풀스택 프레임워크입니다. </p>
<p>웹 페이지를 렌더링하기 위한 일반적인 뷰와, API 를 개발하기 위한 API 뷰를 같이 개발할 경우에 대해서 생각해봅시다. </p>
<h1 id="problem">Problem</h1>
<p>Django 에는 app 이라는 단위로, 기능을 분리해서 구성하게 됩니다.. Domain Driven Development과 비슷한 맥락을 갖고 있습니다. 가독성과 유지보수성 측면에도 강점이 있고, 이 app 만 따로 배포해서 사용하는 것도 가능합니다. </p>
<p>다음과 같은 사례를 생각해봅시다. </p>
<pre><code class="language-shell">/mysite/ # root app
/app-a/
/app-b/
manage.py 
... </code></pre>
<pre><code class="language-python"># /mysite/urls.py
urlpatterns = [
    path(&#39;app-a/&#39;, include(&#39;api-a&#39;, include(&#39;app-a.urls&#39;)),
    path(&#39;app-b/&#39;, include(&#39;api-b&#39;, include(&#39;app-b.urls&#39;)),
    ...
]</code></pre>
<p>api 를 app 들 내부에 작성할 경우, url pattern는 다음과 같을 것입니다. </p>
<pre><code class="language-json">[    
      &lt;!-- 일반적인 웹 페이지들 --&gt; 
      &quot;app-a-index&quot; : &quot;/app-a/&quot;, 
      &quot;app-b-index&quot; : &quot;/app-b/&quot;,

      &lt;!-- DRF는 Browsable API 를 제공한다. --&gt; 
    &quot;app-a-api-root&quot; : &quot;/app-a/api/&quot;,
      &quot;app-a-api-some-api&quot; : &quot;/app-a/api/some-api/&quot;,
      &quot;app-b-api-root&quot; : &quot;/app-b/api/&quot;,
    &quot;app-b-api-some-api&quot; : &quot;/app-b/api/some-api/&quot;,
]</code></pre>
<p>이제 api-root 들로 접근하기 위한 api-root 를 추가해주겠습니다. DRF가 browsable api 를 제공하니만큼, api 를 탐색할 수 있도록 만들어주는 것입니다. <a href="https://velog.io/@poor-food/Django-Rest-Framework-API-Root">이 글에서 다뤘습니다. </a></p>
<pre><code class="language-json">[    
      &quot;app-a-index&quot; : &quot;/app-a/&quot;, 
      &quot;app-b-index&quot; : &quot;/app-b/&quot;,

      &lt;!-- api root 는 app-a, app-b 어느 쪽에도 속하지 않기 때문에 mysite 에 직접 작성할 수 밖에 없다. --&gt; 
      &quot;api-root&quot; : &quot;/api/&quot;,
    &quot;app-a-api-root&quot; : &quot;/app-a/api/&quot;,
      &quot;app-a-api-some-api&quot; : &quot;/app-a/api/some-api/&quot;,
      &quot;app-b-api-root&quot; : &quot;/app-b/api/&quot;,
    &quot;app-b-api-some-api&quot; : &quot;/app-b/api/some-api/&quot;,
]</code></pre>
<p>api root 가 app-a-api-root 와 app-b-api-root의 상위 리소스라는 느낌을 주는데에도 불구하고, url 패턴에서는 일관성이 떨어집니다. 
실제로 browsable api 에서 표시되는 내용을 살펴보겠습니다. </p>
<p><img src="https://velog.velcdn.com/images/poor-food/post/b5cf1b68-1661-4e96-a681-00e7acc52433/image.png" alt=""></p>
<p>/api/ 로 들어가 api 목록을 받았는데, 하위의 항목들이 /api/ 로 시작하지 않고 완전히 다른 경로를 주고 있습니다. </p>
<p>만약, api 라는 별개 app 을 만든 뒤, 그 app 에 api 를 전부 구성하게 되면 위와 같은 문제는 사라지지만, 이번엔 도메인과 api 가 멀어지는 문제가 발생합니다. app-a,b 에 사용되는 api 를 api 라는 별개 앱에 개발하게 되면 app-a, b와 api 는 서로 의존하게 됩니다. (app-a, b가 렌더링하는 템플릿 페이지들이 api의 api를 사용할 것이기 때문에) </p>
<p>심지어 api는 app-a, app-b 양쪽에 의존하기 떄문에, app-a나 app-b 한 쪽만 사용하고 싶어도, 세 개의 앱을 전부 설치 해야 합니다. 좋은 구조라고 보긴 어려울 것 같습니다. </p>
<h1 id="solution">Solution</h1>
<p>다음과 같은 방법을 생각해봤습니다. </p>
<ol>
<li>api 는 app 내부에 개발한다.</li>
<li>api 의 urls 를 urls.py 내부에 개발하지 않고 분리한다. (urls_api.py)</li>
<li>mysite.urls(root app) 에서 urls_api.py 를 따로 추가한다. </li>
</ol>
<pre><code class="language-python"># /mysite/urls.py

url_api_patterns = [
    path(&#39;app-a/&#39;, include(&#39;app-a.urls_api&#39;)),
    path(&#39;app-b/&#39;, include(&#39;app-b.urls_api&#39;)),
]

urlpatterns = [
    path(&#39;app-a/&#39;, include(&#39;app-a.urls&#39;)),
    path(&#39;app-b/&#39;, include(&#39;app-b.urls&#39;)),

    # API views
    path(&#39;api/&#39;, views.api_root, name=&#39;api-root&#39;),
    path(&#39;api/&#39;, include(url_api_patterns)),
]</code></pre>
<p>이제, api url 구조를 안정적으로 유지하면서 도메인과 api 간의 거리도 정상적으로 유지할 수 있습니다. </p>
<hr>
<p>django , DRF 를 같이 사용할 경우 어떤 식으로 URL 을 구성하는 것이 좋을지 고민해봤습니다. 컨벤션이나 베스트 프랙티스는 찾지 못했으나, app 내부에 같이 개발하거나, api app을 따로 개발하는 것이 일반적으로 보입니다. monolithic 한 어플리케이션의 경우에는 위의 형태가 괜찮아보입니다.</p>
<p>browsable api 가 없는 환경에서는 고민한 적이 없었던 문제입니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python과 Pillow로 이미지 변환하기.]]></title>
            <link>https://velog.io/@poor-food/Python%EA%B3%BC-Pillow%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@poor-food/Python%EA%B3%BC-Pillow%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 07 Aug 2023 08:37:05 GMT</pubDate>
            <description><![CDATA[<h1 id="problem">Problem</h1>
<p>이미지 파일의 용량이 너무 커서 줄이고 싶을 때가 있습니다.</p>
<p>인터넷 상의 많은 데이터는 이미지가 차지하고 있습니다. 또한, 최근 스마트폰의 놀라운 화질 역시, 해당 고화질을 표현하기 위해서 데이터 용량을 아낌없이 사용하고 있습니다. 그러나 우리가 가지고 있는 저장소의 용량은 한정되어있기 때문에, 경우에 따라 이것을 효율적으로 사용할 방법이 필요합니다.</p>
<p>저의 경우에는, 구글 포토 등의 대용량 사진 데이터를 NAS로 백업하기 위해 이와 같은 방법을 사용하기로 합니다.</p>
<h1 id="solution">Solution</h1>
<p>Python과 파이썬의 이미지 처리 라이브러리인 Pillow를 사용합니다. </p>
<h2 id="pillow-설치">Pillow 설치</h2>
<pre><code class="language-shell">pip install pillow</code></pre>
<pre><code class="language-python">def compress_image(file_path: str, new_file_path: str, max_resolution: int = 1920, quality: int = 90) -&gt; bool:
    &quot;&quot;&quot;
    file_path: 기존 파일의 경로
    new_file_path: 저장될 파일의 경로
    max_resolution: 저장될 파일의 최대 해상도
    quality: 저장될 파일의 화질
    &quot;&quot;&quot;
    with Image.open(file_path) as img:
        # 예외 처리: 이미지 파일이 아닌 경우에는 False 를 반환합니다.  
        if img.format != &#39;JPEG&#39; and img.format != &#39;PNG&#39;:
            return False
        # 예외 처리: 파일이 존재하지 않을 경우에는 False를 반환합니다. 
        if img is None:
            return False

        # 이미지의 용량은 이미지의 해상도에 큰 영향을 받습니다.
        # 해상도가 높을 경우, 해상도를 낮추는 처리를 합니다.
        if img.height &gt; max_resolution or img.width &gt; max_resolution:
            # pillow 에는 resize 메소드도 있지만, thumbnail 메소드를 이용하면
            # 이미지의 비율을 유지한 상태로 크기를 낮춰 줍니다.

            # 아래의 경우에는 가로나 세로의 길이가 1920보다 클 경우 그에 맞춰서
            # 크기를 낮춰 줍니다. (올리진 않습니다.)

            # argument로 해상도를 받는데, Tuple을 받고 있는 것에 유의.
            # (괄호가 2개(()) ) 
            img.thumbnail((max_resolution, max_resolution))

        # image의 save 메소드는 새 파일 경로을 받고, 
        # quality 등의 추가 파라미터를 명시할 수 있습니다.
        # 아래에서는 webp라고 하는 포맷을 이용해 고화질로 저장하도록 합니다.

        # WebP는 구글에서 개발한 이미지 포맷으로, 낮은 용량으로 고화질 이미지를 저장할 수 있지만
        # 브라우저 호환과 소프트웨어 호환성이 떨어집니다. 

        # 최신 버전 기준으로는 Internet Exploer 이외의 브라우저는 모두 지원하고 있습니다.
        # source : https://caniuse.com/webp

        # 호환성이 더 중요한 환경이라면, &#39;jpeg&#39;를 사용하세요. 
        img.save(new_file_path, &#39;webp&#39;, quality=quality)
        return True
</code></pre>
<p>Pillow 개발문서: <a href="https://pillow.readthedocs.io/en/latest/reference/Image.html#PIL.Image.Image.thumbnail">https://pillow.readthedocs.io/en/latest/reference/Image.html#PIL.Image.Image.thumbnail</a></p>
<h1 id="result">Result</h1>
<p>위의 함수를 이용해서, 원하는 이미지 파일을 해상도와 화질, 포맷으로 전환할 수 있습니다.<br>이것을 사용한 간단한 예시 프로젝트를 첨부합니다.</p>
<p><a href="https://github.com/pjc1991/py-image-storage">https://github.com/pjc1991/py-image-storage</a></p>
<p> (./image_handler.py)<img src="https://velog.velcdn.com/images/poor-food/post/359db1fb-9152-4772-9acf-0edd8898a8fe/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[.env for GitHub Actions]]></title>
            <link>https://velog.io/@poor-food/%EC%9E%90%EB%8F%99%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%9E%90%EB%8F%99%ED%99%94-.env-for-GitHub-Actions</link>
            <guid>https://velog.io/@poor-food/%EC%9E%90%EB%8F%99%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%9E%90%EB%8F%99%ED%99%94-.env-for-GitHub-Actions</guid>
            <pubDate>Thu, 03 Aug 2023 23:59:28 GMT</pubDate>
            <description><![CDATA[<h1 id="secret-은-커밋하지-마세요">Secret 은 커밋하지 마세요.</h1>
<p>코드 레포지토리에는 인증정보(Secret)을 커밋해선 안됩니다. 심지어 프라이빗 레포지토리도 안됩니다. </p>
<p>깃허브는 프라이빗 저장소를 포함해서 해킹 당한 적이 있습니다. 해당 사건 이후 몇몇 사용자가 GitLab을 비롯한 자체 호스팅 레포지토리를 사용하기 시작한 것으로 압니다. </p>
<p>이 문제에 대응하기 위한 올바른 해결책 중 하나는 .env를 사용하는 것입니다. .env 파일에 시크릿을 저장해놓고, dotenv 라이브러리가 이 시크릿을 환경변수로 적용시켜줍니다. </p>
<p>로컬 구동시에는 파일을 직접 전달 받고, 인스턴스에 배포할 때는 해당 인스턴스에 직접 업로드 해놓으면 해결이 가능하지만, 매번 .env 파일에 변경이 있을 때마다 직접 갱신을 해줘야 합니다.</p>
<p>문제는 GitHub Actions를 이용해서 자동 배포할 때에 발생합니다. 서비스 가동을 위해 Secrets이 필요한데, 코드 레포지토리는 이 값들을 가지고 있지 않습니다. 이를 해결하기 위해서 Github Secrets가 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/poor-food/post/57185d9b-2d18-442a-bcbd-1cf6f09e2e33/image.png" alt=""></p>
<h1 id="github-secrets">GitHub Secrets</h1>
<p>repository &gt; Settings &gt; Security &gt; Secrets and Variables &gt; New repository secret </p>
<p>GitHub Secrets 에 원하는 환경변수를 저장해놓으면, GitHub Actions 에서 명령어를 실행할 때 {{ secrets.secret_name }} 의 형태로 가져와서 사용할 수 있습니다. AWS 도 실행이 가능하고, 환경변수도 저장이 가능합니다.</p>
<p>단, 아직 문제가 존재합니다. </p>
<h2 id="env">.env</h2>
<p>.env 파일에 갱신이 발생하는 경우가 잦습니다. 작은 수정이라면 괜찮지만, 갯수가 많으면 상당히 번거로워집니다. </p>
<p>&quot;커밋 하나로 배포가 완료되도록&quot; 구성한 것이 GitHub Actions 인데, 이 GitHub Actions 를 가동하기 위해서 일이 늘어나게 됩니다.</p>
<h2 id="github-actions-workflow">GitHub Actions Workflow</h2>
<p>.env 파일에 수정이 발생할 경우 수정해줘야 하는 곳은 3군데나 됩니다. </p>
<ul>
<li>.env 파일의 갱신</li>
<li>GitHub Secrets 의 갱신. </li>
<li>Github Workflow의 설정 yml 파일 갱신. </li>
</ul>
<p>이러고 나서야 GitHub Actions로 배포 자동화가 가능합니다. 
간단한 스크립트를 사용하여 자동화하도록 합시다. </p>
<h1 id="python-create-env-github-action">python-create-env-github-action</h1>
<p><a href="https://github.com/pjc1991/python-create-env-github-action">https://github.com/pjc1991/python-create-env-github-action</a></p>
<pre><code class="language-shell">python create_env.py</code></pre>
<p>위의 스크립트는 다음의 절차를 걸쳐 문제를 해결합니다. </p>
<ol>
<li>.env 에서 파일내에 존재하는 모든 변수의 이름을 수집합니다. </li>
<li>1의 내용을 기반으로 &quot;echo {변수명}=%{{ secrets.변수명 }} &gt; .env \n&quot; x n 의 문자열을 생성합니다.</li>
<li>.env 의 모든 변수를 불러와서 GitHub API를 통해 GitHub Secrets 에 갱신해줍니다. </li>
<li>Workflow 파일을 yaml로 불러와서, 특정한 작업명(&quot;CREATE_DOT_ENV_FILE&quot;)을 찾습니다.</li>
<li>해당 작업의 RUN 항목에 2에서 생성한 문자열을 넣어줍니다. </li>
<li>파일 실행 한 번으로 github secrets 갱신과, workflow 파일 갱신이 끝났습니다.</li>
<li>.env 파일에 변화가 생길 때마다, 새롭게 create_env.py 를 실행해주면 됩니다. </li>
</ol>
<hr>
<p>어플리케이션을 개발하는 것이 아니라 스크립트를 짠다는 느낌으로 개발했습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[홈 서버 갖고 놀기 - 하드웨어]]></title>
            <link>https://velog.io/@poor-food/%ED%99%88%EC%84%9C%EB%B2%84%EA%B0%96%EA%B3%A0%EB%86%80%EA%B8%B0-%ED%95%98%EB%93%9C%EC%9B%A8%EC%96%B4</link>
            <guid>https://velog.io/@poor-food/%ED%99%88%EC%84%9C%EB%B2%84%EA%B0%96%EA%B3%A0%EB%86%80%EA%B8%B0-%ED%95%98%EB%93%9C%EC%9B%A8%EC%96%B4</guid>
            <pubDate>Thu, 03 Aug 2023 20:45:16 GMT</pubDate>
            <description><![CDATA[<h1 id="홈-서버-만들기">홈 서버 만들기</h1>
<p><img src="https://velog.velcdn.com/images/poor-food/post/e9937e63-b690-438d-8777-1547b45ba91d/image.png" alt=""></p>
<p>이 글은 SBC (싱글 보드 컴퓨터)를 이용해서 NAS와, 개발 환경 구성이 가능한 홈 서버를 구성하는 방법에 대해서 다루며, 이는 실용적이기보다는 취미에 가깝습니다. </p>
<p>이 글에서 사용하는 기기는 ODROID N2+ 입니다. </p>
<h2 id="만드는-이유">만드는 이유</h2>
<h3 id="구글-포토">구글 포토</h3>
<p><img src="https://velog.velcdn.com/images/whdekf/post/e61b73b5-7ef4-4217-95a9-5f00122fbd12/image.png" alt=""></p>
<p>구글 포토가 고화질 용량 무제한 서비스를 중단했습니다.</p>
<p>구글 포토는 최대 15GB 의 사진 저장 용량을 제공하며(이 제공량은 구글 드라이브의 용량과 공유된다), 만약 화질을 &#39;원본&#39; 화질이 아니라 &#39;고화질&#39;로 설정할 경우, 저장 용량에 포함되지 않고 무한정으로 업로드가 가능했습니다. 그리고 그 무한정 서비스가 중단되었습니다. </p>
<p><img src="https://velog.velcdn.com/images/whdekf/post/70fd1479-86cb-4417-b4f7-a3ead79e2561/image.png" alt=""></p>
<p>그래서 저는 간단하게 파일을 옮기거나, 아주 중요한 파일들을 저장하는 용도로  클라우드 저장소의 용도를 되돌리고, 장기 보관하는 대용량 파일들 (사진, 기타 자료들)을 다른 곳에 저장하기로 합니다.</p>
<h3 id="대안">대안</h3>
<p>대용량의 파일을 보관하는 방법은 여러 가지가 있습니다.</p>
<ol>
<li>외장 저장 매체 (하드 디스크 등)를 사용한다.</li>
<li>대용량 클라우드 스토리지 서비스를 유료로 사용한다.</li>
<li>AWS 등의 클라우드 웹 서비스를 사용한다.</li>
</ol>
<p>다만, 제 취향의 문제로 위의 선택지들은 택하지 않기로 합니다. </p>
<h3 id="좀-더-합리적인-대안">좀 더 합리적인 대안</h3>
<p>개인 서버에 파일을 저장하는 것에는 RAID 구성을 하지 않는다면 안정성에 문제가 있습니다. 다음의 선택지도 추가로 추천 드립니다. </p>
<ul>
<li>15기가의 구글 포토 용량을 다 쓰신 뒤에, 새 구글 계정을 만드세요.</li>
<li>구글 계정은 전화 번호 하나당 4개까지 만들 수 있다고 합니다.</li>
<li>만약 4개의 구글 계정(60GB)를 전부 소진했다면 새로운 전화번호가 있다면 또 구글 계정을 만들 수 있습니다. </li>
<li>드랍박스나 N클라우드 등의 별개 클라우드 서비스의 무료 플랜을 활용합니다.</li>
<li>드랍박스는 구글 계정 연동을 지원합니다. 구글 계정 하나당 2기가씩 무료 플랜을 더 사용할 수 있습니다. </li>
<li>구글 드라이브에 유료 플랜을 결제합니다. 1년에 12만원 정도 지불하면 2테라 바이트의 구글 드라이브 저장공간을 사용할 수 있습니다. </li>
</ul>
<p>역시나 취향적인 문제로 위의 선택지는 택하지 않기로 합니다. </p>
<h2 id="목표">목표</h2>
<ul>
<li>NAS 를 구성해서, 홈 네트워크에서 접근이 가능하게 만들기</li>
<li>개발용 소프트웨어 가동이 가능하도록 만들기</li>
<li>가능하면 이 모든 것을 저렴한 가격으로.</li>
</ul>
<h1 id="하드웨어-준비">하드웨어 준비</h1>
<h2 id="서버-pc">서버 PC</h2>
<h3 id="tldr">TL;DR</h3>
<p>OROID N2+를 사용합니다. </p>
<h3 id="nas-제품들">NAS 제품들</h3>
<p>기성 NAS는 가격이 상당하고, 어플리케이션 구동에 필요한 성능을 제공하지 않으므로 선택지에서 제외합니다.</p>
<h3 id="데스크탑-pc-랩탑">데스크탑 PC, 랩탑</h3>
<p>남는 PC 부품을 이용해서 구성할 수 있지만 전력 소모량과 발열, 크기 문제로 선택지에서 제외 합니다. </p>
<h3 id="sbc-싱글-보드-컴퓨터">SBC (싱글 보드 컴퓨터)</h3>
<p>라즈베리파이의 등장 이후로, 유사한 폼팩터의 제품이 많이 출시 되었습니다. 이것들을 통틀어 싱글 보드 컴퓨터라고 합니다. 소형에, 저전력, 저발열, 고성능이고 다양한 I/O를 지원해 인기를 끌었습니다.</p>
<p>다음은 염두에 둔 선택지들입니다. </p>
<ul>
<li><p>라즈베리파이4B는 저성능은 아니지만, 다른 제품에 비해 부족한 편이었고, 부팅용 스토리지로 SD카드를 쓰거나 USB 외장 SSD/HDD를 사용해야 했습니다. SD카드는 수명이, USB 외장은 연결 안정성이 걱정되었습니다. </p>
</li>
<li><p>ODROID N2+는 성능이 무척 뛰어났고, 부팅용 스토리지로 SD카드 말고도 eMMC를 지원했습니다. 수명면에서도 안정성 면에서도 괜찮아보였습니다. 외장 스토리지 역시 사용 가능하고요.</p>
</li>
<li><p>M1은 성능은 라즈베리파이4B와 N2+ 사이로 보입니다. 인공신경망 전용 유닛이 있어서 기계학습에 강하고, SD카드, eMMC, USB 말고도 SATA와 M2 각각 한 슬롯씩 지원했습니다. 락칩이라고 하는 독특한 CPU를 사용하는데, 초기 단계라 문제를 일으키는 경우가 다소 있다고 합니다. </p>
</li>
<li><p>HC4는 NAS 사용을 전제로 만들어져, 연산 성능이 뛰어나지 않다고 합니다. 단, SATA 슬롯이 두개나 있어 안정적으로 연결이 가능합니다. </p>
</li>
</ul>
<p>사용 목적을 고려하여, ODROID N2+ 를 사용하기로 결정합니다. </p>
<p>NAS 목적에 좀 더 가깝다면 SATA 슬롯이 많은 HC4가, 지원 소프트웨어나 넓은 유저층을 생각하면 라즈베리파이4B가, 기계학습용 머신이라면 M1이 좀 더 괜찮아 보입니다. </p>
<h2 id="보조-부품">보조 부품</h2>
<p>대부분의 부품은 ODROID 제조사인 하드커널에서 같이 팔고 있습니다. </p>
<ul>
<li>eMMC 모듈 256GB : 추후 업그레이드가 곤란하므로 최고 사양으로 결정합니다. </li>
<li>eMMC Module Writer : 기본 탑재 OS 이외의 OS를 사용하고 싶다면 필요합니다. </li>
<li>파워 어댑터 12V 5A : 공식 홈페이지에서 12V 2A를 같이 판매하지만, 넉넉한 것으로 따로 구매했습니다. </li>
<li>N2 공식 케이스 :  상품이 담겨 있는 박스의 크기가 적당해서, 이것을 가공해서 케이스처럼 써도 적당할 것 같다고 생각이 듭니다.</li>
<li>공식 쿨링 팬 : 가격이 비싸지 않으니 구입했습니다.</li>
<li>이더넷 케이블 : 케이블메이트의 제품이 왔습니다. 연결 부위에 있는 고무 손잡이(체결 해제 시 누르기 쉽게 하는 용도의)가 공식 케이스에 살짝 간섭이 있지만 큰 문제는 없습니다. 이더넷 케이블은 따로 사는 게 나을 것 같습니다.</li>
<li>와이파이 모듈 : 혹시나 해서 구매했으나, 크게 필요는 없었습니다. </li>
<li>동전 배터리(CR2032) : RTC 유지에 사용됩니다. </li>
<li>USB3.0-SATA 인터페이스 : NEXT-318U3를 구입했습니다.</li>
<li>3.5인치 NAS용 HDD : NAS 구성에 사용합니다.</li>
</ul>
<p>참고로, HDD는 해외직구가 훨씬 저렴합니다.</p>
<p><img src="https://velog.velcdn.com/images/whdekf/post/9bf5289e-f1a9-442a-98ab-e48aab4b710b/image.png" alt=""></p>
<p>사진의 16테라 제품은 Renewed 제품입니다. 새 거는 아닙니다.</p>
<h2 id="기타-부속품">기타 부속품</h2>
<p>키보드와, 모니터는 가지고 있는 것을 사용합니다. 최초 연결시에만 사용하고, SSH 연결 이후에는 사용하지 않습니다.</p>
<h2 id="총계">총계</h2>
<p>약 45만원.
HDD 가격을 제외하면 25만원.</p>
<h1 id="하드웨어-설치">하드웨어 설치</h1>
<p>실제로 도착한 ODROID N2+를 보면 무척 작습니다. 남성용 반지갑과 비슷한 사이즈입니다.</p>
<p><a href="https://wiki.odroid.com/odroid-n2/odroid-n2">https://wiki.odroid.com/odroid-n2/odroid-n2</a></p>
<p>위키 페이지입니다. </p>
<h2 id="os-설치">OS 설치</h2>
<p>가장 먼저 eMMC 모듈을 eMMC 모듈 라이터에 연결합니다. 흰색으로 위치가 그려져 있는 곳에 놓고 움직이면 살짝 걸리는 것을 느낄 수 있습니다. 아주 조금 힘을 주어서 누르면 결합하는 소리가 들립니다. 그 뒤 PC에 연결하면 LED 빛이 들어오면서 USB 메모리처럼 사용이 가능합니다. </p>
<p><img src="https://velog.velcdn.com/images/whdekf/post/3a9d74ae-beb0-4336-91e2-f27c3b64a79d/image.png" alt=""></p>
<p>이렇게 됩니다. </p>
<p><a href="https://wiki.odroid.com/odroid-n2/getting_started/os_installation_guide">https://wiki.odroid.com/odroid-n2/getting_started/os_installation_guide</a></p>
<p>위키에서 제공하는 정보대로, 원하는 OS 이미지를 다운로드한 뒤, 메모리를 PC에 연결하고, Etcher를 다운로드해서 실행한 뒤 이미지를 플래시해줍니다. </p>
<p><a href="https://wiki.odroid.com/odroid-n2/os_images/ubuntu">https://wiki.odroid.com/odroid-n2/os_images/ubuntu</a></p>
<p>하드커널 (ODROID 제조사)에서 제공하는 Ubuntu 22.04 Minimal을 설치했습니다. </p>
<p>그 뒤 eMMC 모듈 라이터에서 eMMC 모듈을 제거한 뒤, 모듈을 기판에 장착해줍니다. 마찬가지로 eMMC 모듈의 위치가 기판 위에 문자와 흰색 선으로 표기되어있고, 그 위에서 살짝 걸리는 곳에서 눌러서 결합하면 됩니다. </p>
<p><img src="https://velog.velcdn.com/images/whdekf/post/02821792-3c2d-41f1-8d45-a833aaa6b9e1/image.png" alt=""></p>
<p>파워 어댑터와 USB 커넥터 근처에 조그만한 핀이 있는데 팬을 연결하는 곳입니다. 팬을 구입하셨다면, 이곳에 팬 커넥터를 연결합니다. 팬 커넥터를 연결하기 전에, 케이블을 살짝 꼬아주세요. </p>
<p><img src="https://velog.velcdn.com/images/poor-food/post/5bca5177-bef6-4cdc-a16e-a88989cd4ad2/image.png" alt=""></p>
<p><a href="https://wiki.odroid.com/odroid-n2/getting_started/examples_wiring_fan">https://wiki.odroid.com/odroid-n2/getting_started/examples_wiring_fan</a></p>
<p>이렇게요. </p>
<p>공식 케이스를 같이 사용하시면, 팬 케이블을 빼낼 공간이 없다는 걸 알게 되셨을 겁니다. 위키에서 보여준 것처럼 MicroUSB 밑의 공간을 쓰거나, 드릴 등을 이용해서 케이스에 구멍을 내줍시다. </p>
<p>팬 케이블을 정돈한 뒤, 팬에 동봉된 육각 스페이서를 이용해서 방열판에 팬을 고정해주시면 팬 설치도 완료됩니다. </p>
<p>동전 배터리를 잘 준비하셨다면 그것도 끼워줍니다.</p>
<p>HDD는 NAS 구성 시 연결하겠습니다.</p>
<p>공식 케이스를 위치에 맞게 연결해주고 (방열판 옆에 있는 홈에 맞춰서 케이스를 결합하는 방식입니다.) 이더넷 케이블을 통해서 가정용 라우터와 연결해줍니다. </p>
<p>추후 SSH로 연결할 것이지만, 당장은 디스플레이와 입력 장치가 필요하니, HDMI 케이블로 모니터와 연결하고, 키보드도 USB로 연결해줍니다. </p>
<p>마지막으로 파워 어댑터 케이블을 꽂으면, 전원이 들어옵니다. </p>
<h1 id="이어서">이어서</h1>
<p>현재 다음의 작업을 마치고 실제로 사용하고 있습니다.</p>
<ul>
<li>리눅스 SSH 터미널 연결</li>
<li>사용자 권한 등 기본적인 리눅스 설정</li>
<li>외장 HDD 포맷, 마운팅, Samba를 통한 NAS 연결</li>
<li>간단한 파이썬 어플리케이션 구동</li>
</ul>
<p>내용이 자세해지므로, 추후 마저 작성할 예정입니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DRF, API Root]]></title>
            <link>https://velog.io/@poor-food/Django-Rest-Framework-API-Root</link>
            <guid>https://velog.io/@poor-food/Django-Rest-Framework-API-Root</guid>
            <pubDate>Thu, 03 Aug 2023 20:40:22 GMT</pubDate>
            <description><![CDATA[<h2 id="browsable-api">Browsable API</h2>
<p>Django Rest Framework 는 Browsable API 를 제공합니다. </p>
<p>제공하는 API 리스트를 쉽게 시각화해서 보여주고, 링크를 클릭하는 것만으로 쉽게 API 를 호출할 수 있습니다. POST 액션이 필요할 경우에는 폼도 제공하고요. </p>
<p><img src="https://velog.velcdn.com/images/whdekf/post/a0a05645-ea80-4d60-a2e7-a33559a085f2/image.png" alt="유저 목록을 조회할 때의 리스폰스 값을 HTML 을 통해서 출력하고 있다. "></p>
<p>사용할 수 있는 API의 목록도 조회할 수 있습니다. 
<img src="https://velog.velcdn.com/images/whdekf/post/3a74891a-c8c7-4296-9493-2ec15ff80ce1/image.png" alt="users 와 groups 라는 api의 주소를 출력해주고 있다. "></p>
<h2 id="issue">Issue</h2>
<p>단, 일반적인 방법으로 개발을 진행하면 실제로 API 서버의 루트(&quot;<a href="http://www.site.com/&quot;)%EC%97%90">www.site.com/&quot;)에</a> 접속했을 때 확인할 수 있는 화면은 다음과 같을 겁니다.</p>
<p><img src="https://velog.velcdn.com/images/whdekf/post/afb1a164-5fe6-4e7f-a414-0fa540b74239/image.png" alt="Page not found"><em>Page not found</em></p>
<h2 id="cause">Cause</h2>
<p>이는 Django 특유의 앱 구성 때문에 그렇습니다. 기본적으로 Django 는 다음과 같은 형식으로 구성됩니다.</p>
<ul>
<li>project folder<ul>
<li>rootapp/<ul>
<li>setting.py </li>
<li>urls.py
. . .
. . .</li>
</ul>
</li>
<li>app1/<ul>
<li>urls.py</li>
<li>views.py 
. . .
. . .</li>
</ul>
</li>
<li>app2/<ul>
<li>urls.py</li>
<li>views.py 
. . . </li>
</ul>
</li>
<li>managy.py</li>
<li>requirements.txt
. . .</li>
</ul>
</li>
</ul>
<p>루트앱에서 app1, app2 을 불러와서 사용하는 것이죠. 이 경우엔 루트앱의 urls.py 에 다른 app1 의 urls 를 모두 등록해놔도 해당 app들의 api 에 도달하기 위한 주소를 제공하지 않습니다. </p>
<p>다른 앱의 API 루트에 도달하기 위한 api를 개발하지 않았기 때문입니다. 
따라서 이걸 만들어봅시다.</p>
<h2 id="fix">Fix</h2>
<h3 id="codes">Codes</h3>
<p>rootapp/views.py</p>
<pre><code class="language-python">
# ...
# ... 

@api_view([&#39;GET&#39;])
@permission_classes([AllowAny]) 
def api_root(request, fmt=None):
    &quot;&quot;&quot;
    API root view
    reverse() 를 사용해 각 앱이 제공하는 url 을 직접 api 뷰에 작성해줍니다. 
    &quot;&quot;&quot;
    return Response({
        &#39;admin&#39;: reverse(&#39;admin:index&#39;, request=request, format=fmt), 
        &#39;common&#39;: reverse(&#39;common:api-root&#39;, request=request, format=fmt),
        &#39;promotion&#39;: reverse(&#39;promotion:api-root&#39;, request=request, format=fmt),
        &#39;login&#39;: reverse(&#39;rest_framework:login&#39;, request=request, format=fmt),
        &#39;logout&#39;: reverse(&#39;rest_framework:logout&#39;, request=request, format=fmt),
    })</code></pre>
<p>rootapp/urls.py</p>
<pre><code class="language-python">

urlpatterns = [
    # 기존의 앱들의 URL
    path(&#39;admin/&#39;, admin.site.urls),
    path(&#39;common/&#39;, include(&#39;common.urls&#39;)),
    path(&#39;promotion/&#39;, include(&#39;promotion.urls&#39;)),
    path(&#39;api-auth/&#39;, include(&#39;rest_framework.urls&#39;, namespace=&#39;rest_framework&#39;)),

    # 방금 개발한 API 루트 URL 을 추가해줍니다. 
    path(&#39;&#39;, views.api_root, name=&#39;api-root&#39;), 
]</code></pre>
<p>app1/views.py</p>
<pre><code class="language-python">
# ...
# ...

# 일반적인 views 입니다. 참고하시라고 보여드립니다.  

app_name = &#39;common&#39;

router = routers.DefaultRouter()
router.register(r&#39;users&#39;, views.UserViewSet)
router.register(r&#39;groups&#39;, views.GroupViewSet)

urlpatterns = [
    path(&#39;&#39;, include(router.urls)),
]
</code></pre>
<h2 id="result">Result</h2>
<p>이제 api 루트에 접근했을 때, 다른 앱의 api 루트로 갈 수 있는 페이지를 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/whdekf/post/1dff9caa-68a5-4f52-9490-864eb050cca1/image.png" alt="API 루트가 정상적으로 보이고 있다. "><em>API Root</em> <img src="https://velog.velcdn.com/images/poor-food/post/989fab50-f4ca-470f-b4c2-49fa9f5525ac/image.png" alt="">
<img src="https://velog.velcdn.com/images/poor-food/post/28b60065-3b79-4a20-b202-72ca7c36e902/image.png" alt="">
<img src="https://velog.velcdn.com/images/poor-food/post/ab3c74b0-f281-40ab-a184-0c55997b5a81/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Creative Pebble V3, 프리징. ]]></title>
            <link>https://velog.io/@poor-food/Creative-Pebble-V3-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EB%B3%BC%EB%A5%A8-%EB%B3%80%EA%B2%BD%EC%8B%9C-%EC%8A%A4%ED%94%BC%EC%BB%A4-%ED%94%84%EB%A6%AC%EC%A7%95</link>
            <guid>https://velog.io/@poor-food/Creative-Pebble-V3-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EB%B3%BC%EB%A5%A8-%EB%B3%80%EA%B2%BD%EC%8B%9C-%EC%8A%A4%ED%94%BC%EC%BB%A4-%ED%94%84%EB%A6%AC%EC%A7%95</guid>
            <pubDate>Thu, 03 Aug 2023 20:37:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/whdekf/post/43e5d70f-8459-4135-a3d1-c7cdd58b803c/image.png" alt=""></p>
<p>얼마전에 구입한 미니 스피커. </p>
<h2 id="문제--소프트웨어-볼륨-변경시-스피커-프리징">문제 : 소프트웨어 볼륨 변경시 스피커 프리징</h2>
<p>Creative Pebble V3 스피커를 사용할 때,스피커의 전원을 킨 상태에서 해당 장치의 볼륨을 윈도우를 통해서 변경하면 스피커에서 소리가 들리지 않게 되는 문제가 있었다. 또한 볼륨 변경도 정상적으로 이뤄지지 않았다. </p>
<p>(100에서 낮추려고 하면 99에서 멈춰서 움직이지 않았고, 원치 않는 수치로 변경되어있었다.) </p>
<p>다소 기다리거나, 스피커를 껐다 키면 정상작동했으나, 한번 변경된 볼륨을 다시 변경하기 위해서는 한번 더 정지시킬 필요가 있었다. </p>
<h2 id="시도1--펌웨어-업데이트">시도1 : 펌웨어 업데이트</h2>
<p>효과가 없었다. </p>
<p>레딧 등에서는 &quot;구버전 펌웨어&quot;로 돌릴 경우 해결 되는 경우가 있었다고 했으나, 따로 시도해보지는 않았다. </p>
<p><img src="https://velog.velcdn.com/images/whdekf/post/0216c563-01fd-4f7d-9151-44c0d776ab73/image.png" alt=""></p>
<p>또한 펌웨어 업데이트는 USB 허브를 이용할 경우에는 진행되지 않았다. 사용하던 허브에서 뽑아서 PC에 직결하고 나서야 펌웨어 업데이트가 진행되었다. 그런데 펌웨어 업데이트 진행이 정상적으로 이뤄지고 있는지를 제대로 알 수가 없다. 성공할 경우 LED가 점멸해서 알 수 있지만, 실패할 경우에는 아무런 변화가 없다. </p>
<h2 id="시도2-해결-pc-직결">시도2 (해결!) PC 직결.</h2>
<p>스피커의 기본 USB 케이블이 데스크탑 PC에 사용하기엔 다소 짧아, 유전원 허브를 통해서 전원을 따로 공급해서 사용하고 있는 상황이었는데 뽑아서 USB 연장선을 통해 PC에 직접 연결하였다. 이것으로 문제가 해결되었음. </p>
<h2 id="잡담">잡담</h2>
<p>V3 이후에 Pro가 발매한 것으로 알고 있다. 추가 전원을 이용하면 더 강한 출력을 보장해주는 기능이 추가된 것으로 아는데 (그리고 RGB 라이팅도), 소프트웨어 문제가 많다길래 V3를 택했었다. </p>
<p><img src="https://velog.velcdn.com/images/poor-food/post/c1b42264-5b5f-4d43-a350-31656ea85d4e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django, Paging, Parameters.]]></title>
            <link>https://velog.io/@poor-food/Django-%ED%8E%98%EC%9D%B4%EC%A7%95%ED%95%98%EB%A9%B4%EC%84%9C-%EA%B2%80%EC%83%89-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EC%9C%A0%EC%A7%80%ED%95%98%EA%B8%B0-f43yg50l</link>
            <guid>https://velog.io/@poor-food/Django-%ED%8E%98%EC%9D%B4%EC%A7%95%ED%95%98%EB%A9%B4%EC%84%9C-%EA%B2%80%EC%83%89-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EC%9C%A0%EC%A7%80%ED%95%98%EA%B8%B0-f43yg50l</guid>
            <pubDate>Thu, 03 Aug 2023 20:36:26 GMT</pubDate>
            <description><![CDATA[<p>페이징을 이용해 게시판을 개발하면 자주 발생하는 문제 중 하나는, 다음 페이지로 넘어갈 때에도 검색에 사용한 쿼리 파라미터가 유지되어야한다는 점입니다. </p>
<p>페이지 링크를 단순히 페이지 넘버만을 이용해서 생성하면, 다른 페이지 번호로 넘어가는 순간, 검색 쿼리를 잃어버리고 맙니다. </p>
<p>Spring 에서 작업할 때는 보통 PagingSearch VO에서 작업합니다. 그 뒤, 쿼리파라미터를 VO에서 보관하고 있다가, 페이지를 그릴 때 입력해서 넣으면 됩니다. </p>
<pre><code class="language-python">    &quot;&quot;&quot;
    views.py
    컨텍스트로 파라미터를 딕셔너리로 넘겨준다.
    &quot;&quot;&quot;
    context = {
        &#39;list&#39;: page_obj
        , &#39;query_dict&#39;: request.GET
    }
</code></pre>
<pre><code class="language-python">
    &quot;&quot;&quot;
    filter.py
    템플렛 태그에서 딕셔너리를 쿼리스트링으로 변경하는 함수를 만들어 등록해준다. 
    &quot;&quot;&quot;
    @register.filter
def get_query_string(query_dict: dict):
    query_string = &quot;&quot;
    for key in list(query_dict.keys()):
        if key == &quot;page&quot;: 
            continue # 페이지는 제외.  
        query_string += f&quot;&amp;{key}={query_dict[key]}&quot;

    return query_string
</code></pre>
<pre><code class="language-html">&lt;!-- 
    등록한 템플릿 태그로 쿼리스트링을 붙이고
    페이지 넘버는 컨텍스트에서 불러와서 뿌린다. 
--&gt;
&lt;ul&gt;
{% for page in list.elided_page_range %}
  {% if page == list.paginator.ELLIPSIS %}
    &lt;li class=&quot;disabled&quot;&gt;...&lt;/li&gt;
  {% else %}
    {% if page == list.number %}
      &lt;li class=&quot;active&quot;&gt;{{ page }}&lt;/li&gt; &lt;!-- 현재 페이지 --&gt; 
    {% else %}
      &lt;li&gt;
        &lt;a href=&quot;?page={{ page }}{{ query_dict|get_query_string }}&quot;&gt;{{ page }}&lt;/a&gt;
      &lt;/li&gt;
    {% endif %}
  {% endif %}
{% endfor %}
&lt;/ul&gt;</code></pre>
<p>더 나은 방법이 있을 거 같은데 지금은 이 정도로 마무리했습니다. </p>
]]></description>
        </item>
    </channel>
</rss>