<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>tk_kim.log</title>
        <link>https://velog.io/</link>
        <description>Backend Developer</description>
        <lastBuildDate>Thu, 03 Jun 2021 18:01:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>tk_kim.log</title>
            <url>https://images.velog.io/images/tk_kim/profile/3284eee0-2d6d-4bae-8571-ec6514dbeafd/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. tk_kim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/tk_kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Python lambda binding to local variables problem]]></title>
            <link>https://velog.io/@tk_kim/Python-lambda-binding-to-local-variables-problem</link>
            <guid>https://velog.io/@tk_kim/Python-lambda-binding-to-local-variables-problem</guid>
            <pubDate>Thu, 03 Jun 2021 18:01:01 GMT</pubDate>
            <description><![CDATA[<h1 id="lambda-in-a-loop">lambda in a loop</h1>
<p>async event loop 기반 framework 인 python tornado 를 사용하여 웹소켓 서버에 데이터를 보내던 중에 문제가 발생했다.</p>
<p>tornado 는 멀티스레딩 사용 시 main event loop 에서 실행할 기능을 콜백에 담아 처리한다.
그래서 for loop 을 돌며 client 에게 메시지를 전달하는 로직을 구현했다.</p>
<pre><code class="language-python"># 중략
for client in self.clients:
    data = json.dumps({self.stock_code: stock_price})

    self.loop.add_callback(lambda client:client.write_message(data))
    # time.sleep(0.0000001)
</code></pre>
<p>self.clients 는 웹소켓에 접속한 클라이언트이다.
다음과 같은 코드를 돌렸을 때 맨 마지막에 접속한 클라이언트에게만 메시지를 보내는 버그가 발생했다.
그런데 아주 임시방편으로 아주 짧은 시간동안 print() 함수를 실행했더니 또 작동하는 것이였다.</p>
<p>왜 그런지 고민끝에 이유를 발견했다.</p>
<p>다음 예제를 보자.</p>
<pre><code class="language-python">funcs = []
for i in range(5):
    funcs.append(lambda : print(i))

# i 값은 이 때 4이다.
for f in funcs:
    f()</code></pre>
<p>결과는 4만 다섯번 출력된다. 이유가 뭘까? </p>
<ul>
<li><p>바로 lambda 가 evaluate 되서야 비로서 코드가 생성되기 때문에 for loop에서 마지막 값인 4가 출력되는 것이다. </p>
</li>
<li><p>그리고 time.sleep 함수가 실행됐을 때 제대로 작동하는 것 처럼 보였던 이유는, <code>add_callback()</code> 에 함수가 담기고 나서 <code>time.sleep()</code> 이 실행되는 동안 <code>add_callback()</code> 에 들어있는 함수가 해당 for loop 안에서 실행이 될 준비가 되었기 때문이였다.</p>
</li>
<li><p>하지만 <code>time.sleep()</code> 이 없었을 떈 for loop 가 너무 빨리 돌아버려서 loop 이 끝나버린 뒤 함수가 실행준비가 되었기 때문에 마지막 client 에게만 데이터가 간 것이였다.</p>
</li>
</ul>
<p>그렇다면 이런 이슈를 해결하기 위해 우리는 어떻게 할 수 있을까?</p>
<h1 id="functoolspartial">functools.partial</h1>
<p>함수가 생성될 때 evaluate 되는 고차함수 <code>partial</code> 을 쓰면 된다.</p>
<pre><code class="language-python">from functools import partial
funcs = []
for i in range(5):
    funcs.append(partial(print, i))

for f in funcs:
    f()</code></pre>
<p> 결과는 차례대로 0 1 2 3 4 가 출력된다.</p>
<p> 하지만 여기서 방법이 끝이아니다.</p>
<h1 id="lambda-default-argument">lambda default argument</h1>
<p>lambda 의 default argument 를 사용하는 방법이 있다.</p>
<blockquote>
<p>Using a default argument works because default arguments are evaluated when the function is created, not when it is called.</p>
<blockquote>
<p><a href="https://stackoverflow.com/a/10452819">https://stackoverflow.com/a/10452819</a></p>
</blockquote>
</blockquote>
<p>default args 를 사용하면 해당 args 는 함수가 실행될 때가 아니라 생성될 때 evaluate 되기 때문에 이 방법도 해답이 될 수 있다.</p>
<h1 id="wrap-up">wrap-up</h1>
<p>중요한 것은 lambda 의 변수가 evaluate 되는 시점이다.</p>
<p>우리가 이 문제를 해결하기 위한 최선의 선택은 lambda 에 들어가는 함수가 얼마나 복잡한지에 따라 결정되는 것 같다.</p>
<ul>
<li>복잡하다면 : functools.partial</li>
<li>간단하다면 : lambda expression</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Linux Environment Variable setting]]></title>
            <link>https://velog.io/@tk_kim/Linux-Environment-Variable-setting</link>
            <guid>https://velog.io/@tk_kim/Linux-Environment-Variable-setting</guid>
            <pubDate>Wed, 26 May 2021 06:10:37 GMT</pubDate>
            <description><![CDATA[<p>파이썬 패키지 모듈을 인식하지 못하는 오류가 생겼다.</p>
<p>파이썬 스크립트에 sys.path.append 를 추가해주는 방법도 있지만,
환경변수 추가를 함으로써 스크립트를 깔끔하게 유지하기로 했다.</p>
<h1 id="-export">$ export</h1>
<p><img src="https://images.velog.io/images/tk_kim/post/eb3c1856-b1cb-422b-9628-f462ef6c9737/image.png" alt=""></p>
<p>그냥 <code>export PYTHONPATH=&quot;${PYTHONPATH}:/your/source/root&quot;</code> 이렇게 해줄수도 있지만, 이건 임시적인 방법이라 이렇게 하면 컴퓨터를 재부팅했을 때 환경변수가 사라진다.</p>
<p>zsh 를 쓰고있기 때문에 
<code>~/.zshenv</code> 에 환경변수를 추가하고 <code>source ~/.zshenv</code> 로 업데이트를 해준 뒤 
<code>echo $PYTHONPATH</code> 로 적용이 되었는지 확인해보았더니
환경변수 경로가 제대로 출력된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python replace method with immutable object]]></title>
            <link>https://velog.io/@tk_kim/Python-replace-method-with-immutable-object</link>
            <guid>https://velog.io/@tk_kim/Python-replace-method-with-immutable-object</guid>
            <pubDate>Thu, 20 May 2021 10:43:54 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python">croatia = [&#39;c=&#39;, &#39;c-&#39;, &#39;dz=&#39;, &#39;d-&#39;, &#39;lj&#39;, &#39;nj&#39;, &#39;s=&#39;, &#39;z=&#39;]

c_alphabet = input()

