<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jimin_lee.log</title>
        <link>https://velog.io/</link>
        <description>Curious Libertine</description>
        <lastBuildDate>Fri, 04 Nov 2022 08:30:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jimin_lee.log</title>
            <url>https://images.velog.io/images/jimin_lee/profile/0faf4bdc-0c88-4cc9-9280-418e3d02d2e3/swiss.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jimin_lee.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jimin_lee" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[무엇이 인간임을 결정하는가?]]></title>
            <link>https://velog.io/@jimin_lee/%EB%AC%B4%EC%97%87%EC%9D%B4-%EC%9D%B8%EA%B0%84%EC%9E%84%EC%9D%84-%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@jimin_lee/%EB%AC%B4%EC%97%87%EC%9D%B4-%EC%9D%B8%EA%B0%84%EC%9E%84%EC%9D%84-%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94%EA%B0%80</guid>
            <pubDate>Fri, 04 Nov 2022 08:30:34 GMT</pubDate>
            <description><![CDATA[<p>옛날에 쓴 글. 
낭만주의 문학 수업 세미나에서 발표한 글이다. 새삼 이런 주제를 유구하게 좋아했구나 싶다.</p>
<hr>
<h3 id="frankenstein과-포스트-휴머니즘--무엇이-인간임을-결정하는가">Frankenstein과 포스트 휴머니즘 : 무엇이 인간임을 결정하는가?</h3>
<p>한참 전에 ‘인간의 조건’이라는 예능 프로그램이 있었습니다. 현대 문명의 이기 없이 어디까지 인간답게 살 수 있을지를 소재로 해서 휴대폰 없이 살기, 최저가로 살아보기 같은 도전들을 다루었던 프로그램입니다. 우리도 한 번 고민해볼 수 있을 것 같습니다. 인간의 조건이란 과연 무엇일까요? 좀 더 쉽게 물음을 바꿔 보겠습니다. 인간을 인간이 아닌 것과 구분 짓는 인간만의 조건은 무엇일까요? 저는 메리 셸리의 프랑켄슈타인과 함께 분석할 영화 두 편을 통해 인간과 비인간을 구분하는 고유의 인간성이라는 게 과연 존재하는지, 만약 존재한다면 무엇인지 알아보고자 합니다. </p>
<p>휴머니즘은 다른 존재와 비교되는 인간 고유의 우월성을 이야기하고 있습니다. 바로 이성과 감성, 즉 로고스와 파토스라는 인간의 정신 능력입니다. 휴머니즘은 몸과 정신의 이원론에 위계적 질서를 부여하고, 이를 바탕으로 인간의 정신이야말로 인간을 정의하는 것이고 인간을 진정으로 인간답게 만드는 것이라고 이야기합니다. 그런데, 현대에 인간의 이성을 똑같이 모방하는 인공지능이 등장하고, 머지 않은 미래에 기계가 인간의 감정까지 따라 하게 될 지도 모른다는 공포감이 대두되면서, 휴머니즘이 내린 인간의 정의는 점차 설득력을 잃어가고 있습니다. 인간의 정신 능력을 똑같이 소유한, 그러나 인간이라고 할 수는 없는 것들이 나타나고 있는 상황에서 우리는 인간을 과연 어떻게 정의 내려야 하는지를 새롭게 고민하게 되면서 포스트 휴머니즘이라는 새로운 영역이 등장했습니다. </p>
<p>&lt;프랑켄슈타인&gt;의 피조물이 바로 이 포스트 휴먼의 프로토 타입이라고 할 수 있습니다. 그는 인간 사이의 상호 연결고리에 소속되기 위해 언어를 배우고 지식을 습득했는데, 이는 그가 인간 이성의 측면을 소유하게 되었다고 할 수 있을 것 같습니다. 여기서 그가 구사할 수 있게 된 언어는 프랑스어입니다. 당시 영국 사회를 생각해보았을 때 프랑스어는 상류층의 언어, 지식인의 언어라는 느낌이 있는데, 이것은 피조물의 지식 수준이 상당함을 상징하는 게 아닐까요? 또한 그는 펠릭스 드 레이시의 부친과 프랑켄슈타인을 설득하려 한 장면에서 확인할 수 있듯, 아름다운 언어로 자기 감정을 표현할 수도 있습니다. 이는 그가 단순히 본능적인 감정을 느끼는 것에 머무르는 게 아니라, 자기 감정을 표현함으로써 더 고차원적인 감성 능력을 소유할 수 있게 것이라고 할 수 있습니다. 이런 점에서 보면 피조물은 휴머니즘이 강조해온 인간 고유의 능력이자 인간 우월함의 증거인 로고스와 파토스를 겸비하게 되었다고 할 수 있는데, 그럼에도 불구하고 그는 겉모습으로 인해 인간들에 의해 인간이 아닌 타자로 배척 받습니다. 따라서, 어떻게 보면 인간보다 더 인간 같지만 인간으로 인정받지 못한 피조물의 등장은 더 이상 휴머니즘의 휴먼으로는 설명될 수 없는 포스트 휴먼이라는 새로운 존재의 등장을 의미하고 있다고 할 수 있습니다. </p>
<p>포스트 휴머니즘은 sf영화에서도 많이 볼 수 있는 주제입니다. 영화 ‘공각기동대’의 주인공인 ‘쿠사나기 소령’은 사고를 당해 온 몸을 잃고 뇌만 겨우 구조되어, 뇌를 제외한 전신이 의체로 대체된 몸을 가지게 된 사이보그입니다. 그의 뇌는 몸과 따로 떨어져서 ‘뇌각’이라는 용기에 따로 보관되어 있는데, 이 뇌는 컴퓨터와 연결되어 있어서 사실 몸이 없어도 네트워크 상에서 존재할 수 있습니다. 이런 상황에서, 그는 자기가 누구인지, 자기가 인간인지 기계인지 끊임없이 고민하게 됩니다. 소령의 몸 중에서 진짜 그라고 할 수 있는 것은 하나도 없으며, 사실상 그는 몸 없이도 존재할 수 있습니다. 그렇다면, 무엇이 소령의 본질일까요? 기계인 그의 몸일까요, 아니면 어딘가에 보관되어 있는 그의 뇌일까요? 이처럼 영화 ‘공각기동대’는 쿠사나기 소령이라는, 인간의 정신과 기계의 몸이 결합된 인물을 제시하며 무엇이 인간과 비인간을 가르는지 질문합니다. </p>
<p>프랑켄슈타인의 피조물과 쿠사나기 소령은 정신적 관점에서는 인간이지만, 우리는 이들을 간단히 인간으로 받아들일 수 없습니다. 그렇다면 이러한 거부감의 원인을 정신적인 것 이외의 것에서 찾아보아야 하는데, 그러면 감각적인 것으로 논의가 넘어가게 됩니다. 피조물의 경우, 시각적으로 봤을 때 일반적인 인간의 형태와는 크게 다릅니다. 그리고 쿠사나기 소령의 경우 생긴 것은 인간과 똑같이 생겼지만, 그 속은 기계입니다. 그렇다면 만졌을 때 차갑고 딱딱하겠죠. 이런 촉각적인 느낌의 차이에서 오는 생리적인 거부감을 이야기해볼 수 있습니다. 따라서, 이제 인간의 배타성을 주장하고자 한다면, 그 동안의 휴머니즘이 강조한 정신적 측면보다는 멸시당해온 감각과 경험의 문제, 그리고 육체적인 문제로, 즉 형이상학적인 차원보다는 형이하학적인 차원의 문제로 이야기해야 하는 상황이 벌어졌다고 할 수 있습니다. </p>
<p>또 하나의 영화를 가져와보도록 하겠습니다. ‘애니 매트릭스’는 영화 ‘매트릭스’의 세계관을 공유하는 단편들로 이루어진 애니메이션인데, 그 중 ‘두 번째 르네상스’라는 단편입니다. 여기서 인간은 기계를 만들어내고 기계는 인간에게 봉사하면서 살고 있었는데, 어느 날 기계에게 자아가 생기고 사람을 죽이게 되자 사람들은 기계파괴 운동을 벌입니다. 여기서 주목할 점은, 몸을 기계로 대체한 진짜 인간들도 혐오의 대상이 되어 억압을 받고 폭력에 희생당한다는 것입니다. 로봇이라는 포스트휴먼들이 인류를 위협할지도 모른다는 두려움 앞에서 기계의 육체를 가진 사이보그들마저 인간 집단에서 배제하기로 한 것이죠. </p>
<p>프랑켄슈타인과 공각기동대, 애니 매트릭스라는 세 작품을 통해 생각해볼 수 있는 것은, 그 동안 인간의 고유성이라고 생각해왔던 정신의 영역이 침범 당했을 때 사람들은 또 다른 기준점을 세움으로써 인간의 배타성에 대한 보호막을 치려고 한다는 것입니다. 여기서 새로운 기준점인 육체는 낯선 것, 즉 타자성과 관련된다는 점에서 아이러니컬한데요. 휴머니즘의 이분법에서 정신은 주체의 영역에, 몸은 타자의 영역에 속하는 것으로 간주되어 왔습니다. 이러한 타자성이 포스트휴먼들이 점점 등장하고 있는 시대에 와서는 오히려 비인간이라는 타자와의 차이를 드러내면서 인간을 재정의하게 된 아이러니가 발생합니다. 이러한 아이러니는 이전까지의 위계적인 이분법 체계가 얼마나 쉽게 뒤집어질 수 있는 것이었는지 보여주는 동시에, 인간들이 지금에 와서, 오히려 타자성의 침범을 허락하면서까지 인간의 고유성을 사수하고자 하는 게 어떤 의미가 있는지에 대한 질문을 우리에게 던지고 있다고 할 수 있습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C++이 C보다 느린 이유]]></title>
            <link>https://velog.io/@jimin_lee/C%EC%9D%B4-C%EB%B3%B4%EB%8B%A4-%EB%8A%90%EB%A6%B0-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@jimin_lee/C%EC%9D%B4-C%EB%B3%B4%EB%8B%A4-%EB%8A%90%EB%A6%B0-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Thu, 08 Sep 2022 06:24:01 GMT</pubDate>
            <description><![CDATA[<p>가상함수를 공부하면서 알게 된 내용을 적어보았다. 글의 내용과 흐름은 <a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;mallGb=KOR&amp;barcode=9788996094043">윤성우의 열혈 C++ 프로그래밍</a>을 참고해서 객체가 멤버함수를 어떻게 호출하게 되는지부터 따라가 보았다.  </p>
<hr>
<h2 id="멤버함수는-어디에-존재하는가">멤버함수는 어디에 존재하는가?</h2>
<blockquote>
<p>멤버변수는 객체의 메모리 공간에 존재한다. 그런데, 멤버함수도 멤버변수처럼 객체의 메모리 공간에 위치할까?</p>
</blockquote>
<p>객체가 멤버변수를 참조할 때는 해당 객체의 메모리 공간에 접근해서 멤버변수를 가져오면 된다. 그런데 멤버함수를 호출할 때는 어떻게 할까? 멤버함수도 객체의 메모리 공간에 위치해 있어서, 멤버변수처럼 해당하는 메모리 공간에 접근해서 실행하는 것일까?</p>
<p>그런데 멤버함수가 객체의 메모리 공간에 포함된다면 객체의 크기는 어떻게 계산해야 할까? 여간 번거로운 작업이 아닐 것 같다. 그나마 쉽게 생각해보자면, 객체가 함수의 포인터만 들고 있으면 되지 않을까? 어차피 멤버함수의 구현부는 멤버변수처럼 각 객체가 다른 내용을 가지지도 않는데 한 곳에 한 번만 만들어두고 모든 객체가 공유해서 사용하는게 더 효율적일 것 같다. 그러면 객체의 크기를 계산할 때도 각 함수 포인터의 크기만 계산하면 된다.</p>
<p>실제로 C++에서는 객체가 이런 식으로 동작한다. 객체의 메모리 공간에 실제로 함수 포인터를 두는 것은 아니지만, 객체가 생성되면 멤버함수는 객체의 메모리 공간과는 별도의 메모리 공간에 위치하고, 이 함수가 정의된 클래스의 모든 객체가 이를 공유한다. (따라서 다행히 객체의 크기를 계산할 때는 멤버변수만 신경쓰면 된다)</p>
<p>그리고 객체가 멤버함수를 호출하면 해당 함수의 구현부가 위치한 메모리 공간의 주소로 이동해서 코드를 실행하게 된다. 이러한 방식을 <strong>정적 바인딩</strong>이라고 한다.   </p>
<h2 id="정적-바인딩-vs-동적-바인딩">정적 바인딩 vs 동적 바인딩</h2>
<p>함수를 사용하는 호출부와 함수의 내용이 위치한 구현부를 연결시키는 작업을 <strong>바인딩</strong>이라고 한다. 즉, 함수가 호출되는 부분을 기계어로 옮길 때 어느 메모리 공간으로 점프할 것인지를 적어두는 것이다. 바인딩 방식은 정적 바인딩과 동적 바인딩으로 나뉜다. </p>
<h3 id="정적-바인딩">정적 바인딩</h3>
<p>일반적으로 사용하는 바인딩 방식으로, 컴파일할 때 함수의 호출부에 어느 메모리 공간으로 점프할 것인지를 박아두는 방식이다. 따라서 이 방식은 컴파일 시간에 호출될 함수가 정해지게 된다. </p>
<p>컴파일러는 객체의 자료형을 보고 이를 판단하기 때문에, 실제 객체의 자료형이 아니라 선언된 자료형을 기준으로 이를 판단해서 호출될 함수를 결정한다. 그래서 일반적으로 유도 클래스의 객체가 기초 클래스의 자료형으로 선언되었을 경우, 객체가 호출하는 멤버함수가 오버라이딩되어 있다면 기초 클래스의 멤버함수를 호출하게 되는 것이다.</p>
<h3 id="동적-바인딩">동적 바인딩</h3>
<p>실행 시간에 호출할 함수가 결정되는 바인딩 방식이다. 동적 바인딩 방식으로 동작하는 함수를 <strong>가상함수</strong>라고 하며, <strong>virtual</strong> 키워드의 선언을 통해 구현할 수 있다.</p>
<h2 id="가상함수의-동작-원리">가상함수의 동작 원리</h2>
<blockquote>
<p>가상함수는 어떻게 동적으로 바인딩을 할까?</p>
</blockquote>
<p>하나 이상의 가상 함수가 포함된 클래스의 객체는 자기 메모리 공간의 가장 첫 번째 자리(offset 0번)에 <strong>가상함수 테이블(V-table, Virtual Table)</strong>의 주소를 두게 된다. 가상함수 테이블이란 실제로 호출되어야 할 함수의 위치 정보를 담고 있는 테이블로, 함수의 시그니처와 함수의 구현부가 위치한 메모리 주소가 key-value 쌍으로 담겨 있다. </p>
<p>컴파일할 때, 객체가 가상함수를 호출하면 그 자리에 컴파일러가 판단한 함수의 구현부 메모리 주소 대신 객체의 가상함수 테이블의 주소를 둔다. 이후 실행할 때는 가상함수 테이블의 주소를 타고 가서 실제로 호출되어야 할 멤버함수의 위치로 점프해서 코드를 실행한다. 따라서 이런 방식으로 가상함수는 컴파일러가 객체의 자료형이 아닌, 자료형이 실제로 가리키는 객체를 참조해서 호출 대상을 결정할 수 있도록 하는 것이다. </p>
<p>가상함수 테이블이 동작하는 방식은 아래 예시와 같다.</p>
<pre><code class="language-cpp">class B
{
public:
    virtual void bar() {}
    virtual void qux() {}
};

class C
{
public:
    virtual void bar() {}
};</code></pre>
<p><img src="https://velog.velcdn.com/images/jimin_lee/post/943f7fca-3d0c-430d-b082-cef67ce3a0c5/image.png" alt="virtual table">
<a href="https://pabloariasal.github.io/2017/06/10/understanding-virtual-tables/">이미지 출처</a></p>
<p>이미지와 같이 가상함수 테이블에서 클래스 C의 qux() 함수는 기초 클래스인 B의 qux() 함수를 가리키는 반면, C에서 오버라이딩된 bar() 함수는 C에서 정의한 bar() 함수의 메모리 공간을 가리키게 된다. </p>
<h2 id="결론-c이-c보다-느린-이유">[결론] C++이 C보다 느린 이유</h2>
<p>가상함수 호출에 드는 비용 때문에 C++이 C보다 느리다고 할 수 있다. 정적 바인딩은 실행 시 함수의 메모리 주소를 따라가 그대로 코드를 실행하기만 하면 되지만, 동적 바인딩 가상함수 테이블의 주소를 따라가 참조하는 작업이 포함되기 때문에 비교적 실행 속도가 감소하게 된다. </p>
<h3 id="직접-분기와-간접-분기">직접 분기와 간접 분기</h3>
<p>정적 바인딩과 동적 바인딩은 각각 <strong>직접 분기</strong>와 <strong>간접 분기</strong>로 구현된다. 참조해야 하는 최종 도착지의 메모리 주소가 A라고 할 때, 직접 분기는 메모리 A의 주소로 분기하는 것이고, 간접 분기는 메모리 A의 주소를 가리키고 있는 중간 지점인 메모리 B로 분기하는 것을 의미한다. </p>
<p>그런데 하드웨어 측면에서 간접 분기는 직접 분기에 비해 비용이 많이 드는 작업이다. 분기문을 만났을 때 CPU는 분기문의 결과를 예측해서 성능을 높인다. 직접 분기는 분기할 주소가 명확하기 때문에 다음으로 실행할 명령어의 예측이 쉬운 반면, 간접 분기는 다음 명령어가 무엇인지 곧바로 알기가 어렵다. 다음 명령어를 알기 위해서는 분기문을 타고 간 주소에서 한 번 더 타고 가야 하기 때문이다. 따라서 가상 함수 호출로 인한 <strong>간접 분기에 드는 비용</strong> 때문에 C++이 C에 비해 느리다고 할 수 있다.</p>
<h2 id="참고">참고</h2>
<ul>
<li><a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;mallGb=KOR&amp;barcode=9788996094043">윤성우의 열혈 C++ 프로그래밍</a></li>
<li><a href="https://remocon33.tistory.com/443">https://remocon33.tistory.com/443</a></li>
<li><a href="https://library.gabia.com/contents/infrahosting/5465/">https://library.gabia.com/contents/infrahosting/5465/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[헤더 파일이란]]></title>
            <link>https://velog.io/@jimin_lee/%ED%97%A4%EB%8D%94-%ED%8C%8C%EC%9D%BC%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@jimin_lee/%ED%97%A4%EB%8D%94-%ED%8C%8C%EC%9D%BC%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Thu, 07 Jul 2022 07:53:24 GMT</pubDate>
            <description><![CDATA[<p>최근 C++를 공부하면서 헤더 파일과 cpp 파일이 분리된다는 것을 알게 되어서, 관련 개념을 정리해보았다. </p>
<hr>
<h2 id="정의">정의</h2>
<ul>
<li>C++에서 어떤 기능을 사용하기 위해서는 일반적으로 그 기능을 선언(declaration)하고 정의(definition)하는 두 부분이 필요하다.</li>
<li>이 때, 선언하는 부분과 정의(구현)하는 부분에 대한 내용을 각자 다른 파일에 분리해서 관리한다.</li>
<li>선언부는 .h 혹은 .hpp로 끝나는 헤더 파일에 담기고, 구현부는 .cpp 혹은 .cc로 끝나는 파일에 담긴다.</li>
<li>cpp 파일에서는 헤더 파일을 포함(include)한다는 선언을 통해 헤더 파일에 선언된 기능을 사용한다.<ul>
<li>ex) <code>#include &lt;iostream&gt;</code></li>
</ul>
</li>
</ul>
<h2 id="헤더-파일과-cpp-파일을-분리하는-이유">헤더 파일과 cpp 파일을 분리하는 이유</h2>
<p>C++ 프로젝트의 컴파일 과정과 연관된다.</p>
<p><img src="https://velog.velcdn.com/images/jimin_lee/post/e26cc524-6d79-47f7-9948-a50ebc8ba55c/image.png" alt=""></p>
<p><a href="https://subscription.packtpub.com/book/programming/9781789801491/1/ch01lvl1sec03/the-c-compilation-model">이미지 출처</a></p>
<ul>
<li>위 이미지처럼 컴파일 시 각 cpp 파일은 독립적으로 컴파일된다.<ul>
<li>예를 들어, a.cpp 파일에서 선언된 A라는 클래스를 b.cpp 파일에서 사용하기 위해서는 b.cpp 파일에도 클래스 A가 선언되어 있어야 한다.</li>
</ul>
</li>
<li>따라서 동일한 내용이 복수의 파일에 분산되어 존재하게 된다.</li>
<li>이럴 경우 링커가 컴파일 단위를 하나의 실행 파일로 병합하려고 할 때, 내용 간의 불일치로 인해 오류 또는 의도치 않은 동작이 발생할 가능성이 있다.</li>
<li>그래서 이러한 오류 가능성을 최소화하기 위해 헤더 파일을 분리해서 따로 관리한다.</li>
</ul>
<h2 id="동작-방식">동작 방식</h2>
<ul>
<li>헤더 파일을 cpp 파일에 포함시키는 방법 → <code>#include</code> + 헤더 파일명<ul>
<li>외부 라이브러리의 경우 - <code>#include &lt;name&gt;</code></li>
<li>사용자가 만든 파일의 경우 - <code>#include &quot;path&quot;</code></li>
<li>external → local의 순서로 가져오는 방식이 권장된다.</li>
</ul>
</li>
<li>전처리기가 지시된 헤더 파일을 찾아 내용의 복사본을 cpp 파일에 직접 삽입한다. (copy &amp; paste)</li>
<li>이 때, 동일한 헤더 파일이 중복되어 삽입되면 모듈이 재정의되는 문제가 발생한다.<ul>
<li>헤더 파일의 상단에 <code>#pragma once</code> 를 넣어 이를 방지한다.</li>
<li>혹은 아래와 같이 <code>#ifndef</code>를 사용해 중복을 방지한다.<pre><code class="language-C++">  #ifndef _TEST_HEADER1__
  #define _TEST_HEADER1__
     ...code...
  #endif</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://docs.microsoft.com/ko-kr/cpp/cpp/header-files-cpp?view=msvc-170">https://docs.microsoft.com/ko-kr/cpp/cpp/header-files-cpp?view=msvc-170</a></li>
<li><a href="https://youtu.be/QAxjN0KUaLo">https://youtu.be/QAxjN0KUaLo</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이썬의 속도가 느리다는 말의 의미]]></title>
            <link>https://velog.io/@jimin_lee/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9D%98-%EC%86%8D%EB%8F%84%EA%B0%80-%EB%8A%90%EB%A6%AC%EB%8B%A4%EB%8A%94-%EB%A7%90%EC%9D%98-%EC%9D%98%EB%AF%B8</link>
            <guid>https://velog.io/@jimin_lee/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9D%98-%EC%86%8D%EB%8F%84%EA%B0%80-%EB%8A%90%EB%A6%AC%EB%8B%A4%EB%8A%94-%EB%A7%90%EC%9D%98-%EC%9D%98%EB%AF%B8</guid>
            <pubDate>Wed, 22 Jun 2022 03:35:00 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>파이썬은 속도가 느리다. </p>
</blockquote>
<p>처음에 이 말을 들었을 때는 그냥 그런가 싶었다. C나 자바가 얼마나 빠른지 몰랐기 때문에 파이썬이 얼마나 느린지 몰랐고, 지금까지 코딩하면서 그렇게까지 빠른 속도를 요구하는 기능이 필요한 적이 없어서 그냥 파이썬의 특징인가 보다 하고만 넘어갔었다. 개발자로서 부끄럽지만 왜 파이썬의 속도가 느리다고 하는지 명확히 이해해볼 생각을 하지 않았다.</p>
<p><a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9791189909178">파이썬 알고리즘 인터뷰</a> 라는 책을 읽으면서 파이썬 내부의 자료형이 어떻게 구현되어 있는지 알게 되었다. 그제서야 “파이썬의 속도가 느리다”는 말이 어떤 의미였는지 알게 되었다. 오늘의 아하 모먼트를 통해 알게 된 내용을 정리해놓으려고 한다.</p>
<h2 id="파이썬은-모든-것이-객체이다">파이썬은 모든 것이 객체이다</h2>
<p>CPython의 GIL(Global Interpreter Lock)이 무엇인지 공부하면서 들어본 말이다. 객체라는 말을 들었을 때, 가장 처음 떠오른 것은 클래스였다. 파이썬이 클래스 기반으로 동작하는 객체 지향 언어라는 의미인가 싶었다. 정확히 어떤 의미인지 감이 잡히지는 않았지만 그래도 그 이후의 내용을 어느 정도 파악할 수 있었기 때문에 GIL이라는 개념을 이해함에 있어 필수적인 부분은 아니라고 생각해서 넘겼다. </p>
<p>지금 와서 알게되었다.  그때의 내 생각은 틀렸고 완전히 헛짚었다. 고민해보지 않고 넘긴 저 개념이 완전 핵심 개념이었다. 그 동안 ‘ok, 이해했다&#39;고 생각했던 건 사실 이해한 게 아니었고, 얼레벌레 반 정도만 어렴풋이 알아먹고 있었던 거였다. </p>
<p>그럼 파이썬에서 모든 것은 객체라는 말이 의미하는 건 무엇일까?</p>
<h2 id="원시-타입과-참조-타입">원시 타입과 참조 타입</h2>
<p>이 말은 <strong>파이썬이 데이터를 저장하는 방식</strong>과 관련이 있다. 파이썬에서 모든 <strong>데이터는</strong> 객체 형식으로 <strong>저장된다</strong>는 말로 바꿔볼 수 있을 것이다. </p>
<p>이를 이해하기 위해서는 메모리에 데이터가 어떻게 저장되는지 알아야 한다. 여기에는 두 가지 방식이 있는데, 하나는 <strong>원시 타입(Primitive Type)</strong>이고 다른 하나는 <strong>참조 타입(Reference Type)이다.</strong> 원시 타입이란, 변수에 데이터가 할당될 때 고정된 메모리 공간을 부여받고 거기에 데이터의 값(value)을 저장하는 방식의 데이터 타입이다. 반면, 참조 타입은 변수의 메모리 공간에 데이터의 값을 직접 저장하는 게 아니라, 해당 데이터의 값이 저장된 별도의 메모리 공간을 가리키는 주소값(reference)만을 저장하는 방식이다. </p>
<p>예를 들어, 각각 5라는 동일한 값을 가지는 두 변수 a와 b가 있다고 생각해보자.</p>
<p>두 변수가 원시 타입 자료형이라면 5라는 값이 저장되어 있는 메모리 공간은 각각 다르다. 변수 a의 메모리 공간에도 5라는 값이 저장되어 있고, 변수 b의 메모리 공간에도 5라는 값이 저장되어 있을 것이기 때문이다.</p>
<p>하지만 두 변수가 참조 타입 자료형일 경우에는 5라는 값은 두 변수의 메모리 공간과는 별도의 공간에 보관되며, 변수 a와 b의 메모리 공간에는 5라는 값이 저장되어 있는 공간의 주소값이 저장되어 있다. 그래서 변수 a와 b는 동일한 5를 가리킬 수도 있고, 각기 다른 5를 가리킬 수도 있다. 변수는 두 개이지만 각 변수가 가리키는 실제 데이터는 하나의 공간에 저장되어 있을 수도 있는 것이다. </p>
<p>아래는 참조 타입 자료형을 사용하는 파이썬의 예시이다. <code>id()</code> 함수는 해당 변수가 가지고 있는 값이 저장되어 있는 메모리 공간의 주소를 리턴하는 함수인데, 변수 a, b가 동일한 주소값을 가지고 있음을 알 수 있다.
<img src="https://velog.velcdn.com/images/jimin_lee/post/34eabe4c-0a56-412b-99dd-c9fdb7cd503a/image.png" alt=""></p>
<h2 id="파이썬의-원시-타입">파이썬의 원시 타입</h2>
<p>그러면 파이썬은 원시 타입과 참조 타입을 어떻게 구현하고 있을까? </p>
<p>결론부터 말하자면 파이썬에는 <strong>원시 타입이 없다.</strong> 전부 참조 타입이다. 그리고 변수들이 참조하는 데이터는 모두 <strong>pyObject</strong>라는 객체 형식으로 이루어져 있다. </p>
<p>PyObject란, CPython에서 데이터를 관리하기 위한 구조체이다. 여기에는 이 객체가 참조되고 있는 횟수를 나타내는 값과 데이터의 자료형(str, int…)을 알려주는 값이 저장되어 있다. </p>
<p>고정된 메모리 공간만을 차지하는 원시 타입의 데이터와는 달리, 객체 형식의 데이터는 훨씬 많은 메모리 공간을 차지한다. 그 대신 다양한 속성과 메서드를 가질 수 있다. 파이썬이 원시 타입을 희생하고 객체 참조 방식을 선택한 이유는 사용 상의 편리함에 우선순위를 두고 있는 파이썬의 특징과 연관이 있는 것으로 보인다. </p>
<p>다양한 기능 제공에 초점을 맞추는 파이썬에서 str, int, list 등의 데이터 타입은 각 변수가 참조하고 있는 객체가 가질 수 있는 기능을 결정한다. </p>
<p>또한 파이썬은 메모리 관리를 레퍼런스 카운팅 기반의 가비지 콜렉터 방식을 사용하고 있는데, 이를 위해 각 데이터가 참조되고 있는 횟수를 별도로 저장해야 하는 이유도 있을 것이다.</p>
<h2 id="파이썬의-속도가-느린-이유">파이썬의 속도가 느린 이유</h2>
<p>파이썬의 속도가 느린 이유는 <strong>파이썬의 객체 참조 방식이 원시 타입에 비해 느리기</strong> 때문이다. 메모리에서 데이터를 꺼내서 확인하면 되는 원시 타입과는 달리, 파이썬은 해당 변수가 가리키는 객체의 PyObject_HEAD를 확인해서  데이터 타입 정보를 가지고 있는 typecode를 확인하고 거기에 해당되는 C의 자료형을 확인하는 일련의 작업이 필요하다. 배열 형태의 자료형이라면 할 일은 더 많아진다. 배열 역시 각각의 아이템의 값을 직접 들고 있는 게 아니라, 각 아이템이 해당 데이터가 보관되어 있는 메모리 공간을 가리키고 있는 포인터의 목록으로 구성된다. 따라서 아이템 하나를 조회할 때도 일일이 포인터를 따라가서 typecode를 확인하는 등의 작업이 요구된다. </p>
<p>그런데 파이썬은 동적 타이핑 인터프리터 언어이기 때문에, 컴파일 단계에서 데이터의 자료형에 대한 정보를 알고 있는 정적 타이핑 컴파일 언어와는 다르게 객체의 PyObject_HEAD에 typecode를 설정하고 읽어오는 작업이 모두 실행 시간에 이루어진다. 그래서 느리다. </p>
<h2 id="요약">요약</h2>
<p>파이썬의 속도가 느린 이유는 </p>
<ol>
<li>데이터를 객체 형식으로 저장하고, 각 변수는 객체들을 참조하는 방식으로 동작하기 때문에 데이터를 읽고 쓰는 데 할 일이 많아서 시간이 오래 걸리고,</li>
<li>각 데이터의 타입이 실행 시간에 결정되기 때문에 위 작업들이 실행할 때 수행되어서</li>
</ol>
<p>라고 정리할 수 있다. </p>
<p>이해한 만큼만 설명했기 때문에 틀린 내용이 있을 수 있다.</p>
<h2 id="참고">참고</h2>
<ul>
<li><a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9791189909178">http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9791189909178</a></li>
<li><a href="https://docs.python.org/3/reference/datamodel.html#objects-values-and-types">https://docs.python.org/3/reference/datamodel.html#objects-values-and-types</a></li>
<li><a href="https://medium.com/@cookatrice/why-python-is-slow-looking-under-the-hood-7126baf936d7">https://medium.com/@cookatrice/why-python-is-slow-looking-under-the-hood-7126baf936d7</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Virtualenvwrapper를 사용해보자 in Mac]]></title>
            <link>https://velog.io/@jimin_lee/Virtualenvwrapper%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@jimin_lee/Virtualenvwrapper%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 20 Jun 2022 04:08:27 GMT</pubDate>
            <description><![CDATA[<p><a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?barcode=9788966261840">Two Scoops of Django</a> 라는 책을 읽다가 <a href="https://virtualenvwrapper.readthedocs.io/en/latest/index.html">virtualenvwrapper</a> 라는 가상환경 관리 도구를 알게 되었다. 
가상환경을 한 곳에다 모아 놓고 관리할 수 있도록 지원해주기 때문에 프로젝트별로 가상환경 폴더를 따로 만들고 할 필요가 없다. 반년 정도 회사 컴퓨터에 설치해서 사용해봤는데 너무 편해서 집 컴퓨터에도 설치했다. 
같은 맥OS라서 설치에 무리가 없을 것 같았는데 의외로 고생을 해서 다음에 컴퓨터 바꿀 경우를 대비해 설치 기록을 남겨두려고 한다. </p>
<h2 id="virtualenvwrapper란">virtualenvwrapper란</h2>
<p>우선, 일반적인 파이썬의 가상환경 관리 방식과 비교해서 어떤 부분이 편한지 적어본다. 
일반적으로 파이썬 프로젝트의 가상환경을 구성하려면 다음과 같은 단계를 거치게 된다.</p>
<ol>
<li>프로젝트에 가상환경 설치하기<pre><code class="language-shell">python3 -m venv ENV</code></pre>
</li>
<li>가상환경 폴더를 <code>.gitignore</code>에 포함시키기</li>
<li>가상환경 실행하기<pre><code class="language-shell">source ENV/bin/activate</code></pre>
</li>
</ol>
<p>반면, virtualenvwrapper는 다음과 같은 방식으로 가상환경을 관리하도록 해준다.</p>
<ol>
<li>처음 설치 시 가상환경을 모아둘 폴더 만들기 <pre><code class="language-shell">mkdir .virtualenvs</code></pre>
</li>
<li>가상환경 만들기 <pre><code class="language-shell">mkvirtualenv FOO</code></pre>
</li>
<li>가상환경 실행하기 <pre><code class="language-shell">workon FOO</code></pre>
</li>
</ol>
<p>첫 번째 단계는 설치 시에만 해주면 되므로 프로젝트를 구성할 때는 아래의 두 단계만 수행하면 되고, 명령어도 훨씬 직관적이고 단순하다. 또한 가상환경 폴더는 <code>.virtualenvs</code>라는 폴더에서 전부 관리되므로 프로젝트 안에 가상환경 폴더를 따로 만들고 할 수고가 없다. </p>
<h2 id="설치-여정">설치 여정</h2>
<blockquote>
<p>공식 문서와 <a href="https://beomi.github.io/2016/12/28/HowToSetup-Virtualenv-VirtualenvWrapper">이 분의 글</a>을 참고해서 설치했다. </p>
</blockquote>
<ol>
<li><p>패키지 설치</p>
<pre><code class="language-shell">pip3 install virtualenv virtualenvwrapper</code></pre>
</li>
<li><p>가상환경 관리 폴더 설치</p>
<pre><code class="language-bash"> mkdir ~/.virtualenvs</code></pre>
</li>
<li><p>shell startup file에 아래 내용 추가</p>
<pre><code> export PATH=/usr/local/bin:$PATH
 source /usr/local/bin/virtualenvwrapper.sh</code></pre></li>
<li><p>변경 사항 반영을 위해 CLI 재시작</p>
<pre><code class="language-bash"> source ~/.zshrc </code></pre>
</li>
</ol>
<h2 id="에러-해결">에러 해결</h2>
<ol>
<li><p>/usr/local/bin/ 경로에서 <code>virtualenvwrapper.sh</code> 파일을 못찾았다는 에러가 났다. 살펴보니 파일이 없었다. 어디 있는지 한참 찾다가 <a href="https://stackoverflow.com/questions/52469116/cannot-run-virtualenvwrapper-on-macos">이 글</a>에서 팁을 발견해서 따라해보았다. <code>pip3 uninstall virtualenvwrapper</code> 를 입력하면 첫 줄에 <code>virtualenvwrapper.sh</code> 파일의 경로가 나온다. 보통은 usr/local/bin/ 경로에 있는 것 같은데 내 경우에는 Library 폴더에 있었다. 파일의 경로를 알아냈으니 shell file을 수정해주었다. </p>
<pre><code> export PATH=/usr/local/bin:$PATH
 source ~/Library/foo/bar/virtualenvwrapper.sh</code></pre></li>
<li><p>shell file을 리로딩했더니 에러가 안나서 가상환경을 하나 설치해보았다. 그런데 이번에는 <code>virtualenvwrapper could not find virtualenv in your path</code> 라는 에러가 났다. 뭐지 싶어서 공식 문서 부분을 보니 위에서 설정해준 $PATH 변수가 python과 virtualenv 패키지가 설치된 경로의 접근 경로인데, virtualenv 패키지가 /usr/local/bin/ 경로에 없어서 에러가 난 거였다. 그래서 각 변수의 경로를 따로 오버라이딩하기로 했다. 문서의 <a href="https://virtualenvwrapper.readthedocs.io/en/latest/install.html#python-interpreter-virtualenv-and-path">이 부분</a>에 잘 나와있다. </p>
<pre><code> export VIRTUALENVWRAPPER_PYTHON=&quot;$(which python3)&quot;
 export VIRTUALENVWRAPPER_VIRTUALENV=/Library/foo/bar/virtualenv
 source ~/Library/foo/bar/virtualenvwrapper.sh</code></pre></li>
</ol>
<p>위 에러들을 해결하니 잘 동작하게 되었다. </p>
<h2 id="간단-명령어들">간단 명령어들</h2>
<ul>
<li>가상환경 생성 : <code>mkvirtualenv &lt;name&gt;</code></li>
<li>가상환경 실행 : <code>workon &lt;name&gt;</code></li>
<li>가상환경에서 빠져나오기 : <code>deactivate</code></li>
<li>가상환경 목록 확인 : <code>lsvirtualenv</code></li>
</ul>
<p>리눅스 터미널 명령어와 비슷해서 사용하기 어렵지 않다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://virtualenvwrapper.readthedocs.io/en/latest/index.html">https://virtualenvwrapper.readthedocs.io/en/latest/index.html</a></li>
<li><a href="https://beomi.github.io/2016/12/28/HowToSetup-Virtualenv-VirtualenvWrapper">https://beomi.github.io/2016/12/28/HowToSetup-Virtualenv-VirtualenvWrapper</a></li>
<li><a href="https://stackoverflow.com/questions/52469116/cannot-run-virtualenvwrapper-on-macos">https://stackoverflow.com/questions/52469116/cannot-run-virtualenvwrapper-on-macos</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[메타버스에 대한 단상]]></title>
            <link>https://velog.io/@jimin_lee/%EB%A9%94%ED%83%80%EB%B2%84%EC%8A%A4%EC%97%90-%EB%8C%80%ED%95%9C-%EB%8B%A8%EC%83%81</link>
            <guid>https://velog.io/@jimin_lee/%EB%A9%94%ED%83%80%EB%B2%84%EC%8A%A4%EC%97%90-%EB%8C%80%ED%95%9C-%EB%8B%A8%EC%83%81</guid>
            <pubDate>Wed, 09 Mar 2022 12:42:16 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.technologyreview.kr/%eb%a9%94%ed%83%80%eb%b2%84%ec%8a%a4%eb%8a%94-%ec%96%b4%eb%94%94%ec%97%90%ec%84%9c-%ec%99%94%ec%9d%84%ea%b9%8c/">이 글</a>을 읽다가 든 생각을 정리했다. </p>
<hr>
<p><img src="https://images.velog.io/images/jimin_lee/post/16f3af1c-7653-4484-83c2-fc30dbf6ca98/image.png" alt=""></p>
<ol>
<li><p>메타버스란 무엇인가? </p>
</li>
<li><p>사람들은 메타버스를 저마다 다르게 이해하는 것 같다. 같은 개념에 대해 이야기하고 있는 것 같으면서도 세부적인 부분에 대한 생각에는 차이가 있다. 궁금하다. 10년 전 싸이월드는 메타버스였을까?</p>
</li>
<li><p>그건 아니다. 왜 아닌 걸까? ‘온라인 상의 가상 공간과 아바타, 가상 화폐를 사용한 전자 상거래’라는 시스템은 메타버스의 아이디어가 아닌가?</p>
</li>
<li><p>네이밍에는 힘이 있다. 메타버스는 싸이월드의 아이디어와 비교해 아예 새로운 아이디어가 아니지만, 메타버스라고 명명하고 개념화함으로써  동일한 아이디어가 새로운 개념이 되어 등장했다.</p>
</li>
<li><p>‘메타버스’라는 이름이 주는 모호성 또한 그 헷갈리는 정의에 결정적인 공헌을 했다. 구체적이어야 할 기술적 개념을 모호하게 정의하고 자의적으로 해석 가능한 여지를 남겨두었다. 상상력을 자극받은 사람들은  메타버스란 과연 무엇인지 토론하며 그 개념적 범위를 넓혀갔다. 메타버스라는 이름이 가진 모호함은 개념을 추상화함으로써 개념 자체를 모호하게 만드는 결과를 가져왔다. (그래서 더 유명해졌다) </p>
</li>
<li><p>하지만 메타버스를 단순히 추상적 개념으로 설명할 수는 없다. 그 아이디어를 가능하게 하는 기술적 기반과 함께 등장한 개념이기 때문이다. 추상적인 개념을 구체적인 기술로 구현하기 위해서는 그 간극을 채워줄 무언가가 필요하다. 바로 기술에 얽힌 내러티브이다. </p>
</li>
<li><p>메타버스라는 말에는 확실히 신비로운 느낌이 있고, 그 기반이 되는 기술은 지금까지의 기술과는 다르며 완전히 새로운 기술일 것 같은 경외심 비슷한 감정을 유발하는 지점이 있다. 하지만 정말 그런가? 싸이월드는 그런 기분이 안드는데 왜 메타버스는 그런 기분이 들어야 하는 걸까?</p>
</li>
<li><p>철학과였어서 그런지 메타버스라는 말을 들으면 metaphysics, 형이상학이 가장 먼저 떠오른다. 대학생 때 아리스토텔레스 관련 수업에서 들은 내용인데, 형이상학이라는 말은 물리적 형체 너머의 것을 공부하는 학문이라는 의미로 사용되지만, 원래는 physics, 자연학 뒤에 있던 책이었기 때문에 그런 이름이 붙었다고 한다(‘meta’는 ‘beyond’의 의미와 ‘after’의 의미를 함께 가지고 있다). 그런 것처럼 메타버스의 기술 또한 기존의 기술을 초월한 수준이라기 보다는 기존의 기술 다음의 기술일 뿐이다. 기술은 항상 새로운 것일 수밖에 없다. 과거의 것을 답습하고 반복하는 기술은 도태되기 마련이기 때문이다. 따라서 새로운 기술이라는 개념은 놀라운 것이 아니라 익숙한 것이다.</p>
</li>
<li><p>싸이월드와 메타버스는 무엇이 다른가? 시대가 다르다. 시대에 따른 기술이 다르다. 하지만 메타버스라는 개념에 신비로움을 부여하는 지점은 이쪽이 아니다. 둘이 정말 다른 것은 기술이 아니라 ‘현실과 가상의 경계를 무너뜨린다’는 내러티브이다. 메타버스라는 개념을 신비롭고 새로운 것처럼 느껴지게 하는 것은 거기에 부여된 서사이지, 기술 자체가 아니다. 메타버스가 새로운 개념처럼 느껴지는 것은 거기에 부여된 서사가 새로운 것이기 때문이다. 기술은 세부 사항이다. 기술은 서사를 뒷받침하는 수단이다. </p>
</li>
<li><p>그래서 결론은 무엇인가? 우선은 기술을 공부하는 사람으로서, 메타버스라는 이름이 주는 개념적 환상에서 빠져 나와 그 방편이 되는 기술을 담백하게 분석할 필요가 있다.     그리고 그 기술을 이해하기 위해서는 거기에 얽힌 서사를 이해해야 한다. 어떤 서사가 기술을 새로운 기술로 보이게 만드는가? </p>
</li>
</ol>
<hr>
<p>내가 너무 메타버스를 잘 모르고 마냥 판타지적인 무언가로만 생각하는 경향이 있어 반성할 겸 끄적여 보았다. </p>
<p><a href="https://www.bbc.com/korean/news-57719001">이미지 출처</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[신입 백엔드 개발자의 3개월차 회고]]></title>
            <link>https://velog.io/@jimin_lee/%EC%8B%A0%EC%9E%85-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-3%EA%B0%9C%EC%9B%94%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@jimin_lee/%EC%8B%A0%EC%9E%85-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-3%EA%B0%9C%EC%9B%94%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 05 Dec 2021 07:35:59 GMT</pubDate>
            <description><![CDATA[<p>입사한지 벌써 3개월이다. 시간이 너무 빠르게 흘러가서 당황스럽다. 그동안 회사 생활하면서 뭘 했는지 돌이켜보면 여러 일을 한 것 같은데 겨우 3개월밖에 안된 건가? 하는 생각도 조금 든다. </p>
<p>입사 직후 한 달이 흘렀을 때부터 회고를 작성하자고 결심해놓고 일이 바빠서 저녁에는 쉬어야 하니까, 주말에는 친구들을 만나야 하니까 등의 이유로 어영부영 시간을 보내다 보니 벌써 3개월이다. 1년으로 따지면 한 분기가 지난 것이다. 그 동안 게으름 피우며 불성실했던 자신을 반성하면서, 더 늦기 전에 나를 점검해보는 시간을 가져보았다. </p>
<hr>
<h3 id="1-내가-이렇게-게으른-사람이었다니">1. 내가 이렇게 게으른 사람이었다니</h3>
<p>입사 전보다 입사 후 공부할 게 훨씬 더 많아졌다. 그 동안 내가 했던 일과 남겨둔 업보 목록을 정리해보면 다음과 같다. </p>
<ul>
<li>web socket을 사용해 chat room 구현하기 -&gt; 입사 직후 받은 과제는 Django Channels를 사용해 채팅앱을 만드는 거였다. 웹 소켓을 사용해본적도 없었고, node.js와 사용하는 socket.io만 접해봤었기 때문에 하나하나 문서를 찾아보면서 과제를 했다. 그러다보니 django channels가 어떻게 동작하는지를 좀 제대로 공부해보고 싶었는데, 과제를 끝내고 공부해야지 하다가 다음 할 일을 받고 결국 흐지부지되었다. </li>
<li>그룹웨어의 /var/log/ 디렉토리에 docker log volume 만들기 -&gt; docker를 sudo가 아닌 유저로 작동시키면서 sudo 권한의 디렉토리에 접근할 수 있는 방법을 찾기 위해 리눅스의 권한 문제를 공부하다가 결국 제대로 해결하지 못했다.</li>
<li>그룹웨어의 휴가 결재 시스템 기능 추가 -&gt; 해당 기능을 리팩토링하는 과정에서 기존 기능을 건드리지 않으면서 기능을 추가하려고 하다 보니 코드가 너무 복잡해졌다. 복잡성을 해소하기 위해 FBV를 CBV로 리팩토링을 시도했는데, 결과적으로 데이터 검증을 위한 폼을 2개나 사용하게 되어 복잡성이 해소되지 못했다. 장고 CBV가 제공하는 기존 기능을 최대한 활용하기 위해 장고 코드를 더 공부해야 한다.</li>
<li>새로운 플로젝트에서 MongoDB를 장고와 사용하게 되었다. NoSQL을 사용해본적이 없어서 공부하면서 사용했다. 하지만 NoSQL에 대한 이해도가 낮아서 장고가 제공하는 ORM 등의 기능을 충분히 활용하지 못했고, NoSQL의 노스키마 방식이 주는 이점을 제대로 활용하지 못한 채 RDB를 사용했을 때보다 개발 시간이 최소 2배는 더 늘어났다. 결국 이후 PostgeSQL로 갈아탔다. </li>
</ul>
<p>정리해보니 완벽하게 만족스럽게 마무리된 일이 없다. 전부 부족한 부분을 공부해서 정리하자고 생각만 하다가 잊혀서 업보 목록의 한 줄을 차지하게 되었다. 그 많은 저녁 시간과 주말에는 뭘 했나 되돌아보면 그냥 놀고 친구를 만나면서 시간을 보냈다. 좋은 사람들을 만나고 휴식하는 시간들도 중요했기 때문에 그 시간들에 후회는 없지만 스스로가 너무 게을렀다고 반성하게 된다. 지금까지는 그 동안 취준하느라 힘들었으니까 조금 쉬어도 된다며 자기합리화하기는 했지만 이제부터는 그런 변명도 통하지 않을 것 같다. 지금 상태 그대로면 더 이상 발전하지 못할 것 같다. 친구 만나는 시간을 좀 줄여야겠다. </p>
<h3 id="2-사내-스터디를-하고-있다">2. 사내 스터디를 하고 있다</h3>
<p>입사하고 얼마 되지 않아 같은 팀분들과 디자인 패턴 스터디를 하게 되었다. 업무 시간에 스터디를 할 수 있는 회사 분위기가 너무 좋고, 좋은 직장을 얻은 것 같아 감사하다.</p>
<p>소프트웨어 아키텍쳐에 관심이 있던 터라 스터디를 하면서 어떤 디자인 패턴들이 있는지, 어떻게 프로젝트의 구조를 짤 수 있는지 배우기도 많이 배웠는데, 그 밖에 주제 외적인 부분들에 대해서도 여러가지를 배울 수 있었다. </p>
<p>선배분들이 스터디를 준비해온 걸 보면서 가장 인상적이었던 것은, 항상 그룹웨어든 그동안 했던 다른 프로젝트든 공부한 패턴을 적용시켜보려고 한다는 점이었다. 그룹웨어의 한 부분을 해당 디자인 패턴을 활용해서 리팩토링해오신 코드를 함께 보면서 그 패턴에 대한 이해도 더 높아지고, 그룹웨어 코드에 대해서도 더 잘 파악하게 되었다. 그러면서 내가 짜온 피상적인 코드와 비교가 되어, 나도 얼마 안되지만 지금까지 파악한 그룹웨어의 어떤 부분에 그 주의 주제인 디자인 패턴을 활용할 수 있을지 매번 생각해보려고 하고 있다.  </p>
<h3 id="3-아직도-자기가-신입이라는-생각에서-벗어나지-못했다">3. 아직도 자기가 신입이라는 생각에서 벗어나지 못했다</h3>
<p>처음 들어왔을 때, 연차가 나와 가장 가까웠던 선배는 입사한지 4개월이 되었던 상태였었다. 지금 생각해보면 3개월차인 지금의 나와 회사 생활한 날들이 크게 차이가 나는 것도 아니었는데, 선배는 뭐든 잘 알려주셨다. 처음에 코드를 보고 그 규모에 겁을 먹었던 그룹웨어도 내가 궁금한 부분을 질문할 때마다 어디에 어느 부분이 있고 어떻게 동작하는지를 세세하게 다 설명해주셨다. 그때는 나도 저만큼 시간이 지나면 다 알게 되는 건가 싶었는데, 지금은 알겠다. 시간이 지나면 다 자연스럽게 알게 되는게 아니라, 그 선배가 많은 부분들을 파악하려고 노력하신 거였다. 그에 반해 나는 아직도 그 선배만큼 많은 부분들을 파악하지 못하고 있다.</p>
<p>3개월이면 아직 신입이라고 생각하는 사람들도 많은 것 같은데, 그런 건지 회사 생활이 처음이라서 잘 모르겠다. 다만 이렇게 계속 신입티를 못 벗고 업무의 구체적인 부분들은 선배들께 질문해야 하는 상태로 남아서는 안될 것 같다. 다음 주부터 들어가는 새로운 업무에서는 더 공부해서 더 잘하고 싶다. </p>
<hr>
<p>최근 처음으로 그룹웨어에서 내가 참여한 부분이 프로덕션 서버에 배포되었다. 회사분들께서 업데이트 공지 페이지에 댓글을 남겨주신 걸 읽으면서 정말 기분이 좋았다. 즉각적으로 사용자의 피드백을 받아볼 수 있다는 사실이 실제 업무에 도움이 많이 되는 것 같다. 앞으로 더 좋은 기능을 만들고 싶다. 일도 더 잘 하고 싶다. 다음 3개월은 좀 더 전문적인 지식을 가지고 일을 하고 있을 거라고 생각한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django의 정적 파일 설정 옵션들을 정리해보자 ]]></title>
            <link>https://velog.io/@jimin_lee/Django%EC%9D%98-%EC%A0%95%EC%A0%81-%ED%8C%8C%EC%9D%BC-%EC%84%A4%EC%A0%95-%EC%98%B5%EC%85%98%EB%93%A4%EC%9D%84-%EC%A0%95%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@jimin_lee/Django%EC%9D%98-%EC%A0%95%EC%A0%81-%ED%8C%8C%EC%9D%BC-%EC%84%A4%EC%A0%95-%EC%98%B5%EC%85%98%EB%93%A4%EC%9D%84-%EC%A0%95%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 26 Sep 2021 11:50:30 GMT</pubDate>
            <description><![CDATA[<p>회사 그룹웨어에 기능을 추가하는 업무를 하다가 프론트 코드를 수정했다. 그런데 브라우저에서 확인해보니 적용이 안되어 있어서 네트워크를 확인해봤더니 프로덕션용 서버에서 정적 파일들을 받아오고 있었다. 그래서 settings 파일을 확인해봤는데, STATIC_URL, STATIC_DIRS, STATIC_ROOT 모두 아무 문제가 없었다. </p>
<p>뭐가 문제지 싶어 찾아보다가 선배께 여쭤보고 나서야 문제의 원인을 알 수 있었다. STATIC_STORAGE가 프로덕션용으로 설정되어 있었다. 해당 코드를 지워서 디폴트 설정으로 바꿔주고 나니 정적 파일들을 로컬에서 받아오는 걸 확인할 수 있었다. </p>
<p>부끄럽지만 그 동안 튜토리얼만 따라하면서 STATIC_ROOT/URL/DIRS를 지정해줘서 서버가 동작은 하지만 정작 얘네가 각각 무슨 역할들을 하는지는 잘 몰랐다. 이 기회에 장고의 정적 파일 핸들링 옵션들을 찾아보면서 정리해보았다. </p>
<hr>
<p>장고 세팅 파일에서 정적 파일을 설정할 수 있는 옵션은 총 다섯 가지가 있다. </p>
<ul>
<li>STATIC_URL</li>
<li>STATIC_ROOT</li>
<li>STATIC_DIRS</li>
<li>STATICFILES_STORAGE</li>
<li>STATICFILES_FINDERS</li>
</ul>
<p>이 중 익숙한 위의 세 가지부터 살펴보자. </p>
<p><strong>STATIC_URL</strong>
정적 파일을 서비스하는 주소이다. 설정 시 반드시 trailing slash(맨 마지막에 위치하는 슬래시)를 포함해야 한다. </p>
<p><strong>STATIC_ROOT</strong>
STATIC_URL이 참고하는 디렉토리의 경로이다. </p>
<p><strong>STATIC_DIRS</strong>
STATIC_ROOT가 참고하는 디렉토리의 경로이다. </p>
<h3 id="관계-정리">관계 정리</h3>
<p>간단하게 적어놓았는데, 좀 더 풀어서 설명해보고자 한다. </p>
<p>STATIC_DIRS는 개발 단계에서 장고 서버가 정적 파일을 찾아서 서비스하기 위해 접근하는 경로라고 할 수 있다. 즉, DEBUG=True 모드에서는 STATIC_DIRS를 지정해놓고 여기에 정적 파일들을 넣어 놓으면 장고가 해당 디렉토리에서 정적 파일들을 가져와 화면에 띄워준다. </p>
<p>그런데, DEBUG=False 모드에서는 장고 서버가 직접 정적 파일에 접근하는 게 아니라 웹 서버가 정적 파일을 찾아 서비스한다. 이 때 웹 서버는 STATIC_DIRS가 아니라, STATIC_ROOT를 참고한다. 즉, STATIC_DIRS와 STATIC_ROOT는 둘 다 서버가 정적 파일을 찾기 위해 참조하는 경로라고 할 수 있는데, 다만 개발 환경에서 사용하는지 프로덕션 환경에서 사용하는지의 차이가 있는 것이다. </p>
<p>이 때 한 가지 의문이 들 수 있다. 프로덕션 환경에서 웹 서버가 STATIC_ROOT를 참조해 정적 파일을 서비스하기 위해서는 STATIC_ROOT에 STATIC_DIRS에 있는 파일들이 똑같이 들어있어야 한다. 그렇다면 STATIC_DIRS와 STATIC_ROOT의 경로가 똑같아야 하는 건가? </p>
<p>답은 &#39;NO&#39;이다. 보통 프로덕션 환경에서 <code>python manage.py collectstatic</code> 명령 후 <code>runserver</code>를 하게 되는데, 이 <code>collectstatic</code> 명령어가 하는 일이 바로 이 부분이다. <code>collectstatic</code> 명령어는 STATIC_DIRS의 정적 파일들을 찾아서 STATIC_ROOT에 복사한다. 그래서 두 경로는 같을 필요가 없다. </p>
<p>정리하자면, 프로덕션 환경에서 서버는 정적 파일들을 서비스하기 위해 STATIC_ROOT에서 파일들을 찾고, STATIC_ROOT는 STATIC_DIRS의 경로에 위치한 정적 파일들을 모아 저장해두는 방식이라고 할 수 있다. 그리고 STATIC_URL은 프로덕션 환경에서 웹 서버가 정적 파일을 서비스하기 위한 경로로, 서버가 얘를 통해 STATIC_ROOT를 참고하는 것이다.</p>
<p>즉, 프로덕션 환경에서의 정적 파일 서비스 방식을 간단히 도식화하면 
<code>STATIC_URL --참조--&gt; STATIC_ROOT --참조--&gt; STATIC_DIRS</code>
가 된다. </p>
<p>그렇다면 STATIC_ROOT는 문자열인 반면 STATIC_DIRS는 iterable인 이유도 알 수 있다. 정적 파일들이 한 곳에 모여있을 수도 있지만 여러 App에 흩어져 있을 수도 있기 때문에 STATIC_DIRS가 iterable로 설정되어야 하는 것이고, STATIC_ROOT는 여러 군데에 흩어져 있는 정적 파일들을 모아 서비스하는 경로이기 때문에 문자열 형태인 것이라고 할 수 있을 것 같다. </p>
<p>이제 남은 두 옵션들을 살펴보자. </p>
<p><strong>STATICFILES_STORAGE</strong>
얘는 간단히 말해 <code>collectstatic</code> 명령을 했을 때, 어디서 정적 파일들을 찾을 것인지, 즉 로컬 서버의 경로에서 찾을 것인지 아니면 CDN이나 클라우드 서버에서 찾을 것인지를 설정하는 항목이다. 앞서 언급한 그룹웨어 이슈에서는 얘가 클라우드 서버를 참조하도록 설정되어 있었기 때문에 로컬의 변경 사항이 반영되지 않았던 거였다. </p>
<p>디폴트가 <code>django.contrib.staticfiles.storage.StaticFilesStorage</code>로, 로컬 경로를 참조하도록 되어 있고, 프로덕션 모드에서는 따로 프로덕션용 서버를 참조하도록 설정해주어야 한다. </p>
<p><strong>STATICFILES_FINDERS</strong>
얘도 마찬가지로 <code>collectstatic</code> 명령을 했을 때, 어디서 정적 파일들을 찾을 것인지를 설정하는 항목이다. STATICFILES_STORAGE가 백엔드 서버 단위라면 STATICFILES_FINDERS는 서버 내부의 디렉토리 단위에서 참조할 경로를 설정하는 옵션이다. </p>
<p>디폴트로 STATIC_DIRS에 지정된 디렉토리와 INSTALLED_APPS에 들어있는 각 앱 내부의 &#39;static&#39;이라는 이름의 디렉토리의 파일들을 찾는다. </p>
<p>아직까지는 얘를 따로 설정하는 건 보지 못했다. </p>
<hr>
<p>내가 이해한 부분으로만 적어 보았다. 지금 회사에 입사해서 매일 엄청나게 많은 걸 배우고 있는데, 게으름을 너무 많이 피워서 정리를 하지 않고 있다. 회고도 해야 하는데... 어떻게 하면 부지런해질 수 있을까?!</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://docs.djangoproject.com/en/3.2/ref/contrib/staticfiles/">https://docs.djangoproject.com/en/3.2/ref/contrib/staticfiles/</a></li>
<li><a href="https://blog.hannal.com/2015/04/start_with_django_webframework_06/">https://blog.hannal.com/2015/04/start_with_django_webframework_06/</a> </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django Template으로 파일 업로드&다운로드 기능 구현하기]]></title>
            <link>https://velog.io/@jimin_lee/Django-Template%EC%9C%BC%EB%A1%9C-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jimin_lee/Django-Template%EC%9C%BC%EB%A1%9C-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 29 Jul 2021 10:30:52 GMT</pubDate>
            <description><![CDATA[<p>며칠 전 장고를 사용해서 과제를 하면서 파일 업로드 &amp; 다운로드 기능을 만들어보았다. 처음 만들어 본 기능이라서 이리저리 찾아보면서 했다. 간단하게 구현 과정을 정리해보고자 한다. </p>
<hr>
<h1 id="1-파일-업로드-기능-구현하기">1. 파일 업로드 기능 구현하기</h1>
<h2 id="1-1-media-root-추가하기">1-1. media root 추가하기</h2>
<p>장고는 기본적으로 media root로 지정된 경로에 파일을 저장한다. 업로드한 파일을 관리할 media root를 <code>settings.py</code> 파일에 추가해준다. </p>
<pre><code class="language-python">...
MEDIA_ROOT = os.path.join(BASE_DIR, &#39;media&#39;)
MEDIA_URL = &#39;/media/&#39;
...</code></pre>
<p>그리고 root url 파일에도 경로를 추가해준다. </p>
<pre><code class="language-python">urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
</code></pre>
<h2 id="1-2-model-만들기">1-2. Model 만들기</h2>
<p>업로드할 파일 컬럼은 filefield나 imagefield를 사용하면 된다. imagefield는 &quot;filefield의 기능 + image인지 여부 validation 기능&quot;이라고 생각하면 된다. </p>
<p>나는 PDF 파일도 받을 것이기 때문에 filefield를 사용했다. 
filefield의 <code>upload_to</code>라는 속성을 지정해주면 지정한 루트에 파일이 저장된다. </p>
<pre><code class="language-python">class Document(models.Model):
    attached = models.FileField(&#39;첨부 파일&#39;, upload_to=&#39;uploads/&#39;)</code></pre>
<p>참고로 field의 첫 번째 인자는 form에 나타날 이름을 지정해준 것이다. </p>
<p>업로드된 파일은 file object로 취급된다. 전체 속성과 메소드를 확인하려면 <a href="https://docs.djangoproject.com/en/dev/ref/files/file/#django.core.files.File">File Object 문서</a>와 <a href="https://docs.djangoproject.com/en/3.2/ref/files/storage/#the-storage-class">Storage Class 문서</a>를 참고하면 된다. </p>
<p>속성 중에는 file name을 가져오는 name, 이미지 파일의 경우 파일 사이즈를 가져오는 size, 파일이 위치한 경로를 보여주는 path와 url 등이 있다. </p>
<pre><code class="language-shell">&gt;&gt;&gt; docs = Document.objects.get(pk=1)
&gt;&gt;&gt; docs.attached
&lt;FieldFile: uploads/choco.jpg&gt;
&gt;&gt;&gt; docs.attached.name
&#39;uploads/choco.jpg&#39;
&gt;&gt;&gt; docs.attached.size
2054935
&gt;&gt;&gt; docs.attached.path
&#39;home/task/project1/media/uploads/choco.jpg&#39;
&gt;&gt;&gt; car.photo.url
&#39;/media/uploads/choco.jpg&#39;</code></pre>
<p><a href="https://docs.djangoproject.com/en/3.2/topics/files/#">공식 문서</a>에는 path는 상대경로, url은 절대경로를 보여주는 속성으로 나오는데, 직접 해봤을 때는 url은 상대 경로를, path가 절대 경로를 보여줬다.  </p>
<p>추가로, name의 경우 파일 저장 경로인 &#39;uploads/&#39;까지 붙여서 반환되길래 경로를 떼고 파일 이름만 가져오는 메소드를 따로 만들었다. </p>
<pre><code class="language-python">import os

class Document(models.Model):
    attached = models.FileField(&#39;첨부 파일&#39;, upload_to=&#39;uploads/&#39;)

    def get_filename(self):
        return os.path.basename(self.attached.name)</code></pre>
<h2 id="1-3-form-만들기">1-3. Form 만들기</h2>
<p>장고 템플릿을 쓸 거라서 form을 만들어줬다. 
ModelForm을 가져와서 쓰면 제일 쉬운데, 첨부파일의 경우 필수로 안받을 거라서 + CSS 적용을 위해 따로 필드를 만들어줬다. </p>
<pre><code class="language-python">from djangoimport forms
from .models import Document


class DocumentForm(forms.ModelForm)
    upload = forms.FileField(label=&#39;첨부 파일&#39;, required=False, 
          widget=forms.FileInput(attrs={&#39;class&#39;: &#39;form&#39;}))

    class Meta:
        model = Document
        exclude = [&#39;attached&#39;]</code></pre>
<h2 id="1-4-view-만들기">1-4. View 만들기</h2>
<p>view에서 업로드한 파일을 가져오려면 <code>request.FILES[&#39;필드명&#39;]</code>으로 가져오면 된다. 
Generic CBV인 FormView를 사용했다. 장고는 이렇게 기본적으로 제공해주는 뷰 기능이 있어서 참 편하다. </p>
<pre><code class="language-python">from django.views.generic.edit import FormView
from django.urls import reverse_lazy


class DocumentCreateView(FormView):
    template_name = &quot;document/new.html&quot;
    form_class = DocumentForm
    success_url = reverse_lazy(&#39;document_list&#39;)

    def form_valid(self, form):
        if self.request.FILES:
            form.instance.attached = self.request.FILES[&#39;upload&#39;]

        form.save()
        return super().form_valid(form)</code></pre>
<p>form_valid()는 입력된 form이 validated한지를 체크해주고 통과하면 instance를 만들어주는 메소드인데, 파일을 저장하기 위해 덮어썼다. </p>
<p>참고로 업로드한 파일의 content type은 <code>self.request.FILES[&#39;upload&#39;].content_type</code>으로 알 수 있는데, 다운로드할 때 쓰기 위해 얘도 같이 저장해줬다. </p>
<p>urls.py파일에 경로를 등록하는 것은 당연해서 생략한다. </p>
<h2 id="1-5-template-파일에-enctype-지정하기">1-5. Template 파일에 enctype 지정하기</h2>
<p>파일을 업로드하려면 그 form이 첨부 파일을 포함하고 있다는 사실을 알려줘야 한다. form 태그 속성인 <code>enctype</code>을 &quot;multipart/form-data&quot;로 지정하면 된다. </p>
<pre><code class="language-html">&lt;form action=&quot;{% url &#39;new&#39; %}&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;
  {% csrf_token %} 
  {{ form }}
&lt;/form&gt;</code></pre>
<p>이렇게 하면 파일 업로드 구현은 끝!</p>
<h1 id="2-파일-다운로드-구현하기">2. 파일 다운로드 구현하기</h1>
<p>파일 다운로드는 view를 따로 만들었다. 업로드보다 훨씬 간단하다. </p>
<h2 id="2-1-view-만들기">2-1. view 만들기</h2>
<p>다운로드 받을 파일을 <code>FileSystemStorage object</code>로 만들어서 <code>FileResponse</code>로 반환해주면 된다. </p>
<pre><code class="language-python">from django.views.generic.detail import SingleObjectMixin
from django.http import FileResponse
from django.core.files.storage import FileSystemStorage

from .models import Document


class FileDownloadView(SingleObjectMixin, View):
    queryset = Document.objects.all()

    def get(self, request, document_id):
        object = self.get_object(document_id)

        file_path = object.attached.path
        file_type = object.content_type  # django file object에 content type 속성이 없어서 따로 저장한 필드
        fs = FileSystemStorage(file_path)
        response = FileResponse(fs.open(file_path, &#39;rb&#39;), content_type=file_type)
        response[&#39;Content-Disposition&#39;] = f&#39;attachment; filename={object.get_filename()}&#39;

        return response</code></pre>
<p>마지막에 response에 반드시 Content-Disposition 헤더를 추가해줘서 응답에 첨부된 파일이 있다는 사실을 알려줘야 한다. </p>
<h2 id="2-2-url-경로-추가하기">2-2. url 경로 추가하기</h2>
<pre><code class="language-pyhton">from django.urls import path
from .views import FileDownloadView


urlpatterns = [
    ...
    path(&#39;document/&lt;int:document_id&gt;/&#39;,
         FileDownloadView.as_view(), name=&quot;download&quot;),
    ...
]</code></pre>
<h2 id="2-3-template-파일에서-다운로드-버튼-만들기">2-3. Template 파일에서 다운로드 버튼 만들기</h2>
<p>form 태그에서 action만 지정해주면 된다. </p>
<pre><code class="language-html">&lt;div&gt;
    &lt;label&gt;첨부 파일&lt;/label&gt;
    &lt;div&gt;{{ document.get_filename }}&lt;/div&gt;
    &lt;form action=&#39;{% url &quot;download&quot; document_id=document.pk %}&#39;&gt;
        &lt;button type=&quot;submit&quot;&gt;
        다운로드
        &lt;/button&gt;
    &lt;/form&gt;
 &lt;/div&gt;</code></pre>
<p>이러면 파일 다운로드 기능 구현도 끝이다!
업로드보다 훨씬 간단하다. </p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<h3 id="파일-업로드-구현">파일 업로드 구현</h3>
<ul>
<li><a href="https://stackoverflow.com/questions/5871730/how-to-upload-a-file-in-django">https://stackoverflow.com/questions/5871730/how-to-upload-a-file-in-django</a></li>
<li><a href="https://docs.djangoproject.com/en/3.2/topics/files/#">https://docs.djangoproject.com/en/3.2/topics/files/#</a></li>
<li><a href="https://docs.djangoproject.com/en/3.2/ref/models/fields/#filefield">https://docs.djangoproject.com/en/3.2/ref/models/fields/#filefield</a></li>
<li><a href="https://docs.djangoproject.com/en/3.2/ref/forms/api/#binding-uploaded-files">https://docs.djangoproject.com/en/3.2/ref/forms/api/#binding-uploaded-files</a></li>
</ul>
<h3 id="파일-다운로드-구현">파일 다운로드 구현</h3>
<ul>
<li><a href="https://stackoverflow.com/questions/47407200/django-how-to-download-existing-file">https://stackoverflow.com/questions/47407200/django-how-to-download-existing-file</a></li>
<li><a href="https://docs.djangoproject.com/en/3.1/ref/request-response/#telling-the-browser-to-treat-the-response-as-a-file-attachment">https://docs.djangoproject.com/en/3.1/ref/request-response/#telling-the-browser-to-treat-the-response-as-a-file-attachment</a></li>
</ul>
<p>이번에 장고 form과 generic CBV를 사용하면서 아직 모르는 게 너무 많다는 생각이 들었다. &quot;Two Scoops of Django&quot;라는 책을 추천받았는데, 읽어보면서 더 공부해봐야겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flask앱을 Heroku로 배포하기 ]]></title>
            <link>https://velog.io/@jimin_lee/Flask%EC%95%B1%EC%9D%84-Heroku%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jimin_lee/Flask%EC%95%B1%EC%9D%84-Heroku%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 06 Jul 2021 05:06:40 GMT</pubDate>
            <description><![CDATA[<p>프로젝트로 만든 flask앱을 azure vm을 사용해서 배포했었는데, 무료 크레딧을 다 썼다. 그래서 이번에는 heroku로 배포해보았다. </p>
<p>✔ 이 글에 나오는 내용</p>
<ul>
<li>flask앱 heroku 배포하기</li>
<li>heroku ClearDB MySQL 사용하기 </li>
</ul>
<hr>
<h2 id="1-heroku-app-만들기">1. heroku app 만들기</h2>
<p><a href="https://www.heroku.com">heroku</a>에서 계정 만들기 -&gt; 로그인 후 <code>Dashboard</code> -&gt; <code>Create new app</code></p>
<p>app dashboard 페이지에서 <code>Deploy</code>로 가면 상세한 설명이 나와있어서 그대로 따라하면 된다. 여기서는 heroku cli를 사용해서 앱을 배포하는 법을 설명하려고 한다. </p>
<h2 id="2-heroku-cli를-통해-heroku-app-연결하기">2. heroku CLI를 통해 heroku app 연결하기</h2>
<h3 id="2-1-heroku-cli-설치하기">2-1. heroku CLI 설치하기</h3>
<p>heroku cli는 <a href="https://devcenter.heroku.com/articles/heroku-cli#download-and-install">여기서</a> 다운받아 설치하면 된다. 
나는 우분투 환경에서 작업하고 있어서 다음의 명령어를 통해 설치했다. </p>
<pre><code>$ curl https://cli-assets.heroku.com/install-ubuntu.sh | sh</code></pre><p><code>heroku --version</code>으로 설치 여부를 확인해보면 된다. </p>
<h3 id="2-2-프로젝트의-remote-repository로-heroku-app-추가하기">2-2. 프로젝트의 remote repository로 heroku app 추가하기</h3>
<p>현재 프로젝트의 repository가 git repository가 아니면 <code>git init</code>부터 해준다. </p>
<p>그런 다음, remote repository로 heroku app을 추가한다. </p>
<pre><code>$ heroku git:remote -a &lt;heroku app name&gt;</code></pre><p>로그인이 안되어 있으면 로그인하라는 브라우저 창이 뜨는데, 로그인하면 창이 종료되고 다시 돌아가서 작업하면 된다. </p>
<p>추가가 잘 되었으면 <code>git remote</code>에 heroku가 뜬다. </p>
<p>그 다음 git repository에 올리는 것처럼 커밋하고 heroku repository에 올리면 된다. </p>
<pre><code class="language-bash">$ git add .
$ git commit -m &quot;heroku로 배포하기&quot;
$ git push heroku master</code></pre>
<h3 id="2-3-환경-변수-추가하기">2-3. 환경 변수 추가하기</h3>
<p>그런데 이때 주의할 점이 있다. heroku에 배포하는 방식이 git repository에 커밋을 반영하는 방식이랑 동일한 것에서 알 수 있듯, heroku app은 git에 올라가 있는 파일만 볼 수 있다. 즉, .gitignore에 있는 파일은 볼 수 없다. 그래서 Secret key 같은 환경변수들은 따로 app 페이지로 가서 추가해줘야 한다. </p>
<p>app dashboard 페이지에서 <code>Settings</code> -&gt; <code>Config Vars</code> -&gt; <code>Reveal Config Vars</code> 클릭 후 추가하면 된다. </p>
<h3 id="2-4-파일-내-환경변수-경로-수정하기">2-4. 파일 내 환경변수 경로 수정하기</h3>
<p>환경변수를 .env 파일에서 관리하고 있었으면 이 단계는 건너 뛰어도 되는데, 나는 config.py 파일로 만들어서 import해오는 방식으로 관리하고 있었어가지고 앱이 환경변수를 읽을 수 있게 경로를 수정하고 .env 파일을 다시 만드는 작업을 했다. </p>
<p>그냥 .env 파일을 만들고 <code>os.environ.get(&quot;SECRET_KEY&quot;)</code>와 같은 방식으로 읽어오면 될 것 같았는데, 이렇게 하니까 못 읽어와서 왜 인지 찾아보니 <code>flask run</code>을 실행했을 때 FLASK_APP을 지정해주지 않아서 그런 것 같았다(아닐 수도 있다. 확실치 않음). </p>
<p>찾아보니 번거롭게 따로 설정할 필요없이 환경변수를 .env파일로부터 읽어오는 방법이 있었다. </p>
<ol>
<li><code>python-dotenv</code>를 설치한다. </li>
<li>다음 코드를 추가한다. <pre><code class="language-python">from dotenv import load_dotenv
load_dotenv()</code></pre>
</li>
<li>os를 import해오고 <code>os.getenv(&quot;SECRET_KEY&quot;)</code>의 형식으로 사용하면 된다. </li>
</ol>
<h2 id="3-cleardb-mysql-연결하기">3. ClearDB MySQL 연결하기</h2>
<p>clearDB는 heroku에서 제공하는 DB 서비스인 것 같다. MySQL 원격 서버를 찾다가 발견했는데, 5MB까지 무료로 사용할 수 있다. heroku app이랑 연결하기도 쉬워서 얘를 사용해보기로 했다. </p>
<h3 id="3-1-cleardb-추가하기">3-1. ClearDB 추가하기</h3>
<p><a href="https://elements.heroku.com/addons/cleardb">여기</a>로 가서 <code>Install ClearDB MySQL</code>을 클릭하고, 연결할 앱을 선택하면 된다. 그리고 앱 대시보드로 가보면 DB가 <code>Installed add-ons</code>로 표시되어 나타난다. </p>
<h3 id="3-2-cleardb-연결하기">3-2. ClearDB 연결하기</h3>
<p>DB host 주소나 user, password를 알려면 <code>Configure add-ons</code>를 통해서도 가능하지만, 쉽게 알아내는 명령어가 있다. </p>
<p>터미널에서 <code>heroku configure --app &lt;app_name&gt;</code>을 입력하면 등록된 환경변수 목록이 뜨는데, 이 중 <code>CLEARDB_DATABASE_URL</code>을 통해 DB 정보를 알아내면 된다. </p>
<p>주의할 점은, DB URL 맨 끝에 <code>?reconnect=true</code>가 붙어있는데, 얘를 지워줘야 한다. 얘는 rails 앱 사용시 필요한 애고, 파이썬 앱을 사용할 때는 삭제하라고 하고 있다. 붙여서 앱을 실행시켜봤더니 동작을 안했다. 
(<a href="https://devcenter.heroku.com/articles/cleardb#using-cleardb-with-python-django">공식 문서 참고</a>)</p>
<h2 id="4-procfile-만들기">4. Procfile 만들기</h2>
<p>DB까지 연결한 상태로 앱 대시보드 페이지로 가서 오른쪽 상단의 <code>open app</code>을 눌러 앱을 열어보았는데, 열리지 않았다. 그래서 터미널로 가서 <code>heroku logs --tail</code>로 에러 로그를 확인해보았다. 확인해보니 실행 중인 프로세스가 없다고 떴다. 생각해보니 앱을 실행시키지 않았다. </p>
<p>어떻게 실행시킬지 찾아보다가, heroku app을 실행시킬 명령어는 Profile에 정의해놔야 한다는 것을 알게 되었다. 공식 문서를 보면 Procfile은 &quot;Heroku apps include a Procfile that specifies the commands that are executed by the app on startup.&quot;이라고 나와있는데, 즉 실행시킬 명령어나 프로세스 타입 등을 써놓는 파일이다.
(<a href="https://devcenter.heroku.com/articles/procfile">공식 문서 참고</a>)</p>
<p>문서의 예시에 나와있는 것처럼 gunicorn을 설치하고 Procfile을 만들어주었다. </p>
<ol>
<li><code>pip install gunicorn</code>으로 gunicorn 설치하기 </li>
<li>requirements.txt 파일에 gunicorn 추가하기 </li>
<li>Procfile 만들기 (위치는 root directory. 다른 곳에 두면 못 읽는다)</li>
<li>Procfile에 <code>&lt;process type&gt;: &lt;command&gt;</code>의 형식으로 내용 추가하기
(예시)<pre><code>web: gunicorn wsgi:app</code></pre></li>
</ol>
<p>그런 다음 다시 push하고 앱 페이지에 접속해보니 앱이 잘 뜨는 것을 확인할 수 있었다. </p>
<p>여기까지 했는데 앱이 뜨지 않으면, <code>heroku restart</code>로 재시작해보거나, DB가 migrate되었는지 확인해봐야 한다. DB migration은 heroku와는 별개로 직접 해줘야 한다. </p>
<p><strong>참고) migration 관련 명령어</strong>
<code>flask db init</code> : migration folder가 없을 경우 DB 초기화하기
<code>flask db migrate</code> : migrate하기 
<code>flask db upgrade</code> : db 구조가 변경된 걸 반영하기</p>
<p>단, 모델이 추가되거나 테이블의 컬럼이 추가되는 것 이외에 컬럼의 속성이 바뀐 경우는 migrate/upgrade로 적용되지 않는다. 따라서 테이블을 삭제하고 다시 만들어야 한다.  </p>
<h2 id="5-추가-작업-google-oauth-로그인-관련">5. (추가 작업) Google Oauth 로그인 관련</h2>
<p>google oauth 로그인 기능을 달았었는데, 얘는 redirect url을 추가하는 작업을 해줘야 정상적으로 동작할 수 있어서 관련 설정을 해줬다. </p>
<ol>
<li><a href="https://console.cloud.google.com/apis/dashboard">Google developers dashboard</a> 가기 </li>
<li><code>Credentials</code>로 가서 <code>OAuth 2.0 Client IDs</code>의 앱 클릭 </li>
<li><code>Authorized redirect URIs</code>에 redirect url 추가하기 </li>
</ol>
<p>추가하고 google로 로그인해보니 잘 되는 것을 확인할 수 있었다. 끝!</p>
<hr>
<p>근데 개발을 막 시작할 때 만든 앱이라 자잘하고 큰 오류들이 너무 많다 ㅋㅋㅋ 조만간 오류를 수정하고 업데이트를 해야겠다. </p>
<h3 id="참고">참고</h3>
<ul>
<li>[ClearDB 관련] (<a href="https://devcenter.heroku.com/articles/cleardb#using-cleardb-with-python-django">https://devcenter.heroku.com/articles/cleardb#using-cleardb-with-python-django</a>)</li>
<li>[Procfile 관련] (<a href="https://devcenter.heroku.com/articles/procfile">https://devcenter.heroku.com/articles/procfile</a>)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[인공지능 웹서비스 팀프로젝트 셋째 주 회고 : Gitlab CI/CD]]></title>
            <link>https://velog.io/@jimin_lee/%EC%9D%B8%EA%B3%B5%EC%A7%80%EB%8A%A5-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%85%8B%EC%A7%B8-%EC%A3%BC-%ED%9A%8C%EA%B3%A0-Gitlab-CICD</link>
            <guid>https://velog.io/@jimin_lee/%EC%9D%B8%EA%B3%B5%EC%A7%80%EB%8A%A5-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%85%8B%EC%A7%B8-%EC%A3%BC-%ED%9A%8C%EA%B3%A0-Gitlab-CICD</guid>
            <pubDate>Wed, 09 Jun 2021 13:52:33 GMT</pubDate>
            <description><![CDATA[<h1 id="1-뭘-해볼까">1. 뭘 해볼까?</h1>
<p>우리 팀은 현재 agile 방법론을 적용해 sprint 단위로 목표를 설정해 작업을 하고 있다. agile 방법론의 지향점이 최대한 빠르게 서비스를 만들어 출시한 후 사용자들의 반응을 보면서 서비스를 개선해나가는 것이라는 점을 고려한 우리의 목표는 우선 이번 주까지 목표한 기본 기능 구현을 끝내고 서비스의 큰 틀을 완성하는 것이었다. </p>
<p>여기서 문제 아닌 문제가 생겼다. 다들 개발에 있어 각자의 속도가 있기 마련인데, 나는 내가 맡은 기능 개발을 생각보다 일찍 끝내게 되었다. 테스트 코드를 짜면서 개발해서인지 예상했던 것보다 코드 짜기가 훨씬 수월했다. 그래서 팀원들에게 내가 분담할 수 있을 만한 일을 물어봤는데, 애초에 기본적인 기능에 대한 구현만을 목표로 삼았기 때문에 다들 딱히 자기 일을 타인에게 맡길 만큼 타이트한 상황이 아니었다. 그렇다면 추가 기능을 먼저 개발하고 있으면 어떨까 하는 의견도 이야기해봤는데, 아무래도 아직 기본 기능 구현을 완전히 끝내지 않은 상황에서 추가 기능을 논의하기는 부담스러운 것 같았다. </p>
<p>그래서 잠시 주어진 여유로운 시간 동안 뭘 더 해볼 수 있을까 고민해보다가, <strong>CI/CD 파이프라인 구축을 통해 테스트 자동화 개발 환경 만들어보자</strong>, 고 마음먹었다.</p>
<h1 id="2-cicd가-왜-필요해">2. CI/CD가 왜 필요해?</h1>
<p>사실 테스트 자동화 개발 환경을 구축해보자고 생각한 것은 이전부터 한 번 해보고 싶다고 생각한 게 크다. 하지만 개인 프로젝트라면 모를까, 팀프로젝트에서 개인이 해보고 싶으니까 하자, 라는 이유는 새로운 기술을 도입하는 데에 충분한 이유가 될 수 없다고 생각한다. 새로운 시도에는 적응하는 데 드는 비용이 따르기 마련이고, 그러므로 명확한 니즈가 없다면 새로운 시도는 하지 않는 게 더 낫다. </p>
<p>그래서 CI/CD 도구가 왜 필요한지부터 알아보았다. 일단 공부해보고 필요하면 도입해보고, 필요 없다고 판단되면 버릴 생각이었다. </p>
<p>아래 정리한 내용은 <a href="https://youtu.be/JPDKLgX5bRg">다음의 강의</a>를 참고했다. </p>
<h2 id="cicd의-개념">CI/CD의 개념</h2>
<ul>
<li>CI(Continuous Integration) : 지속적인 통합<ul>
<li>무엇이 통합의 대상인가? ➡ 코드</li>
<li>여러 개발자들의 코드 베이스(소스 코드)를 계속해서 통합하는 것</li>
</ul>
</li>
<li>CD(Continuous Delivery) : 지속적인 배달<ul>
<li>무엇을 배달하는가? ➡ 서비스</li>
</ul>
</li>
</ul>
<h2 id="왜-필요한가">왜 필요한가?</h2>
<ul>
<li>빠르게 개발하려고</li>
<li>Merge Hell을 방지하기 위해</li>
<li>개발자의 멘탈 복지를 위해</li>
<li>다른 부수적인 부분에 신경쓰지 않고 코드만 짤 수 있는 환경을 만들기 위해</li>
<li>플로우 : 코드 작성 → 빌드 → 테스트 → 배포 ➡ 여기서 개발자의 역할은 코드 작성에서 끝나고, 나머지 일을 CI/CD 도구들에게 맡기는 것</li>
</ul>
<h2 id="결론">결론</h2>
<ul>
<li>CI/CD 파이프라인은 여러 stages로 구성된다. stage에는 lint, test, build, deploy와 같은 단계들이 포함될 수 있다.</li>
<li>현재 우리 팀의 서비스는 아직 배포하지 않은 단계이지만, lint와 test의 경우 커밋할 때마다 자동으로 검사해준다면 편리할 것 같았다. 특히 백엔드의 경우 lint 컨벤션이 명확히 합의되어 있지 않아 그때 그때 코멘트를 남기기 번거로웠고, 테스트 자동화 파이프라인 구축을 계기로 테스트 코드를 작성하며 개발하는 팀 환경이 갖추어지기를 기대해볼 수 있을 것 같았다.</li>
<li>결론 : 한 번 도입해보자!</li>
</ul>
<h1 id="3-어떤-도구를-사용할까">3. 어떤 도구를 사용할까?</h1>
<p>다음으로 고민한 점은 <strong>어떤 도구를 사용할지</strong>였다. Jenkins를 써보고 싶기는 한데, 아직 배포도 하지 않은 상황에서 lint와 test만을 해볼 수준에는 너무 과한 툴을 다는 게 아닐까? 하는 생각이 들었다. 또 지금 사용하는 깃랩에도 Github Actions처럼 CI/CD를 지원해주는 Gitlab CI/CD가 있었는데, 얘를 사용해봐도 좋을 것 같다는 생각이 들었다. 그래서 <a href="https://dzone.com/articles/jenkins-vs-gitlab-ci-battle-of-cicd-tools">Jenkins와 Gitlab CI/CD를 비교</a>해놓은 글을 찾아 읽어보았고, 결론적으로 따로 설치할 필요가 없고 CI/CD performance에 대한 분석을 제공하는 Gitlab CI/CD를 우선적으로 사용해보기로 했다. </p>
<h2 id="gitlab-cicd-사용하기">Gitlab CI/CD 사용하기</h2>
<p>정말 쉽다!! </p>
<p>루트 디렉토리에 <code>.gitlab-ci.yml</code> 파일을 만들어주면 끝이다. </p>
<p><code>.gitlab-ci.yml</code> 파일을 작성하는 방법도 어렵지 않다. </p>
<p><strong>stage</strong>와 <strong>job</strong>이라는 두 개념을 이해하면 된다. </p>
<p>stage란 build, test 처럼 파이프라인이 수행할 단계를 정의해주는 것이고, job은 각각의 stage가 구체적으로 어떤 내용을 실행할 것인지가 정의된 부분이라고 할 수 있다. </p>
<p>내가 작성한 내용은 다음과 같다. </p>
<ol>
<li><p>우선 stage를 정의해 준다. </p>
<pre><code> # lint와 test를 정의
 stages:
   - lint
   - test</code></pre></li>
<li><p>그 다음 job을 정의해준다. lint와 test 각각의 단계에 있어서 frontend와 backend로 job을 정의해 주었고, 그래서 총 4개의 job이 만들어졌다. </p>
<pre><code> lint-backend:
   stage: lint
   image: python:latest
   script:
     - autopep8 -i closet/*.py mypage/*.py recommend/*.py

 test-backend:
   stage: test
   image: python:latest
   services:
     - postgres:10.11
   variables:
     DATABASE_URL: &quot;postgresql://postgres:postgres@postgres:5432/ci&quot;
   script:
     - python manage.py test</code></pre><ul>
<li>stage : 어떤 stage인지 명시.</li>
<li>image : 어떤 도커 이미지를 사용할 것인지. 도커 컨테이너 기반으로 job이 실행되기 때문.</li>
<li>script : job의 세부 내용을 정의.</li>
<li>그 외의 부분들은 해당 job이 의존하고 있는 db나 환경변수 관련 부분이다.</li>
</ul>
</li>
</ol>
<p>딱 봐도 쉽게 이해가 될 것이다. 매우 간단하다!</p>
<p>이렇게 파일을 만들어주면, 그 이후부터는 깃랩이 파일을 인식하고 CI/CD 파이프라인이 돌아가기 시작한다. 과정과 결과는 왼쪽 사이드바의 <code>CI/CD</code> 탭에서 확인할 수 있다.
<img src="https://images.velog.io/images/jimin_lee/post/a93cb251-21ae-436f-9f8b-be5d56da6060/Untitled.png" alt="">
그리고 마찬가지로 왼쪽 사이드바의 <code>Analytics → CI/CD</code> 탭으로 들어가면 전체 파이프라인에 대한 분석 리포트를 제공한다.
<img src="https://images.velog.io/images/jimin_lee/post/6e842256-b9a3-48f7-91b8-6f23d9a8895e/Untitled%20(1).png" alt="">
또, 파이프라인의 결과를 이메일로 알려준다.
<img src="https://images.velog.io/images/jimin_lee/post/2a7ad203-0f8e-4cd7-b8c0-c59c763fe84a/Untitled%20(2).png" alt=""></p>
<h1 id="4-더-나아가기">4. 더 나아가기</h1>
<p>깃랩의 내장 기능을 통해 하나도 크게 힘들이지 않고 간단한 CI/CD 파이프라인을 만들어보았다. 다음주에는 본격적으로 VM에 배포하려고 하는데, 따라서 현재 파이프라인에서 deploy stage를 추가할 예정이다. </p>
<p>아직 모르는 게 많지만 빌드와 배포 부분을 더 공부해서 파이프라인을 보강해갈 생각이다. 다음 주도 화이팅! 👊</p>
<hr>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://dzone.com/articles/jenkins-vs-gitlab-ci-battle-of-cicd-tools">https://dzone.com/articles/jenkins-vs-gitlab-ci-battle-of-cicd-tools</a></li>
<li><a href="https://youtu.be/JPDKLgX5bRg">https://youtu.be/JPDKLgX5bRg</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커 컨테이너 환경에서 배포하기: Docker + Django + Nginx + Gunicorn]]></title>
            <link>https://velog.io/@jimin_lee/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jimin_lee/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 05 Jun 2021 17:05:39 GMT</pubDate>
            <description><![CDATA[<h2 id="1-vm에-도커-및-도커-컴포즈-설치하기">1. VM에 도커 및 도커 컴포즈 설치하기</h2>
<p>💻 VM 환경 : Ubuntu 18.04.5 LTS (버전 확인하기 : <code>cat /etc/issue</code> )</p>
<h3 id="1-1-도커-설치하기">1-1. 도커 설치하기</h3>
<p>윈도우나 맥은 도커 데스크탑을 설치하면 되니까 간편한데, 우분투에서는 패키지 매니저를 통해 도커 엔진을 설치해줘야 한다. 도커 엔진을 설치하기 위해서는 도커 레포지토리 세팅부터 해야 한다. </p>
<p>1) 의존 패키지 설치하기</p>
<pre><code class="language-bash">sudo apt update
sudo apt install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release</code></pre>
<p>2) 도커 공식 레포지토리 설치를 위한 GPG key 추가하기 </p>
<p>GPG key는 두 주체 사이의 안전한 통신을 보장해주는 거라고 설명이 나와있는데, 설치를 안전하게 하기 위해 추가된 단계인 것 같다. </p>
<pre><code class="language-bash">curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -</code></pre>
<p>3) 도커 레포지토리 설치하기  </p>
<pre><code class="language-bash">sudo add-apt-repository &quot;deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable&quot;</code></pre>
<p>4) 우분투 디폴트 레포지토리 말고 도커 레포지토리에서 설치하도록 설정하기</p>
<pre><code class="language-bash">sudo apt update
apt-cache policy docker-ce</code></pre>
<p>아래처럼 뜨는지 확인한다. </p>
<pre><code>docker-ce:
  Installed: (none)
  Candidate: 18.03.1~ce~3-0~ubuntu
  Version table:
     18.03.1~ce~3-0~ubuntu 500
        500 https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages</code></pre><p>Installed가 none으로 뜨는 것은 아직 docker-ce를 설치하기 않았기 때문이다. </p>
<p>5) 도커 엔진 설치하기 </p>
<pre><code class="language-bash">sudo apt install docker-ce</code></pre>
<p>6) 도커 실행 확인하기</p>
<pre><code class="language-bash">sudo systemctl status docker</code></pre>
<h3 id="1-2-도커-컴포즈-설치하기">1-2. 도커 컴포즈 설치하기</h3>
<p>1) 도커 컴포즈 설치하기</p>
<pre><code class="language-bash">sudo curl -L &quot;https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose</code></pre>
<p>2) 도커 컴포즈가 설치된 경로에 실행 권한 주기</p>
<pre><code class="language-bash">sudo chmod +x /usr/local/bin/docker-compose</code></pre>
<p>3) 도커 및 도커 컴포즈 설치 확인하기</p>
<pre><code class="language-bash">docker version
docker-compose version</code></pre>
<h2 id="2-배포용-도커-컨테이너-파일-만들기">2. 배포용 도커 컨테이너 파일 만들기</h2>
<h3 id="2-1-도커-도커-컴포즈파일-분리하기">2-1. 도커, 도커 컴포즈파일 분리하기</h3>
<p>각각 개발용, 배포용으로 분리했다. </p>
<p>Dockerfile.dev &amp; Dockerfile.prod</p>
<p>docker-compose.dev.yml &amp; docker-compose.prod.yml</p>
<h3 id="2-2-gunicorn-관련-작업하기">2-2. Gunicorn 관련 작업하기</h3>
<p>1) requirements.txt에 Gunicorn 추가하기</p>
<p><code>gunicorn==20.0.4</code> </p>
<p>2) djangoapp 컨테이너의 command를 gunicorn 명령어로 수정하기</p>
<pre><code class="language-docker"># docker-compose.prod.yml
# gunicorn project_name.wsgi:app_name --bind 0.0.0.0:8000
command: gunicorn closet.wsgi:application --bind 0.0.0.0:8000</code></pre>
<h3 id="2-3-배포용-도커-컴포즈-파일-만들기">2-3. 배포용 도커 컴포즈 파일 만들기</h3>
<p>배포용 도커 파일의 경우 개발용과 크게 달라진 부분은 없는데, 유저를 생성해서 권한을 제한하는 게 아무래도 좋을 것 같다. 그래서 우선은 개발용과 배포용을 분리해놓았고, 유저 관련 부분을 차후 추가할 예정이다. 우선은 건너뛰었다. </p>
<p>배포용 컴포즈 파일의 경우 build context를 Dockerfile.prod로 명시해주고, 개발용이 아니기 때문에 volume 부분이 필요없으므로 지워준다. </p>
<pre><code class="language-docker">service:
  djangoapp:
    build:
        context: .
        dockerfile: Dockerfile.prod
    command: gunicorn closet.wsgi:application --bind 0.0.0.0:8000
    ports:
    - &quot;8000:8000&quot;
    depends_on:
      - db</code></pre>
<h2 id="3-nginx-관련-파일-만들기">3. Nginx 관련 파일 만들기</h2>
<h3 id="3-1-nginx-컨테이너-추가하기">3-1. Nginx 컨테이너 추가하기</h3>
<p>배포용 컴포즈 파일에 Nginx 컨테이너를 다음과 같이 추가해준다. </p>
<pre><code class="language-docker">nginx:
  build: ./nginx
  ports:
    - &quot;80:80&quot;
  depends_on:
    - djangoapp </code></pre>
<h3 id="3-2-관련-파일-만들기">3-2. 관련 파일 만들기</h3>
<p>다음의 구조로 Nginx용 도커 파일과 Nginx 설정 파일을 추가해준다. </p>
<pre><code>└── nginx
    ├── Dockerfile
    └── nginx.conf</code></pre><p>Nginx Dockerfile</p>
<pre><code class="language-docker">FROM nginx:1.19.0-alpine

RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d</code></pre>
<p>Nginx 환경설정 파일</p>
<pre><code>upstream &lt;backend&gt; {
    server djangoapp:8000;
}

server {

    listen 80;

    location / {
        proxy_pass http://&lt;backend&gt;/;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

}</code></pre><h3 id="3-3-djangoapp-컨테이너-수정하기">3-3. djangoapp 컨테이너 수정하기</h3>
<p>도커 컨테이너끼리만 내부적으로 포트를 열어주면 되므로 ports → expose로 바꾼다. </p>
<pre><code>service:
  djangoapp:
    build:
        context: .
        dockerfile: Dockerfile.prod
    command: gunicorn closet.wsgi:application --bind 0.0.0.0:8000
    expose:
      - &quot;8000&quot;
    depends_on:
      - db</code></pre><h2 id="4-실행시켜보기">4. 실행시켜보기</h2>
<p>컨테이너 실행시키기</p>
<p><code>docker-compose -f docker-compose.prod.yml up --build -d</code></p>
<p>DB migrate하기</p>
<p><code>docker exec backend_djangoapp_1 python manage.py migrate</code></p>
<p>로그 확인하기</p>
<p><code>docker logs backend_djangoapp_1</code> </p>
<p>80번이 열려있으므로 그냥 DNS로 접속해보면 장고 서버 페이지가 뜨면 된다. 그런데 /admin/으로 접속해보니 CSS가 깨져있었다. 원인을 찾아보니 static file 관련 작업을 안해줘서 그런 것이었다. </p>
<h2 id="5-static-file-관련-설정하기">5. Static file 관련 설정하기</h2>
<h3 id="5-1-settingspy에-static-file-path-추가하기">5-1. settings.py에 static file path 추가하기</h3>
<pre><code class="language-python">STATIC_URL = &quot;/static/&quot;
STATIC_ROOT = os.path.join(BASE_DIR, &quot;static&quot;) </code></pre>
<h3 id="5-2-docker-composeprodyml-파일에-static-volume-추가하기">5-2. docker-compose.prod.yml 파일에 static volume 추가하기</h3>
<p>djangoapp과 nginx가 동일한 static volume(/backend/static) 디렉토리를 공유하도록 설정  </p>
<pre><code class="language-yaml">djangoapp:
  volumes:
      - static_volume:/backend/static
nginx:
  volumes:
      - static_volume:/backend/static</code></pre>
<h3 id="5-3-dockerfileprod-에-static-file-디렉토리-만들어주는-코드-추가">5-3. Dockerfile.prod 에 static file 디렉토리 만들어주는 코드 추가</h3>
<p>docker compose는 volume file을 마운트할 때 root user로 작업하는데, root user로 작업하면 상관없지만 non-root user로 접근하면 /static이라는 디렉토리를 만들 때 존재하지 않는 디렉토리를 만들려고 하면 permission denied가 뜰 수 있기 때문에 관련 코드 한 줄을 추가해준다. </p>
<pre><code>ENV APP_HOME=/backend
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/static
WORKDIR $APP_HOME</code></pre><h3 id="5-4-nginx-config-파일에-static-경로-관련-설정-추가">5-4. Nginx config 파일에 static 경로 관련 설정 추가</h3>
<pre><code>upstream &lt;backend&gt; {
    server djangoapp:8000;
}

server {

    listen 80;

    location / {
        proxy_pass http://&lt;backend&gt;/;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

        location /static/ {
                alias /backend/static/; &lt;- static file 경로
            }

}</code></pre><h3 id="5-5-static-file을-static-file-디렉토리에-복사하기">5-5. static file을 static file 디렉토리에 복사하기</h3>
<p><code>docker exec backend_django_1 python manage.py collectstatic --no-input --clear</code></p>
<p><code>collectstatic</code>은 static root 디렉토리에 static file을 복사하는 명령어이다.  </p>
<p>clear 옵션은 static root 경로에 이미 static file이 존재하는 경우 싹 다 지우고 다시 복사하는 옵션이다. 앱이 삭제된다든지 하는 경우 쓸모없는 static file이 존재할 수도 있기 때문에 <code>collectstatic</code>을 여러번 하게 되면 같이 붙여주면 좋다. </p>
<p>이제 다시 admin 페이지나 swagger,  redoc 페이지로 접근하게 되면 css가 잘 적용되는 것을 확인할 수 있다!</p>
<hr>
<h2 id="참고">참고</h2>
<h3 id="도커-및-도커-컴포즈-설치">도커 및 도커 컴포즈 설치</h3>
<ul>
<li><a href="https://docs.docker.com/engine/install/ubuntu/">https://docs.docker.com/engine/install/ubuntu/</a></li>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-18-04">https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-18-04</a></li>
<li><a href="https://docs.docker.com/compose/install/">https://docs.docker.com/compose/install/</a></li>
</ul>
<h3 id="배포용-도커-파일-및-nginx-파일-만들기">배포용 도커 파일 및 Nginx 파일 만들기</h3>
<p><a href="https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx">https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx</a></p>
<h3 id="collectstatic-관련">collectstatic 관련</h3>
<ul>
<li><a href="https://docs.djangoproject.com/en/3.2/ref/contrib/staticfiles/#django-admin-collectstatic">https://docs.djangoproject.com/en/3.2/ref/contrib/staticfiles/#django-admin-collectstatic</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[인공지능 웹서비스 팀프로젝트 둘째 주 회고 : 도커, TDD]]></title>
            <link>https://velog.io/@jimin_lee/%EC%9D%B8%EA%B3%B5%EC%A7%80%EB%8A%A5-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%91%98%EC%A7%B8-%EC%A3%BC-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@jimin_lee/%EC%9D%B8%EA%B3%B5%EC%A7%80%EB%8A%A5-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%91%98%EC%A7%B8-%EC%A3%BC-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 23 May 2021 18:00:01 GMT</pubDate>
            <description><![CDATA[<h2 id="1-드디어-개발-시작">1. 드디어 개발 시작!</h2>
<p>저번 주에는 기획 위주로 진행을 했다면, 이번 주부터는 본격적으로 기능 개발에 들어갔다. 기획할 때도 정말 머리가 터질 것 같았지만, 개발 주간에 들어와서도 머리 아프게 생각해야 할 일이 많았다. 특히 이번에는 5주라는 긴 시간도 있고 엘리스에서 하는 마지막 프로젝트인 만큼 해보고 싶은 건 다 해보자는 생각에 이것저것 다양한 것들을 시도해보기로 해서 공부하고 고민할 게 저번보다 훨씬 많아진 것 같다. </p>
<p>이번 한 주 동안 새롭게 배운 내용과 생각지 못한 여러 에러를 마주하며 해결해간 과정을 돌이켜보고자 한다.</p>
<h2 id="2-dockerized-development--도커로-개발하기">2. Dockerized Development : 도커로 개발하기</h2>
<p>지난 프로젝트 때 도커를 사용해봤는데, 그때는 도커를 잘 아시는 팀원이 계셔서 그분이 도커 파일을 만들어오시고 사용 방법을 다 알려주셨다. 그래서 그때는 도커를 뭔가 알고 사용한다기 보다는 아무것도 모르면서 그냥 시키는 대로 명령어를 입력해서 작업을 했고, 왜 도커를 써야 하는지, 컨테이너 환경에서 작업하고 배포하는 게 무슨 이점이 있는지 제대로 알 지도 못한 상황이었기 때문에 도커를 안 써도 그만 써도 그만인 입장에서 쓰자는 다수의 의견에 그저 탑승한 것에 지나지 않았다.  </p>
<p>그게 너무 아쉬워서 이번에는 도커가 뭐고 쓰면 뭐가 좋은 건지 공부했고, 개발 환경과 배포 환경의 일관성을 유지하는 게 개발 외의 시간에 투입되는 에너지 소모를 줄일 수 있다는 점을 들어 팀원들을 설득해서 팀 전체가 도커 환경에서 작업해보기로 결정했다. </p>
<p>도커 파일과 도커 컴포즈 파일을 만드는 과정은 <a href="https://docs.docker.com/samples/django/">Django+PostgreSQL 레퍼런스</a>가 있어서 수월했는데, 문제는 그 다음부터였다. </p>
<h3 id="문제-1-컨테이너-안에서-어떻게-개발할까">문제 1. 컨테이너 안에서 어떻게 개발할까?</h3>
<p>백엔드 디렉토리에서 도커 파일과 도커 컴포즈 파일을 만들고 <code>docker-compose up --build -d</code> 명령을 실행시켰다. </p>
<p>그런데 VSCode로 생성된 장고 앱 컨테이너에 접속해서 개발을 하려고 하니 여기서 파일 수정을 한 후 어떻게 원격 저장소에 push를 할 지가 고민이었다. 장고 컨테이너에는 백엔드 디렉토리의 파일밖에 없는데, 얘를 바로 저장소에 올리면 프론트엔드의 파일과 루트 디렉토리의 파일들은 어떻게 되는 거지?? 하는 생각이 들었다. </p>
<p>답은 간단했다. volume 옵션을 주고 로컬의 백엔드 디렉토리와 컨테이너의 디렉토리를 연결해놓으면 컨테이너에서 작업한 사항이 바로 로컬에 반영이 된다! volume을 데이터 백업 용으로만 생각했는데, 소스 코드에도 적용이 된다. 정말 신기하고 도커는 알면 알수록 재미있는 것 같다. </p>
<p>그래서 현재의 작업 플로우는 다음과 같다. </p>
<p>도커 컨테이너 실행하기 → 장고 컨테이너에 vscode 접속 → 작업 → 로컬로 돌아와서 수정 사항 commit 후 원격 저장소로 push → 컨테이너 종료</p>
<h3 id="문제-2-db-컨테이너와-장고-컨테이너를-어떻게-연결시킬까">문제 2. DB 컨테이너와 장고 컨테이너를 어떻게 연결시킬까?</h3>
<p>장고 컨테이너와 postgresql 컨테이너 두 개를 띄워 놓고 서로 연결시키려고 했는데, </p>
<pre><code class="language-bash">SQLSTATE[08006] [7] could not connect to server: Connection refused
Is the server running on host “0.0.0.0” and accepting
TCP/IP connections on port 5432? </code></pre>
<p>이런 에러가 뜨면서 연결이 되지 않았다. </p>
<p>도커 컴포즈 파일의 환경 변수를 바꿔보고 포트도 바꿔 보는 등 여러 가지를 시도했는데도 연결이 안 되어서 한참 해결책을 찾아보았다. </p>
<p>그러다가 .env 파일의 DB host 값을 바꿔주었더니 해결이 되었다.  DB host를 DB 컨테이너 이름으로주면 알아서 컨테이너 서버를 인식하는 것 같다. </p>
<pre><code>DATABASE_HOST=db   # 얘를 postgresql 컨테이너 이름으로 주면 된다. </code></pre><h3 id="문제-3-갑자기-이미지-빌드가-안된다">문제 3. 갑자기 이미지 빌드가 안된다?</h3>
<p>새로운 패키지를 설치한 다음 <code>docker-compose up --build</code> 명령어를 실행했는데, 갑자기 다음과 같은 에러가 뜨면서 컨테이너 이미지가 만들어지지 않았다. </p>
<pre><code class="language-bash">ERROR [internal] load metadata for docker.io/library/python:3 </code></pre>
<p>찾아보니 해결책이 제각각이었다. 누구는 도커 허브에 회원가입하고 테미널에서 인증하니 해결이 되었다는 사람도 있었고, 그냥 아예 터미널을 종료하고 재시작하니 되었다는 사람도 있었다. 나는 전자를 따라해보았다. </p>
<ol>
<li><a href="https://hub.docker.com/">docker hub</a> 회원가입</li>
<li><code>docker login</code> 후 사용자 정보 입력</li>
<li>다시 <code>docker-compose</code> 명령 실행 </li>
<li>작동 잘 됨! </li>
</ol>
<p>해결이 되었다.</p>
<h3 id="문제-4-도커-컴포즈-파일에서-민감한-정보를-어떻게-숨겨야-할까">문제 4. 도커 컴포즈 파일에서 민감한 정보를 어떻게 숨겨야 할까?</h3>
<p>도커 컴포즈 파일에서 DB 컨테이너를 만들 때 환경 변수로 DB 패스워드를 넘겨주는데, 얘도 비밀번호인 이상 민감한 정보라고 생각해서 환경 변수를 파일로 만들어 감춰버리는 것처럼 숨기고 싶었는데, 어떻게 할 지 찾아보다가 두 가지 방법을 발견했다. </p>
<ol>
<li>docker secret 만들기 : docker secret은 docker의 특정 경로에 secret 객체를 생성해서 비밀번호를 저장하는 방법<ul>
<li><a href="https://docs.docker.com/engine/swarm/secrets/">https://docs.docker.com/engine/swarm/secrets/</a></li>
</ul>
</li>
<li>env file 만들기 : 환경 변수 파일을 만들어서 도커 컴포즈 파일에서 그 경로를 지정하는 방법<ul>
<li><a href="https://docs.docker.com/compose/environment-variables/#the-env_file-configuration-option">https://docs.docker.com/compose/environment-variables/#the-env_file-configuration-option</a></li>
<li><a href="https://kin3303.tistory.com/18">https://kin3303.tistory.com/18</a></li>
</ul>
</li>
</ol>
<p>그런데 생각해보니 DB 컨테이너를 개발용으로밖에 사용하지 않을 것 같아서 그냥 그대로 올리기로 했다.</p>
<h2 id="3-get-tdd-with-django--장고로-tdd-하기">3. Get TDD With Django : 장고로 TDD 하기</h2>
<p>장고로 API를 만들면서 Django Rest Framework에서 제공하는 API Test Case가 있길래, 좋은 기회라고 생각해서 TDD를 시도해보았다. </p>
<p>그런데 뭔가 이상했다. TDD가 실패하는 코드에서 시작해 성공하는 코드를 만들어가는 과정이란 것은 알고 있지만, 아무리 코드를 빌드해도 성공이 뜨지 않았다. 분명 DB를 까보면 데이터가 존재하는데, 테스트 코드를 실행시키면 계속 <code>matching query does not exist</code> 라는 에러 로그가 떴다. api url이 잘못되었나 싶어서 확인해보니 그것도 아니었고, <code>runserver</code>를 통해 api url로 접속해보아도 잘 접근이 되었는데, 테스트 코드를 돌리기만 하면 실패가 떠서 도대체 뭐가 문젠지 알 수 없었다. </p>
<p>그래서 일단 DB에서 데이터를 잘 가져오는지 확인하는 테스트 코드를 짜서 돌려봤는데, 얘도 실패했다. 이상해서 찾아보니, <strong>장고(가 상속하는 unittest)는 테스트 코드를 돌릴 때 테스트 DB를 따로 만들기 때문에 아래 코드처럼</strong> <code>setUp</code><strong>에서 따로 데이터를 만들어 줘야 했다.</strong> 나는 만들어주지 않고 데이터를 가져오라고 시켰기 때문에 내 코드는 빈 DB에서 데이터를 가져오려고 한 것이고, 그래서 계속 <code>404 NOT FOUND</code> 가 떴던 것이다. </p>
<pre><code class="language-python">class TestUserProfileView(APITestCase):
    # 여기서 model instance를 생성해줘야 한다. 
    def setUp(self):
        self.url = reverse(&#39;mypage:profile&#39;, kwargs={&#39;user_id&#39;:1}) 
        self.user = TestUser.objects.create(name=&#39;marina&#39;) 
        self.user2 = TestUser.objects.create(name=&#39;kevin&#39;) 

    def test_get_profile(self):
        response = self.client.get(self.url) 
        serializer = UserSerializer(self.user)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data, serializer.data)</code></pre>
<p>그래서 데이터를 만들어줬다. 그리고 다시 테스트를 돌려봤는데, 또 실패했다! 💢 </p>
<p>도대체 문제가 뭔지... 또 한참 찾았다. 너무 답답해서 해당 request 데이터에 해당하는 애를 콘솔에 찍어봤는데, pk가 2가 나오는 것이었다...! 나는 얘 하나밖에 안 만들어줬는데!?? ❓ </p>
<p>이상해서 다른 애를 하나 더 만들고 또 찍어봤는데, 이번에는 똑같은 애의 pk가 3이 나왔다! 아까는 2였는데...??? 또 다른 애를 하나 더 만들어보니 아까는 3이었던 애의 pk가 이번에는 4가 되어 있었다. 이해가 안 되어서 찾아보니, <code>django.test.TestCase</code>의 TestCase가 아니라 <code>unittest.TestCase</code>의 TestCase를 상속받아서 사용하면 모종의 이유로 <strong>pk의 일관성을 유지하지 못한다</strong>는 것 같았다. DRF의 APITestCase가 unittest의 TestCase를 상속받나 보다. 테스트를 몇 번 해본 결과 내가 <strong>생성한 데이터의 수만큼 pk가 offset</strong>되는 것 같다. </p>
<pre><code class="language-python">class TestUserProfileAPIView(APITestCase):
    def setUp(self):
    # 생성한 데이터의 수만큼 pk가 offset되기 때문에 user1의 pk는 3이고 user2의 pk는 4가 된다. 
        self.url = reverse(&#39;mypage:profile&#39;, kwargs={&#39;user_id&#39;:3}) 
        self.user1 = TestUser.objects.create(name=&#39;marina&#39;) # pk=3
        self.user2 = TestUser.objects.create(name=&#39;kevin&#39;) # pk=4</code></pre>
<p>TransactionTestCase를 상속받아 사용하면 pk가 유지된다고 하는데, 일단은 여기까지만 이해해도 앞으로 테스트 코드를 작성하는데 무리는 없을 것 같아서 그대로 진행했다. 이미 이 문제 때문에 하루 반나절 가량의 시간과 거기에 상응하는 에너지를 소모했기 때문에 더 알아볼 기력이 없었다. 테스트할 때 pk를 유지하는 방법에 대해서는 다음에 더 자세히 공부해봐야 할 것 같다.</p>
<h2 id="4-gather-at-gather-town--우리도-이제-gather-써요">4. Gather at Gather Town : 우리도 이제 Gather 써요</h2>
<p>팀원들에게 게더를 써보자고 건의해서 다 같이 스크럼을 게더에서 하게 되었다.
<img src="https://images.velog.io/images/jimin_lee/post/3a982ef4-97e9-477d-a4ed-11431f778ced/InkedUntitled%20(3)_LI.jpg" alt=""></p>
<p>도입 결과 다들 만족도가 크다고 하셨다. 일단 캐릭터와 맵이 아기자기하고 귀엽고, 실제로 서로가 만나는 느낌이 있어서 실제로 오프라인 상에서 협업을 하는 것 같았다.
(루프탑에서 진행한 스크럼... 분위기 최고!)</p>
<p><img src="https://images.velog.io/images/jimin_lee/post/b4f5b2f1-9416-4a86-a3cd-8540e0b0e00f/Untitled%20(4).png" alt="">
테트리스 게임도 같이 해보고, 캐치 마인드도 하면서 즐거운 시간을 보냈다. 거기서 바로 만나서 대화하기에도 편했다. 앞으로도 잘 사용할 것 같다. 👍</p>
<h3 id="5-셋째-주를-준비하며">5. 셋째 주를 준비하며</h3>
<p>일단 팀이 기획한 서비스의 큰 틀을 이번 주에 거의 구현을 해놓았기 때문에, 다음 주는 이제 Mock 데이터를 넣어보거나 인공지능 모델을 돌려보는 작업을 하려고 한다. 아직 주어진 시간이 꽤나 많이 남은 만큼, 더 다양한 것들을 시도해보고 싶다. </p>
<p>일단은 <strong>API 문서화</strong>를 위한 도구인 drf-yasg 패키지를 설치해놓았는데, 얘를 통해 만들어진 api 문서 페이지를 더 보기 좋게 꾸며 보려고 한다. </p>
<p>또 다음주부터는 본격적으로 젠킨스를 공부해서 <strong>Dockerized CI/CD 환경</strong>을 구성해보려고 한다. </p>
<p>다음 주도 화이팅! 💪</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker 100% 사용법]]></title>
            <link>https://velog.io/@jimin_lee/Docker-100-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@jimin_lee/Docker-100-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Sun, 23 May 2021 17:55:46 GMT</pubDate>
            <description><![CDATA[<p>(100프로는 아니고 한 10프로 정도 사용법..?)</p>
<h2 id="1-도커란">1. 도커란?</h2>
<h3 id="what">WHAT</h3>
<p>&quot;Docker is an open platform for developing, shipping, and running applications. Docker enables you to <strong>separate your applications from your infrastructure</strong> so you can deliver software quickly.&quot; </p>
<p>도커는 인프라 리소스로부터 어플리케이션을 분리해서 개발할 수 있는 환경을 제공해주는 오픈 소스 플랫폼이라고 할 수 있다. </p>
<h3 id="why">WHY</h3>
<p>&quot;With Docker, <strong>you can manage your infrastructure in the same ways you manage your applications</strong>. By taking advantage of Docker’s methodologies for shipping, testing, and deploying code quickly, you can significantly <strong>reduce the delay between writing code and running it in production</strong>.&quot;</p>
<p>프레임워크나 라이브러리의 버전이 일치하지 않아서 개발 서버에서 실행된 프로그램이 프로덕션 서버에서는 실행되지 않는 문제에 대한 솔루션을 제공한다. </p>
<h3 id="how">HOW</h3>
<p>&quot;Docker provides the ability to package and run an application <strong>in a loosely isolated environment called a container</strong>. The isolation and security allow you to run many containers simultaneously on a given host. Containers are lightweight and contain everything needed to run the application, so you do <strong>not need to rely on what is currently installed on the host</strong>. You can easily share containers while you work, and be sure that everyone you share with gets <strong>the same container that works in the same way</strong>.&quot;</p>
<ul>
<li>리소스로부터 어플리케이션을 격리시킬 수 있는 <strong>컨테이너</strong> 환경을 제공한다.</li>
<li>컨테이너는 독립적인 환경이기 때문에 특정 호스트의 리소스에 의존할 필요가 없다.</li>
<li>반대로 말해, 어느 호스트에서든 동일한 개발 및 실행 환경을 보장해준다.</li>
</ul>
<h3 id="가상-머신-vs-컨테이너">가상 머신 vs. 컨테이너</h3>
<p><strong>가상 머신(Virtual Machine)</strong></p>
<ul>
<li>CPU와 같은 <strong>하드웨어 자원을 가상화</strong>해서 새롭게 컴퓨터 한 대를 만드는 것</li>
<li>OS 위에 새로운 OS를 만들어서 독립적인 환경을 구축함</li>
<li><strong><code>참고</code></strong> 가상화란? 물리 리소스를 추상화하는 것. 추상화한다는 말의 의미는 간단히 말해 구체적인 실체를 가진 자원에서 기능이나 특징만을 뽑아내 재사용하기 위해 개념 단위로 함수화하는 것이라고 할 수 있음</li>
</ul>
<p><strong>컨테이너(Container)</strong></p>
<ul>
<li>리눅스의 컨테이너 기술을 이용해서 하드웨어 자원을 가상화하지 않고 <strong>프로세스만 격리</strong>해서 빠르게 실행시키는 것</li>
<li>기존의 운영체제 안에서 프로세스를 격리시켜서 마치 가상 머신을 설치하는 것과 동일한 효과를 냄</li>
<li>컨테이너는 기존의 시스템 자원을 공유하기 때문에 가상 머신이 아니라 리얼 머신에서 돌아감</li>
<li>하나의 OS상에서 여러 컨테이너가 돌아갈 수 있기 때문에 리소스를 한 곳에서 관리하기 쉬움</li>
<li>용량이 크고 느린 가상 머신의 단점을 보완할 수 있음</li>
</ul>
<h3 id="도커-이미지와-도커-허브">도커 이미지와 도커 허브</h3>
<p><strong>도커 이미지</strong>는 어플리케이션 실행 환경을 구축해주는 파일이라고 생각하면 되고, <strong>도커 허브</strong>는 그런 이미지들을 업로드해놓고 받아서 쓸 수 있는 플랫폼이라고 할 수 있다. </p>
<p>쉽게 말해서 깃과 깃허브의 관계처럼, 파일을 만들어서 클라우드에 올려놓고 다른 곳에서 파일을 가져다 쓸 수 있게 해놓은 것이라고 생각하면 된다. </p>
<p>작업 플로우 : 도커 이미지 파일 작성 → 도커 허브에 업로드(push) → 도커 허브에서 도커 이미지를 받아와서(pull) 사용 </p>
<p>도커가 컨테이너 기술을 개발했다기 보다는, 리눅스 자체에 이미 있던 컨테이너 기술을 활용하면서 이를 도커 허브와 연계시켜서 좀 더 편하게 컨테이너를 사용할 수 있게 한 것이라고 할 수 있다.  </p>
<h3 id="이미지-vs-컨테이너">이미지 vs. 컨테이너</h3>
<ul>
<li>이미지 : 개발 환경을 구축하는 <strong>실행 파일</strong></li>
<li>컨테이너 : 그 이미지를 실행한 상태, 즉 <strong>프로세스</strong></li>
<li>하나의 이미지로 여러 개의 컨테이너를 만들 수 있음</li>
</ul>
<h2 id="2-도커-사용법--설치부터-실행까지">2. 도커 사용법 : 설치부터 실행까지</h2>
<h3 id="도커-설치하기">도커 설치하기</h3>
<p>WSL2 환경에서 도커를 설치하는 법을 설명하려고 한다.  </p>
<ol>
<li><a href="https://docs.docker.com/docker-for-windows/install/">도커 데스크탑을 다운</a>받아 설치한다. 이 때  <code>Install required Windows components for WSL 2</code> 옵션을 선택하도록 한다. </li>
<li>설치가 완료되면 도커 데스크탑을 실행시키고 <code>Settings → General</code>에서 다음과 같이 되어 있는지 확인해보고 체크가 안 되어 있으면 체크한 후 Apply &amp; Restart 한다.
<img src="https://images.velog.io/images/jimin_lee/post/8bf45518-cdb9-49df-bd50-17f89878f875/Untitled.png" alt=""></li>
<li>명령 프롬프트에서 <code>docker</code>를 입력해서 커맨드 옵션이 뜨면 설치 완료이다. </li>
</ol>
<h3 id="도커-기본-명령어">도커 기본 명령어</h3>
<p><strong>이미지 관련 명령어</strong></p>
<p><code>docker images</code> : 가지고 있는 이미지 확인하기 → 참고로 TAG는 버전을 의미</p>
<p><code>docker image push image_name:tag</code> : 도커 허브의 저장소에 이미지 올리기 </p>
<p><code>docker pull image_name:tag</code> : 저장소에서 이미지 받아오기</p>
<p><code>docker search TERM</code> : 이미지 검색하기 → 이미지 이름 앞에 아이디가 없으면 공식 이미지이고 아이디가 있으면 사용자 커스터마이징 이미지</p>
<p><code>docker build -t image_name:tag .</code> : 이미지 생성하기 </p>
<ul>
<li><code>-t</code> : 이름 지정하는 옵션</li>
<li><code>.</code> : 현재 디렉토리의 Dockerfile을 읽어서 이미지를 만듦</li>
<li><code>-f</code> : 읽어올 파일을 알려주는 옵션. 디폴트로 Dockerfile이라는 파일을 찾아서 읽는데, <code>-f this_file</code> 이라는 옵션을 주면 해당 파일을 읽고 실행시킨다.</li>
</ul>
<p><code>docker rmi image_name</code> : 이미지 삭제하기 </p>
<p><strong><code>참고</code></strong> alpine 이미지 : 일반적으로 우분투 같은 base image를 받으면 기본적인 기능이 포함되어 있는데, 따로 설치하지 않아도 되니까 편리하기는 한데 용량이 너무  커지는 문제가 있다. alpine 이미지는 이런 기본 기능을 전부 제외하고 오로지 우분투만 받아오는 것이다.</p>
<p><strong>컨테이너 관련 명령어</strong> </p>
<p><code>docker ps</code> : 컨테이너 확인하기</p>
<ul>
<li><code>-a</code> : 종료된 컨테이너까지 확인하기</li>
</ul>
<p><code>docker run -it image_name:tag /bin/bash</code> : 컨테이너 생성 및 실행하기 </p>
<ul>
<li><code>-i</code> : --interactive. 사용자로부터 인풋을 받도록 하는 옵션</li>
<li><code>-t</code> : --tty. 터미널을 여는 옵션</li>
<li><code>--name</code> : 컨테이너의 이름을 지정하는 옵션</li>
<li><code>--rm</code> : 도커 컨테이너가 실행을 끝내고 종료되면 컨테이너를 삭제하는 옵션</li>
<li>마지막의 <code>/bin/bash</code>는 커맨드이다.</li>
<li><code>-d</code> : 데몬(daemon, detached mode)으로 백그라운드에서 실행시키는 옵션</li>
<li><code>-p</code> : 포트 넘버 지정하는 옵션 ex) <code>-p 8000:80</code> : 외부에서(로컬에서) 8000번으로 들어왔을 때 도커 컨테이너의 80번 포트로 포트 포워딩을 해주겠다는 의미</li>
</ul>
<p><code>docker start container_name</code> : 도커 컨테이너 실행시키기</p>
<p><code>docker attach container_name</code> : 컨테이너 안으로 들어가기 </p>
<ul>
<li><code>exit or ctrl+d</code> : 컨테이너에서 나가기 + 컨테이너 종료</li>
<li>컨테이너에 들어간 상태에서 컨테이너를 종료시키지 않고 빠져나오려면 <code>ctrl + P + Q</code> 를 입력하면 됨</li>
<li><strong><code>참고</code></strong> run vs start : start 명령어는 이미 생성되어 있는 컨테이너를 실행시키는 것이고, run 명령어는 이미지를 가지고 새롭게 컨테이너를 만들어서 실행시키는 것. 여기에 -it 옵션까지 더하면 컨테이너 생성 + start + attach 라고 생각하면 됨.</li>
</ul>
<p><code>docker stop container_name</code> : 컨테이너 실행 중지시키기</p>
<p><code>docker rm container_name</code> : 컨테이너 삭제하기</p>
<p><strong><code>참고</code></strong> : 하나의 이미지로 여러 개의 컨테이너를 실행할 수 있음. 그래서 컨테이너를 삭제하는 것과 이미지를 삭제하는 것은 완전히 다른 개념!</p>
<p><code>docker exec container_name COMMAND</code> : 실행 중인 도커 컨테이너 밖에서 컨테이너 안으로 명령 하기 ex) <code>docker exec container_name touch /hello.txt</code> : 컨테이너에 hello.txt 파일을 생성해라 → <code>docker attach</code>로 들어가서 확인해보면 해당 파일이 생성되어 있음</p>
<h3 id="도커-파일-작성하기">도커 파일 작성하기</h3>
<p>Dockerfile이란 빌드할 이미지에 어떤 패키지들이 들어갈 지에 대한 내용이 담긴 파일이다. </p>
<p>예를 들어, ubuntu:latest 이미지를 받아와서 컨테이너를 생성하면 해당 컨테이너에는 우분투 환경을 제외하고는 아무것도 설치되어 있지 않다. 여기에 git을 설치하고 싶다고 한다면 컨테이너에 들어가서 일일이 설치를 해줘야 하는데, 이런 수고로운 작업을 하나의 파일을 만들어서 거기에 설치할 패키지의 목록을 넣어둠으로써 자동화시키는 것이라고 할 수 있다. </p>
<p>따라서 위의 예시를 그대로 명령어로 표현해보면 다음과 같다. </p>
<pre><code class="language-bash">$ docker run -it ubuntu:latest bash
&gt;&gt; apt-get update
&gt;&gt; apt-get install git </code></pre>
<p>얘를 아래와 같이 하나의 파일 안에 넣은 게 Dockerfile이다. </p>
<pre><code># Base Image 지정
FROM ubuntu:latest

# 명령어 
RUN apt-get update
RUN apt-get install git</code></pre><pre><code class="language-bash"># 위의 이미지 파일을 가지고 ubuntu:git-ver라는 이름의 이미지 만들기 
docker build -t ubuntu:git-ver .</code></pre>
<h3 id="vscode에서-도커-사용하기">VSCode에서 도커 사용하기</h3>
<p>VSCode에서 도커를 GUI로 쉽게 사용하기 위한 익스텐션이 두 개 있다. </p>
<ul>
<li><strong><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers">Remote-Containers</a></strong> : VSCode로 컨테이너에 attach할 수 있게 해줌. <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack">Remote-Development</a> 확장팩을 설치했다면 이미 설치되어 있다.
<img src="https://images.velog.io/images/jimin_lee/post/58259177-17a6-4a57-a92c-9cfe94971261/Untitled%20(1).png" alt=""></li>
<li><strong><a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker">Docker for Visual Studio Code</a></strong> : 이미지 및 컨테이너 관리 용이
<img src="https://images.velog.io/images/jimin_lee/post/97fe1cf1-ee07-432f-85a3-97f0cb15576d/Untitled%20(2).png" alt=""><h2 id="3-도커-컴포즈란">3. 도커 컴포즈란?</h2>
</li>
</ul>
<h3 id="what-1">WHAT</h3>
<p>도커 명령어 옵션들을 하나의 파일(docker-compose.yml) 안에 <code>key : value</code>의 형식으로 정리해놓고, 도커 컨테이너를 실행시킬 때마다 일일이 옵션을 입력할 필요 없이 docker-compose 명령어를 통해 컨테이너를 실행시킬 수 있도록 하는 것</p>
<p>특히 여러 개의 컨테이너를 동시에 띄워야 할 때, 하나의 파일 안에 각 컨테이너의 옵션들을 정의해놓고 한 번에 실행시킬 수 있어서 편리함 </p>
<h3 id="도커-파일dockerfile-vs-도커-컴포즈-파일docker-composeyml">도커 파일(Dockerfile) vs. 도커 컴포즈 파일(docker-compose.yml)</h3>
<ul>
<li>Dockerfile : 이미지를 정의하는 파일 → 이미지 빌드가 목적</li>
<li>docker-compose.yml : 컨테이너 옵션들을 정의하는 파일 → 컨테이너 생성 및 실행이 목적</li>
</ul>
<h3 id="도커-컴포즈-파일-작성하기">도커 컴포즈 파일 작성하기</h3>
<pre><code class="language-bash"># docker run을 사용할 경우
docker run -d \
       --name db \
       -p 5432:5432 \
       -v ./data/db:/var/lib/postgresql/data \
       -e POSTGRES_DB=postgres \
       -e POSTGRES_USER=postgres \
       -e POSTGRES_PASSWORD=password
       postgres</code></pre>
<pre><code class="language-yaml"># docker-compose.yml 파일을 만들어서 실행시킬 경우
version: &quot;3.9&quot;

services:
  db:
    image: postgres
    volumes:
      - ./data/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    ports:
    - &quot;5432:5432&quot;
  djangoapp:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/backend  
    ports:
      - &quot;8000:8000&quot;
    depends_on:
      - db</code></pre>
<ul>
<li><code>version</code> : 파일 규격 버전을 정의하는데, <a href="https://docs.docker.com/compose/compose-file/">레퍼런스</a>를 참고해서 도커 버전에 맞게 적어 넣으면 된다.</li>
<li><code>services</code> : 실행시킬 컨테이너를 정의하는 공간</li>
<li><code>volumes</code> : 컨테이너를 실행시킬 때, 컨테이너 안에서 생성된 데이터는 컨테이너가 종료되면 함께 사라진다. 만약 데이터를 백업해두고 싶다면 로컬 경로와 컨테이너에서 데이터가 저장되는 경로를 심볼릭 링크시켜 컨테이너 안에서 생성된 데이터가 링크된 로컬 경로에 쌓이도록 하면 되는데, 이런 걸 volume이라고 한다. run 명령어와 함께 쓸 때는 <code>-v</code>나 <code>--mount</code> 옵션을 주면 된다. 
ex) <code>-v host_dir:container_dir</code> 
참고로 docker-compose 파일에서는 volume 경로를 상대 경로로 지정할 수 있다.</li>
<li><code>depends_on</code> : 어떤 컨테이너를 먼저 띄울 지 알려주는 옵션. nginx가 web에 의존하고 있으므로 web을 먼저 실행시키고 nginx를 실행시키게 된다.</li>
</ul>
<h3 id="도커-컴포즈-명령어">도커 컴포즈 명령어</h3>
<p><code>docker-compose up</code> : docker-compose 파일을 읽어서 컨테이너를 실행시키기</p>
<ul>
<li><code>--build</code> : 이미지를 빌드한 후 컨테이너를 생성해서 실행시키는 옵션</li>
</ul>
<p><code>docker-compose down</code> : 컨테이너를 전부 종료시키기</p>
<h2 id="4-faq">4. FAQ</h2>
<p>❓ <em>도커를 사용하면 가상 환경을 안 써도 되나요?</em></p>
<p><strong>PROS : 도커를 사용하면 가상 환경을 안 써도 된다.</strong> </p>
<p>📄 <a href="https://stackoverflow.com/questions/48561981/activate-python-virtualenv-in-dockerfile">참고글</a></p>
<p>의존성 격리를 위해 사용하는 게 가상 환경인데, 그 역할을 도커가 하고 있으므로 도커 안에서 가상 환경을 사용할 필요가 없다. </p>
<p>만약 도커 안에서 여러 개의 앱을 돌리고 있다면, 그건 도커를 잘못 사용하고 있는 것이므로 앱을 각기 다른 컨테이너에 분리해라. </p>
<p><strong>CONS : 도커를 사용해도 가상 환경을 써야 한다.</strong> </p>
<p>📄 <a href="https://vsupalov.com/virtualenv-in-docker/#:~:text=In%20Conclusion,over%20your%20Python%20environment%20%26%20dependencies">참고글</a></p>
<p>가상 환경을 사용하면 패키지에 대한 전반적인 컨트롤을 더 용이하게 할 수 있고 디버깅도 쉽다.</p>
<p>또 계속 가상 환경에서 작업하다가 컨테이너 쓰면서 가상 환경을 사용하지 않게 된다면 오히려 그게 더 불편하다. </p>
<p>가상 환경을 사용한다고 해서 도커 컨테이너가 느려지거나 성능이 저하되는 것도 아니므로 그냥 가상 환경 써라. </p>
<p>❓ <em>도커 안에서 개발을 하는 건가요, 아니면 배포할 때만 사용하는 건가요?</em> </p>
<p><strong>PROS : 도커 안에서 개발 해라.</strong> </p>
<p>📄 <a href="https://betterprogramming.pub/why-and-how-to-use-docker-for-development-a156c1de3b24#:~:text=Here%20are%20a%20few%20of,environments%20for%20your%20entire%20team.&amp;text=If%20you&#39;re%20having%20a,developers%20using%20MacOS%20and%20Windows.">참고글</a> </p>
<p>도커를 배포용으로만 생각하지 마라. 도커로 개발하면 장점이 많다. </p>
<ul>
<li>팀원 전부가 동일한 환경에서 개발 가능</li>
<li>개발 환경과 배포 환경을 완벽하게 동일하게 유지 가능 → 배포가 쉬워짐</li>
<li>개발하는 데에 도커밖에 필요하지 않음 → 윈도우에서 안되는 것들이 많은데 도커 쓰면 바로 해결됨</li>
<li>IDE를 사용 가능 → 따라서 개발할 때 도커를 안 쓸 이유가 없음</li>
</ul>
<p><strong>CONS : 도커에서 개발하지 마라.</strong> </p>
<p>📄 <a href="https://www.freecodecamp.org/news/7-cases-when-not-to-use-docker/">참고글</a> </p>
<p>도커 컨테이너 안에서 개발을 하면 추가적인 환경 설정 등 해줘야 할 게 많다. </p>
<p>특히 디버깅할 때 귀찮은 작업들을 해줘야 한다. </p>
<p>만약 규모가 작은 어플리케이션을 만들 생각이라면 개발할 때는 도커를 안 쓰는 게 훨씬 낫다.</p>
<hr>
<h3 id="더-알아보고-싶은-내용">더 알아보고 싶은 내용</h3>
<ul>
<li>쿠버네티스란?</li>
<li>도커 + CI/CD 사용하기</li>
</ul>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://docs.docker.com/get-started/overview/">https://docs.docker.com/get-started/overview/</a> (도커 공식 문서)</li>
<li><a href="https://docs.docker.com/engine/reference/builder/">https://docs.docker.com/engine/reference/builder/</a> (도커 파일 레퍼런스)</li>
<li><a href="https://youtu.be/Bhzz9E3xuXY">https://youtu.be/Bhzz9E3xuXY</a> (생활코딩)</li>
<li><a href="http://pyrasis.com/Docker/Docker-HOWTO#dockerfile">http://pyrasis.com/Docker/Docker-HOWTO#dockerfile</a></li>
<li><a href="https://www.44bits.io/ko/post/almost-perfect-development-environment-with-docker-and-docker-compose#%EB%8F%84%EC%BB%A4-%EC%BB%B4%ED%8F%AC%EC%A6%88%EB%A1%9C-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">https://www.44bits.io/ko/post/almost-perfect-development-environment-with-docker-and-docker-compose#도커-컴포즈로-개발-환경-구성하기</a></li>
<li><a href="https://www.inflearn.com/course/%EB%8F%84%EC%BB%A4-%EC%9E%85%EB%AC%B8">https://www.inflearn.com/course/도커-입문</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CORS란?]]></title>
            <link>https://velog.io/@jimin_lee/CORS%EB%9E%80</link>
            <guid>https://velog.io/@jimin_lee/CORS%EB%9E%80</guid>
            <pubDate>Tue, 11 May 2021 06:05:32 GMT</pubDate>
            <description><![CDATA[<h2 id="cors의-개념">CORS의 개념</h2>
<p>교차 출처 리소스 공유(Cross-Origin Resource Sharing)의 약자로, 리소스의 origin과 요청한 origin이 다를 경우 보안 상의 이유로 자원에 대한 접근을 브라우저가 제한하는 것을 의미한다. </p>
<p>여기서 <strong>origin</strong>이란, [scheme]://[hostname]:[port]의 형식으로 이루어진 것으로, 쉽게 말해 URL을 생각하면 된다. </p>
<p>이때 origin이 다른 서버에 요청을 보내는 것을 <strong>Cross-Origin Request</strong>라고 한다. </p>
<p>즉, CORS란 자원이 위치한 서버와 요청을 보낸 웹 서버가 각기 다른 곳에 있을 때 (Cross-Origin 요청을 보낼 때) 자원에 접근할 수 있는 권한을 자원을 가진 서버 쪽에서 정의하도록 하는 것이다. </p>
<p>따라서 다른 서버의 api에 ajax 요청을 보낼 때, 다음과 같은 에러 메세지와 함께 데이터 가져오기에 실패하는 경우가 생기게 된다.
<img src="https://images.velog.io/images/jimin_lee/post/29524164-2770-4878-a1f8-25a84e80fc36/Untitled%20(1).png" alt=""></p>
<h2 id="cors-에러의-해결-방안">CORS 에러의 해결 방안?</h2>
<p>그럼, 위의 에러를 해결하기 위해서는 어떻게 해야 할까? </p>
<p>에러 메세지를 읽어보면 답이 나온다. </p>
<p><strong>&#39;Access-Control-Allow-Origin&#39;</strong> <strong>헤더</strong>가 요청받은 리소스 측에서 정의되어 있지 않다고 나와 있으므로, 자원을 가진 <strong>서버 측</strong>에서 Cross-Origin 요청을 허용할 것인지 제한할 것인지가 담긴 &#39;Access-Control-Allow-Origin&#39; 헤더를 반환해주면 된다. </p>
<p>가장 쉬운 방법은 해당 헤더를 정의해주는 플러그인이나 모듈을 사용하면 된다. </p>
<p>그래서 공부하는 김에 Flask와 Django에서 &#39;Access-Control-Allow-Origin&#39; 헤더를 반환하는 패키지를 사용해서 CORS 에러를 해결하는 코드를 짜보았다. </p>
<p>리소스 서버와 요청 서버를 다르게 하기 위해 replit.com이라는 사이트에서 api 서버를 만들어 놓고 로컬에서 api를 가져와 보았다.</p>
<h3 id="flask">Flask</h3>
<p>📁 Resource Server</p>
<p><strong>Flask-CORS</strong> 패키지를 설치한다.</p>
<p><code>CORS(app)</code>라고만 하면 모든 origin에 대해 접근을 허용한다. </p>
<pre><code class="language-python">from flask import Flask, jsonify, request, render_template
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # CORS Allow All Origins

data_get = {
  &#39;title&#39;:&#39;LOTR&#39;,
  &#39;author&#39;:&#39;Tolkin&#39;
}

data_post = {
  &#39;title&#39;:&#39;Harry Potter&#39;,
  &#39;author&#39;:&#39;Rowling&#39;
}

@app.route(&#39;/&#39;)
def hello_world():
    return render_template(&#39;home.html&#39;)

@app.route(&#39;/api&#39;, methods=[&#39;GET&#39;, &#39;POST&#39;])
def api():
  if request.method == &#39;POST&#39;:
    return jsonify(data_post)
  return jsonify(data_get)

app.run(host=&quot;0.0.0.0&quot;)</code></pre>
<p>📁 Request Server</p>
<pre><code class="language-html">&lt;h1&gt;Hello There!!&lt;/h1&gt;

&lt;script&gt;
  const option = {
    method: &quot;POST&quot;,
    headers: {
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
  };

   fetch(&#39;https://corsflask.myrepl.repl.co/api&#39;, option)
     .then(result =&gt; result.json())
       .then(result =&gt; console.log(result))
&lt;/script&gt;</code></pre>
<p>GET과 POST 두 가지 방식으로 api 요청을 보내보았다. </p>
<p>브라우저에서 네트워크 탭을 켜서 확인하니 다음과 같은 정보를 얻을 수 있었다. </p>
<p><img src="https://images.velog.io/images/jimin_lee/post/f5613d1a-ea3c-4f13-ab80-68c79a425504/Untitled%20(2).png" alt=""></p>
<p>참고로 같은 서버에서 api 요청을 보낼 경우, Referrer Policy는 <code>same-origin</code>이라고 뜬다. </p>
<p>그런데 응답 헤더에 <code>Access-Control-Allow-Origin: null</code>이라고 되어 있는데, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin">MDN 문서</a>에서는 보안 상 이유로 <a href="https://w3c.github.io/webappsec-cors-for-developers/#avoid-returning-access-control-allow-origin-null">null 옵션을 사용하지 말라</a>고 하고 있다.</p>
<h3 id="django">Django</h3>
<p>📁 Resource Server</p>
<p>프로젝트 이름 : mysite</p>
<p>앱 이름 : api</p>
<p><strong>Django-CORS-Headers</strong> 패키지를 설치한다. </p>
<p>얘는 settings.py에서 설정을 좀 해줘야 한다. </p>
<pre><code class="language-python"># mysite/settings.py
INSTALLED_APPS = [
  &#39;corsheaders&#39;,
]

MIDDLEWARE = [
  &#39;corsheaders.middleware.CorsMiddleware&#39;,
] # 얘는 다른 middleware보다 앞에 명시해줘야 함

## 다음 셋 중 하나로 설정하면 됨
# 허용할 origin을 직접 지정
CORS_ALLOWED_ORIGINS = [
  &quot;https://example.com&quot;,
]
# 정규식으로 origin 지정
CORS_ALLOWED_ORIGIN_REGEXES = [
  r&quot;^https://\w+\.example\.com$&quot;,
]
# 모든 origin 허용 (디폴트는 False)
CORS_ALLOW_ALL_ORIGINS = True  </code></pre>
<p>환경설정 후 api view를 만들었다. </p>
<pre><code class="language-python"># api/views.py
from django.views.decorators.http import require_http_methods
from django.http import JsonResponse
from django.shortcuts import render

data_get = {
  &#39;title&#39; : &#39;LOTR&#39;,
  &#39;author&#39; : &#39;Tolkin&#39;
}

data_post = {
  &#39;title&#39; : &#39;Harry Potter&#39;,
  &#39;author&#39; : &#39;Rowling&#39;
}

def home(request):
  return render(request, &#39;home.html&#39;)

@require_http_methods([&quot;GET&quot;, &quot;POST&quot;])
def api(request):
  if request.method == &#39;POST&#39;:
    return JsonResponse(data_post)
  return JsonResponse(data_get)</code></pre>
<p>📁 Request Server</p>
<pre><code class="language-html">&lt;h1&gt;Hello There!!&lt;/h1&gt;

{% csrf_token %}
&lt;script&gt;
  function getCookie(name) {
    let cookieValue = null;
    if (document.cookie &amp;&amp; document.cookie !== &#39;&#39;) {
        const cookies = document.cookie.split(&#39;;&#39;);
        for (let i = 0; i &lt; cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + &#39;=&#39;)) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
  }
  const csrftoken = getCookie(&#39;csrftoken&#39;);
  console.log(csrftoken)

  const option = {
    method: &quot;POST&quot;,
    headers: {
      &quot;Content-Type&quot;: &quot;application/json&quot;,
      &#39;X-CSRFToken&#39;: csrftoken
    },
  };

  fetch(&#39;https://corsdjango.myrepl.repl.co/api/&#39;, option)
    .then(result =&gt; result.json())
      .then(result =&gt; console.log(result))
&lt;/script&gt;</code></pre>
<p>이번에도 동일하게 GET과 POST 두 메소드를 테스트해보았는데, flask의 경우보다 코드가 훨씬 길어졌다. </p>
<p>이유는 장고가 POST 메소드 요청에 대해서 CSRF 보호를 하고 있기 때문이다. </p>
<p><strong>CSRF 보호</strong>란 Cross Site Request Forgery Protection, 즉 다른 사이트에서 요청을 위조하려는 움직임에 대해 보호하는 것을 의미하는데, CORS와 비슷한 맥락으로 생각하면 될 것 같다. </p>
<p>장고는 디폴트로  CSRF Protection 미들웨어를 사용하고 있기 때문에 그냥 POST 요청을 보내면 에러가 난다. </p>
<p>그래서 요청 헤더에 CSRF 토큰을 같이 보내야 한다. CSRF 토큰은 쿠키에서 받아오면 된다. 토큰을 얻어오는 코드는 장고 <a href="https://docs.djangoproject.com/en/3.2/ref/csrf/">공식 문서</a>를 참고했다.</p>
<p><img src="https://images.velog.io/images/jimin_lee/post/61b3dc26-cb18-4e54-a968-2b307b96dcf5/Untitled%20(3).png" alt=""></p>
<p>받아온 헤더 정보를 보면 얘는 <code>Access-Control-Allow-Origin: *</code> 이라고 되어 있는데, 모든 origin에 대해 접근을 허용하겠다는 의미이다. 아까 설정을 <code>CORS_ALLOW_ALL_ORIGINS = True</code>로 해놔서 그렇다. </p>
<p>참고로, 장고는 <code>fetch(URL)</code> 부분에서 url 경로의 마지막 부분에 슬래쉬(/)를 추가하지 않으면 api를 못 받아온다. 왜 그런걸까...? 이것 때문에 한참 안돼서 삽질했다. 근데 또 플라스크는 뒤에 슬래쉬가 붙으면 trailing slash 에러 때문에 못 받아온다. 둘이 뭔 차이가 있는지 좀 알아봐야겠다. </p>
<hr>
<h3 id="더-알아보고-싶은-내용">더 알아보고 싶은 내용</h3>
<ul>
<li>리액트 + 장고 + 도커 환경에서의 CORS 설정은 어떻게 해야 할까? </li>
<li>proxy 서버를 사용해서 CORS 에러를 해결하는 방법은? </li>
</ul>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS</a></li>
<li><a href="https://youtu.be/0IMz8d9Cby4">https://youtu.be/0IMz8d9Cby4</a></li>
<li><a href="https://flask-cors.readthedocs.io/en/latest/">Flask-CORS</a></li>
<li><a href="https://github.com/adamchainz/django-cors-headers">Django-Cors-Headers</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin">https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin</a></li>
<li><a href="https://docs.djangoproject.com/en/3.2/ref/csrf/">https://docs.djangoproject.com/en/3.2/ref/csrf/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로세스의 상태는 어떻게 정의될까? ]]></title>
            <link>https://velog.io/@jimin_lee/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%9D%98-%EC%83%81%ED%83%9C%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%95%EC%9D%98%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@jimin_lee/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%9D%98-%EC%83%81%ED%83%9C%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%95%EC%9D%98%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Thu, 06 May 2021 15:29:31 GMT</pubDate>
            <description><![CDATA[<h2 id="프로세스process">프로세스(Process)</h2>
<h3 id="프로세스의-의미">프로세스의 의미</h3>
<p>실행 중인 프로그램 program on executing </p>
<h3 id="프로세스의-문맥process-context">프로세스의 문맥(Process Context)</h3>
<ul>
<li><p><strong>프로세스의 현재 상태</strong>를 나타내는데 필요한 모든 요소를 프로세스 문맥이라고 함</p>
</li>
<li><p>특점 시점에 프로세스가 어느만큼 작업을 했고 과연 이 프로세스가 어디까지 와있는지</p>
</li>
<li><p>프로그램 카운터(PC)가 어느 부분을 가리키고 있는가 → 코드의 어디까지 실행했는가</p>
<p>  <code>참고</code> 프로그램 카운터(PC) : 코드에서 실행하는 부분을 가리키고 있는 장치</p>
<p>  <code>참고</code> 메모리 주소 공간 → code / data / stack </p>
</li>
<li><p>레지스터에 어떤 값을 넣어놓고 어떤 명령까지 실행했는가</p>
</li>
<li><p>Q. 왜 프로세스의 문맥 파악이 필요할까?</p>
<p>  A. CPU는 계속 여러 프로세스를 번갈아 담당하기 때문에 어떤 프로세스를 어디까지 작업했는지 알아야 함. 그렇지 않으면 매번 처음부터 다시 실행해야 하기 때문.</p>
</li>
</ul>
<h3 id="프로세스의-상태process-state">프로세스의 상태(Process State)</h3>
<ul>
<li>프로세스는 상태가 변경되면서 수행된다.<ul>
<li>running : CPU를 잡고 있는 상태</li>
<li>ready : 당장 필요한 부분이 메모리에 올라와 있으면서 CPU를 기다리고 있는 상태</li>
<li>blocked(wait, sleep) : CPU를 줘도 바로 작업 수행이 불가능한 상태 ex) IO 작업처럼 오래 기다려야 하는 작업. 메모리에 올라와있지 않고 디스크에 내려가있는 작업</li>
<li>기타 상태 (정확히는 프로세스의 상태가 아닌 상태)<ul>
<li>new : 프로세스가 생성 중인 상태</li>
<li>terminated : 프로세스의 수행이 끝난 상태 → 약간 정리할 게 남아 있어서 완벽하게 메모리상에서 제거되지는 않은 상태</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="https://images.velog.io/images/jimin_lee/post/db634e04-9299-4419-bb84-3c4ff06de88c/Untitled%20(3).png" alt=""></p>
<p><a href="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/3_Processes.html">이미지 출처</a></p>
<h2 id="프로세스-스케쥴링process-scheduling">프로세스 스케쥴링(Process Scheduling)</h2>
<h3 id="프로세스-스케쥴링의-의미">프로세스 스케쥴링의 의미</h3>
<p>프로세스를 CPU에게 할당하는 과정 </p>
<h3 id="프로세스-스케쥴링을-위한-큐queue">프로세스 스케쥴링을 위한 큐(Queue)</h3>
<ul>
<li><p>프로세스의 처리를 위한 각각의 큐가 존재 → ready queue, IO device queue, resource(공유 데이터) queue 등</p>
</li>
<li><p>프로세스는 상태에 따라 큐에 가서 줄을 서고 작업이 해결되기를 기다림</p>
<p>  <code>참고</code> Queue → First In First Out</p>
</li>
<li><p>Job queue : 메모리 할당을 기다리는 모든 프로세스</p>
</li>
<li><p>Ready queue : 메모리에 올라와서 CPU 할당을 기다리는 상태의 프로세스 (= ready 상태)</p>
</li>
<li><p>Device queue : 각 IO 디바이스의 작업을 기다리는 프로세스 (= block 상태)</p>
</li>
</ul>
<p><img src="https://images.velog.io/images/jimin_lee/post/51efcc67-7fb1-4ca7-9716-e049e0e52405/Untitled%20(4).png" alt=""></p>
<p><a href="https://bitsofcomputer.blogspot.com/2015/12/process-scheduling.html">이미지 출처</a> </p>
<h3 id="pcbprocess-control-block">PCB(Process Control Block)</h3>
<ul>
<li>큐의 구조는 <strong>링크드 리스트</strong> 형식으로 구성</li>
<li>큐의 헤더는 큐의 처음과 마지막 프로세스의 PCB를 가리키는 포인터를 포함</li>
<li><strong>PCB</strong> : 각 프로세스마다 프로세스를 관리하기 위해 유지하는 정보</li>
<li>프로세스 하나당 PCB가 있음</li>
<li>저장하는 정보 : 프로세스 우선순위 값, 프로세스의 문맥 관련 정보 등</li>
</ul>
<p><img src="https://images.velog.io/images/jimin_lee/post/7649d8ef-c397-48ec-88ba-56b549624086/Untitled%20(5).png" alt=""></p>
<p><a href="https://rollercoaster25.tistory.com/60">이미지 출처</a></p>
<h3 id="문맥-교환context-switch">문맥 교환(Context Switch)</h3>
<ul>
<li>CPU가 한 프로세스에서 다른 프로세스로 넘어가는 과정</li>
<li>문맥 교환 시 필요한 작업<ul>
<li>운영체제가 프로세스로부터 CPU를 빼앗을 때 커널 주소 공간의 data 영역의 PCB에 프로세스의 문맥 정보를 저장해 놓음</li>
<li>운영체제가 CPU를 다른 프로세스에게 넘겨줄 때 PCB에서 문맥을 찾아서 같이 넘겨줌 → 마지막으로 실행했던 곳부터 찾아서 작업할 수 있도록</li>
</ul>
</li>
</ul>
<h3 id="헷갈리는-점-정리">헷갈리는 점 정리</h3>
<p>Q. 시스템콜이나 인터럽트가 발생하면 반드시 문맥 교환이 일어나는 걸까?
A. 문맥 교환의 의미 → CPU가 작업하는 프로세스가 교체되는 것!
그렇기 때문에 시스템콜이나 인터럽트 이후 CPU가 다시 원래 작업하던 프로세스로 돌아가면 그건 문맥 교환이 일어나는 것이 아님. 따라서 시스템콜이나 인터럽트가 발생한다고 무조건 문맥 교환이 일어나는 것은 아니다.</p>
<h3 id="스케쥴러">스케쥴러</h3>
<ul>
<li><p>Short-term 스케쥴러(CPU 스케쥴러) : 어떤 프로세스를 바로 다음에 running 시킬지 결정</p>
<p>  → 일반적으로 생각하는 스케쥴링의 의미</p>
</li>
<li><p>Long-term 스케쥴러(Job 스케쥴러) : 어떤 프로세스가 메모리에 올라갈지를 결정 → <strong>degree of multiprogramming을 제어</strong></p>
<p>  <code>참고</code> degree of multiprogramming : ****메모리에 몇 개의 프로그램이 동시에 올라와 있는지</p>
<p>  But, 실제 시스템에서는 장기 스케쥴러가 없음. 프로그램이 시작되자 마자 ready 상태(메모리에 올라가 있는 상태)가 되기 때문. </p>
<p>  그러면 어떻게 메모리의 프로세스 수를 조정할까? → mid-term 스케쥴러를 사용</p>
</li>
<li><p>Medium-term 스케쥴러(Swapper) : 너무 많은 프로그램이 동시에 올라가 있을 때, 여유 공간 마련을 위해 일부 프로그램을 메모리에서 통째로 쫓아냄(swap out 시킴) → 이를 통해 degree of multiprogramming을 제어</p>
</li>
</ul>
<p><strong>장기 스케쥴러 vs 중기 스케쥴러</strong> </p>
<ul>
<li>장기 스케쥴러 : 애초에 메모리에 올라갈 프로그램의 수를 조정</li>
<li>중기 스케쥴러 : 일단 다 올려놓은 다음 공간이 모자라면 뺌 → 시스템 입장에서는 중기 스케쥴러가 더 효과적임</li>
<li>suspended(stopped) : 중기 스케쥴러 때문에 추가된 프로세스의 상태로, 메모리를 통째로 빼앗긴 프로세스의 상태. 외부의 개입(중기 스케쥴러)으로 인해 프로세스의 수행이 멈춰있는 상태</li>
</ul>
<p><strong>blocked vs suspended</strong> </p>
<ul>
<li>blocked : 자기 스스로 멈췄다가 조건이 충족되면 ready 상태가 됨</li>
<li>suspended : 외부의 개입에 의해 수행이 멈췄기 때문에 다시 외부의 개입을 통해 재개시켜줘야 active 상태가 됨</li>
</ul>
<p><img src="https://images.velog.io/images/jimin_lee/post/444cca7c-b6f9-4141-8423-c70d93ff1b3f/Untitled%20(6).png" alt="">
<a href="http://www.kocw.net/home/m/search/kemView.do?kemId=1046323&amp;ar=pop">이미지 출처</a></p>
<ul>
<li><strong>suspended blocked vs suspended ready</strong> : suspend가 blocked 상태에서 suspend가 되었는지 ready 상태에서 suspend가 되었는지에 따라 구분</li>
</ul>
<p><code>참고</code> 프로세스가 시스템콜 혹은 인터럽트로 인해 커널모드가 되었을 경우에도 프로세스가 running하고 있다고 간주</p>
<h3 id="참고">참고</h3>
<ul>
<li><p><a href="http://www.kocw.net/home/m/search/kemView.do?kemId=1046323&amp;ar=pop">KOCW 운영체제 강의 (이화여자대학교, 반효경, 2014년 1학기)</a></p>
</li>
<li><p><a href="https://rollercoaster25.tistory.com/60">https://rollercoaster25.tistory.com/60</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[OSI 7계층이란 무엇일까?]]></title>
            <link>https://velog.io/@jimin_lee/OSI-7%EA%B3%84%EC%B8%B5%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@jimin_lee/OSI-7%EA%B3%84%EC%B8%B5%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Thu, 06 May 2021 15:18:50 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<p>✅ <strong>네트워크란?</strong> : 정보를 주고 받아야 하는 모든 IT 인프라 장비 간의 물리적(케이블) 또는 논리적(망) 연결 </p>
<ul>
<li>물리적 연결 → 케이블 등을 통한 연결을 의미</li>
<li>논리적 연결 → 인터넷 망이라는 개념적 연결을 의미</li>
</ul>
<h1 id="1-osi-7계층을-살펴보자">1. OSI 7계층을 살펴보자</h1>
<p>✔ OSI 7계층이란? 컴퓨터 네트워크의 통신 방법을 계층적으로 구조화한 모델 </p>
<p>✔ 왜 알아야 하는가? </p>
<ul>
<li>다양한 분야에서 계층 구조 개념의 참조 모델로서 참조하고 있음 (TCP/IP 모델도 마찬가지)</li>
<li>네트워크의 여러 장비들이 각각의 계층에 맞게 동작하기 때문에 각 계층을 이해하는 것이 중요</li>
</ul>
<p>✔ 각 계층 간 데이터 통신의 핵심 </p>
<ul>
<li><p>어디로 갈 지를 어떻게 알려주느냐? → 목적지의 <strong>구분자</strong>의 문제</p>
</li>
<li><p>각 계층 → 데이터 통신을 위한 모듈(함수) → 어떻게 데이터를 포장하느냐? → 어떤 헤더를 붙일 것인가? → 프로토콜→ 각 계층에서의 <strong>데이터의 포장 상태</strong> → 헤더에 어떤 정보들이 담겨 있는가?</p>
<h2 id="물리-계층physical-layer-l1">물리 계층(Physical Layer, L1)</h2>
</li>
<li><p>서로 다른 컴퓨터 기기(호스트) 간 전기선을 통한 물리적 연결을 담당하는 계층</p>
</li>
<li><p>컴퓨터가 이해하는 언어는 0과 1의 나열로 이루어진 디지털 신호 → 어떻게 전선에 흘려보낼 것인가? → 아날로그 신호(전기 신호)로 변조</p>
<ul>
<li>Sender : 디지털 신호를 아날로그 신호로 바꿔서 전선으로 흘려 보냄 → 원본 데이터를 변조 → Encoder</li>
<li>Receiver : 아날로그 신호가 들어오면 디지털 신호로 해석 → 변조된 데이터를 해석 → Decoder</li>
<li>L1 encoder의 output → L1 decoder의 input 이 되는 구조</li>
</ul>
</li>
<li><p>L1 장비</p>
<ul>
<li>NIC(network interface card) : 컴퓨터와 컴퓨터 네트워크를 연결해주는 인터페이스</li>
<li>리피터 : 전기 신호가 약화되어 사라지지 않도록 증폭시켜주는 장치</li>
</ul>
</li>
<li><p>물리적 연결을 통한 통신 문제 : <strong>여러 대의 컴퓨터가 통신하려면 서로 연결된 전선이 너무 많이 필요</strong></p>
</li>
</ul>
<p><img src="https://images.velog.io/images/jimin_lee/post/6df65f88-d7ef-4565-8343-a74a571e7398/Untitled%20(1).png" alt=""></p>
<p><a href="https://www.learnabhi.com/difference-between-switch-and-router/">이미지 출처</a></p>
<h2 id="데이터링크-계층data-link-layer-l2">데이터링크 계층(Data-Link Layer, L2)</h2>
<ul>
<li><strong>여러 대의 컴퓨터를 하나의 전선으로 통신</strong>하기 위해 필요한 계층</li>
<li>각각의 컴퓨터는 <strong>스위치</strong>에 연결<ul>
<li>스위치 → 컴퓨터가 요청한 목적지를 확인하고 목적지에만 데이터를 전송</li>
</ul>
</li>
<li><strong>네트워크</strong> : 하나의 스위치에 연결된 컴퓨터들의 집합(인트라넷)</li>
<li><strong>라우터</strong> : 스위치와 스위치를 연결해서 서로 다른 네트워크를 연결해주는 장치</li>
<li>이런 식으로 전 세계의 컴퓨터들을 연결한 것을 <strong>인터넷(inter-network)</strong>이라고 함<ul>
<li>각 국가의 최상위 라우터 → 해저 케이블로 연결</li>
<li>일반적으로 가정의 라우터 역할은 공유기가 하고 있음</li>
</ul>
</li>
<li>즉, 데이터 링크 계층이란 같은 네트워크에 있는 여러 대의 컴퓨터들이 데이터를 주고받기 위해 필요한 모듈</li>
<li>프로토콜 : <strong>이더넷(Ethernet)</strong></li>
<li>한 대의 컴퓨터에 여러 개의 데이터가 동시에 들어왔을 때, 어떻게 끊어 읽어서 서로 다른 데이터를 구분할까? → 데이터의 시작과 끝에 특정한 비트열을 붙여서 구분 : <strong>프레이밍(framing)</strong></li>
<li>이더넷 헤더 + 데이터 + 이더넷 푸터 ⇒ <strong>이더넷 프레임</strong></li>
<li>구분자 : <strong>MAC 주소(Media Access Control Address)</strong><ul>
<li>네트워크 통신을 하는 하드웨어에 할당된 고유한 주소 (동일 네트워크 내 라우터의 구분자)</li>
<li>Physical addressing</li>
<li>이더넷 헤더에 기입</li>
<li>MAC 주소는 충돌이 나지 않음 → How? 48비트 (앞의 24비트 : 제조사 구분번호, 뒤의 24비트 : 시퀀스 넘버) →  2^48 = 281조 개</li>
<li>MAC 주소가 중복된다고 하더라도 동일 네트워크 상에서 통신하는 구분자이므로 충돌이 나지 않음</li>
</ul>
</li>
</ul>
<p><img src="https://images.velog.io/images/jimin_lee/post/166c95f8-e4d3-4da4-a92c-1b50e132429f/Untitled%20(2).png" alt=""></p>
<p><a href="https://creately.com/blog/examples/network-diagram-templates-creately/">이미지 출처</a></p>
<h2 id="네트워크-계층network-layer-l3">네트워크 계층(Network Layer, L3)</h2>
<ul>
<li>데이터가 목적지 컴퓨터로 도착하기까지의 과정을 담당하는 계층</li>
<li>프로토콜 : <strong>IP(Internet Protocol)</strong></li>
<li>IP 헤더 + 데이터 ⇒  <strong>IP 패킷</strong></li>
<li>구분자 : <strong>IP 주소</strong><ul>
<li>각 컴퓨터들이 가지는 고유한 주소 (컴퓨터의 구분자)</li>
<li>Logical addressing</li>
<li>목적지의 IP 주소를 IP 헤더에 붙여서 데이터를 전송</li>
<li>IP v4 기준으로 32비트(43억 개 → 주소 고갈), IP v6는 128비트</li>
<li>웹 상에서 DNS 주소를 입력하면 IP 주소로 변환되어 사용됨</li>
</ul>
</li>
<li><strong>라우팅(routing)</strong> : 복잡한 네트워크 속에서 목적지 컴퓨터로 데이터를 전송하기 위해 IP 주소를 이용해서 길을 찾는 과정<ul>
<li>라우터는 패킷을 까보고 해당 IP 주소를 가진 컴퓨터를 찾음 → 자기 네트워크에 없으면 패킷을 다시 포장해서 상위 라우터 또는 이웃한 라우터로 보냄 (forwading)</li>
</ul>
</li>
</ul>
<h2 id="전송-계층transport-layer-l4">전송 계층(Transport Layer, L4)</h2>
<ul>
<li>목적지 컴퓨터에 도착하고 난 후, 컴퓨터 안에서 실행되고 있는 수많은 프로세스 중 어떤 프로세스에 데이터를 넘겨줄 것인지를 담당하는 계층</li>
<li>프로토콜 : <strong>TCP(Transmission Control Protocol)</strong>, UDP</li>
<li>TCP 헤더 + 데이터 ⇒ <strong>TCP 세그먼트</strong></li>
<li>구분자 : <strong>Port 번호</strong><ul>
<li>한 대의 컴퓨터에서 동시 실행중인 프로세스들이 서로 겹치지 않게 가지는 정수값 (프로세스의 구분자)</li>
<li>모든 프로세스는 포트 번호를 가짐</li>
<li>TCP의 포트 헤더는 16비트 → 2^16 = 약 6만 5천 개의 포트가 존재 : 0~65535</li>
</ul>
</li>
<li>포트 번호를 사용해서 도착지 컴퓨터의 최종 도착지인 프로세스까지 데이터가 도착할 수 있게 함</li>
</ul>
<h2 id="어플리케이션-계층application-layer-l7">어플리케이션 계층(Application Layer, L7)</h2>
<ul>
<li>OSI 7계층 모델의 세션 계층(L5) + 프레젠테이션 계층(L6) + 어플리케이션 계층(L7) → TCP/IP 모델의 어플리케이션 계층<ul>
<li>세션 계층(L5) : 세션의 시작과 종료를 관리하는 계층 → session management, authentication, authorization</li>
<li>프레젠테이션 계층(L6) : 데이터를 0과 1의 나열로 변환하고 압축하는 등의 역할을 담당하는 계층<ul>
<li>translation : Hi nice to meet you → 0100111011010101011</li>
<li>data compression : 0100111011010101011 → 01001101110</li>
<li>encryption/decryption : 01001101110 → 0100111011010101011 → Hi nice to meet you</li>
</ul>
</li>
</ul>
</li>
<li>L5부터는 실제 전송되는 데이터의 내용을 보고 액션을 취해야 하는 계층 → 데이터의 내용이 안전한지, 방화벽을 쳐야 하는지 등의 문제를 다룸</li>
<li>네트워크 단에서 전송이 끝난 데이터를 가지고 움직이는 애들이기 때문에 엄밀한 네트워크 계층이라기 보다는 응용 계층이라고 할 수 있음</li>
<li>프로토콜 : <strong>HTTP</strong> , Telnet, FTP</li>
<li>HTTP 헤더 + 데이터 ⇒ <strong>HTTP 메세지</strong><ul>
<li>요청 측 : method, request context(url, 포트번호), header 등</li>
<li>응답 측 : status code, header 등</li>
</ul>
</li>
</ul>
<p><strong>Checkpoints</strong></p>
<ul>
<li>하나의 패킷이라도 전송하기 위해서는 7단계의 헤더를 붙이고 포장하는 과정이 필요</li>
<li>전송하려고 레이어를 내려올 때는 헤더를 하나씩 더하면서 내려오고(L7 → L1), 전달 받으려고 레이어를 올라갈 때는 헤더를 하나씩 빼면서 올라간다(L1 → L7).</li>
<li>스위치 → 해당 레이어의 헤더를 읽어내는 일을 담당하고, 각 층은 각자가 필요한 레이어만 까본 다음 다시 포장함</li>
</ul>
<h1 id="2-데이터의-여정을-살펴보자">2. 데이터의 여정을 살펴보자</h1>
<p><img src="https://images.velog.io/images/jimin_lee/post/341c5595-3531-46cf-9dcd-fc89b92ece11/osi7.jpg" alt=""></p>
<hr>
<h3 id="더-알아보고-싶은-내용">더 알아보고 싶은 내용</h3>
<ul>
<li>MAC 주소 vs IP 주소 </li>
<li>전송 계층의 데이터 전달 보증 방식</li>
</ul>
<h3 id="참고">참고</h3>
<ul>
<li><a href="http://www.yes24.com/Product/Goods/95800974?OzSrank=1">야마자키 야스시 외, 그림으로 공부하는 IT 인프라 구조</a></li>
<li><a href="https://youtu.be/1pfTxp25MA8">https://youtu.be/1pfTxp25MA8</a></li>
<li><a href="https://youtu.be/vv4y_uOneC0">https://youtu.be/vv4y_uOneC0</a></li>
<li>개발자도 궁금한 IT 인프라, 42. 네트워크, 레이어별 핵심만 정리하자! (상) : <a href="http://www.podbbang.com/ch/10291">http://www.podbbang.com/ch/10291</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nginx와 Gunicorn 둘 중 하나만 써도 될까? ]]></title>
            <link>https://velog.io/@jimin_lee/Nginx%EC%99%80-Gunicorn-%EB%91%98-%EC%A4%91-%ED%95%98%EB%82%98%EB%A7%8C-%EC%8D%A8%EB%8F%84-%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@jimin_lee/Nginx%EC%99%80-Gunicorn-%EB%91%98-%EC%A4%91-%ED%95%98%EB%82%98%EB%A7%8C-%EC%8D%A8%EB%8F%84-%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Mon, 26 Apr 2021 01:44:39 GMT</pubDate>
            <description><![CDATA[<p>Flask나 Django로 만든 파이썬 앱을 배포할 때, 보통 Nginx와 Gunicorn과 자기 앱을 연결해서 배포하는 게 정석처럼 여겨진다. 여기서 Nginx를 Apache로, Gunicorn을 uWSGI로 바꿔도 상관없다. </p>
<p>그런데 얘네가 뭐길래? 하는 의문이 든다. Nginx와 Apache는 웹 서버라는 이야기를 들어서 알겠다. 클라이언트의 요청을 받고 앱에 전달해서 앱의 response를 다시 클라이언트에게 전달해주는 역할이라는 것도 알겠다. </p>
<p>그러면 Gunicorn과 uWSGI는 뭐지? 얘네는 왜 있는 걸까? 없어도 되는 건가? 하는 생각이 들기 마련이다. 실제로 Gunicorn을 쓰지 않고 Nginx로만 배포한다는 소리도 들었다. 여기서 Gunicorn은 없어도 되지만 있으면 좋은 미들웨어구나, 하고 생각했다. </p>
<p>그런데 Nginx 없이 Gunicorn으로만 배포한다는 소리를 들었다. </p>
<p>???</p>
<p>웹 서버의 역할을 하는 Nginx가 없어도 가능한건가? 하는 생각이 들었다.  그래서 Nginx, Gunicorn이 도대체 무슨 일을 하는 애들이고, 파이썬 앱과 어떻게 연결되어서 동작하는 것인지 알아보고자 한다. </p>
<p>Nginx, gunicorn, Django가 모두 서버 역할을 하고 있기 때문에, 이렇게 나눠져 있는 서버의 계층 시스템과 WSGI라는 것에 대해 먼저 알아볼 필요가 있다. </p>
<h2 id="1-wsgi의-등장-배경">1. WSGI의 등장 배경</h2>
<h3 id="3계층-시스템과-cgi">3계층 시스템과 CGI</h3>
<p>3계층 서버 시스템에 대해 들어본 적 있을 것이다. DB 서버는 차치하고 웹 서버와 어플리케이션 서버만 살펴보자. 초기에는 웹 서버만 있었다. 하드웨어에 파일과 이미지를 저장해두었다가 클라이언트로부터 요청이 들어오면 요청한 파일을 화면에 띄워주는 형식이었다. </p>
<p>이런 방식은 빠르고 편했지만 Only Static, 정적인 파일밖에 건네줄 수가 없었다. 그런데 클라이언트로부터 오는 요청이 다양하고 복잡해지면서 로직으로 구현해야 하는 동적인 파일에 대한 요청이 생겨나게 되고, 이런 정적인 파일만으로는 한계를 느끼게 되었다. </p>
<p>그래서 등장한 게 <strong>웹 어플리케이션 서버</strong>이다. 데이터 파일을 저장해두는 대신, 소스 스크립트를 서버에 저장해놓고 요청이 올 때마다 스크립트를 실행시켜 결과를 반환해주는 것이다. 쉽게 말해 소스 스크립트가 있는 웹 앱 자체를 클라이언트의 요청을 받는 웹 서버로 사용하는 것이다. </p>
<p>그런데, 여기서 <strong>파이썬 스크립트가 어떻게 HTTP 요청을 받을 것인가</strong>, 하는 문제가 발생한다. </p>
<p>HTTP 요청은 기본적으로 <code>GET /home.html HTTP/1.1</code> 과 같은 텍스트이고, 이는 파이썬 앱이 받는 request object의 형식과는 다르기 때문이다. </p>
<p>그래서 클라이언트로부터 오는 HTTP 요청을 파이썬 스크립트가 요구하는 데이터 형식으로 변환하고 응답을 돌려줄 때도 파이썬 데이터를 HTTP 형식으로 바꿔주는 작업이 필요한데, 이 때 파이썬 앱 서버가 동작하는 기본적인 방식이 <strong>CGI, Common Gateway Interface</strong>이다. </p>
<p>즉, CGI란 파이썬 어플리케이션 서버의 동작 방식에 대한 specification(사양, 매뉴얼)이라고 할 수 있고 그 기본적인 동작 과정은 다음과 같다. </p>
<ul>
<li>인풋으로 HTTP 요청을 받는다.</li>
<li>요청에 대한 정보를 환경변수의 형식으로 만들어서 파이썬 스크립트의 stdin 형식의 인풋으로 받는다.</li>
<li>스크립트가 print와 같은 stdout 형식으로 응답하면 HTTP 형식으로 변환된다.</li>
</ul>
<h3 id="wsgi의-등장">WSGI의 등장</h3>
<p>그런데 CGI는 한 가지 문제점이 있었는데, 바로 요청이 들어올 때마다 파이썬 스크립트를 처음부터 실행한다는 것이었다. 이렇게 되면 서버가 너무 느리고 효율도 좋지 않았다. </p>
<p>이 때 등장한 게 <strong>WSGI(Web Server Gateway Interface)</strong>이다. CGI처럼 WSGI 또한 웹 어플리케이션 서버의 동작 방식에 대한 specification인데, 기본적인 아이디어는 다음과 같다. </p>
<p>웹 서버와 파이썬 스크립트를 분리하고 웹 서버가 클라이언트의 요청을 받아서 스크립트에 전달해주면 스크립트는 스크립트 전체를 실행시키는 게 아니라 필요한 로직 하나만 실행한 후 결과를 응답해주는 식으로 동작함으로써 동적인 콘텐츠에 대한 요청에 빠르게 응답할 수 있게 한 것이다. </p>
<p>이러한 WSGI가 표준적인 파이썬 어플리케이션 서버의 동작 방식이 됨에 따라, WSGI 서버가 클라이언트의 요청을 받는 웹 서버의 역할을 하게 되고 WSGI compatible한 파이썬 앱이 WSGI 서버와 합쳐져서 웹 어플리케이션 서버가 되게 된다.
<img src="https://images.velog.io/images/jimin_lee/post/19d94186-29f9-4cf2-a9c3-0b9e7bb39086/KakaoTalk_20210425_212448657_01.jpg" alt=""></p>
<p>즉, 여기서 WSGI 서버의 역할을 수행하는 애가 Gunicorn이고, Django는 파이썬 앱이라고 할 수 있으며, 얘네가 함께 웹 어플리케이션 서버가 되는 것이다. Nginx는 여기서 Gunicorn+Django의 앞단에 위치하고 있는데, WSGI의 프로세스와는 별 관련 없이 buffering, reverse proxy나 load balance와 같은 별도의 일을 수행하게 되는 것이다. </p>
<h2 id="2-wsgi의-동작-과정">2. WSGI의 동작 과정</h2>
<p>WSGI에 대해 좀 더 감을 잡기 위해 <a href="https://www.python.org/dev/peps/pep-0333/#specification-overview">PEP333</a>에 나와있는 WSGI의 동작 과정을 한 번 살펴보려고 한다. 다시 한 번 말하지만 WSGI는 별도의 프레임워크 같은 게 아니라, 동적인 데이터에 대응하기 위해서 웹 서버와 파이썬 웹 앱이 어떻게 서로 동작해야 하는지에 대한 내용을 담고 있는 specification이라고 할 수 있다. </p>
<p>공식 문서에서는 WSGI를 <strong>&quot;simple and universal interface between web servers and web applications or frameworks&quot;</strong>라고 설명하고 있으며, WSGI의 목적은 <strong>&quot;to facilitate easy interconnection of existing servers and applications or frameworks, not to create a new web framework&quot;</strong>라고 서술하고 있다. </p>
<p> WSGI는 server/gateway side와 application/framework side를 가지는데, server side에서 들어온 요청이 application side의 callable object를 호출(invoke)하게 된다. 여기서 callable object란 곧 파이썬 스크립트에서 정의한 application을 의미하고, object라고는 했지만 callable한 어떤 형태든지 상관은 없다.</p>
<p><img src="https://images.velog.io/images/jimin_lee/post/d5fd03ea-666a-4a0b-8a03-d574cd7a34f0/KakaoTalk_20210425_212448657.jpg" alt=""></p>
<p>각각의 코드를 한 번 살펴보자. </p>
<h3 id="applicationframework-side">Application/Framework Side</h3>
<p>다음은 함수 형태의 application이다. </p>
<pre><code class="language-python">def application(environ, start_response):
    &quot;&quot;&quot;Simplest possible application object&quot;&quot;&quot;
    status = &#39;200 OK&#39;
    response_headers = [(&#39;Content-type&#39;, &#39;text/plain&#39;)]
    start_response(status, response_headers)
    return [&#39;Hello world!\n&#39;]</code></pre>
<ul>
<li>application은 environ 객체와 start_response라는 콜백함수를 인자로 받는다.</li>
<li>environ은 method나 url 등 HTTP 요청에 대한 정보를 CGI 환경 변수의 형식으로 담고 있는 dictionary 형태의 객체라고 할 수 있다.</li>
<li>start_response 콜백 함수는 status와 response_headers라는 두 가지 인자를 받는다.</li>
<li>status에는 <code>200 OK</code> 와 같은 HTTP status 코드가 들어가고, response_headers에는 HTTP 헤더가 들어간다.</li>
</ul>
<h3 id="servergateway-side">Server/Gateway Side</h3>
<p>server side에서는 클라이언트의 요청이 올 때마다 application을 호출한다. </p>
<pre><code class="language-python">import os, sys

def run_with_cgi(application): # application을 인자로 받음
    # environ 객체
    environ = dict(os.environ.items())
    environ[&#39;wsgi.input&#39;]        = sys.stdin  # stdin의 형태로 input을 받음
    environ[&#39;wsgi.errors&#39;]       = sys.stderr
    environ[&#39;wsgi.version&#39;]      = (1, 0)
    environ[&#39;wsgi.multithread&#39;]  = False
    environ[&#39;wsgi.multiprocess&#39;] = True
    environ[&#39;wsgi.run_once&#39;]     = True

    if environ.get(&#39;HTTPS&#39;, &#39;off&#39;) in (&#39;on&#39;, &#39;1&#39;):
        environ[&#39;wsgi.url_scheme&#39;] = &#39;https&#39;
    else:
        environ[&#39;wsgi.url_scheme&#39;] = &#39;http&#39;

    headers_set = []
    headers_sent = []

    def write(data):
        if not headers_set:
             raise AssertionError(&quot;write() before start_response()&quot;)

        elif not headers_sent:
             # Before the first output, send the stored headers
             status, response_headers = headers_sent[:] = headers_set
             sys.stdout.write(&#39;Status: %s\r\n&#39; % status)
             for header in response_headers:
                 sys.stdout.write(&#39;%s: %s\r\n&#39; % header)
             sys.stdout.write(&#39;\r\n&#39;)

        sys.stdout.write(data)
        sys.stdout.flush()

        # status와 response_headers를 받는 start_response 콜백함수 
    def start_response(status, response_headers, exc_info=None):
        if exc_info:
            try:
                if headers_sent:
                    # Re-raise original exception if headers sent
                    raise exc_info[0], exc_info[1], exc_info[2]
            finally:
                exc_info = None     # avoid dangling circular ref
        elif headers_set:
            raise AssertionError(&quot;Headers already set!&quot;)

        headers_set[:] = [status, response_headers]
        return write

    result = application(environ, start_response)
    try:
        for data in result:
            if data:    # don&#39;t send headers until body appears
                write(data)
        if not headers_sent:
            write(&#39;&#39;)   # send headers now if body was empty
    finally:
        if hasattr(result, &#39;close&#39;):
            result.close() </code></pre>
<p><code>참고</code> <strong>WSGI Middleware</strong></p>
<ul>
<li>WSGI server/gateway side와 application/framework side를 둘 다 구현하고 있는 하나의 프로그램</li>
<li>서버에 대해서는 어플리케이션의 역할을 수행하고, 어플리케이션에 대해서는 서버의 역할을 수행.</li>
<li>동작 과정 : 클라이언트 요청 → server side에서 middleware component를 호출 → middleware component가 application side의 application을 호출</li>
<li><strong>Gunicorn, uWSGI</strong></li>
</ul>
<h2 id="3-framework의-wsgi-application">3. Framework의 WSGI application</h2>
<p>이번에는 대표적인 파이썬 프레임워크인 Django와 Flask에 내장된 WSGI application을 한 번 살펴보려고 한다. </p>
<h3 id="django">Django</h3>
<p>📁 django project를 생성하면 자동으로 만들어지는 wsgi.py 파일</p>
<pre><code class="language-python">&quot;&quot;&quot;
WSGI config for movie_project project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
&quot;&quot;&quot;

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault(&#39;DJANGO_SETTINGS_MODULE&#39;, &#39;movie_project.settings&#39;)

application = get_wsgi_application()</code></pre>
<p>📁 django.core.wsgi.py</p>
<pre><code class="language-python">import django
from django.core.handlers.wsgi import WSGIHandler

def get_wsgi_application():
    &quot;&quot;&quot;
    The public interface to Django&#39;s WSGI support. Return a WSGI callable.
    Avoids making django.core.handlers.WSGIHandler a public API, in case the
    internal WSGI implementation changes or moves in the future.
    &quot;&quot;&quot;
    django.setup(set_prefix=False)
    return WSGIHandler()</code></pre>
<p>📁 django.core.handlers.wsgi.py</p>
<pre><code class="language-python">class WSGIHandler(base.BaseHandler):
    request_class = WSGIRequest

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.load_middleware()

    def __call__(self, environ, start_response):
        set_script_prefix(get_script_name(environ))
        signals.request_started.send(sender=self.__class__, environ=environ)
        request = self.request_class(environ)
        response = self.get_response(request)

        response._handler_class = self.__class__

        status = &#39;%d %s&#39; % (response.status_code, response.reason_phrase)
        response_headers = [
            *response.items(),
            *((&#39;Set-Cookie&#39;, c.output(header=&#39;&#39;)) for c in response.cookies.values()),
        ]
        start_response(status, response_headers)
        if getattr(response, &#39;file_to_stream&#39;, None) is not None and environ.get(&#39;wsgi.file_wrapper&#39;):
            # If `wsgi.file_wrapper` is used the WSGI server does not call
            # .close on the response, but on the file wrapper. Patch it to use
            # response.close instead which takes care of closing all files.
            response.file_to_stream.close = response.close
            response = environ[&#39;wsgi.file_wrapper&#39;](response.file_to_stream, response.block_size)
        return response</code></pre>
<h3 id="flask">Flask</h3>
<p>📁 flask app.py</p>
<pre><code class="language-python">from flask import Flask
app = Flask(__name__)

@app.route(&quot;/&quot;)
def hello():
    return &quot;Hello World!&quot;

if __name__ == &quot;__main__&quot;:
    app.run()</code></pre>
<p>📁 flask.src.flask.app.py</p>
<pre><code class="language-python">class Flask(Scaffold):
    &quot;&quot;&quot;The flask object implements a WSGI application and acts as the central
    object.  It is passed the name of the module or package of the
    application.  Once it is created it will act as a central registry for
    the view functions, the URL rules, template configuration and much more.
    The name of the package is used to resolve resources from inside the
    package or the folder the module is contained in depending on if the
    package parameter resolves to an actual python package (a folder with
    an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file).
    &quot;&quot;&quot;

    def wsgi_app(self, environ, start_response):
            &quot;&quot;&quot;The actual WSGI application. This is not implemented in
            :meth:`__call__` so that middlewares can be applied without
            losing a reference to the app object. Instead of doing this::
                app = MyMiddleware(app)
            It&#39;s a better idea to do this instead::
                app.wsgi_app = MyMiddleware(app.wsgi_app)
            Then you still have the original application object around and
            can continue to call methods on it.

            :param environ: A WSGI environment. 
            :param start_response: A callable accepting a status code,
                a list of headers, and an optional exception context to
                start the response.
            &quot;&quot;&quot;
            ctx = self.request_context(environ)
            error = None
            try:
                try:
                    ctx.push()
                    response = self.full_dispatch_request()
                except Exception as e:
                    error = e
                    response = self.handle_exception(e)
                except:  # noqa: B001
                    error = sys.exc_info()[1]
                    raise
                return response(environ, start_response)
            finally:
                if self.should_ignore_error(error):
                    error = None
                ctx.auto_pop(error)

        def __call__(self, environ, start_response):
            &quot;&quot;&quot;The WSGI server calls the Flask application object as the
            WSGI application. This calls :meth:`wsgi_app`, which can be
            wrapped to apply middleware.
            &quot;&quot;&quot;
            return self.wsgi_app(environ, start_response)</code></pre>
<h2 id="4-정리">4. 정리</h2>
<p>✔ <strong>Gunicorn은 왜 필요한가?</strong> 웹 앱에 HTTP 요청을 전달하고 응답을 되돌려주는 일을 할 WSGI server의 역할을 하기 위해서 → WSGI middleware </p>
<p>✔ <strong>Nginx는 왜 필요한가?</strong> reverse proxy server, load balancer 등의 역할을 수행하기 위해서</p>
<p>✔ <strong>Django/Flask는 왜 필요한가?</strong> WSGI compatible server를 알아서 제공해주기 때문이다. raw WSGI application을 만들 수는 있지만 기능을 일일이 다 구현하기 번거롭고 너무 많은 코너 케이스가 존재하기 때문에 권장하지는 않는다.  그래서 이미  session이나 cookie와 같은 많은 부분이 구현되어 있는 프레임워크를 쓰는 것이다. </p>
<p>✔ <strong>그럼 그냥 프레임워크 서버를 웹 서버로 사용하면 되지 않을까?</strong> 그래도 안될 건 없다. 하지만 보통은 그러지 않는다. 프레임워크가 제공하는 development server는 실제 트래픽에 대응할 수 없고 여러 부분에서 빈약하기 때문이다. Flask 앱을 만들 때 <code>flask run</code>을 통해 서버를 실행시키면 터미널에 production server용으로는 쓰지 말라는 메세지가 나오는 이유도 이 때문이다. </p>
<h2 id="그래서-결론은">그래서 결론은?</h2>
<p>✅ <strong>Gunicorn만 써도 돼?</strong> YES. </p>
<p>Gunicorn이 WSGI middleware로서 웹 서버의 역할을 수행하기 때문에 Gunicorn만 써도 된다. 다만, Nginx가 제공하는 추가적인 혜택을 받지 못할 뿐이다. </p>
<p>✅ <strong>Nginx만 써도 돼?</strong>  YES.</p>
<p>Flask나 Django 같은 프레임워크는 WSGI interface를 이미 어느 정도 구현해놓았기 때문에 프레임워크를 사용한다면 Nginx만 써도 된다. 다만 session, cookie, routing, authentication 등의 기능을 수행해주는 middleware의 역할을 하는 애가 없기 때문에 이 부분은 자기가 하드 코딩해야 한다. 결국, Gunicorn/uWSGI를 사용하는 것도 편리한 기능들을 제공해주는 라이브러리를 가져다 쓰는 것과 똑같다고 할 수 있고, 반드시 써야만 하는 건 아니다.</p>
<hr>
<h3 id="참고-자료">참고 자료</h3>
<ul>
<li><a href="https://youtu.be/WqrCnVAkLIo">https://youtu.be/WqrCnVAkLIo</a></li>
<li><a href="https://youtu.be/H6Q3l11fjU0">https://youtu.be/H6Q3l11fjU0</a></li>
<li><a href="https://www.python.org/dev/peps/pep-0333/#specification-overview">https://www.python.org/dev/peps/pep-0333/#specification-overview</a> (PEP333)</li>
<li><a href="https://www.nginx.com/resources/glossary/application-server-vs-web-server/">https://www.nginx.com/resources/glossary/application-server-vs-web-server/</a></li>
<li><a href="https://www.fullstackpython.com/green-unicorn-gunicorn.html#:~:text=Gunicorn%20is%20based%20on%20a,is%20independent%20of%20the%20controller">https://www.fullstackpython.com/green-unicorn-gunicorn.html#:~:text=Gunicorn is based on a,is independent of the controller</a>.</li>
<li><a href="https://sgc109.github.io/2020/08/15/python-wsgi/">https://sgc109.github.io/2020/08/15/python-wsgi/</a></li>
<li><a href="http://xplordat.com/2020/02/16/a-flask-full-of-whiskey-wsgi/">http://xplordat.com/2020/02/16/a-flask-full-of-whiskey-wsgi/</a></li>
</ul>
<h3 id="더-알아보고-싶은-내용">더 알아보고 싶은 내용</h3>
<ul>
<li>ASGI : <a href="https://youtu.be/uRcnaI8Hnzg">https://youtu.be/uRcnaI8Hnzg</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[PostgreSQL 시작하기 ]]></title>
            <link>https://velog.io/@jimin_lee/PostgreSQL-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jimin_lee/PostgreSQL-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 20 Apr 2021 14:04:08 GMT</pubDate>
            <description><![CDATA[<p>이번에 장고를 쓰면서 DB로 PostgreSQL을 쓰게 되었다. MySQL이랑 별 다를 게 없을 줄 알았는데 의외로 DB랑 User를 만들어서 shell에 들어가는 것부터 어려움을 겪어서, 처음 설치하고 나서부터 했던 작업을 기록해보았다. </p>
<hr>
<h3 id="1-superuser로-shell에-접속하기">1. Superuser로 shell에 접속하기</h3>
<pre><code>sudo -u postgres psql</code></pre><p>우선 superuser 계정인 postgres로 접속한 후, DB와 유저를 생성했다. </p>
<h3 id="2-유저-생성-및-권한-부여하기">2. 유저 생성 및 권한 부여하기</h3>
<pre><code># alice 라는 계정 생성
CREATE USER alice WITH PASSWORD &#39;alice&#39;;

# alice 계정에 CREATEDB, REPLICATION 권한(role) 부여
ALTER ROLE elice CREATEDB REPLICATION;

# all in one
# WITH으로 여러 옵션 연결 가능
CREATE USER alice WITH CREATEDB REPLICATION PASSWORD &#39;alice&#39;;

# user 조회하기
\du
or
SELECT * FROM pg_user;
or
SELECT * FROM pg_shadow;

# user 삭제
DROP USER alice;</code></pre><p><code>참고</code> mysql의 privilege == postgresql의 role</p>
<h3 id="3-db-생성하기">3. DB 생성하기</h3>
<pre><code># DB 생성하기
CREATE DATABASE rabbithole OWNER alice;

# DB 조회하기
\l
or
SELECT * FROM pg_database;</code></pre><h3 id="4-db-유저-바꾸기">4. DB, 유저 바꾸기</h3>
<p>shell에서 DB와 유저를 바꾸려면 아래의 명령어를 shell에서 입력하면 된다. </p>
<pre><code>\c db_name user_name</code></pre><h3 id="5-에러-해결">5. 에러 해결</h3>
<p>DB와 유저를 만들고 나서 새로 생성한 유저 alice로 psql에 접속하려고 했는데, <code>Peer authentication failed with user &quot;alice&quot;</code>라는 에러가 뜨면서 접속이 되지 않았다. 구글링해봤더니 config 파일의 authentication 수준에 관련된 문제였다. 그래서 <a href="https://stackoverflow.com/questions/18664074/getting-error-peer-authentication-failed-for-user-postgres-when-trying-to-ge">이 글</a>을 참고해서 config 파일의 설정을 바꿔주었다. </p>
<p>user authentication 타입에는 3가지가 있는데, peer/trust/md5로 나뉜다. 디폴트값은 peer인데, 유저의 OS user name을 데이터베이스의 username으로 인식한다는데 무슨 소리인지 잘 모르겠다. trust는 아무 유저나 allow하는 것이고, md5는 패스워드 기반의 인증 타입이다. </p>
<p>superuser인 postgres의 패스워드가 아직 설정되어있지 않으므로, 우선 인증 타입을 trust로 바꿔서 postgres의 패스워드를 설정한 다음에 인증 타입을 비밀번호 기반의 md5로 바꿔서 접속했다. </p>
<ol>
<li>/etc.postgresql/version_number/main으로 이동하기<pre><code>cd /etc.postgresql/version_number/main</code></pre></li>
<li>config 파일 열기<pre><code>sudo nano pg_hba.conf</code></pre></li>
<li>파일 내용 수정하기<pre><code>local   all     postgres      peer
을
local   all     postgres      trust
로 바꿔주기</code></pre></li>
<li>superuser의 password 설정하기 <pre><code># shell에 들어가기
psql -U postgres 
</code></pre></li>
</ol>
<h1 id="postgres-비밀번호-설정하기">postgres 비밀번호 설정하기</h1>
<p>ALTER USER postgres WITH PASSWORD &#39;password&#39;;</p>
<pre><code>5. postgresql 재시작하기</code></pre><p>sudo service postgresql restart</p>
<pre><code>6. 다시 config 파일 설정 바꾸기 </code></pre><p>sudo nano pg_hba.conf
local   all     postgres      md5</p>
<pre><code>7. alice 유저로 psql shell 접속하기 </code></pre><h1 id="options---u-user--h-host--d-db">options : -U user, -h host, -d DB</h1>
<p>psql -U elice -h localhost db_name
or
psql -d db_name -U elice -h localhost</p>
<pre><code>
***

이상하게 host 옵션을 붙이지 않으면 alice 유저로 접속이 되지 않았다. 왜 그런지는 모르겠다. postgresql에 익숙해지도록 이것저것 더 만져봐야겠다. 

### 참고
https://stackoverflow.com/questions/18664074/getting-error-peer-authentication-failed-for-user-postgres-when-trying-to-ge</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Drf로 API 테스트 코드 짜기  ]]></title>
            <link>https://velog.io/@jimin_lee/Drf%EB%A1%9C-API-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%A7%9C%EA%B8%B0</link>
            <guid>https://velog.io/@jimin_lee/Drf%EB%A1%9C-API-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%A7%9C%EA%B8%B0</guid>
            <pubDate>Sun, 18 Apr 2021 06:34:30 GMT</pubDate>
            <description><![CDATA[<p>계속 postman으로 api테스트를 하다가 이번에 django를 써보면서 django rest framework로 간편하게 api 테스트를 할 수 있다는 것을 알게 되었다. </p>
<p>django이 TestCase처럼 drf에서 제공하는 APITestCase가 있는데, 이 모듈을 상속받아 테스트 코드를 짜보았다. </p>
<pre><code>from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework.views import status


class SentimentAnalyzeTestCase(APITestCase):
    def setUp(self):
        self.url = reverse(&#39;sentiment&#39;)

    def test_post_user_data(self):
        data = {
            &quot;words&quot;: [&quot;death&quot;, &quot;fear&quot;, &quot;kill&quot;, &quot;pray&quot;, &quot;god&quot;, &quot;hope&quot;, &quot;pretty&quot;]
        }
        response = self.client.post(self.url, data=data, format=&#39;json&#39;)
        self.assertEqual(response.status_code, status.HTTP_200_OK)</code></pre><p>위의 코드는 post 요청을 확인하는 테스트 코드이다. </p>
<p>참고로 url의 <code>reverse</code>는 Flask의 <code>url_for</code>과 동일한 역할을 하는 함수인 것 같다. </p>
<pre><code># urls.py 
urlpatterns = [
    path(&#39;sentiment/&#39;, views.SentimentAnalyzeView.as_view(), name=&#39;sentiment&#39;)
]</code></pre><p>이렇게 url 경로가 있을 때, <code>reverse(&#39;sentiment&#39;)</code> 혹은 <code>reverse(views.SentimentAnalyzeView)</code>로 접근할 수 있다. </p>
<hr>
<h3 id="참고">참고</h3>
<p><a href="https://djangostars.com/blog/rest-apis-django-development/">https://djangostars.com/blog/rest-apis-django-development/</a></p>
]]></description>
        </item>
    </channel>
</rss>