c_alphabet_list = list()
for c in croatia:
    while True:
        if c in c_alphabet:
            c_alphabet = c_alphabet.replace(c, &#39;?&#39;, 1)

            c_alphabet_list.append(c)
        else:
            break

alphabets_without_croatia = c_alphabet.replace(&#39;?&#39;, &#39;&#39;)
num = len(c_alphabet_list) + len(alphabets_without_croatia)
print(num)</code></pre>
<ul>
<li>처음에 c_alphabet.replace 를 했을 때 c_alphabet 객체 자체의 변화가 있을 줄 알았는데 그렇지 않아서</li>
<li><code>c_alphabet = c_alphabet.replace(c, &#39;?&#39;, 1)</code> 처럼 같은 변수에 값을 다시 재할당 해주었다.</li>
<li>왜냐면 string 자료형은 immutable 하다.</li>
<li>immutable 객체들은 값이 변경될 때 새로운 객체로 생성이 되기 때문에 변경이 일어날 때 기존 객체는 변하지 않는다.</li>
<li>마찬가지로 string 자료형을 replace 로 변경하려고 하면, 새로운 메모리를 참조하는 객체가 새로 생성되며, 이를 새로운 변수에 다시 할당해주어야 한다.</li>
</ul>
<p>immutable 한 객체들은 call by value 의 속성을 띄고 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django ORM query Error solve]]></title>
            <link>https://velog.io/@tk_kim/Django-ORM-query-Error-solve</link>
            <guid>https://velog.io/@tk_kim/Django-ORM-query-Error-solve</guid>
            <pubDate>Sun, 02 May 2021 15:16:42 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python">today       = datetime.datetime.today()
this_friday = today + datetime.timedelta((calendar.FRIDAY - today.weekday()) % 7)
base_date   = this_friday.date()
stock_qs = StockPrice.objects.filter(ticker_id=3)
stock_qs = stock_qs.values(&#39;date&#39;).annotate(group_id=(base_date - F(&#39;date&#39;))/ datetime.timedelta(days=7))
s1 = stock_qs.annotate(weekly_bprc_adj=Window(
     expression=FirstValue(&#39;bprc_adj&#39;),
     partition_by=[F(&#39;group_id&#39;)],
     order_by=F(&#39;date&#39;).asc()
     ))</code></pre>
<pre><code class="language-ProgrammingError:">LINE 1: ...&#39; * (&#39;2021-05-07&#39;::date - &quot;stock_prices&quot;.&quot;date&quot;)) / &#39;7 days ...
                                                             ^
HINT:  No operator matches the given name and argument types. You might need to add explicit type casts.</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 44 || Stock Candle Chart on DB Level Optimizing]]></title>
            <link>https://velog.io/@tk_kim/Stock-Candle-Chart</link>
            <guid>https://velog.io/@tk_kim/Stock-Candle-Chart</guid>
            <pubDate>Sun, 02 May 2021 09:00:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/tk_kim/post/f79a00b7-6958-4c4c-8617-1e6805ed1ffc/image.png" alt=""></p>
<p>위의 결과는 각각 캔들차트를 monthly, weekly 옵션을 넣어서 돌렸을 때 걸린 시간을 나타낸 것이다.</p>
<h1 id="application-level">Application Level</h1>
<p>기존에는 다음과 같이 시간복잡도가 O(n^2) 인 코드를 작성했다.</p>
<pre><code class="language-python">  class StockCandleChart(View):
    def get(self, request):
        chart_type  = request.GET.get(&#39;chart_type&#39;, &#39;daily&#39;)
        code        = request.GET.get(&#39;ticker&#39;, None)

        error_handler_res = handle_candle_chart_input_error(chart_type, code)
        if isinstance(error_handler_res, JsonResponse):
            return error_handler_res

        two_years_from_now = datetime.datetime.now() - relativedelta(years=2)

        ticker          = Ticker.objects.get(code=code)
        stock_prices_qs = StockPrice.objects.filter(ticker=ticker, date__gte=two_years_from_now).order_by(&#39;date&#39;)

        if chart_type == &#39;daily&#39;:
            stock_prices  = [
                                {
                                &#39;date&#39;    : str(stock_price_qs.date),
                                &#39;bprc_adj&#39;: stock_price_qs.bprc_adj,
                                &#39;prc_adj&#39; : stock_price_qs.prc_adj,
                                &#39;hi_adj&#39;  : stock_price_qs.hi_adj,
                                &#39;lo_adj&#39;  : stock_price_qs.lo_adj,
                                &#39;volume&#39;  : stock_price_qs.volume
                                } for stock_price_qs in stock_prices_qs]
        else:
            groups_dict  = self.get_stock_price_groups_by_chart_type(chart_type, stock_prices_qs)
            stock_prices = self.get_stock_prices_list(groups_dict)

        data = {
                &#39;name&#39;  : ticker.stock_name,
                &#39;ticker&#39;: ticker.code,
                &#39;values&#39;: stock_prices
            }

        return JsonResponse({&#39;results&#39;: data}, status=200)

    def get_stock_price_groups_by_chart_type(self, chart_type, stock_prices_qs):
        pre_group_num = int()
        groups_dict   = dict()

        for stock_price_qs in stock_prices_qs:
            if chart_type == &#39;weekly&#39;:
                today       = datetime.datetime.today()
                this_friday = today + datetime.timedelta((calendar.FRIDAY - today.weekday()) % 7)
                base_date   = this_friday.date()

                time_diff         = (base_date - stock_price_qs.date).days
                current_group_num = int(time_diff / 7)

            elif chart_type == &#39;monthly&#39;:
                current_group_num = stock_price_qs.date.strftime(&#39;%Y-%m&#39;)

            groups_dict.setdefault(current_group_num, [stock_price_qs])

            if current_group_num == pre_group_num:
                groups_dict[current_group_num].append(stock_price_qs)
            pre_group_num = current_group_num

        return groups_dict

    def get_stock_prices_list(self, groups_dict):
        stock_prices = list()

        for group in groups_dict:
            stock_price_list = groups_dict[group]

            first_stock_price = stock_price_list[0]
            last_stock_price  = stock_price_list[-1]

            bprc_adj = first_stock_price.bprc_adj
            prc_adj  = last_stock_price.prc_adj

            lowest  = first_stock_price.lo_adj
            highest = first_stock_price.hi_adj
            volume  = 0

            for stock_price in stock_price_list:
                if stock_price.lo_adj &lt; lowest:
                    lowest = stock_price.lo_adj

                if stock_price.hi_adj &gt; highest:
                    highest = stock_price.hi_adj

                volume += stock_price.volume

            weekly_stock_price = {
                &#39;date&#39;: last_stock_price.date,
                &#39;bprc_adj&#39;: bprc_adj,
                &#39;prc_adj&#39;: prc_adj,
                &#39;hi_adj&#39;: highest,
                &#39;lo_adj&#39;: lowest,
                &#39;volume&#39;: volume
            }
            stock_prices.append(weekly_stock_price)

        return stock_prices</code></pre>
<p>하지만 아무리 생각해도 너무 for 문이 많고 복잡도가 높은 것 같아서, 
App Level 이 아니라, DB Level 에서 처리해보기로 했다.</p>
<h2 id="db-level">DB Level</h2>
<h4 id="일주일-그룹-데이터의-거래량volume-고가hi_adj-저가lo_adj-구하기">일주일 그룹 데이터의 거래량(volume), 고가(hi_adj), 저가(lo_adj) 구하기</h4>
<pre><code class="language-sql">SELECT time_diff, SUM(volume) volume, MAX(hi_adj) hi_adj, MIN(lo_adj) lo_adj 
FROM (SELECT *, (date &#39;2021-04-29&#39; - date) / 7 as time_diff
      FROM stock_prices WHERE ticker_id=3 ORDER BY date) td
GROUP BY time_diff ORDER BY time_diff;</code></pre>
<p><img src="https://images.velog.io/images/tk_kim/post/ecc9b42c-283c-4249-afd1-f31d3fc966f6/image.png" alt=""></p>
<h4 id="일주일-그룹-데이터의-종가prc_adj-구하기">일주일 그룹 데이터의 종가(prc_adj) 구하기</h4>
<p>weekly candle chart 는 
일주일으로 묶인 주식 가격 그룹의 가장 마지막 날의 종가를 구해야한다.</p>
<pre><code class="language-sql">SELECT *
FROM 
    (SELECT *, row_number() OVER(PARTITION BY time_diff ORDER BY date DESC) &quot;row_number&quot; 
     FROM (SELECT *, (date &#39;2021-04-29&#39; - date) / 7 as time_diff 
           FROM stock_prices
           WHERE ticker_id=3
           ORDER BY date) td) td2 WHERE row_number=1;</code></pre>
<p><img src="https://images.velog.io/images/tk_kim/post/0cd627c0-cf9d-4eb9-8f4c-afbc678d4660/image.png" alt=""></p>
<h4 id="일주일-그룹-데이터의-시가bprc_adj-구하기">일주일 그룹 데이터의 시가(bprc_adj) 구하기</h4>
<p>PARTITION BY 구문에서 DESC 를 ASC 로 바꿔주기만 하면 된다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/3f53680a-6d05-4462-8e1b-c582fef271c7/image.png" alt=""></p>
<blockquote>
<p>위 쿼리들은 각각 (거래량, 고가, 저가), 시가, 종가 를 세번에 걸쳐서 구한다. 
이 모든걸 한 번의 쿼리로 수행되게끔 바꿀 수 있는 쿼리는 무엇인지 생각해보았다.</p>
</blockquote>
<h4 id="날짜-거래량-고가-저가-시가-종가-한번에-select-하기">날짜, 거래량, 고가, 저가, 시가, 종가 한번에 SELECT 하기</h4>
<pre><code class="language-sql">SELECT 
    MAX(date), SUM(volume) volume, MAX(hi_adj) hi_adj, MIN(lo_adj) lo_adj, MAX(first_bprc_adj) bprc_adj, MAX(last_prc_adj) prc_adj
FROM
    (
    SELECT
        FIRST_VALUE(bprc_adj) OVER(PARTITION BY group_id ORDER BY date) first_bprc_adj,
        FIRST_VALUE(prc_adj) OVER(PARTITION BY group_id ORDER BY date DESC) last_prc_adj,
        *
        FROM
            (SELECT *, (date &#39;2021-04-30&#39; - date) / 7 group_id FROM stock_prices) t1
        WHERE ticker_id = 3
    ) t2
    GROUP BY group_id
</code></pre>
<p>group by 에서 지정되지 않은 컬럼은 집계함수를 못쓰지만,
어차피 모두 동일한 값을 가지고 있는 필드 값들에서 max 든 min 등 하나의 값을 반환하기 때문에 이렇게 했다. </p>
<h1 id="16-times-faster">16 times faster</h1>
<p><img src="https://images.velog.io/images/tk_kim/post/ddf3108f-962b-41b6-bdea-7f55ec16240c/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/tk_kim/post/6797d653-ac12-45f2-8ae2-c012201a60ac/image.png" alt=""></p>
<p>weekly 기준 db level 에서의 속도가 약 16배 빠르다.</p>
<h1 id="wrap-up-final-codes">Wrap-up (Final Codes)</h1>
<pre><code class="language-python">def get_candle_chart_by_type(self, chart_type, stock_prices_qs):
        if chart_type == &#39;daily&#39;:
            results_qs = stock_prices_qs.values(&#39;date&#39;, &#39;bprc_adj&#39;, &#39;prc_adj&#39;, &#39;hi_adj&#39;, &#39;lo_adj&#39;, &#39;volume&#39;).order_by(&#39;date&#39;)
            return list(results_qs)

        if chart_type == &#39;weekly&#39;:
            today       = datetime.datetime.today()
            this_friday = today + datetime.timedelta((calendar.FRIDAY - today.weekday()) % 7)
            base_date   = this_friday.date()

            first_qs = stock_prices_qs.annotate(group_id=Cast(
                                        ExtractDay(base_date - F(&#39;date&#39;)), IntegerField()) / 7).order_by(&#39;date&#39;)
        elif chart_type == &#39;monthly&#39;:
             first_qs = stock_prices_qs.annotate(group_id=TruncMonth(F(&#39;date&#39;))).order_by(&#39;date&#39;)

        second_qs = first_qs.values(&#39;group_id&#39;)\
                            .annotate(
                                    bprc_adj=Window(
                                        expression   = FirstValue(&#39;bprc_adj&#39;),
                                        partition_by = F(&#39;group_id&#39;),
                                        order_by     = F(&#39;date&#39;).asc()
                                    ),
                                    prc_adj=Window(
                                        expression   = FirstValue(&#39;prc_adj&#39;),
                                        partition_by = F(&#39;group_id&#39;),
                                        order_by     = F(&#39;date&#39;).desc()
                                    ),
                                    date=Window(
                                        expression   = Max(&#39;date&#39;),
                                        partition_by = F(&#39;group_id&#39;)
                                    ),
                                    hi_adj=Window(
                                        expression   = Max(&#39;hi_adj&#39;),
                                        partition_by = F(&#39;group_id&#39;)
                                    ),
                                    lo_adj=Window(
                                        expression   = Min(&#39;lo_adj&#39;),
                                        partition_by = F(&#39;group_id&#39;)
                                    ),
                                    volume=Window(
                                        expression   = Sum(&#39;volume&#39;),
                                        partition_by = F(&#39;group_id&#39;)
                                    )
                            )\
                            .distinct(&#39;date&#39;)\
                            .values(&#39;date&#39;, &#39;bprc_adj&#39;, &#39;prc_adj&#39;, &#39;hi_adj&#39;, &#39;lo_adj&#39;, &#39;volume&#39;)
        results = list(second_qs)
        return results
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 43 || Django Standalone file settings]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-41-Django-Standalone-file-settings</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-41-Django-Standalone-file-settings</guid>
            <pubDate>Fri, 30 Apr 2021 02:20:59 GMT</pubDate>
            <description><![CDATA[<p>csv 파일을 db 에 밀어넣는 작업을 하다가 해당 파일이 실행이 안되는 경우가 발생했다. 
그래서 단독파일을 실행할 때는 standalone file 세팅을 해줘야 한다는 사실을 문서를 읽어보다가 알았다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/8ecf27bf-fd90-4aca-bfbc-f380b68fc818/image.png" alt=""></p>
<p>위와 같은 예제 snippet 처럼 작성하거나</p>
<p><img src="https://images.velog.io/images/tk_kim/post/dcf95125-47ac-441a-9c2e-e30f3849ba75/image.png" alt=""></p>
<p>이렇게 직접 환경변수의 <code>DJANGO_SETTINGS_MODULE</code> 에 접근해서 default 로 사용할 settings 파일을 지정해줘도 된다.</p>
<p>둘중 택 1을 해서 작성한 뒤,
django.setup() 을 실행해준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 42 || Django Channels with Redis]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-42-Django-Channels-with-Redis</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-42-Django-Channels-with-Redis</guid>
            <pubDate>Sun, 25 Apr 2021 10:25:55 GMT</pubDate>
            <description><![CDATA[<h4 id="수정-210429">(수정 21.04.29)</h4>
<p>주식 가격을 클라이언트들에게 websocket 을 통해 push 하려고 Django 의 Channels 를 사용하던 중 의문점이 들었다.</p>
<ul>
<li>Django Channels 에서 유저가 consumer 를 통해 접근하면</li>
<li>다음과 같이 해당 유저의 individual channel 이 설정해놓은 group 으로 참여되도록 했다. </li>
</ul>
<p><img src="https://images.velog.io/images/tk_kim/post/1a4b5a9a-9434-42f3-ad7b-01c917a8114b/image.png" alt=""></p>
<p>해당 group 의 이름을 알면 그 group 에 연결된 모든 channel 들에게 메시지를 전달하는게 가능하다.
그래서 그 group 의 이름을 알아야했는데, 문서 어디를 찾아봐도 group 리스트를 구하는 방법이 나와있지 않았다.</p>
<p>그 때 갑자기 channel layers 를 관리하기 위해 redis 서버를 세팅했던 것이 생각났다.</p>
<p>바로 redis 서버에 접속해서 모든 keys 를 확인해봤더니,</p>
<p><img src="https://images.velog.io/images/tk_kim/post/337312e0-df83-488c-a28a-dda58550ad81/image.png" alt=""></p>
<p>서버에 모든게 저장되어있었다.</p>
<blockquote>
<p><a href="https://github.com/django/channels_redis#prefix">https://github.com/django/channels_redis#prefix</a></p>
</blockquote>
<p>처음 세팅했던 <code>channels_redis</code> 모듈이</p>
<ul>
<li>채널과 그룹이 생성되고 어떤 채널들이 특정 그룹에 연결되는지와 같은 정보들을 저장하고,</li>
<li>그 정보들을 사용하여 그룹에 있는 채널들에게 메시지를 보낼 수 있도록 한다.  </li>
</ul>
<p>다음은 redis 서버와 django channels 가 어떤식으로 상호작용하는지 설명한 글이다.</p>
<blockquote>
<p>It depends on how you are using it. The primary purpose of redis in django-channel_layers is to store the necessary information required for different instances of consumers to communicate with one another.
For example, in the tutorial section of channels documentation, it is clear that Redis is used as a storage layer for channel names and group names. These are stored within Redis so that they can be accessed from any consumer instance. If for example, I create a group called &#39;users&#39; and then add 3 different channel names to it, this information is stored in Redis. Now, whenever I want to send data to the channels in the group I can simply reference the group from my consumer and Django-channels will automatically retrieve the channel names stored under that group in Redis.
On the other hand, if you want to use consumers in a non-conventional way, that is, as background workers then Redis becomes a message queue. That&#39;s because when you send a message containing a task to be done by one of the background workers (a consumer that &#39;consumes&#39; the tasks) those messages have to be stored somewhere so that the background workers can retrieve them as they finish up other tasks.</p>
<blockquote>
<p><a href="https://stackoverflow.com/questions/63754950/what-role-does-redis-serve-in-django-channels">https://stackoverflow.com/questions/63754950/what-role-does-redis-serve-in-django-channels</a></p>
</blockquote>
</blockquote>
<h2 id="how-to-retreive-data-from-redis-server">How to retreive data from redis server</h2>
<p>django-channels 자체에서 모든 groups 와 연결된 channels 들을 가져오는 것이 불가능하다고 해서, 아 그럼 django 프로젝트 안에서 redis 서버에 접근한 다음 저 group name 을 가지고 오기만 하면 된다 라고 생각했으나, 그렇게 다시한번 서버에 접근하는것이 비효율적이라고 판단했다. </p>
<p>그래서 그냥 백그라운드 스레드를 실행시켜서 해당 ticker 를 key 로 잡고 연결된 channels 들을 해당 key 의 value 로 관리했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 41-2 || [프로그래머스 SQL JOIN - 오랜 기간 보호한 동물(1),(2) & 보호소에서 중성화한 동물]]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-41-2-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-SQL-JOIN-%EC%98%A4%EB%9E%9C-%EA%B8%B0%EA%B0%84-%EB%B3%B4%ED%98%B8%ED%95%9C-%EB%8F%99%EB%AC%BC1-%EB%B3%B4%ED%98%B8%EC%86%8C%EC%97%90%EC%84%9C-%EC%A4%91%EC%84%B1%ED%99%94%ED%95%9C-%EB%8F%99%EB%AC%BC</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-41-2-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-SQL-JOIN-%EC%98%A4%EB%9E%9C-%EA%B8%B0%EA%B0%84-%EB%B3%B4%ED%98%B8%ED%95%9C-%EB%8F%99%EB%AC%BC1-%EB%B3%B4%ED%98%B8%EC%86%8C%EC%97%90%EC%84%9C-%EC%A4%91%EC%84%B1%ED%99%94%ED%95%9C-%EB%8F%99%EB%AC%BC</guid>
            <pubDate>Thu, 22 Apr 2021 08:13:57 GMT</pubDate>
            <description><![CDATA[<h1 id="오랜-기간-보호한-동물1">오랜 기간 보호한 동물(1)</h1>
<ul>
<li>ANIMAL_INS 에서 ANIMAL_OUTS 를 LEFT JOIN 해준 뒤,</li>
<li>ANIMAL_OUTS 의 ANIMAL_ID 값이 NULL 인 부분을 가지고 와서</li>
<li>오래된 순으로 ORDER BY 뒤 LIMIT 3 을 걸어주면 된다. </li>
</ul>
<pre><code class="language-sql">SELECT A.NAME, A.DATETIME
FROM ANIMAL_INS A LEFT JOIN ANIMAL_OUTS B
                         ON A.ANIMAL_ID = B.ANIMAL_ID
WHERE B.ANIMAL_ID IS NULL
ORDER BY A.DATETIME LIMIT 3;</code></pre>
<p><img src="https://images.velog.io/images/tk_kim/post/55ad6ea2-6380-402b-b8e8-ed410fc6d897/image.png" alt=""></p>
<h1 id="오랜-기간-보호한-동물2">오랜 기간 보호한 동물(2)</h1>
<ul>
<li>INNER JOIN 으로 ANIMAL_INS 와 ANIMAL_OUTS 를 합친다.</li>
<li>ANIMAL_OUTS.DATETIME 에서 ANIMAL_INS.DATETIME 를 뺀 값을 기준으로 ORDER BY 한다.</li>
<li>두 값을 뺀 값이 가장 높은 것이, 보호소에 있었던 시간이 가장 길기 때문에 DESC 으로 정렬한다.</li>
</ul>
<pre><code class="language-sql">-- 코드를 입력하세요
SELECT A.ANIMAL_ID, A.NAME
FROM ANIMAL_INS A INNER JOIN ANIMAL_OUTS B
                         ON A.ANIMAL_ID = B.ANIMAL_ID
ORDER BY (B.DATETIME - A.DATETIME) DESC LIMIT 2;</code></pre>
<h1 id="보호소에서-중성화한-동물">보호소에서 중성화한 동물</h1>
<ul>
<li>두 테이블을 OUTER JOIN 한 뒤 입양할 때와 입양보내졌을 때의 SEX 상태가 다른걸 찾으면 된다.</li>
</ul>
<pre><code class="language-sql">-- 코드를 입력하세요
SELECT A.ANIMAL_ID, A.ANIMAL_TYPE, A.NAME
FROM ANIMAL_INS A INNER JOIN ANIMAL_OUTS B
                          ON A.ANIMAL_ID = B.ANIMAL_ID
WHERE A.SEX_UPON_INTAKE &lt;&gt; B.SEX_UPON_OUTCOME
ORDER BY A.ANIMAL_ID;</code></pre>
<p><img src="https://images.velog.io/images/tk_kim/post/905707ca-b58b-40e0-afa9-6ba1369911e5/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 41 || SQL JOIN + GROUP BY Practice]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-41-SQL-JOIN-GROUP-BY-Practice</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-41-SQL-JOIN-GROUP-BY-Practice</guid>
            <pubDate>Thu, 22 Apr 2021 06:15:28 GMT</pubDate>
            <description><![CDATA[<p>1차 프로젝트 때 Django orm 쿼리를 실제 SQL 쿼리로 바꿔보면서
inner join, outer join 을 언제 써야 하는지에 대해 익혀보게 되는 계기가 되었다.</p>
<h1 id="problem">Problem</h1>
<h2 id="장바구니에-물건이-8개보다-많은-유저의-이름을-구해보자">장바구니에 물건이 8개보다 많은 유저의 이름을 구해보자</h2>
<p>테이블은 다음과 같이 구성돼있다. </p>
<h3 id="carts">carts</h3>
<p><img src="https://images.velog.io/images/tk_kim/post/579dbaf7-fe01-4353-bb4f-54318fc5d031/image.png" alt=""></p>
<h3 id="orders">orders</h3>
<p><img src="https://images.velog.io/images/tk_kim/post/5d3fddf5-fb31-4f30-97a1-a96e34c8befc/image.png" alt=""></p>
<h3 id="order_status">order_status</h3>
<p><img src="https://images.velog.io/images/tk_kim/post/1acc030a-a2d7-40fc-a08a-b6d7a7e0e3bb/image.png" alt=""></p>
<h3 id="users">users</h3>
<p><img src="https://images.velog.io/images/tk_kim/post/42867fbc-cf9f-43e9-926a-e27db6726b46/image.png" alt=""></p>
<ul>
<li>우선 carts 테이블에 들어있는 order_id 건에 대해서</li>
<li>order_status 의 status 가 &quot;구매전&quot;인 order 의 order_id 만 가져와야한다.</li>
</ul>
<pre><code class="language-sql">SELECT * a.id
FROM orders a INNER JOIN order_status b
                      ON a.order_status_id = b.id
WHERE b.status = &quot;구매전&quot;;</code></pre>
<ul>
<li><p>가져온 order_id 가 있는 테이블과 carts 테이블을 inner join 한다. (left join 해도 상관없음)</p>
</li>
<li><p>그 뒤 order_id 로 group by 한 뒤 sum(carts.quantity) 가 8 보다 큰 order_id 만 뽑아온다.</p>
</li>
</ul>
<pre><code class="language-sql">SELECT b.order_id
FROM (SELECT * a.id
      FROM orders a INNER JOIN order_status b
                            ON a.order_status_id = b.id
      WHERE b.status = &quot;구매전&quot;) a INNER JOIN carts b
                      ON a.id = b.order_id
      GROUP BY order_id
      HAVING SUM(b.quantity) &gt; 8;</code></pre>
<ul>
<li>8 보다 큰 order_id 가 있는 테이블과 orders 테이블을 join 하고, join 된 테이블에서 또 users 테이블을 join 한 뒤 users.name 만 SELECT 해주면 끝!</li>
</ul>
<pre><code class="language-sql">SELECT c.name
FROM (SELECT b.order_id
FROM (SELECT * a.id
      FROM orders a INNER JOIN order_status b
                            ON a.order_status_id = b.id
      WHERE b.status = &quot;구매전&quot;) a INNER JOIN carts b
                      ON a.id = b.order_id
      GROUP BY order_id
      HAVING SUM(b.quantity) &gt; 8) a INNER JOIN orders b
                                            ON a.order_id = b.id
                                    INNER JOIN users c
                                            ON b.user_id = c.id;</code></pre>
<p><img src="https://images.velog.io/images/tk_kim/post/777bef8a-5549-4edc-b9be-c7bdba2084e6/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 40 || [프로그래머스  SQL JOIN - 없어진 기록 찾기]]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-40-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-SQL-JOIN</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-40-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-SQL-JOIN</guid>
            <pubDate>Sun, 18 Apr 2021 13:24:43 GMT</pubDate>
            <description><![CDATA[<h1 id="없어진-기록-찾기">없어진 기록 찾기</h1>
<pre><code class="language-sql">-- 코드를 입력하세요
SELECT ANIMAL_OUTS.ANIMAL_ID, ANIMAL_OUTS.NAME
FROM ANIMAL_OUTS LEFT JOIN ANIMAL_INS
ON ANIMAL_OUTS.ANIMAL_ID = ANIMAL_INS.ANIMAL_ID
WHERE ANIMAL_OUTS.ANIMAL_ID NOT IN (SELECT ANIMAL_ID FROM ANIMAL_INS)
</code></pre>
<ul>
<li>ANIMAL_OUTS 에만 있고, ANIMAL_INS 에는 없는 row 를 반환하면 된다.</li>
<li>그러기 위해서는, ANIMAL_OUTS 를 기준으로 (LEFT) OUTER JOIN 을 한다.</li>
<li>그 다음, WHERE 구에서 ANIMAL_OUTS.ANIMAL_ID 가 ANIMAL_INS.ANIMAL_ID 에 들어있지 않는 경우를 NOT IN (서브쿼리) 를 사용해 조건을 지정해준다.</li>
</ul>
<p>마지막 WHERE 구를 다음과 같이 변경해도 된다.</p>
<pre><code class="language-sql">-- 코드를 입력하세요
SELECT ANIMAL_OUTS.ANIMAL_ID, ANIMAL_OUTS.NAME
FROM ANIMAL_OUTS LEFT JOIN ANIMAL_INS
ON ANIMAL_OUTS.ANIMAL_ID = ANIMAL_INS.ANIMAL_ID
WHERE ANIMAL_INS.ANIMAL_ID IS NULL;</code></pre>
<h1 id="결과">결과</h1>
<p><img src="https://images.velog.io/images/tk_kim/post/8fee5f88-2347-43bd-804f-7d2726dbf581/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 39 || SQL index]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-39-SQL-index</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-39-SQL-index</guid>
            <pubDate>Sun, 18 Apr 2021 08:21:30 GMT</pubDate>
            <description><![CDATA[<h1 id="sql-index">SQL INDEX</h1>
<p>인덱스는 테이블에 붙여진 색인이며, 검색속도를 향상시키는 역할을 한다. </p>
<p>여기서 &quot;검색&quot; 이란, SELECT 명령에 WHERE 구로 조건을 지정하고 그에 일치하는 행을 찾는 일련의 과정을 말한다.</p>
<p>따라서, 테이블에 인덱스가 지정되어 있으면 효율적으로 검색할 수 있으므로 WHERE 로 조건이 지정된 SELECT 명령의 처리 속도가 향상된다.</p>
<h2 id="algorithm-for-the-indexing">ALGORITHM FOR THE INDEXING</h2>
<ul>
<li>테이블에 인덱스를 작성하면 테이블 데이터와 별개로 인덱스용 데이터가 저장장치에 만들어진다.</li>
<li>이 때 이진 트리(binary tree) 라는 데이터구조로 작성된다.</li>
</ul>
<p><img src="https://images.velog.io/images/tk_kim/post/461129de-1cbe-4170-8aa7-dd35a96357dc/image.png" alt=""></p>
<ul>
<li><p>원하는 수치와 비교해서 더 크면 오른쪽, 작으면 왼쪽의 가지를 조사해나간다.</p>
</li>
<li><p>인덱스를 작성하면 애초에 이진 트리구조로 데이터를 저장하기 때문에 이진탐색만 하면 된다.</p>
</li>
<li><p>이진트리에서 같은 값을 가지는 노드를 여러 개 만들 수 없다는 특징은, 키값이 unique 한 컬럼에 대해서만 해당되는 이야기다.</p>
</li>
<li><p>키값이 unique 하지 않으면 가지가 두개가 아니라 세개 (하나는 같은경우) 가 된다.</p>
</li>
</ul>
<h2 id="how-to-create-index">How to create INDEX?</h2>
<pre><code class="language-sql">CREATE INDEX 인덱스명 ON 테이블명 (컬럼명1, 컬럼명2, ...)</code></pre>
<p><img src="https://images.velog.io/images/tk_kim/post/31d85a66-2bce-42bd-97a7-bacdfc36c7e6/image.png" alt=""></p>
<h2 id="how-to-use-index">How to use INDEX?</h2>
<pre><code class="language-sql">EXPLAIN SELECT * FROM sample62 WHERE a = &#39;a&#39;;</code></pre>
<p>sample62의 a 컬럼에 대해서 인덱스를 작성했기 때문에, possible_keys 와 key 컬럼을 보면 해당 인덱스가 활용된 것을 볼 수 있다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/3e2620c7-45a2-4531-98e2-ed9ac439f60e/image.png" alt=""></p>
<p>WHERE 조건을 바꿔서 a 열을 사용하지 않도록 한다면 당연한 얘기지만 인덱스를 사용할 수 없다.</p>
<h1 id="when-to-use-and-when-to-not-use">when to use and when to not use?</h1>
<p>만약 특정 컬럼의 데이터가 yes 나 no 로 데이터의 종류가 두개밖에 없다면 이진탐색에 의한 효율성을 기대할 수 없다. 데이터의 종류가 적으면 적을수록 인덱스의 효율이 떨어진다.</p>
<p>반대로 서로 다른 값으로 여러 종류의 데이터가 존재하면 그만큼 인덱싱의 효율은 좋아진다.</p>
<p>따라서 인덱스를 무조건 쓴다고 좋은것이 아니라, 언제 써야할 것인지를 고려하고 써야한다.</p>
<blockquote>
<p>SQL 첫걸음 29강 : 인덱스의 작성과 삭제</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 38 || Cookie? JWT? What's your choice]]></title>
            <link>https://velog.io/@tk_kim/Why-we-should-avoid-saving-PII-in-Cookie</link>
            <guid>https://velog.io/@tk_kim/Why-we-should-avoid-saving-PII-in-Cookie</guid>
            <pubDate>Sun, 18 Apr 2021 07:32:51 GMT</pubDate>
            <description><![CDATA[<p>jwt 를 배우기 전 나는 tornado framework 의 set_secure_cookie 라는 메소드를 이용해서 다음 사진이 설명하는 과정을 거쳐 클라이언트에게 user_id 를 저장하게 했다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/9b4c9a17-f744-41d6-93d7-3b23d473eef6/image.png" alt=""></p>
<p>그런데 cookie 에 대해서 좀 더 공부를 하려고 하던 중 다음 글을 보고 의문이 들었다.</p>
<blockquote>
<p>Secure과 HttpOnly 쿠키
Secure 쿠키는 HTTPS 프로토콜 상에서 암호화된(encrypted ) 요청일 경우에만 전송됩니다. 하지만 Secure일지라도 민감한 정보는 절대 쿠키에 저장되면 안됩니다, 본질적으로 안전하지 않고 이 플래그가 당신에게 실질적인 보안(real protection)를 제공하지 않기 때문입니다. 크롬52 혹은 파이어폭스52로 시작한다면, 안전하지 않은 사이트(http:) 는 쿠키에 Secure 설정을 지시할 수 없습니다.
출처 : <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies">https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies</a></p>
</blockquote>
<p>user_id 도 민감정보 아닌가 ? 라는 의문이 들어서 검색해보니 여기서 말하는 민감정보란</p>
<p>다음과 같이 특정인을 지칭할 수 있는 정보들(PII)인 것 같았다.</p>
<ul>
<li>전화번호</li>
<li>주민등록번호</li>
<li>주소</li>
<li>etc...</li>
</ul>
<p>그리고 위와 같은 민감 정보를 쿠키에 저장하게 되면,</p>
<p>쿠키는 로컬 저장소에 공유되기 때문에
A 라는 사이트에 접속해서 쿠키를 저장해두면,
B 라는 사이트에 접속하면 거기서 A 라는 사이트에 저장된 쿠키를 얻을 수 있다.
또는 저장된 쿠키를 다른 악성 코드가 가져갈 수도 있다.</p>
<h2 id="then-is-it-okay-to-store-in-web-storage">Then, is it okay to store in web-storage?</h2>
<p>보안의 측면에서 웹 스토리지(로컬 스토리지 + 세션 스토리지)는 좋지않다고 말할 수 있다.
웹 스토리지에 저장된 값은 javascript 코드를 통해 언제든지 접근할 수 있는데,
이는 XSS(cross-site scripting) 공격에 취약하기 때문이다.</p>
<p>반면 쿠키에 저장한 값은 HttpOnly 속성을 통해 javascript를 통한 접근을 막을 수 있다.
그렇기 때문에 보안적인 측면을 생각한다면 웹 스토리지보다는 쿠키에 저장하는 것이 좋다고 할 수 있다.</p>
<blockquote>
<p><a href="https://woowacourse.github.io/javable/post/2020-08-31-where_to_store_token/">https://woowacourse.github.io/javable/post/2020-08-31-where_to_store_token/</a></p>
</blockquote>
<h2 id="cookie-vulnerabilities">Cookie vulnerabilities</h2>
<ul>
<li>XSS(크로스 사이트 스크립트)공격</li>
</ul>
<p>˙ 자바스크립트를 이용하여 document.cookie 값을 탈취할 수 있음
-&gt; 쿠키의 httpOnly 설정을 통해 차단가능, 하지만 이것도 완벽한 방법은 아니다. </p>
<ul>
<li>스니핑(Sniffing)공격을 이용</li>
</ul>
<p>˙ 네트워크를 통해 전송되는 쿠키값을 암호화하지 않고 전송하는 경우 네트워크 스니핑 공격을 통해 쿠키값을 탈취할 수 있음</p>
<ul>
<li>공용 PC에서 쿠키값 유출</li>
</ul>
<p>˙ 영속성 쿠키는 하드디스크에 저장되며, 간단한 방법으로 접근 가능하기 때문에 공용PC 사용시 PC에 저장된 사용자 정보가 유출될 수 있음</p>
<p>출처: <a href="https://coyagi.tistory.com/entry/%EC%8B%9C%ED%81%90%EC%96%B4%EC%BD%94%EB%94%A9-%EC%BF%A0%ED%82%A4-%EB%B0%8F-%EC%84%B8%EC%85%98%EA%B4%80%EB%A6%AC">https://coyagi.tistory.com/entry/시큐어코딩-쿠키-및-세션관리</a> [코코야이야기]</p>
<h1 id="conclusion">Conclusion</h1>
<p>그래서 결론이 뭐냐?
결국 뭐든 안전해야하는건 클라이언트한테 넘기지 않는것이 좋다.</p>
<p>-&gt; 그래서 로그인 할 때 토큰을 사용하는것이다.</p>
<p>토큰은 로컬스토리지에 저장하고 털려도 그냥 uuid 일뿐이다.
그러나 세션하이재킹같은 것은 가능하다.</p>
<p>그럼 이것을 어떻게 막느냐 ?</p>
<p>토큰 유효시간을 짧게? -&gt; 소용없다, 실시간으로 해킹할 수도 있다.</p>
<p>그래서 보통</p>
<ul>
<li>IP</li>
<li>접속 pc 정보</li>
<li>브라우저 정보</li>
</ul>
<p>와 같은 정보를 함께 암호화해서 토큰화시킨 뒤 나중에 값을 받을 때 복호화해서 대조한다.
위 정보들은 request 를 날릴 때 기본적으로 함께 들어가는 값이라서 서버에서 잘 꺼내서 토큰화시키면 된다. 토큰화 시킴과 동시에 서버에서 해당 값을 가지고 있어야 대조가 가능하기 때문에, 세션이든 db 든 메모리든 저장을 하고 있어야 한다.</p>
<p>나는 나중에 해당 ip 정보를 db 에 저장한 뒤 메모리에 캐싱하는 방식으로 해볼 예정이다.</p>
<p>어쨌든,
그래서 보안이 중요한 사이트들을 보면,</p>
<ul>
<li>로그인을 하나의 ip 에서만 허용하거나</li>
<li>이 pc 기억하기와 같은 것이 있어서</li>
</ul>
<p>pc 기억이 안되어있으면 이메일이나 휴대폰으로 인증을 하라고 하는 경우도 많다.</p>
<p>이 전까지는 그냥 user_id 만 payload 에 담아서 보내줬는데, 이후에는 좀 더 보안을 중요하게 생각해서 소개한 방법들을 적용할 계획이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2차 프로젝트 먹방 21.03.29 ~ 04.09] Project Retrospective ]]></title>
            <link>https://velog.io/@tk_kim/2%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A8%B9%EB%B0%A9-21.03.29-04.09-Project-Retrospective</link>
            <guid>https://velog.io/@tk_kim/2%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A8%B9%EB%B0%A9-21.03.29-04.09-Project-Retrospective</guid>
            <pubDate>Sat, 10 Apr 2021 12:17:48 GMT</pubDate>
            <description><![CDATA[<h1 id="what-is-it-about">What is it about?</h1>
<ul>
<li>먹방 프로젝트는, 직방 사이트에 영감을 받아 시작하게 된 프로젝트이다.</li>
<li>디자인과 기획 부분만 직방 레이아웃을 참고하였으며, 나머지는 프론트 3명 백엔드 2명으로 구성된 우리 먹방 팀에서 전부 다 구현하였다.</li>
<li>지도위에 맛집정보들을 표시해주는 사이트이며, 사용자가 직접 맛집을 등록할 수도 있고 리뷰와 평점, 사진을 남길수도 있게 제작하였다. </li>
<li>키워드 검색기능을 추가하여 주소, 맛집이름, 근처 지하철역 키워드로 검색할 수 있게 하였다.</li>
<li>카카오 API 로그인을 추가하여 사용자의 편리성을 더했고, 일반 회원가입(문자인증포함)을 추가하여 보안을 강화하려고 했다. </li>
</ul>
<h1 id="how-is-it-different-from-the-first-one">How is it different from the first one?</h1>
<ul>
<li><p>1차 프로젝트때와 달리 프로젝트의 pm 을 맡아, &quot;Agile in scrum&quot; 방식으로 매일 stand-up meeting 을 진행하고 Trello 로 스케줄을 관리하며 프로젝트를 주도적으로 진행했다.</p>
</li>
<li><p>postman 으로 만든 API 들을 (요청할 uri, 요청 method, key 값, query params 값... 및 요청 예시) 와 함께 문서화하려고 노력했다.</p>
</li>
<li><p>프론트/백에서 겪는 blocker 를 혼자서만 해결하게 두지 않고 다같이 고민해보는 시간을 가졌다.</p>
</li>
<li><p>프론트/백 기능을 처음부터 떨어져서 각자 만들지 않았다. 같은 기능을 담당하는 프론트/백이 같이 모여서 만들면서, 서로가 데이터를 어떻게 처리할 것인지 로직을 충분히 설명한 이후에 코딩하는 방식을 고수했다.</p>
</li>
<li><p>만들다가 기능에서 이해가 되지 않는 부분이 있으면 바로바로 이야기 하도록 주도하여, 나중에 프론트/백 어느 한곳에서 기능을 다시 갈아엎어야하는 수고를 덜었다.</p>
</li>
<li><p>1차 때 하지 못했던 기능들을 최대한 많이 해보고 이해하려고 노력했다.</p>
</li>
</ul>
<ol>
<li>geopy, haversine 모듈을 활용한 위,경도 거리 계산</li>
<li>카카오 API 를 활용한 소셜로그인 구현</li>
<li>네이버 문자서비스를 통한 문자인증 로직 구현</li>
<li>select_related, prefetch_related 를 통한 쿼리 수 줄이기</li>
<li>django orm method 를 이용한 다양한 filtering 방법들 적용</li>
<li>unit test 작성</li>
</ol>
<h1 id="agile-in-scrum">Agile in scrum</h1>
<p><img src="https://images.velog.io/images/tk_kim/post/beda87c6-bc7f-463c-89b3-f8a922b605d4/image.png" alt=""></p>
<ul>
<li>스크럼 방식으로 agile 하게 진행했다.</li>
<li>말로만 스크럼이 아니라, 실제로도 최대한 빨리 기능을 만든 다음 프론트/백 에서 맞춰보게 했다.</li>
<li>그렇게 함으로써 빠르게 전체적인 틀을 잡았고, 각자 세부적인 부분에 집중할 수 있었다.</li>
<li>Trello 를 활용해 작업했으며, board 에 크게 스케줄을 크게 네가지 파트로 나눴다.</li>
<li>Backlog -&gt; This Week -&gt; In Progress -&gt; Done</li>
<li>팀/개인 별로 티켓 색깔을 구분하여 할당했으며, 우리 팀이 어느위치에 와있는지 한눈에 알 수 있었다. </li>
<li>필수 구현사항/추가 기능 구현사항 을 나누어 작업의 우선순위를 정했다.</li>
<li>매일 stand-up meeting 을 통해 팀/개인 별 작업 진행속도 및 각자가 처한 blocker 를 파악하는 시간을 가졌다. blocker 가 있으면, 혼자 고민하게 두지 않고 백엔드, 프론트엔드가 모두가 함께 고민하도록 했다. 이렇게 함으로써 한사람이 너무 앞서가거나 뒤쳐지지 않게 조절하려고 노력했다.</li>
</ul>
<h1 id="communication">Communication</h1>
<ul>
<li>팀 내 communication tool 은 slack 을 사용했고,</li>
<li>코드 reviewer 와의 소통은 slack 및 github 에 올린 PR 의 code review 를 통해 진행했다.</li>
</ul>
<h1 id="my-role-in-the-project">My role in the project</h1>
<h2 id="as-a-project-manager">As a Project Manager</h2>
<ul>
<li>pm 을 맡아, 프로젝트의 전체적인 work flow 를 관리했다.</li>
<li>프론트/백엔드 가릴 것 없이 문제가 있으면 칠판 및 노트에 설명을 하면서 blocker 를 해결하려고 애썼다.</li>
<li>작업 속도를 보면서 2주 내에 구현하지 못할 기능들은 과감하게 제하고 추가 구현사항으로 빼놓음으로써, 필수적으로 구현해야 할 것들에 먼저 집중하게끔 노력했다.</li>
<li>stand-up meeting 및 팀 내 communication 을 주도했다.</li>
</ul>
<h2 id="as-a-backend">As a Backend</h2>
<ul>
<li>모델링</li>
<li>Django project 초기 세팅</li>
<li>Kakao API 소셜 로그인 &amp; 회원가입, Naver 문자 서비스를 이용한 문자인증</li>
<li>가게 등록</li>
<li>위도, 경도, 축척, 화면 픽셀 &amp; DPI 기준 가게 정보 표시</li>
<li>가게와 근처 지하철 사이의 거리 표시</li>
<li>가게 세부정보 표시</li>
<li>리뷰 등록 &amp; 삭제</li>
<li>가게 검색</li>
<li>가게 찜하기, 찜하기 삭제</li>
<li>기준에 맞는 가게 필터링</li>
<li>Django project RDS 에 연결 및 AWS EC2 인스턴스에 docker 를 활용하여 배포</li>
</ul>
<p><img src="https://images.velog.io/images/tk_kim/post/dce6f582-19de-44dd-acce-ab9a35156555/image.png" alt=""></p>
<h1 id="backend-modeling--apis">Backend Modeling &amp; APIs</h1>
<h2 id="1-modeling">1. Modeling</h2>
<p> 1차 프로젝트 때는 커머스 사이트라서 주문관리 때문에 모델링이 복잡했었다. 그래서 모델링에 대해 정말 많이 고민했었고 힘든 만큼 많이 배웠었다. 그 덕분에 2차 프로젝트는 수월하게 진행했다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/8acdfc25-d648-4583-b515-21ca4a1a50ca/image.png" alt=""></p>
<ul>
<li><p>모델링은 user app, store app 두개로 나눠서 진행했다.</p>
</li>
<li><p>가게(stores) 테이블안에 주소(address) 정보를 넣으려고 하다보니 한 테이블 안에 정보가 너무 많기도 하고, 따로 독립적으로 addresses 로 테이블을 빼서 관리하는 것이 깔끔해 보였다.</p>
</li>
<li><p>그래서 처음엔 그냥 foreign key 로 address 가 stores 를 참조하도록 했다가,</p>
</li>
<li><p>Django 의 one-to-one field 에 대해 공부했던 것이 생각나서 바로 적용해보았다.</p>
</li>
</ul>
<p><img src="https://images.velog.io/images/tk_kim/post/d2ce507d-4b90-480e-b92c-a39fe36bc59b/image.png" alt=""></p>
<ul>
<li><p>1차 때는 one-to-one 관계 설정을 안해봤었는데, one-to-one field 를 쓰다보니 왜 사용하는지 몸소 깨닫게되었다. store.address_set 으로 접근을 안해도 된다는 점이 너무 편했다.</p>
</li>
<li><p>나머지 테이블에 대해서는 필요한 부분에 one-to-many, many-to-many 관계를 정했다.</p>
</li>
</ul>
<h2 id="2-kakao-api-login--sign-up">2. Kakao API Login &amp; Sign Up</h2>
<ul>
<li><p>카카오 소셜로그인 시 처음에 백엔드에서 인가토큰, 엑세스 토큰, 사용자정보를 다 받았었다.</p>
</li>
<li><p>그렇게 되면 백엔드에서 로그인 페이지로 redirect 까지 시켜줘야 했다. 이 부분은 프론트엔드의 부분이기 때문에 최종적으로</p>
</li>
<li><blockquote>
<p>프론트에서 엑세스토큰을 받는 부분까지 처리한 뒤 토큰을 넘겨주면, 백엔드에서 해당 토큰으로 Kakao API 에 사용자정보를 요청하는 로직으로 변경하였다.</p>
</blockquote>
</li>
<li><p>사용자정보를 받아 db에 정보가 없으면 회원가입을 시키고 없으면 그냥 or 로그인을 시킨 뒤 jwt 토큰을 발행해서 프론트에게 넘겨주었다.</p>
</li>
</ul>
<p><img src="https://images.velog.io/images/tk_kim/post/aa0011ba-9a27-4af8-81ee-1f12914d5f98/image.png" alt=""></p>
<ul>
<li>추가적으로 통합회원 관리를 할 것인가 그렇지 않을 것인가에 대한 고민 끝에, 로그인 방식에 따라 각자 다른 유저로 취급하기로 했다. 그래서 다음과 같이 모델링 수정을 한번 거쳤다.</li>
</ul>
<p><img src="https://images.velog.io/images/tk_kim/post/fb196281-1670-414e-8577-123dc96359e7/image.png" alt=""></p>
<ul>
<li>id, created_at, updated_at 을 제외한 모든 field 값을 null 로 변경해주었다. </li>
</ul>
<p>또 만약에 <strong>통합회원 관리</strong>를 하려면 어떻게 해야하는가에 대한 궁금증이 있었는데, 그렇게 하려면
회원가입을 먼저 시키고 로그인을 하게 한 다음 소셜 로그인을 연동하는 방식으로 진행해야한다.</p>
<p>그래야만 어떤 유저인지 알 수가 있고,
그 때 소셜 로그인을 연동하면 해당 유저 object 를 불러와서 kakao_id 든 google_id 든 해당 field 에 id 값만 추가하면 되기 때문이다.</p>
<h2 id="3-store-display-depending-on-latitudelongitude--map-scale--pixel">3. Store Display depending on Latitude/Longitude &amp; map scale &amp; pixel</h2>
<ul>
<li>프론트에서 위도, 경도, 지도의 축척, 화면픽셀 값을 받으면 백엔드에서 화면의 위도 경도 바운더리를 계산 후, DB 에서 바운더리 내부에 있는 가게정보 표시했다.</li>
</ul>
<p>로직은 생각보다 간단했다.</p>
<ul>
<li><p>지도의 중앙으로 부터 지도의 위,아래/양옆 변에 해당하는 좌표를 구해야했다.  </p>
</li>
<li><p>그러려면 사용자의 지도 좌표에서 지도 맨 끝쪽 까지의 거리를 알아야 했다.</p>
</li>
<li><p>축척이 주어져있어서 cm 당 몇 m 인지 알 수 있었다.</p>
</li>
<li><p>모니터 해상도 및 픽셀을 기준으로 실제 사용자의 화면에서 지도가 어느정도 크기인지 구했다.</p>
</li>
<li><p>지도의 길이와 축척으로 지도에 나타난 실제 가로세로 길이를 구했다. </p>
</li>
<li><p>이후에 중앙으로부터 수직으로 지도 오른쪽 끝부분까지 500m 라는 것을 알았으면
중앙의 위도/경도 + 동쪽으로 500m 를 계산해야했는데, 이 부분을 찾기 위해 스택오버플로우를 뒤져가며 위도/경도 에 거리를 더했을 때 위도 경도를 구하는 공식을 찾으려고 노력했다.</p>
</li>
<li><p>위도 경도 사이의 거리를 구하는 모듈은 많았는데, 위도 경도에 거리를 더했을 때 위도 경도를 구하는 모듈은 찾기 힘들었다. </p>
</li>
<li><p>그래도 결국 찾아냈다. geopy 라는 모듈의 distance method 를 사용하여 해결했다.</p>
</li>
<li><p>동서남북의 boundary 를 구한 뒤, db 에 addresses 테이블을 Q 객체로 필터링 했다.</p>
</li>
</ul>
<h4 id="dpi-inch-scale_level-상수로-선언">DPI, INCH, SCALE_LEVEL 상수로 선언</h4>
<p><img src="https://images.velog.io/images/tk_kim/post/42ca952b-1aaa-48c1-8e3b-5680cc315cd2/image.png" alt=""></p>
<h4 id="get_boundary_range">get_boundary_range()</h4>
<p><img src="https://images.velog.io/images/tk_kim/post/dfd1a404-f89b-4a10-878c-fd00c4bc4eeb/image.png" alt=""></p>
<p>--&gt; 한가지 문제점은 넘겨받는 인자 수가 5개나 된다는 것이다. 
그래서 리팩토링 할 때 저 위도, 경도, 축척, 가로픽셀, 세로픽셀의 정보를 담고있는
*<em>object 를 만든 뒤 그 object 자체를 함수에 넘겨줄 예정이다.
*</em></p>
<p>그렇게 하면 훨씬 더 깔끔해질 것 같다.</p>
<h4 id="store-filtering-with-latlng-boundary-range">store filtering with lat,lng boundary range</h4>
<p><img src="https://images.velog.io/images/tk_kim/post/d303777c-ca5d-41a3-a6b5-5cdf2171444a/image.png" alt=""></p>
<p>이 때 프론트 단에서 DPI 값을 구하는 모듈을 결국 못찾아서 백엔드에서 직접 DPI 값을 역으로 조정하면서 적절한 값을 찾았다. 프론트 테스트 환경에서 200 이 딱 오차없이 맞아서 DPI 를 200 으로 고정했다.</p>
<h3 id="distance-between-store-and-metro-station">Distance between Store and Metro Station</h3>
<p>haversine 모듈을 이용하여 위도/경도 간 거리를 구했다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/cd93e49a-c5df-4d6b-a341-887af755cc36/image.png" alt=""></p>
<h2 id="4-store-filtering-with-many-categories--pagination">4. Store filtering with many categories &amp; pagination</h2>
<ul>
<li>가게 정보를 보내줄 때, 프론트에서 두번 요청을 했다.</li>
<li>첫 번째 요청은 지도에 표시할 가게를 먼저 요청(업종, 가격범위 필터링 포함)</li>
<li>이후, 두 번째 요청은 가게 페이지 목록에 들어갈 가게 요청(리뷰순, 평점순 필터링 포함)</li>
</ul>
<p>두 개의 API 를 만들기에는, 필터링을 해도 어차피 같은 data set 인데,
 여러 API 를 만드는 것이 비효율적이라고 생각했다.</p>
<p>-&gt; 같은 view 에서 가격범위, 업종, 리뷰 개수, 평점평균 에 따라 필터링 후
페이지네이션 까지 작성하고 파라미터만 다르게 받아서 처리하는 방식으로 로직을 구현했다.
<img src="https://images.velog.io/images/tk_kim/post/359c4f37-75db-4539-b1e4-992e6561a975/image.png" alt=""></p>
<ul>
<li><p>필터링 과정에서 exclude(), annotate(), order_by(), field lookup 등 다양한 QuerySet API 를 활용하여 깔끔한 코드를 작성할 수 있었다.</p>
</li>
<li><p>또한 이 과정에서 select_related, prefetch_related 를 통해 DB 데이터를 메모리에 caching 하여 반복문 마다 쓸데없는 쿼리 수를 감소시켰다.</p>
</li>
</ul>
<h2 id="5-store-search-with-keywords">5. Store Search with keywords</h2>
<ul>
<li><p>가게 이름, 가게 주소, 근처 지하철역, 업종 이름으로 필터링을 했다.</p>
</li>
<li><p>Q 객체를 사용하여 | 연산자로 필터링에 걸린 모든 store object 를 담았고, 중복된 object 들을 거르기 위해 distinct() 메소드를 사용했다.</p>
</li>
<li><p>select_related, prefetch_related 를 사용하여 for 문마다 쿼리가 db 를 hit 하는 횟수를 감소시켰다.
<img src="https://images.velog.io/images/tk_kim/post/523e1a2e-da0d-4a39-9ee8-1357ac992573/image.png" alt=""></p>
</li>
</ul>
<h2 id="6-sign-up-text-authentication-with-naver-api">6. Sign-Up, Text Authentication with Naver API</h2>
<ul>
<li>원래는 네이버에 문자인증 서비스라는 것이 따로 API 로 존재하는 줄 알았는데, 그냥 sms API 만 있어서 우리가 직접 문자인증 로직을 구현하기로 했다.</li>
</ul>
<ol>
<li>휴대폰 번호를 입력받으면 프론트에서 백엔드로 핸드폰 번호 전달</li>
<li>네자리 난수를 생성하고 네이버 문자인증 API 를 사용하여 해당 핸드폰 번호로 난수 전달</li>
<li>성공적으로 메시지가 전달 되면, 프론트엔드에 해당 난수를 bcrypt 로 복호화 불가능하게 해싱해서 local storage 에 보관하게 함</li>
<li>사용자가 문자를 받고 인증번호를 입력한 뒤 인증버튼을 누르면 프론트엔드는 local storage 에 가지고 있던 해싱된 난수 값과 사용자가 입력한 값을 백엔드에게 보낸다.</li>
<li>백엔드는 해싱된 난수값과 사용자가 입력한 값을 bcrypt.checkpw 메소드로 비교한 뒤 맞으면 프론트에게 jwt 의 payload 에 exp 정보를 담아 토큰의 유효시간을 설정한 뒤 SECRET_KEY 로 SHA-256 알고리즘을 통해 jwt 를 암호화하여 프론트에게 전달한다.</li>
<li>프론트는 jwt 를 local storage 에 저장한 뒤, 회원가입 시 form 에 입력된 값과 jwt 를 함께 백엔드에게 전달한다.</li>
<li>백엔드는 jwt 를 검사하고 서버가 발행한 jwt 가 맞으면 회원가입을 시킨다.</li>
</ol>
<p>--&gt; 이렇게하면 db 를 거치지 않고 문자인증을 할 수 있다.</p>
<p>우리가 외부 도움없이 스스로 이런 로직을 생각해내고 적용했다는 점이 정말 뿌듯했고, 잘 할 수 있다는 자신감을 심어준 기능이였다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/bbb8c30c-b806-456a-8f4c-a3d5d0124224/image.png" alt=""></p>
<h1 id="clean-codes">Clean Codes</h1>
<h2 id="1-classmethod">1. @classmethod</h2>
<ul>
<li>비슷한 정보들을 넘겨주다 보니, 같은 로직이 너무 많은 문제를 직면했다.</li>
</ul>
<p>-&gt; 빈번하게 사용되는 해당 가게의 리뷰 평점 평균과, 전체 리뷰 개수를 구하는 로직을
classmethod 로 만들어서 필요할 때마다 간편하게 사용했다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/0e9c12c6-dd81-4c4c-a998-742f739d1455/image.png" alt=""></p>
<h2 id="2-class-inheritance--method-overiding-with-super">2. class inheritance &amp; method overiding with super()</h2>
<ul>
<li>unit test 진행 시, 한 Test 클래스의 setUp() 에서 세팅해뒀던 코드가 다른 Test 클래스들에게도 많이 사용되고 있었다.</li>
</ul>
<p>-&gt; 불필요한 반복을 줄이기 위해 Base 클래스 를 만든 뒤, 다른 클래스에서 Base 클래스를 상속했다.
-&gt; setUp() 부분만 super() 로 메소드를 오버라이딩해서 사용했다. 
-&gt; 이 때 Base 클래스에 <code>__test__ = False</code> 를 안하면 테스트 실행마다 
Base 클래스가 계속 테스트되기 때문에 꼭 설정해줘야한다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/ed8248fa-b957-4f99-ba52-7a0d6c3a90fd/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/tk_kim/post/b54b8121-00c5-4b63-8667-03178ca6e378/image.png" alt=""></p>
<h1 id="wrap-up">wrap-up</h1>
<p>1차 프로젝트와 비교했을 때 훨씬 성장한 나를 볼 수 있었다.</p>
<p>내가 구현해야 할 기능에 매몰되서 내 것만 하는 것이 아니라, 내가 좀 느리게 가더라도 팀원들을 도우며 같은 속도로 가려고 했다.</p>
<p>다만 아쉬운점은, 프로젝트 발표였는데 우리는 마지막에 욕심을 내서 추가로 기능을 구현하려다가 발표준비가 다른 팀들에 비해 조금 미흡했던 점이 걸렸고, 팀원들한테 미안했다.
정말 잘 했다고 생각했고 기능 구현도 많이 했는데 발표에서 전부 다 우리가 했던 것들을 자랑하지 못해서 살짝 아쉬웠다.</p>
<p>개발자로서 코딩을 잘하는 것이 물론 중요하지만, 
잘 만들고 마지막에 만든 결과물을 어필하는 능력까지 개발자의 몫이고 중요한 덕목이라고 생각한다.</p>
<p>다음부터는 준비를 더 잘 해야겠다!</p>
<p>그리고 다른 팀원들은 속으로 우리 팀을 어떻게 평가할지는 모르겠지만 내가 생각할 때 우리는 목표를 위해서 잘 달려온 것 같다. </p>
<p>서로를 믿고 도와주고 힘들더라도 묵묵하게 각자 맡은 부분을 끝까지 잘 해낸 승옥 님, 성훈 님, 나은 님, 호열 님에게 감사하고</p>
<p>코드리뷰를 통해 많은 인사이트를 주신 지훈 멘토님께도 감사드린다.</p>
<p>좋은 프로젝트에 좋은 팀원들이여서 좋았다.</p>
<p>두 달동안 정말 바쁘게 달려와서 두달이란 시간이 정말 빠르게 흘러갔다는 걸 이제서야 체감한다.
앞으로 한달동안 협업을 하게 될 회사에서는 또 어떤 일들이 있을지 무엇을 또 새롭게 배울지 기대하는 마음으로 회고를 마무리한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 37 || Python Django select_related, prefetch_related]]></title>
            <link>https://velog.io/@tk_kim/Python-Django-selectrelated-prefetchrelated</link>
            <guid>https://velog.io/@tk_kim/Python-Django-selectrelated-prefetchrelated</guid>
            <pubDate>Sat, 10 Apr 2021 08:45:25 GMT</pubDate>
            <description><![CDATA[<p>select_related 와 prefetch_related 를 써서 쿼리 수를 줄여보았다.</p>
<h3 id="before">BEFORE</h3>
<p><img src="https://images.velog.io/images/tk_kim/post/6089658e-29c7-4e67-bf1e-1ee9d2681849/image.png" alt=""></p>
<h3 id="after">AFTER</h3>
<p><img src="https://images.velog.io/images/tk_kim/post/f43bef24-9c87-45e0-85bf-929134d995c1/image.png" alt="">
select_related 쿼리셋 메소드를 사용하여 store 와 one-to-one 관계에 있는
address, category, open_status 를
미리 가져와 python 메모리에 caching 해뒀다.</p>
<p>prefetch_related 를 사용하여 one-to-many 관계에 있는 storeimage_set 을 미리가져와 caching 해두었다.</p>
<p>두 메소드의 차이점은 기준이 되는 Entry object 에서 참조하는 모델이 single object 이냐 multiple object 냐의 차이다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/b802a123-206c-464c-8468-9a17b483c9c6/image.png" alt=""></p>
<p>거의 절반으로 쿼리가 줄었다.
지금은 store object 두개에 대해서 for 문을 돌아서 차이가 별로 안나는 것 처럼 보일 수 있지만, 나중에 10개 100 개에 대해 for 문을 돌면 차이가 매우 크게 날 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 36 || Python Unittest on Django, How to Mock data ]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-33-Python-Unittest-on-Django-How-to-Mock-data</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-33-Python-Unittest-on-Django-How-to-Mock-data</guid>
            <pubDate>Sat, 10 Apr 2021 08:41:33 GMT</pubDate>
            <description><![CDATA[<p>Kakao API 를 사용하여 requests.get 으로 사용자의 정보를 받아오는 view 를 unit test 해야했다. 하지만 unit test 는 서드파티 앱에 의존하지 않아야하고, 자체적으로 test 할 수 있어야 하기 때문에 실제로 요청을 보내면 안된다.</p>
<p>그래서 해당 요청을 mocking 하던 중 마주했던 어려움들을 어떻게 풀어갔는지 소개하려고 한다. </p>
<p><img src="https://images.velog.io/images/tk_kim/post/875f13dc-7345-4eaa-bb29-278072d6de18/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>mock.patch 로 requests.get() 의 return_value 를 그냥 dict 로 했더니, 뒤에 오는 json() 에서 
<code>dict has no attribute json</code> 라는 에러가 발생했다.
그래서 return_value 를 requests.Response() 로 바꿔보기로 했다.</p>
<h1 id="modifying-requestsresponse-object">Modifying requests.Response() object</h1>
<h2 id="with-patch">with patch</h2>
<p>requests.get 의 return_value 를 
실제 requests.Response object 를 만들어 값을 변경해주는 방법을 사용했다.</p>
<p>왜냐면 views.py 코드에서 requests.get 의 attribute 인 json() 까지 mocking 해 줄 생각을 못했었기 때문이다.</p>
<p>어쨌든 그 생각을 하기 전에는 다음과 같은 과정을 거쳐서 문제를 해결했다.</p>
<p>실제로 requests.Response() 객체를 만든 뒤
<code>._content</code> attribute 에 
mock_content dictionary type 객체를 
bytes 객체로 변환해주면
requests.Response.json() 의 값이 dictionary type 의 mock_content 가 된다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/86c2ffd4-15ce-4ca2-a86e-3af02fd91cdf/image.png" alt=""></p>
<p>--&gt; 이렇게 했을 때는 쓸데없이 실제 object 를 만들어서 조작해야되기 떄문에 1. 귀찮고 2. 그만큼 메모리를 낭비하게 된다.</p>
<p>다음 방법을 보자.</p>
<h2 id="patch-decorator">patch decorator</h2>
<p>실제로 requests.Response() 객체 자체를 만들지 않고, requests.get.json() 자체의 return_value 를 mocking 하는 아주 간단한 방법이 있었다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/af1bb59e-655f-44e6-ab3a-285c5c857bc6/image.png" alt=""></p>
<ol>
<li><p>mocking 할 모듈을 patch 데코레이터 인자로 넘겨줌</p>
</li>
<li><p>함수의 파라미터로 데코레이터 인자로 넘겨받은 mocking 할 모듈 object를 받음
이 때 넘겨받은 인자를 print 해보면 name 이 get 인 MagicMock object 라는 것을 알 수 있다.
<code>&lt;MagicMock name=&#39;get&#39; id=&#39;140646286606096&#39;&gt;</code></p>
</li>
<li><p>MagicMock object 의 return_value 값을 response 에 할당한다.
이 때 response 를 print 해보면 name 이 get() 인 MagicMock object 로 변했다.
<code>&lt;MagicMock name=&#39;get()&#39; id=&#39;140369968892704&#39;&gt;</code></p>
</li>
</ol>
<p>get 과 get() 의 차이다.</p>
<p>get 은 모듈 자체이고 get() 은 object 이다.</p>
<ul>
<li><p><code>print(mocked_request.json.return_value)</code> 를 해보면
<code>&lt;MagicMock name=&#39;get.json()&#39; id=&#39;140564181636624&#39;&gt;</code></p>
</li>
<li><p><code>print(mocked_request.return_value.json.return_value</code> 를 해보면
<code>&lt;MagicMock name=&#39;get().json()&#39; id=&#39;140260556133904&#39;&gt;</code></p>
</li>
</ul>
<p>우리가 mocking 해야 할 instance 는 다음과 같다.</p>
<p><code>response = requests.get(KAKAO_USERINFO_REQUEST_URL, headers=headers).json()</code>
간략히 나타내면</p>
<p>requests.get().json() 이고, 이를 풀어보면 
requests.Response().json() 이 된다.</p>
<p>설명이 길었지만, 핵심은 </p>
<h3 id="wrong-way">wrong way</h3>
<p>이 방법이 아니라</p>
<pre><code class="language-python">
  response = mocked_request
  response.json.return_value = mock_content        
</code></pre>
<h3 id="right-way">right way</h3>
<p>이 방법으로 해주어야 한다는 것이다.</p>
<pre><code class="language-python">
  response = mocked_request.return_value
  response.json.return_value = mock_content        
</code></pre>
<p>이렇게 하면 성공적으로 mocking 이 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 35 || Map range depending on DPI ]]></title>
            <link>https://velog.io/@tk_kim/Map-range-depending-on-DPI</link>
            <guid>https://velog.io/@tk_kim/Map-range-depending-on-DPI</guid>
            <pubDate>Tue, 06 Apr 2021 07:58:17 GMT</pubDate>
            <description><![CDATA[<p>프론트 단에서 카카오 map 에 가게 마커를 표시하는 과정에서 벌어진 문제였다.</p>
<ul>
<li><p>프론트에서는 카카오 map api 로 가게가 영역 내에 있을 때 백엔드로 부터 가게 좌표와 정보를 받아와 해당 좌표에 마커를 표시한다.</p>
</li>
<li><p>가게가 보여지는 지도의 영역에서 벗어나면 백엔드에서 가게 정보를 더이상 보내지 말아야 한다.</p>
</li>
<li><p>근데 영역에서 벗어나도 계속 백엔드에서 정보가 보내져서 알고봤더니 DPI 의 문제였다.</p>
</li>
</ul>
<p><img src="https://images.velog.io/images/tk_kim/post/f249194f-b529-4b02-8286-620b1662b6ea/image.png" alt=""></p>
<ul>
<li>프론트쪽에서 지도를 이동하는 테스트를 진행했는데, 이 때 사용됐던 제품은 맥북프로 m1 기준이였다.</li>
<li>나는 DPI 96 으로 세팅해놓고 하고 있었는데, DPI 값을 바꾸면서 테스트해보니 테스트 모니터 해상도 기준 DPI 는 200 이였다.</li>
<li>그래서 DPI 를 200 으로 놓고하니 아주 정확하게 지도영역 바깥으로 벗어나는 순간 가게 정보를 받아오지 않았다.</li>
</ul>
<p>근데 문제는 DPI 값이 기종마다 해상도마다 다를것이기 때문에 이 값을 프론트에서 받아오는 방법을 찾아야한다.</p>
<p>아직 못찾았다 .. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 34 || How to validate input data with Enum Class]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-33-Enum-%EC%9C%BC%EB%A1%9C-input-%EA%B0%92-%EC%A0%9C%ED%95%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-33-Enum-%EC%9C%BC%EB%A1%9C-input-%EA%B0%92-%EC%A0%9C%ED%95%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 04 Apr 2021 09:57:53 GMT</pubDate>
            <description><![CDATA[<h1 id="enum-class">Enum class</h1>
<ul>
<li>Enum class 에 인스턴스들을 선언한다.</li>
<li>처음에는 <code>Enum.__memebers__</code> 로 Enum class 의 변수 명들을 가져와서, 입력되는 데이터가 그 중에 없으면 에러를 리턴하려고 했으나</li>
<li>input 값은 모두 베이커리, 카페 이런 한글로 들어오기 때문에, 한글을 변수로 선언할 수는 없었다.</li>
<li>그래서 Enum class 를 iterate 하면서 각 변수의 value 들을 담은 list 를 만드는ExtendedEnum 클래스를 생성했다. 해당 클래스는 Enum 클래스 상속을 받았다.</li>
</ul>
<p><img src="https://images.velog.io/images/tk_kim/post/72bf83bf-159d-4063-b367-cde4db3aaf02/image.png" alt=""></p>
<h1 id="validation">validation</h1>
<p><img src="https://images.velog.io/images/tk_kim/post/706226b4-55ec-44ca-9829-4f7db8204e06/image.png" alt=""></p>
<p>다음과 같이 category_name 이 ExtendedEnum 클래스를 상속받아 만든 CategorySet 에 없으면 <code>transaction.set_rollback(True)</code> 를 call 해서 모든 db 수정 값을 초기화 시킨다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 33 || Postman 으로 API 예제 공유하기]]></title>
            <link>https://velog.io/@tk_kim/Postman-%EC%9C%BC%EB%A1%9C-API-%EC%98%88%EC%A0%9C-%EA%B3%B5%EC%9C%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tk_kim/Postman-%EC%9C%BC%EB%A1%9C-API-%EC%98%88%EC%A0%9C-%EA%B3%B5%EC%9C%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 04 Apr 2021 09:53:54 GMT</pubDate>
            <description><![CDATA[<h2 id="1-postman-으로-api-요청-테스트">1. Postman 으로 API 요청 테스트</h2>
<p><img src="https://images.velog.io/images/tk_kim/post/00a74d97-78de-47af-8553-e56410279893/image.png" alt=""></p>
<h2 id="2-성공하면-해당-api-요청을-save-하기">2. 성공하면 해당 API 요청을 save 하기</h2>
<p><img src="https://images.velog.io/images/tk_kim/post/ed36a122-c3b2-47f5-a595-d0e5b9ba4bbe/image.png" alt=""></p>
<h2 id="3-공유하기">3. 공유하기</h2>
<p><img src="https://images.velog.io/images/tk_kim/post/37a06435-0ea1-4813-a32f-2a0e1c323a19/image.png" alt=""></p>
<p>스크럼 작업 스케줄 관리 공간인 Trello 에 프론트 팀원들이 키값을 알 수 있도록 공유했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 32-2 || Kakao Login API with Django]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-32-Kakao-Login-API-with-Django</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-32-Kakao-Login-API-with-Django</guid>
            <pubDate>Sun, 04 Apr 2021 09:52:27 GMT</pubDate>
            <description><![CDATA[<h1 id="python-django-kakao-api-login">Python Django Kakao API Login</h1>
<h2 id="only-backend-side-without-frontend">Only Backend side Without Frontend</h2>
<p>앞서 말한 것 처럼 이 코드는 프론트엔드 없이 백엔드에서 혼자 모든걸 다 처리하는 코드이다.</p>
<pre><code class="language-python">import jwt
import requests

from django.shortcuts import redirect
from django.views     import View
from django.http      import JsonResponse

from .models import User

from my_settings import SECRET_KEY, HASHING_ALGORITHM, KAKAO_RESTAPI_KEY


KAKAO_REDIRECT_URL         = &#39;http://localhost:8000/user/login/kakao/oauth&#39;
KAKAO_TOKEN_REQUEST_URL    = &#39;https://kauth.kakao.com/oauth/token&#39;
KAKAO_USERINFO_REQUEST_URL = &#39;https://kapi.kakao.com/v2/user/me&#39;


class KakaoLoginView(View):
    def get(self, request):
        client_id    = KAKAO_RESTAPI_KEY
        redirect_url = KAKAO_REDIRECT_URL

        return redirect(&#39;https://kauth.kakao.com/oauth/authorize?client_id={}&amp;redirect_uri={}&amp;response_type=code&#39;.format(client_id, redirect_url))


class KakaoLoginCallbackView(View):
    def get(self, request):
        try:
            code = request.GET.get(&#39;code&#39;)

            data = {
                    &#39;grant_type&#39;  : &#39;authorization_code&#39;,
                    &#39;client_id&#39;   : KAKAO_RESTAPI_KEY,
                    &#39;redirect_uri&#39;: KAKAO_REDIRECT_URL,
                    &#39;code&#39;        : code
                }

            token_response = requests.post(KAKAO_TOKEN_REQUEST_URL, data=data).json()

            access_token  = token_response.get(&#39;access_token&#39;)
            refresh_token = token_response.get(&#39;refresh_token&#39;)
            error         = token_response.get(&#39;error&#39;)

            if error:
                return JsonResponse({&#39;message&#39;: &#39;INVAILD_REQUEST, {}&#39;.format(error)}, status=400)

            headers = {&#39;Authorization&#39;: &#39;Bearer {}&#39;.format(access_token)}

            response = requests.get(KAKAO_USERINFO_REQUEST_URL, headers=headers).json()

            kakao_user_id       = response.get(&#39;id&#39;)

            if kakao_user_id:
                user, _   = User.objects.get_or_create(kakao_id=kakao_user_id)
                jwt_token = jwt.encode({&#39;user_id&#39;: user.id}, SECRET_KEY, algorithm=HASHING_ALGORITHM)

                return JsonResponse({&#39;token&#39; : jwt_token}, status=200)
            else:
                return JsonResponse({&#39;message&#39;: &#39;INVALID_ACCESS_TOKEN&#39;}, status=400)

        except KeyError:
            return JsonResponse({&#39;message&#39;: &#39;KEY_ERROR&#39;}, status=400)</code></pre>
<h1 id="1-how-to-get-authorization-code-인가코드">1. How to get Authorization Code (인가코드)</h1>
<ul>
<li>RESTAPI KEY 로 인가코드부터 받아야 한다.</li>
</ul>
<p>카카오 API 명세의 인가 코드 받기 부분을 참조해보자.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/3758b703-6811-427f-a39b-b47e65e50c12/image.png" alt=""></p>
<p>postman 을 이용해 GET 요청을 통해 파라미터로 값을 넘겨주었다.
<img src="https://images.velog.io/images/tk_kim/post/d68d3c5a-a244-4bee-86ea-1b129be1c943/image.png" alt=""></p>
<p>--&gt; 성공적으로 인가코드를 받을 수 있었다.</p>
<h1 id="2-how-to-get-access_token">2. How to get access_token</h1>
<ul>
<li>받은 인가코드로 access_token 을 받아야 한다.</li>
</ul>
<p>카카오 API 명세의 토큰 받기 부분을 보자.
<img src="https://images.velog.io/images/tk_kim/post/a3b7746e-c2e6-4001-8c74-1a3b32a860af/image.png" alt=""></p>
<p>postman 을 이용해 POST 요청으로 파라미터로 값을 넘겨주었다.
<img src="https://images.velog.io/images/tk_kim/post/d6ae855f-9374-4ece-be36-dd757cbec530/image.png" alt=""></p>
<p>--&gt; 성공적으로 access_token 을 받을 수 있었다.</p>
<h1 id="3-how-to-get-user-information">3. How to get user information</h1>
<ul>
<li>받은 access_token 으로 카카오톡에 가입 된 유저 정보를 받아올 수 있다.</li>
</ul>
<h1 id="csrf-방어">CSRF 방어</h1>
<p>나중에 자세히 공부 할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL DAY 32 || [2차프로젝트 먹방 21.03.31]]]></title>
            <link>https://velog.io/@tk_kim/TIL-DAY-32-2%EC%B0%A8%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A7%81%EB%B0%A9-21.03.31</link>
            <guid>https://velog.io/@tk_kim/TIL-DAY-32-2%EC%B0%A8%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A7%81%EB%B0%A9-21.03.31</guid>
            <pubDate>Sun, 04 Apr 2021 09:51:03 GMT</pubDate>
            <description><![CDATA[<h1 id="dpi-별-pixel-값이-다른가-">DPI 별 PIXEL 값이 다른가 ?</h1>
<h2 id="결론은-css-1인치는-무조건-96px-이다">결론은 css 1인치는 무조건 96px 이다.</h2>
<p><a href="https://www.pixelto.net/px-to-cm-converter">https://www.pixelto.net/px-to-cm-converter</a></p>
<p><a href="https://blog.hyungsub.com/entry/DPI-%EC%8A%A4%ED%84%B0%EB%94%94-V1">https://blog.hyungsub.com/entry/DPI-%EC%8A%A4%ED%84%B0%EB%94%94-V1</a></p>
<h1 id="modelspy-안에-함수를-정의하면-viewspy-에서-할-일을-줄일-수-있다">Models.py 안에 함수를 정의하면 views.py 에서 할 일을 줄일 수 있다.</h1>
<p>예를들어 스타벅스 사이즈마다 가격 다를 때 view 에서 처리해주면 귀찮으니까, Model 자체에서 사이즈 별로 연산하는 메소드를 클래스 안에 정의해놓고, <code>&lt;Model object&gt;.method()</code> 로 쓰면 됨.</p>
<h1 id="소셜-로그인-db-관리">소셜 로그인 DB 관리</h1>
<p><img src="https://images.velog.io/images/tk_kim/post/3085debe-af31-4da6-9ba0-9a0079c1de6d/image.png" alt=""></p>
<h2 id="통합회원관리">통합회원관리</h2>
<p>어떤 유저가 google, fb, kakao 로 가입했을 때 그 유저가 한 유저인 걸 알고싶을 땐, 통합회원관리를 해야한다.</p>
<p>그래서 먼저 로그인을 한 상태에서 google, fb, kakao 를 각각 인증하면 나중에 로그인 할 때 해당 소셜로그인으로 로그인 할 수 있도록 db 에 해당 필드 id 를 추가한다.</p>
<h2 id="통합회원관리-x">통합회원관리 x</h2>
<p>통합회원관리를 안하고 동일한 유저가 google, fb, kakao 로 가입한 걸 서로 다른 세명의 유저로 인식할것이라면, 앞서 설명했던 것들을 생각할 필요 없다.</p>
<p>그냥 위 코드처럼 모든 field 를 null=True 해서 저장하면 된다.
그런다음 kakao 로 로그인을 하면, kakao_id 필드만 체크해서 로그인시키면 된다.</p>
<h1 id="curl-syntax---python-syntax">Curl syntax -&gt; Python syntax</h1>
<p>API 문서를 읽다보면 다음과 같이 종종 request 방법에 대한 예제가 curl 문으로 적혀있는경우가 많다.</p>
<p><img src="https://images.velog.io/images/tk_kim/post/7e28d23b-e9d5-4c0a-801c-61bbd73beff3/image.png" alt=""></p>
<p>그럴 땐 <a href="https://curl.trillworks.com/">https://curl.trillworks.com/</a> 로 가서 변환해보면 된다.
<img src="https://images.velog.io/images/tk_kim/post/f4721297-2ad9-42d4-a700-c5f264934e0c/image.png" alt=""></p>
<p>물론 100 프로 동작한다는 보장은 없기 때문에, 에러가 날 경우 오탈자가 있는지 체크해봐야한다.</p>
]]></description>
        </item>
    </channel>
</rss>