<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[None - Medium]]></title>
        <description><![CDATA[None - Medium]]></description>
        <link>https://medium.com/humanscape-tech?source=rss----53c6a2785a10---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>None - Medium</title>
            <link>https://medium.com/humanscape-tech?source=rss----53c6a2785a10---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 10:15:18 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/humanscape-tech" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Koa project NestJS 적용기: 레거시 개선을 통한 코드 구조 개선]]></title>
            <link>https://medium.com/humanscape-tech/koa-project-nestjs-%EC%A0%81%EC%9A%A9%EA%B8%B0-%EB%A0%88%EA%B1%B0%EC%8B%9C-%EA%B0%9C%EC%84%A0%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%BD%94%EB%93%9C-%EA%B5%AC%EC%A1%B0-%EA%B0%9C%EC%84%A0-fab40bf987f4?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/fab40bf987f4</guid>
            <category><![CDATA[koajs]]></category>
            <category><![CDATA[nestjs]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Wed, 28 Jun 2023 04:48:35 GMT</pubDate>
            <atom:updated>2023-07-12T01:49:46.582Z</atom:updated>
            <content:encoded><![CDATA[<h3>Koa 기반 project NestJS 변환: 레거시 코드 개선</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/820/0*xqTsRQ3MOnE1j2D9.png" /><figcaption>출처: nestjs</figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>오늘은 Koa 기반의 프로젝트에 NestJS를 적용한 과정을 공유하려고 합니다.</p><h4>거대해지는 서비스 레이어</h4><p>우리가 참여한 서비스는 기존에 Koa로 개발되었고, MVC 패턴으로 설계되었습니다. 초기에는 간단한 서비스를 개발하기에 적합한 직관적이고 경제적인 구조였지만, 비즈니스 로직이 점점 복잡해지면서 코드의 가독성과 의존성 관리가 어려워지며 함수 재사용이 어려운 상황이었습니다.</p><p>우리는 코드 가독성을 향상시키고 관심사를 분리하기 위해 다양한 방법을 고려했습니다.</p><p>우리는 아래의 여러 장점을 이유로 NestJS를 도입하기로 결정했습니다.</p><ol><li>코드 가독성과 유지보수성 향상</li></ol><ul><li>koa 기반 프로젝트는 구조의 자유도가 높아 팀마다 원하는 구조를 가져갈 수 있고, 우리의 Koa 기반 프로젝트는 단순하고 직관적인 구조로 개발되었습니다. 하지만 비즈니스 로직이 복잡해지고 코드베이스가 커지면서 현재의 구조로 확장성 있는 개발을 기대하기 어려워 졌습니다.</li><li>NestJS는 모듈, 컨트롤러, 서비스 등의 구조를 제공하여 코드를 구성하고 관리할 수 있는 강력한 구조를 제공합니다. 이를 통해 코드를 모듈화하고 관심사를 분리하여 가독성을 향상시킬 수 있습니다.</li></ul><p>2. 구조 강제와 표준화</p><ul><li>NestJS는 의존성 주입과 같은 특성을 활용하여 구조를 강제하고, 일관된 코드 작성을 장려합니다. 이는 팀원 간의 협업이 원활해지고, 코드의 일관성과 유지보수성을 향상시킵니다.</li></ul><p>NestJS는 어댑터 패턴으로 작성되어 있으며, nest-koa-adapter 라이브러리를 사용하여 Koa와 NestJS를 혼용하면서 자연스럽게 NestJS로 전환할 수 있으리란 기대가 있었습니다.</p><p>Nestjs도입 외에 아래와 같은 옵션도 고려했습니다.</p><ul><li>Koa 프로젝트의 코드 구조를 개선하는 방법을 고민했습니다. 기존 로직을 수정하는 작업이기 때문에 러닝 커브가 적을 수 있었지만, 구조를 강제하기 어려워 다시 이전 구조로 돌아갈 가능성이 크다는 판단을 했습니다. 또, 기존 로직을 해석하고 수정하는 대신 코드를 새로 작성하는 것이 더 쉬울 수 있다고 판단했습니다.</li><li>각 서비스를 Microservice로 분리하는 방안도 고려했습니다(MSA). 그러나 각 서비스를 배치해야 할 인력 부족과 분리된 서비스 간 통신을 위한 API 작성에 드는 시간 대비 가성비가 좋지 않다고 판단했습니다.</li></ul><h4>Nestjs 도입기</h4><p>위의 장단점을 고려한 후, 우리는 NestJS를 도입하기로 결정했습니다. 기존 Koa 프로젝트의 app.js에서 <a href="https://github.com/seidelmartin/nest-koa-adapter">nest-koa-adapter</a>를 사용하여 NestJS를 연결했습니다.</p><pre>async function bootstrap() {<br>  const koa = new Koa();<br>  // error-handling<br>  koa.on(&#39;error&#39;, (err, ctx) =&gt; {<br>    console.error(&#39;onerror&#39;, err.stack);<br>    sentry.withScope((scope) =&gt; {<br>      scope.addEventProcessor(async (event) =&gt;<br>        sentry.Handlers.parseRequest(event, ctx.request),<br>      );<br>      const { userId, email, deviceId } = ctx.req.user ?? {};<br>      scope.setUser({ userId, email, deviceId });<br>      sentry.captureException(err);<br>    });<br>  });<br>  koa.proxy = true  <br><br>  const app = await NestFactory.create&lt;NestKoaApplication&gt;(<br>    AppModule,<br>    new KoaAdapter(koa),<br>  );<br><br>  app.use(cors());<br>  app.use(helmet());</pre><p>하지만 문제가 발생했습니다. NestJS와 Koa에서 사용하는 라이브러리 간 충돌이 발생하여 Nestjs와 Koa를혼용할 수 없는 상황이었습니다. 앞으로 발생할 수 있는 이슈를 고려하여 라이브러리 충돌을 해결하는 것보다는 독립된 환경으로 빠르게 전환하는 것이 좋다고 판단했습니다.</p><h4>Docker-compose를 이용한 다중 컨테이너 오케스트레이션</h4><p>따라서 우리는 Docker Compose를 사용하여 NestJS와 Koa 서버를 동시에 실행하고, Nginx의 리버스 프록시를 이용하여 /v1/ 경로는 NestJS로, 그 외의 경로는 Koa로 라우팅되도록 설정했습니다.</p><p>혹시 모를 충돌이 우려되어 npm monorepo를 사용하지 않고, 두 개의 프로젝트를 완전히 분리시켜 개발했습니다.</p><p><strong>Docker-compse.yaml</strong></p><pre>version: &#39;3.8&#39;<br>services:<br>  koa:<br>    build:<br>      context: .<br>      dockerfile: ./Dockerfile.koa<br>  nest:<br>    extra_hosts:<br>          - &#39;host.docker.internal:host-gateway&#39;<br>    build:<br>      context: .<br>      dockerfile: ./Dockerfile.nest<br>  nginx:<br>    container_name: nginx<br>    image: nginx<br>    restart: always<br>    ports:<br>      - &quot;80:80&quot;<br>    depends_on:<br>      - koa<br>      - nest<br>    volumes:<br>      - ./nginx.conf:/etc/nginx/nginx.conf</pre><p><strong>nginx.conf</strong></p><pre>user  nginx;<br>worker_processes  auto;<br>error_log  /var/log/nginx/error.log warn;<br>pid        /var/run/nginx.pid;<br>events {<br>    worker_connections  1024;<br>}<br><br>http {<br>    upstream koa {<br>        server koa:3000;<br>    }<br><br>    upstream nest {<br>        server nest:4000;<br>    }<br><br>    server {<br>        listen 80;<br><br>        location / {<br>        proxy_pass http://koa/;<br>        proxy_http_version 1.1;<br>        proxy_set_header Host $host;<br>        proxy_set_header X-Real-IP $remote_addr;<br>        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;<br>        }<br><br>        location /v1/ {<br>            proxy_pass http://nest/;<br>            proxy_http_version 1.1;<br>            proxy_set_header Host $host;<br>            proxy_set_header X-Real-IP $remote_addr;<br>            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;<br>        }<br>    }<br>}</pre><h4>회고</h4><p>위의 작업을 수행하며 느낀 아쉬운점과 잘한점은 다음과 같습니다.</p><p>아쉬운점</p><ul><li>낯선 프레임워크를 도입하려다 보니 코드 한줄 한줄에 대해 고민하는 시간이 길어졌습니다. 때로는 고민보다 빠르게 개발해보고 다시 수정하는게 나을 수 있다는 생각을 하게 되었습니다.</li></ul><p>잘한점</p><ul><li>nestjs 프로젝트 개발 중 이슈가 발생했을 때 이슈의 원인을 정확하게 분석하고, 라이브러리 충돌이라는 결론이 나왔을 때 빠르게 Docker-compose를 이용하는 쪽으로 의사결정하는 과정이 결과적으로 빠르게 nestjs를 도입하는데에 큰 도움이 되었습니다.</li></ul><p>NestJS와 Koa 서비스를 동시에 실행한 후에는 점차적으로 NestJS에서 비즈니스 로직을 재작성하고 새로운 구조에 대해 지속적으로 고민하고 있습니다. 컨버팅 작업이 완료된 후에는 컨버팅 후기를 다시 공유하도록 하겠습니다.</p><p>이상으로 Koa 프로젝트에 NestJS를 적용한 과정과 레거시 개선을 통한 코드 구조 개선에 대해 공유드렸습니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fab40bf987f4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/koa-project-nestjs-%EC%A0%81%EC%9A%A9%EA%B8%B0-%EB%A0%88%EA%B1%B0%EC%8B%9C-%EA%B0%9C%EC%84%A0%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%BD%94%EB%93%9C-%EA%B5%AC%EC%A1%B0-%EA%B0%9C%EC%84%A0-fab40bf987f4">Koa project NestJS 적용기: 레거시 개선을 통한 코드 구조 개선</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Docker 적용기(aws ecr, github action)]]></title>
            <link>https://medium.com/humanscape-tech/docker-%EC%A0%81%EC%9A%A9%EA%B8%B0-aws-ecr-github-action-4427cafe0a9c?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/4427cafe0a9c</guid>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[aws-ecr]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Tue, 27 Jun 2023 10:58:23 GMT</pubDate>
            <atom:updated>2023-07-11T12:24:00.744Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pz2TVN_PXprUvUSI.png" /><figcaption>출처: Docker</figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>이번에는 개발 중인 서비스에 도커를 도입하면서 겪은 경험에 대해 공유하고자 합니다.</p><p><a href="https://medium.com/p/e3231e233400">이전 글</a>에서 언급한 대로, 우리는 임상 연구 데이터를 적재하는 크론 서버를 운영하고 있었습니다. 그러나 시간이 흐를수록 EC2 환경이 조금씩 변경되어 배포에 실패하는 문제가 발생했습니다. 이 문제를 해결하면서 아래의 추가적인 이점들을 가져가기 위해 도커를 도입하기로 결정했습니다.</p><ol><li>일관된 환경: Docker는 이미지를 통해 개발 환경과 프로덕션 환경 간의 일관성을 제공합니다. 개발 환경에서 이미지를 빌드하고, 해당 이미지를 프로덕션 환경에서 실행함으로써 일관성을 유지할 수 있습니다.</li><li>빠른 배포: Docker는 이미지를 사용하여 애플리케이션을 빠르게 배포할 수 있습니다. 이미지는 가볍고 빠르게 생성되며, 필요한 경우 이미지를 공유하고 재사용할 수 있습니다.</li><li>확장성: Docker는 컨테이너 단위로 애플리케이션을 실행하므로, 필요에 따라 애플리케이션의 인스턴스 수를 쉽게 확장할 수 있습니다. 이는 애플리케이션의 성능과 가용성을 향상시키는 데 도움이 됩니다.</li><li>격리와 보안: Docker는 각 컨테이너를 격리된 환경에서 실행하므로, 애플리케이션 간의 상호 영향을 최소화하고 보안을 강화할 수 있습니다.</li></ol><h4>Docker 도입</h4><p>도커를 도입하기 전에, 도커의 장점인 일관적인 환경 제공과 빠른 배포를 활용하려면 도커의 작동 방식에 대한 이해가 필요합니다. 도커는 Dockerfile의 명령문을 순차적으로 읽어가며 환경을 구성합니다. 이전에 동일한 Dockerfile을 사용하여 환경을 구성한 적이 있다면, 레이어 캐시를 사용하여 명령문 단위로 이전 구성을 가져와 적용합니다. 또한 Dockerfile의 첫 줄에서 FROM 키워드를 사용하여 이미 빌드된 이미지를 다운로드하여 사용할 수 있습니다.</p><p>이러한 이해를 바탕으로 빠른 배포를 위한 방법을 고민하였습니다.</p><p>우리는 세 가지 레포지토리 구조를 고려했습니다.</p><ol><li>레포지토리를 사용하지 않고 배포 시마다 Beanstalk 또는 EC2에서 이미지를 빌드하여 사용하는 방법.</li><li>빌드 시간이 오래 걸리고 자주 변경되지 않는 부분(종속성 패키지 설치 등)을 base 도커 파일로 분리하고, base docker image를 다운로드하여 소스 코드만 복사하여 사용하는 방법.</li><li>각 환경마다 별도의 레포지토리(api, cron)를 만들어 배포 시마다 빌드하여 사용하는 방법.</li></ol><p>위의 방법 중 우리는 2번 방법을 택했습니다.</p><p>각 방법의 장단점은 다음과 같이 정리했습니다.</p><ol><li>레포지토리를 사용하지 않아 요금은 증가하지 않지만, 매번 API/Cron 서버에서 빌드를 해야 하므로 원하는 빠른 배포를 실현하기 어려워 보였습니다.</li><li>하나의 레포지토리만 사용하고, 설치할 패키지 변경 사항이 없으면 이미지를 업로드하지 않아 배포 시간을 단축할 수 있는 효과가 있었습니다. 또한 API/Cron 서버에서 소스 코드를 복사하고 프로그램을 실행하는 시간도 빠르므로 효율적인 방식으로 판단했습니다.</li><li>여러 개의 레포지토리를 사용하여 비용이 증가하고, GitHub Actions에서 여러 번 업로드하는 문제로 인해 빌드 시간이 늘어날 것으로 예상되었습니다.</li></ol><p>먼저 Dockerfile을 베이스, API, Cron으로 나누었습니다. 리눅스와 파이썬 패키지를 설치하고, 모든 환경에 공통적으로 적용되는 명령문은 base file에 넣었습니다. API와 Cron 파일에서는 각각 API와 Cron 서버에 배포하기 위해 소스 코드를 복사하고 프로그램을 실행하는 명령문을 추가했습니다.</p><p><strong>Dockerfile.base</strong></p><pre>FROM python:3.8<br><br>ENV LANG C.UTF-8<br>ENV LC_ALL C.UTF-8<br>ENV LC_NUMERIC C.UTF-8<br>ENV LC_TIME C.UTF-8<br>ENV LC_COLLATE C.UTF-8<br>ENV LC_MONETARY C.UTF-8<br>ENV LC_MESSAGES C.UTF-8    <br>ENV PYTHONDONTWRITEBYTECODE 1<br>ENV TZ=Asia/Seoul<br><br>WORKDIR /app<br><br>COPY requirements.txt /app<br><br>RUN apt-get update<br>RUN apt-get install gcc g++ libffi-dev build-essential nginx supervisor make gunicorn curl libssl-dev libbz2-dev libffi-dev cron git libsqlite3-dev  -y<br>RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime &amp;&amp; echo $TZ &gt; /etc/timezone<br><br>RUN pip3 install -r requirements.txt</pre><p><strong>Dockerfile.api</strong></p><pre>FROM ecr_image<br><br>COPY . /app<br>WORKDIR /app<br><br>COPY infra/docker-entrypoint.production.sh /docker-entrypoint.production.sh<br><br>EXPOSE 80<br><br>RUN chmod +x /docker-entrypoint.production.sh</pre><h4>Github action을 이용한 docker image build</h4><p>우리는 GitHub Actions에서 베이스 이미지를 빌드하고 ECR로 업로드하는 과정에서 레이어 캐시를 사용하여 빌드 시간을 줄이려고 노력했습니다. <a href="https://github.com/docker/build-push-action">build-push-action</a>을 사용하여 buildx를 이용해 도커 이미지를 빌드하며, 이 과정에서 GitHub Actions 캐시를 사용하여 레이어 캐시를 활용했습니다.</p><p>그러나 해당 스크립트에 문제가 있었는데, buildx toolkit을 사용하다 보니 멀티 플랫폼을 지원하는 이미지로 빌드되어 ECR에 정상적이지 않은 형태로 업로드되었습니다.</p><p>이 문제를 해결하기 위해 직접 빌드 스크립트를 작성했습니다.</p><pre>jobs:<br>  deploy:<br>    runs-on: ubuntu-latest<br>    steps:<br>      - uses: actions/checkout@v2<br><br>      - name: Configure AWS credentials<br>        uses: aws-actions/configure-aws-credentials@v1<br>        with:<br>          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}<br>          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}<br>          aws-region: ap-northeast-2<br><br>      - name: Login to Amazon ECR<br>        id: login-ecr<br>        uses: aws-actions/amazon-ecr-login@v1<br><br>      - name: pull base image<br>        continue-on-error: true<br>        env:<br>          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}<br>          ECR_REPOSITORY: repository_name<br>        run: |<br>          docker pull ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:latest<br><br>      - name: push base image<br>        env:<br>          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}<br>          ECR_REPOSITORY: repository_name<br>        run: |<br>          docker build --cache-from=${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:latest -f ./infra/Dockerfile.base -t tag_name .<br>          docker tag image_name ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:latest<br>          docker tag image_name ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}<br>          docker push ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }} --all-tags</pre><h4>회고</h4><p>위의 작업들을 진행하며 아쉬운점, 잘한점이 생겼습니다.</p><p>아쉬운점은 다음과 같습니다.</p><ul><li>작성한 빌드 스크립트에서는 API와 Cron 서버에서 도커 빌드 시 가장 최근의(latest) 태그가 붙은 이미지를 가져와 빌드하도록 되어 있었습니다. 하지만 롤백 등의 상황에서 최신 베이스 이미지를 가져와 빌드하면 문제가 발생할 수 있어 해당 부분을 개선해야 한다고 생각합니다.</li><li>위의 레포지토리 구조 고민에서 1번 방법을 선택하고 API/Cron 서버에서 레이어 캐시를 사용해 빌드하는 방식으로 진행했다면 속도 측면에서 큰 차이가 없었을 것으로 예상되는데, 이 부분을 인지하지 못하고 넘어간 것이 아쉬웠습니다.</li></ul><p>잘한점은 다음과 같습니다.</p><ul><li>docker기반 배포의 이점인 빠른 배포 속도를 가져가기 위해 github actions에서 많은 삽질을 했고, 우리 상황에 맞는 방법을 찾아낸 것이 잘한점이라 생각됩니다.</li></ul><p>지금까지 Docker 적용 과정에 대해 적어 보았습니다. 감사합니다!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4427cafe0a9c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/docker-%EC%A0%81%EC%9A%A9%EA%B8%B0-aws-ecr-github-action-4427cafe0a9c">Docker 적용기(aws ecr, github action)</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[cron서버 환경 구성 삽질 일지]]></title>
            <link>https://medium.com/humanscape-tech/cron%EC%84%9C%EB%B2%84-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1-%EC%97%AC%ED%96%89%EA%B8%B0-e3231e233400?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/e3231e233400</guid>
            <category><![CDATA[cron]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[django]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Tue, 27 Jun 2023 07:24:34 GMT</pubDate>
            <atom:updated>2023-07-11T11:20:38.745Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UHfRlKPr4Dj-JtTq.jpeg" /><figcaption>이미지 출처: <a href="https://edge.id/article/what-is-a-web-server-understanding-functions-and-types/">https://edge.id/article/what-is-a-web-server-understanding-functions-and-types/</a></figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>오늘은 Django 기반의 서비스에서 크론 서버를 구성하는 과정에 대해 공유하려고 합니다. 이 글에서는 크론 서버 구성을 위해 우리가 겪었던 어려움과 선택한 해결책에 대해 설명하려 합니다.</p><h4>크론 서버 구성</h4><p>우리는 임상 연구를 적재하는 서비스를 개발하고 있었습니다. 처음에는 크론 서버 없이 필요할 때마다 API를 호출하여 임상 연구를 적재하는 방식으로 작업을 수행했습니다. 그러나 추가 리소스를 투입하지 않고도 주기적으로 임상 연구를 적재하기 위해 크론 서비스를 도입하게 되었습니다.</p><p>우리는 크게 2가지의 옵션을 두고 고려했습니다.</p><ol><li>aws에서 제공하는 크론 관련 서비스 사용</li><li>Django에서 크론 작업을 관리하기 위해 django-crontab을 사용하고, EC2를 사용하여 관리</li></ol><p>(Api 서버에 크론을 추가하지 않고 크론서버를 따로 만드는 이유는 이후 auto scaling시 같은 크론 작업이 두번 돌아갈 수 있기 때문입니다.)</p><p>첫 번째 옵션인 AWS의 크론 관련 서비스는 AWS 콘솔을 통해 동작 상황을 쉽게 파악할 수 있으며, AWS에서 제공하는 다양한 옵션을 통해 확장성을 기대할 수 있었습니다. 하지만 관리 포인트가 증가하고 코드로 크론을 관리할 수 없는 단점이 있었습니다.</p><p>두 번째 옵션인 Django와 django-crontab을 사용하여 EC2에 크론 작업을 관리하는 방법은 구조가 직관적이고 코드로 크론을 관리할 수 있는 장점이 있었습니다.</p><p>우리는 고민 끝에 두 번째 옵션을 선택하여 Django 서비스를 EC2에 배포하고, django-crontab 패키지를 사용하여 크론 작업을 관리하기로 결정했습니다.</p><h4>django-crontab을 통한 cronjob 관리</h4><blockquote>크론은 크게 크론 데몬과 크론 테이블로 구성됩니다. 크론 데몬은 운영체제에서 동작하는 백그라운드 프로세스로, 정해진 시간에 크론 테이블에 등록된 작업을 실행합니다. 크론 테이블은 크론 작업의 목록을 관리하는 데이터베이스나 텍스트 파일로, 각 작업의 실행 주기와 명령어 등을 포함합니다.</blockquote><p>django-crontab 패키지를 사용하면 Django 애플리케이션 내에서 크론 작업을 등록할 수 있습니다. 먼저, settings.py 파일에서 크론 작업을 등록할 수 있는 크론 설정(CRONJOBS)을 추가해야 합니다. 이 설정은 크론 작업의 주기와 실행할 함수 또는 커맨드를 지정합니다.</p><p>예를 들어, 매일 오전 3시에 실행되는 크론 작업을 등록하고 싶다면, settings.py 파일에 다음과 같은 내용을 추가합니다</p><pre>CRONJOBS = [<br>    (&#39;0 3 * * *&#39;, &#39;myapp.cron.my_cron_job&#39;)<br>]</pre><p>위의 예시에서 &#39;0 3 * * *&#39;는 크론 표현식으로, 매일 오전 3시에 실행됨을 나타냅니다. &#39;myapp.cron.my_cron_job&#39;은 실행할 함수의 경로를 나타내며, 이는 실제로 실행될 크론 작업 함수를 가리킵니다.</p><p>우리는 테스트의 용이성을 위해 작업들을 django command로 만든 후, command를 호출하는 함수를 CRONJOBS로 등록했습니다.</p><p>django-crontab 패키지를 사용하면 Django 애플리케이션과 크론 작업을 함께 버전 관리할 수 있습니다. 크론 작업을 Django 프로젝트 내에서 관리하면 코드베이스의 일부로서 크론 작업을 유지하고, 배포 및 관리가 편리해집니다. 또한, Django의 ORM과 다른 기능들을 크론 작업 내에서 활용할 수 있어 개발과 유지보수가 용이해집니다.</p><p>이와 같은 방식으로 Django 애플리케이션과 크론 작업을 통합하면, 크론 작업을 실행해야 할 시간과 주기를 Django 설정 파일에서 관리할 수 있으며, Django 애플리케이션의 다른 부분과 일관된 방식으로 크론 작업을 개발하고 관리할 수 있습니다.</p><h4>Slack Webhook 도입</h4><p>우리는 크론 서버의 상태를 실시간으로 모니터링하고, 잠재적인 문제를 빠르게 감지하기 위해 Slack webhook을 도입했습니다.</p><p>webhook은 크론 작업이 실행될 때마다 성공 여부를 확인하는 메시지를 전송하는 방식으로 개발하였습니다.</p><p>Slack의 경우 API 제공이 원활하고, 사내 메신저로 사용중이기 때문에 접근성이 좋아 사용하기로 했습니다.</p><p>예기치 않은 상황으로 인 크론 서버가 다운되는 경우, Slack webhook을 통한 이슈 전달이 불가능할 수도 있습니다. 이를 고려하여 우리는 크론 작업이 실행될 때마다 성공 여부를 확인하는 메시지를 전송하는 방식으로 개발하였습니다.</p><p>이를 통해 우리의 시스템은 안정성을 유지하며, 개발자들은 크론 서버의 동작 상태에 대해 쉽고 편리하게 알 수 있게 되었습니다.</p><h4>크론서버 docker 도입</h4><p>그러나 문제가 발생했습니다. 배포할 때마다 EC2 환경이 조금씩 달라져서 배포에 실패하는 경우가 발생했습니다. 이 문제를 해결하기 위해 우리는 Docker를 도입하기로 결정했습니다. Docker를 사용하여 항상 동일한 서버 환경을 제공하여 환경 구성과 관련된 문제 및 변수를 제거하고자 했습니다.</p><p>우리는 Docker를 사용할 수 있는 여러 서비스를 조사했습니다. 크게 네 가지 옵션이 있었습니다.</p><ol><li>기존 EC2에 Docker를 설치하여 사용하는 방법</li><li>AWS Batch 사용</li><li>Beanstalk Worker 사용</li><li>ECS 사용</li></ol><p>우리는 인프라 의존성을 줄이고자 코드로 가능한 많은 부분을 관리하려고 했으며, 실제로 동작하는 크론 작업은 Django admin을 통해 모두 파악할 수 있었기 때문에 AWS 콘솔을 통해 현황을 파악하는 일이 거의 없었습니다. 따라서 우리는 해야 할 작업이 적고 러닝 커브가 낮은 EC2를 선택했습니다. 도커 이미지는 AWS ECR을 통해 관리하기로 결정했습니다.</p><p>Docker를 사용할 수 있는 여러 서비스를 조사하며 알아낸 장단점은 다음과 같습니다.</p><ul><li>EC2에 Docker를 설치하여 사용하는 방법은 EC2를 재생성해야 할 경우 Codedeploy 서비스와 연결하고, Docker를 설치하기 위해 SSH로 서버에 접근하여 작업해야 하는 단점이 있었습니다. 그러나 자유도가 높고 이전에 EC2를 사용하고 있었기 때문에 작업량이 적은 장점이 있었습니다.</li><li>AWS Batch는 작업 당 작업 정의를 통해 세밀한 옵션 조정이 가능하며, 작업 현황을 쉽게 파악할 수 있는 장점이 있었습니다. 그러나 세밀한 옵션 조정이 필요하지 않은 경우 오히려 단점이 될 수 있었고, 우리는 이미 각 데이터의 처리 상태를 관리하기 위해 control_status_type이라는 컬럼을 사용하고 있었기 때문에 작업 현황 파악 기능이 필요하지 않았습니다.</li><li>Beanstalk Worker는 EC2와 Batch 사이에 위치한 서비스로 보입니다. 세밀한 옵션 조정이 가능하지만 원하지 않는 경우에는 기본 옵션으로 작업을 진행할 수 있었으며, 이미 API 서버를 Beanstalk를 통해 서비스하고 있어 한꺼번에 관리하기 편리했습니다. 그러나 Beanstalk Worker를 사용하기 위해서는 기존에 커맨드로 만들어 둔 크론 작업을 API로 변환해야 하는 작업이 필요했습니다.</li><li>ECS는 AWS Batch와 유사한 방식으로 사용되는 것으로 보였습니다.</li></ul><h4>회고</h4><p>위에서 서술한 여러 작업을 진행한 후 스스로에 대한 회고를 간단하게 진행해 보았습니다.</p><p>아쉬운점은 다음과 같습니다.</p><ul><li>위에서 서술한 ec2의 단점을 상쇄할만한 방법을 찾지 않고 작업을 마무리 한 점이 아쉬웠습니다. 현재는 ec2 인스턴스 개설 후 작성해야 하는 명령어 모음을 메모해두고 복붙하는 형태로 사용중입니다.</li><li>slack webhook이 주기적으로 계속 날아오기 때문에 매번 확인해야 하는 작업자 입장에서 피로감이 생기게 됩니다. slack webhook이 정해진 시간내에 오지 않을 경우에만 알려주는 다른 서비스를 접목시켰으면 어땠을까 하는 생각이 있습니다.</li></ul><p>잘한점은 다음과 같습니다.</p><ul><li>인프라 의존성을 줄이고자 시도했고, 스타트업의 경우 크레딧을 얻어 클라우드 플랫폼을 옮기는 경우가 종종 있어 이 경우에 매우 유용했습니다.</li><li>남들이 많이 사용하는 서비스를 이용해 그냥 개발하기 보다 현재 상황을 정확히 파악하고, 상황에 맞는 구현방법을 찾아 오버/언더 엔지니어링을 피하고자 했습니다.</li></ul><p>지금까지 Django 기반의 크론 서버 환경 구성에 대해 알아보았습니다. 다음 글에서는 도커 이미지 관리에 대해 자세히 다루도록 하겠습니다. 감사합니다!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e3231e233400" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/cron%EC%84%9C%EB%B2%84-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1-%EC%97%AC%ED%96%89%EA%B8%B0-e3231e233400">cron서버 환경 구성 삽질 일지</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[하루만에 완성하는 Django+DRF 서비스(5) — END]]></title>
            <link>https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-5-end-1daea7f491b7?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/1daea7f491b7</guid>
            <category><![CDATA[django-rest-framework]]></category>
            <category><![CDATA[django]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Wed, 26 Apr 2023 02:11:45 GMT</pubDate>
            <atom:updated>2023-04-26T02:11:45.531Z</atom:updated>
            <content:encoded><![CDATA[<h3>하루만에 완성하는 Django+DRF 서비스(5) — END</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*Hq8O7EECyCpwSGuz.png" /><figcaption>출처: <a href="https://www.django-rest-framework.org/">https://www.django-rest-framework.org/</a></figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>이번 시간엔 App을 추가하는 방법에 대해 알아보도록 하겠습니다.</p><p>프로젝트를 진행한 코드는 깃허브에 올라가 있으니 참고하시기 바랍니다.</p><p><a href="https://github.com/Root-kjh/drf_practice">GitHub - Root-kjh/drf_practice</a></p><h4>App</h4><p>app은 Django 프로젝트 내에서 특정 기능 또는 업무 영역을 담당하는 독립적인 모듈입니다. 각 앱은 자체적으로 데이터 모델, 뷰, 템플릿, URL 매핑, 정적 파일 등을 가질 수 있으며, Django 프로젝트 내에서 재사용이 가능합니다.</p><p><strong>App을 구분하는 기준</strong>은 상황에따라, 사람마다 다를 수 있습니다.</p><p>저는 DDD(Domain-driven Design)의 Bounded Context 개념을 참고하여 app을 구분합니다.</p><p>방대한 프로젝트가 하나의 app에 들어가 있을 경우 복잡도가 급격하게 증가해 유지보수가 힘들어질 수 있고, 너무 잘게 쪼개면 각 app간의 의존성이 너무 많아져 관리하기 힘들어 질 수 있습니다.</p><p>따라서 상황에따라, 필요에따라 적절히 app을 나누어야 합니다.</p><p><strong>App을 생성하는 방법</strong>은 다음과 같습니다.</p><p>python3 manage.py startapp users</p><p>위 명령어를 입력하면 users app이 생성됩니다.</p><p>settings의 INSTALLED_APPS에 users를 추가하면 해당 app을 사용할 수 있습니다.</p><p>개인적으로는 외부에서 가져온 app과 내부에서 작성한 app을 구분하기 위해 THIRD_PARTY_APPS, LOCAL_APPS 리스트를 나눠 작성하고 INSTALLED_APPS = THIRD_PARTY_APPS + LOCAL_APPS 로 합쳐서 사용합니다.</p><p>지금까지 배운 것들을 활용해 우리는 원하는 api들을 자유자재로 만들 수 있습니다.</p><p>하루만에 완성하는 Django+DRF 서비스는 여기서 마무리 짓도록 하겠습니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1daea7f491b7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-5-end-1daea7f491b7">하루만에 완성하는 Django+DRF 서비스(5) — END</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[하루만에 완성하는 Django+DRF 서비스(4)]]></title>
            <link>https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-4-46eabb8112d4?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/46eabb8112d4</guid>
            <category><![CDATA[django]]></category>
            <category><![CDATA[django-rest-framework]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Mon, 20 Mar 2023 10:57:59 GMT</pubDate>
            <atom:updated>2023-03-20T10:57:59.127Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*rAOSVT8NWlZTI44z.png" /><figcaption>출처: <a href="https://www.django-rest-framework.org/">https://www.django-rest-framework.org/</a></figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>이번 시간엔 쿼리성능 개선 방법에 대해 알아보도록 하겠습니다.</p><p>프로젝트를 진행한 코드는 깃허브에 올라가 있으니 참고하시기 바랍니다.</p><p><a href="https://github.com/Root-kjh/drf_practice">GitHub - Root-kjh/drf_practice</a></p><h3>쿼리 확인</h3><p>django+drf의 쿼리를 확인할 수 있는 방법은 여러가지가 있습니다.</p><p>그 중 가장 간단한 방식 2가지를 소개드리도록 하겠습니다.</p><h4>Django Debug Toolbar</h4><p><a href="https://github.com/jazzband/django-debug-toolbar">GitHub - jazzband/django-debug-toolbar: A configurable set of panels that display various debug information about the current request/response.</a></p><p>문서를 보고 debug toolbar를 설치합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1HDMemHgDzU5RY5TIfh2XA.png" /></figure><p>서버 내의 모든 view에 툴바가 생긴것을 확인할 수 있습니다.</p><p>SQL탭을 누르면 어떤 쿼리가 실행되었는지, 쿼리 explain등을 볼 수 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D7MTtAeLry_WakQKCdFE3w.png" /></figure><h4>Query logging</h4><p>django의 logging 문서를 참고해 settings.py에 로깅 설정을 작성합니다.</p><p><a href="https://docs.djangoproject.com/en/4.1/topics/logging/">Django</a></p><pre>LOGGING = {<br>    &#39;version&#39;: 1,<br>    &#39;disable_existing_loggers&#39;: False,<br>    &#39;handlers&#39;: {<br>        &#39;console&#39;: {<br>            &#39;level&#39;: &#39;DEBUG&#39;,<br>            &#39;class&#39;: &#39;logging.StreamHandler&#39;,<br>        },<br>    },<br>    &#39;loggers&#39;: {<br>        &#39;django.db.backends&#39;: {<br>            &#39;handlers&#39;: [&#39;console&#39;],<br>            &#39;level&#39;: &#39;DEBUG&#39;,<br>        },<br>    },<br>}</pre><p>api를 호출하면 console에 쿼리가 찍히는것을 확인할 수 있습니다.</p><pre>(0.000) SELECT &quot;django_session&quot;.&quot;session_key&quot;, &quot;django_session&quot;.&quot;session_data&quot;, &quot;django_session&quot;.&quot;expire_date&quot; FROM &quot;django_session&quot; WHERE (&quot;django_session&quot;.&quot;expire_date&quot; &gt; &#39;2023-03-07 12:56:17.442700&#39; AND &quot;django_session&quot;.&quot;session_key&quot; = &#39;2pa7rg8hnhtummtoem4pcr8nzb26mf1r&#39;) LIMIT 21; args=(&#39;2023-03-07 12:56:17.442700&#39;, &#39;2pa7rg8hnhtummtoem4pcr8nzb26mf1r&#39;); alias=default<br>(0.000) SELECT &quot;auth_user&quot;.&quot;id&quot;, &quot;auth_user&quot;.&quot;password&quot;, &quot;auth_user&quot;.&quot;last_login&quot;, &quot;auth_user&quot;.&quot;is_superuser&quot;, &quot;auth_user&quot;.&quot;username&quot;, &quot;auth_user&quot;.&quot;first_name&quot;, &quot;auth_user&quot;.&quot;last_name&quot;, &quot;auth_user&quot;.&quot;email&quot;, &quot;auth_user&quot;.&quot;is_staff&quot;, &quot;auth_user&quot;.&quot;is_active&quot;, &quot;auth_user&quot;.&quot;date_joined&quot; FROM &quot;auth_user&quot; WHERE &quot;auth_user&quot;.&quot;id&quot; = 1 LIMIT 21; args=(1,); alias=default<br>[07/Mar/2023 12:56:17] &quot;GET /admin/jsi18n/ HTTP/1.1&quot; 200 3343</pre><h3>쿼리 성능 개선</h3><p>비효율적인 쿼리를 발생시키기 위해 item list view에 ItemDetailSerializer를 연결시켜보겠습니다.</p><pre>class ItemViewSet(viewsets.ModelViewSet):<br>    queryset = Item.objects.all()<br>    serializer_class = ItemDetailSerializer<br>    permission_classes = [DjangoModelPermissionsOrAnonReadOnly]<br><br>    # def get_serializer_class(self):<br>    #     if self.action == &#39;retrieve&#39;:<br>    #         return ItemDetailSerializer<br>    #     return ItemSerializer</pre><p>item list를 조회하게 되면 아래와 같이 각 item당 review를 검색하는 쿼리가 실행됩니다.</p><p>이렇게 object하나 당 해당 object에 연결된 objects를 가져오기 위해 쿼리를 추가로 실행하는 문제를 <strong>n+1문제</strong>라 부릅니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZA4Cs9ReYT-ttZIT9qybhw.png" /></figure><p>n+1문제를 해결하는 방법은 2가지가 있습니다.</p><h4>selected_related</h4><p>내부적으로 join을 사용해 데이터를 한번에 가져옵니다.</p><p>따라서 join으로 가져올 수 있는 관계 (연결된 object가 하나일경우)에서 사용됩니다.</p><h4>prefetch_related</h4><p>가져오는 objects의 id list를 이용해 in 연산으로 연결된 objects를 가져옵니다.</p><p>select_related와 달리 쿼리를 2개로 나누고, python에서 join합니다.</p><p>이번 경우엔 prefetch_related를 통해 쿼리를 최적화합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_n5TfU55F--R0SUv2TexrQ.png" /></figure><h4>Index</h4><p>index는 데이터 검색 속도를 향상시키기 위해 사용하는 데이터 구조입니다. 인덱스는 특정 column 값을 색인화하여 데이터베이스에서 빠르게 검색할 수 있도록 합니다.</p><p>일반적으로 데이터를 검색할 때, 전체 데이터를 하나씩 탐색하면서 조건에 맞는 데이터를 찾습니다. 이는 데이터의 양이 적을 때는 큰 문제가 되지 않지만, 데이터의 양이 많아질수록 검색 속도가 느려지는 문제가 발생할 수 있습니다.</p><p>이 때, 인덱스는 데이터베이스에서 데이터를 빠르게 검색할 수 있도록 도와줍니다.</p><p>예를 들어, 특정 테이블에서 ‘이름’ 필드를 인덱스로 설정하면, 이름으로 검색하는 작업에서 데이터베이스는 전체 데이터를 탐색하는 것이 아니라, 인덱스를 사용하여 더 빠르게 해당 데이터를 찾아낼 수 있습니다.</p><p>인덱스는 데이터베이스에서 자주 검색되는 필드에 대해서 생성하는 것이 좋습니다.</p><p>데이터베이스에서 데이터를 검색할 때 빠르게 찾을 수 있는 장점이 있지만, 데이터를 추가, 수정, 삭제할 때 추가적인 작업이 필요하며, 인덱스의 크기가 크면 디스크 공간도 많이 차지하게 됩니다.</p><p>따라서 인덱스를 사용할 때에는 신중하게 결정해야 하며, 자주 사용하는 필드에 대해서만 생성하는 것이 좋습니다.</p><p>Django에서는 모델 클래스의 필드에 db_index=True 속성을 설정하여 해당 필드를 인덱스로 사용할 수 있습니다.</p><p>item의 date range로 검색하는 필터 기능이 추가된다고 가정해봅시다.</p><p>우리는 date 검색을 빠르게 하기 위해 date 필드에 db_index=True 속성을 설정할 수 있습니다.</p><pre>class Item(models.Model):<br>    name = models.CharField(max_length=100)<br>    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name=&#39;items&#39;)<br>    description = models.TextField(null=True, blank=True)<br>    image = models.ImageField(upload_to=&#39;images/&#39;, null=True)<br>    date = models.DateTimeField(auto_now_add=True, db_index=True)<br><br>    def __str__(self):<br>        return self.name</pre><p>지금까지 쿼리 최적화 방법에 대해 알아봤습니다.</p><p>다음 시간엔 새로운 앱을 추가해 여러 앱을 운용하는 방법에 대해 알아보도록 하겠습니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=46eabb8112d4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-4-46eabb8112d4">하루만에 완성하는 Django+DRF 서비스(4)</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[개발자의 해킹 얘기 — N-day 취약점 리뷰(Django 4.0.5/SQL Injection)]]></title>
            <link>https://medium.com/humanscape-tech/%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%ED%95%B4%ED%82%B9-%EC%96%98%EA%B8%B0-n-day-%EC%B7%A8%EC%95%BD%EC%A0%90-%EB%A6%AC%EB%B7%B0-django-4-0-5-sql-injection-b35c798effec?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/b35c798effec</guid>
            <category><![CDATA[sql-injection]]></category>
            <category><![CDATA[django]]></category>
            <category><![CDATA[vulnerability]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Sun, 05 Mar 2023 07:28:21 GMT</pubDate>
            <atom:updated>2023-03-05T07:28:21.537Z</atom:updated>
            <content:encoded><![CDATA[<h3>개발자의 해킹 얘기 — N-day 취약점 리뷰(Django 4.0.5/SQL Injection)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/0*Br_TxumaQeeSiQwx.jpg" /><figcaption>출처: <a href="https://www.sitepoint.com/how-to-protect-your-website-against-sql-injection-attacks/">https://www.sitepoint.com/how-to-protect-your-website-against-sql-injection-attacks/</a></figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>이번 시리즈에서는 해킹, 취약점에 대한 얘기를 자유형식으로 연재할 예정입니다.</p><p>그리고 이번 시간엔 Django 4.0.5버전에서 발견된 SQL Injection 취약점에 대해 얘기해보도록 하겠습니다.</p><p>사용하는 패키지관련 취약점 정보는 snyk사이트를 이용해 간편하게 확인할 수 있습니다.</p><p><a href="https://security.snyk.io/package/pip/django">django vulnerabilities | Snyk</a></p><p>POC는 github에 잘 올라와 있으며, 이번 편에서는 해당 POC를 기반으로 설명합니다.</p><p><a href="https://github.com/aeyesec/CVE-2022-34265">GitHub - aeyesec/CVE-2022-34265: PoC for CVE-2022-34265 (Django)</a></p><h4>SQL Injection</h4><p>sql injection은 user의 입력값이 들어가는 곳에 sql을 넣어 비정상적인 동작을 하도록 유도하는것을 말합니다.</p><p>로그인 sql구문을 예로 들어보겠습니다.</p><p>select * from users where id=’{user_id}’ and password=’{password}’;</p><p>만약 user_id와 password에 ‘ or ‘’=’ 구문을 넣으면 모든 user를 가져오게 되며 로그인에 성공하게 됩니다.</p><p>select * from users where id=’&#39; or &#39;&#39;=&#39;’ and password=’&#39; or &#39;&#39;=&#39;’;</p><p>id가 test인 유저가 있을 경우 로그인에 통과하도록 할 수도 있습니다.</p><p>select * from users where id=’test’ and password=’&#39; or &#39;&#39;=&#39;’;</p><p>이를 응용하면 db의 모든 정보들을 가져올 수 있습니다.</p><p>SQL Injection의 방어방법은 크게 2가지가 있습니다.</p><p><strong>입력값 검증</strong></p><ul><li>입력값을 검증해 SQL Injection을 일으키는 입력값을 사전에 치환/제거할 수 있습니다.</li><li>aws, cloudflare등에서 waf 등의 방화벽을 이용할 수 있습니다.</li></ul><p><strong>Prepared Statement 사용</strong></p><ul><li>SQL Injection에 대한 방어방법으로 가장 권장되는 방식입니다.</li><li>쿼리파싱을 한 후 입력값을 바인딩합니다.</li><li>입력값은 단순 텍스트로 인식되어 쿼리에 전혀 영향을 끼치지 못합니다.</li></ul><h4>CVE-2022–34265</h4><p>이번 취약점은 django orm의 Extract, Trunc함수에서 발견되었습니다.</p><p>Extract는 인자로 들어온 날짜의 단위를 숫자로 추출하는 함수, Trunc는 소수점을 버리듯이 인자로 들어온 단위(hour, day, month 등)까지 날짜를 잘라 반환하는 함수입니다.</p><p>날짜의 단위를 인자로 받는 부분에서 취약점이 발생하게 되는데, 해당 함수를 사용했을때의 실제 sql 구문을 보면 다음과 같습니다.</p><p><strong>Extract</strong></p><pre>SELECT django_datetime_extract(&quot;year&quot;,&quot;start_datetime&quot;,&quot;UTC&quot;) AS &quot;start_year&quot;</pre><p>여기서year 부분을 year’ FROM start_datetime)) OR ``=``;SELECT PG_SLEEP(5) --으로 바꾸게 되면 sql injection이 발생합니다.</p><p>(poc의 구문을 그대로 가져오면 해킹으로 인식되어 글 작성이 안되기 때문에 일부 수정했습니다.)</p><pre>SELECT django_datetime_extract(&quot;year from table_name)) or ``=``;SELECT PG_SLEEP(5) --,&quot;start_datetime&quot;,&#39;UTC&#39;) AS &quot;start_year&quot;</pre><p>POC에선 SELECT PG_SLEEP(5) 를 넣었지만, 데이터베이스 삭제 등의 위험한 쿼리도 충분히 실행할 수 있습니다.</p><p><strong>Trunc</strong></p><p>trunc도 extract와 거의 동일하게 동작합니다.</p><pre>SELECT django_datetime_trunc(&#39;year&#39;, &quot;start_datetime&quot;, &#39;UTC&#39;) AS &quot;start_year&quot;</pre><p>year 부분을year’, start_datetime)) OR ``=``;SELECT PG_SLEEP(5)-- 으로 바꿔줍니다.</p><pre>SELECT django_datetime_trunc(&#39;year&#39;, start_datetime)) OR ``=``;SELECT PG_SLEEP(5)--&#39;, &quot;start_datetime&quot;, &#39;UTC&#39;) AS &quot;start_year&quot;</pre><p>위와 같이 sql injection이 발생합니다.</p><p>해당 취약점들은 trunc,extract함수의 단위 인자를 api 파라미터 등의 유저 입력으로 받아오는 경우 발생합니다.</p><p>따라서 해당 인자들을 유저 입력으로 받아오지 않도록 수정하거나 django 버전을 4.0.6 이상으로 업데이트함을 권고드립니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b35c798effec" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%ED%95%B4%ED%82%B9-%EC%96%98%EA%B8%B0-n-day-%EC%B7%A8%EC%95%BD%EC%A0%90-%EB%A6%AC%EB%B7%B0-django-4-0-5-sql-injection-b35c798effec">개발자의 해킹 얘기 — N-day 취약점 리뷰(Django 4.0.5/SQL Injection)</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[하루만에 완성하는 Django+DRF 서비스(3)]]></title>
            <link>https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-3-dd45cd967a6d?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/dd45cd967a6d</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[django-rest-framework]]></category>
            <category><![CDATA[django]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Sun, 05 Mar 2023 07:28:12 GMT</pubDate>
            <atom:updated>2023-03-05T07:28:12.208Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*8GEGvzmqVthHGOFl.png" /><figcaption>출처: <a href="https://www.django-rest-framework.org/">https://www.django-rest-framework.org/</a></figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>이번 시간엔category, item, review의 모델 연결관계를 api에서 표현하는 방법, permission을 적용하는 방법(일반유저가 items를 삭제/수정하지 못하도록 관리)에 대해 알아보도록 하겠습니다.</p><p>프로젝트를 진행한 코드는 깃허브에 올라가 있으니 참고하시기 바랍니다.</p><p><a href="https://github.com/Root-kjh/drf_practice">GitHub - Root-kjh/drf_practice</a></p><p>이전 시간에 만든 category api, testcode와 같은 형식으로 item, review에 대한 api, testcode도 생성합니다.</p><p>자세한 코드는 github repo에서 확인해주세요.</p><pre>from test_plus.test import APITestCase<br><br>from ..models import Item, Category<br><br>class ItemApiTestCase(APITestCase):<br>    def setUp(self) -&gt; None:<br>        self.category = Category.objects.create(name=&#39;test category&#39;)<br>        self.item = Item.objects.create(name=&#39;test item&#39;, category=self.category, description=&#39;test description&#39;)<br>        self.item2 = Item.objects.create(name=&#39;test item 2&#39;, category=self.category, description=&#39;test description 2&#39;)<br><br>    def test_get_items(self):<br>        response = self.get(&#39;/reviews/items/&#39;)<br>        self.assert_http_200_ok(response)<br>        self.assertEqual(len(response.data), 2)<br>        self.assertEqual(response.data[0][&#39;name&#39;], self.item.name)<br>        self.assertEqual(response.data[1][&#39;name&#39;], self.item2.name)<br><br>    def test_get_item(self):<br>        response = self.get(f&#39;/reviews/items/{self.item.id}/&#39;)<br>        self.assert_http_200_ok(response)<br>        self.assertEqual(response.data[&#39;name&#39;], self.item.name)<br><br>    def test_create_item(self):<br>        data = {&#39;name&#39;: &#39;test item 3&#39;, &#39;category&#39;: self.category.id, &#39;description&#39;: &#39;test description 3&#39;}<br>        response = self.post(&#39;/reviews/items/&#39;, data=data)<br>        self.assert_http_201_created(response)<br>        self.assertEqual(response.data[&#39;name&#39;], data[&#39;name&#39;])<br>        self.assertEqual(Item.objects.count(), 3)<br>        self.assertEqual(Item.objects.last().name, data[&#39;name&#39;])<br><br>    def test_update_item(self):<br>        data = {&#39;name&#39;: &#39;test item 3&#39;, &#39;category&#39;: self.category.id, &#39;description&#39;: &#39;test description 3&#39;}<br>        response = self.put(f&#39;/reviews/items/{self.item.id}/&#39;, data=data)<br>        self.assert_http_200_ok(response)<br>        self.assertEqual(response.data[&#39;name&#39;], data[&#39;name&#39;])<br>        self.assertTrue(Item.objects.filter(name=data[&#39;name&#39;]).exists())<br>        self.assertFalse(Item.objects.filter(name=self.item.name).exists())<br><br>    def test_delete_item(self):<br>        response = self.delete(f&#39;/reviews/items/{self.item.id}/&#39;)<br>        self.assert_http_204_no_content(response)<br>        self.assertFalse(Item.objects.filter(name=self.item.name).exists())<br>        self.assertEqual(Item.objects.count(), 1)</pre><p>만든 api들을 확인해보면 잘 동작하는걸 알 수 있습니다.</p><h4>Model의 연결관계 표현</h4><p>우리는 category retrieve api를 호출할때 items를, item retrieve api를 호출할 때 reviews를 반환하도록 하고 싶습니다.</p><p>(클라이언트 단에서 api 호출을 적게 할수록 로딩 시간이 줄어듭니다.)</p><p>CategoryDetailSerializer를 만들어 items를, ItemDetailSerializer를 만들어 reviews field를 추가하고, view에서 action이 retrieve일 경우 DetailSerializer를 사용하도록 설정합니다.</p><p>Category retrieve api로 예를 들어 보겠습니다.</p><p>DetailSerializer, ~Serializer등 여러 개의 클래스를 생성하면 serializers.py 파일이 너무 커지기 때문에 category_serializers.py로 분리했습니다.</p><p>이전에 생성한 ItemSerializer클래스를 불러와 사용하면 ModelSerializer내부에서 items라는 attribute를 찾아 items field에 넣어줍니다.</p><pre>class CategoryDetailSerializer(serializers.ModelSerializer):<br>    items = ItemSerializer(many=True)<br>    class Meta:<br>        model = Category<br>        fields = &#39;__all__&#39;</pre><p>CategoryViewSet에서는 retrieve인 경우 DetailSerializer를 사용하도록 수정합니다.</p><p>ModelViewSet의 코드를 까보면 내부적으로 get_serializer_class라는 함수를 호출해 serializer를 가져옵니다.</p><p>따라서 해당 함수를 overwrite해주면 됩니다.</p><pre><br>class CategoryViewSet(viewsets.ModelViewSet):<br>    queryset = Category.objects.all()<br>    serializer_class = CategorySerializer<br><br>    def get_serializer_class(self):<br>        if self.action == &#39;retrieve&#39;:<br>            return CategoryDetailSerializer<br>        return CategorySerializer</pre><p>실제로확인해보면 아래와 같이 잘 나오는것을 볼 수 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MRQduC3bg1U9pm-WCSQGkQ.png" /></figure><h4>Permission 적용</h4><p>category, item항목은 유저가 직접 편집하기에 위험부담이 있습니다.</p><p>따라서 해당 항목들은 admin유저만이 편집할 수 있도록 권한을 제어하려 합니다.</p><p>DRF에서 Permission을 손쉽게 제어하는 방식을 소개하고 있으니, 이를 활용하면 됩니다.</p><p><a href="https://www.django-rest-framework.org/api-guide/permissions/">Permissions</a></p><p>우리는 <a href="https://www.django-rest-framework.org/api-guide/permissions/#djangomodelpermissionsoranonreadonly">DjangoModelPermissionsOrAnonReadOnly</a> 라는 permission_class 를 사용합니다.</p><p>views에서 IsAdminUser와 DjangoModelPermissionsOrAnonReadOnly를 활용해 permission_classes를 작성합니다.</p><pre>class CategoryViewSet(viewsets.ModelViewSet):<br>    queryset = Category.objects.all()<br>    serializer_class = CategorySerializer<br>    permission_classes = [DjangoModelPermissionsOrAnonReadOnly]</pre><p>이제 testcode를 다시 실행해보면 테스트가 실패하는것을 볼 수 있습니다.</p><p>category, item의 수정 권한이 admin으로 변경되었기 때문에 해당 api를 이용하기 위해선 admin user로 로그인 해야합니다.</p><p>category api testcode를 예로 들겠습니다.</p><p>아래와 같이 setUp함수에서 권한을 가진 user를 생성하고, 테스트시에 해당 user로 로그인하도록 코드를 수정합니다.</p><pre>def setUp(self) -&gt; None:<br>    self.user = self.make_user(perms=[&#39;reviews.add_category&#39;, &#39;reviews.change_category&#39;, &#39;reviews.delete_category&#39;])<br>    self.category = Category.objects.create(name=&#39;test category&#39;)<br>    self.category2 = Category.objects.create(name=&#39;test category 2&#39;)<br><br>...<br><br>def test_create_category(self):<br>    with self.login(username=self.user.username):<br>        data = {&#39;name&#39;: &#39;test category 3&#39;}<br>        response = self.post(&#39;/reviews/categories/&#39;, data=data)<br>        self.assert_http_201_created(response)<br>        self.assertEqual(response.data[&#39;name&#39;], data[&#39;name&#39;])<br>        self.assertEqual(Category.objects.count(), 3)<br>        self.assertEqual(Category.objects.last().name, data[&#39;name&#39;])</pre><p>다음으로 각 item에 대한 review는 로그인을 한 사람만이 작성할 수 있고, 해당 리뷰를 작성한 사람만이 수정할 수 있도록 제한합니다.</p><p>이번 단계에선 <a href="https://www.django-rest-framework.org/api-guide/permissions/#object-level-permissions">Object level permissions</a> 을 활용합니다.</p><p>permissions.py 파일을 작성합니다.</p><pre>class ReviewAuthorOrReadOnly(permissions.BasePermission):<br>    &quot;&quot;&quot;<br>    인증 시 사용자는 자신의 리뷰만 수정할 수 있습니다.<br>    인증되지 않은 사용자는 읽기 전용입니다.<br>    &quot;&quot;&quot;<br>    def has_permission(self, request, view):<br>        if request.method in permissions.SAFE_METHODS:<br>            return True<br>        return request.user.is_authenticated<br><br>    def has_object_permission(self, request, view, obj):<br>        if request.method in permissions.SAFE_METHODS:<br>            return True<br>        return obj.user == request.user</pre><p>ReviewViewSet에 추가합니다.</p><pre>class ReviewViewSet(viewsets.ModelViewSet):<br>    queryset = Review.objects.all()<br>    serializer_class = ReviewSerializer<br>    permission_classes = [ReviewAuthorOrReadOnly]</pre><p>다시 testcode를 실행해보면 review 부분에서 권한이슈로 테스트가 실패하는걸 확인할 수 있습니다.</p><p>review testcode에서도 로그인 코드를 추가해줍니다.</p><p>지금까지 연결된 model을 한번에 출력하고, permission을 적용해보았습니다.</p><p>다음시간엔 쿼리성능 개선 방법에 대해 알아보도록 하겠습니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dd45cd967a6d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-3-dd45cd967a6d">하루만에 완성하는 Django+DRF 서비스(3)</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[하루만에 완성하는 Django+DRF 서비스(2)]]></title>
            <link>https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-2-81a55c49bfed?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/81a55c49bfed</guid>
            <category><![CDATA[django]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[django-rest-framework]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Thu, 02 Mar 2023 13:57:22 GMT</pubDate>
            <atom:updated>2023-03-05T05:41:49.573Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*8HcuXSeRju0X0lWn.png" /><figcaption>출처: <a href="https://www.django-rest-framework.org/">https://www.django-rest-framework.org/</a></figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>이번 시간엔 testcode를 작성, api 생성에 대해 알아보도록 하겠습니다.</p><p>프로젝트를 진행한 코드는 깃허브에 올라가 있으니 참고하시기 바랍니다.</p><p><a href="https://github.com/Root-kjh/drf_practice">GitHub - Root-kjh/drf_practice</a></p><h4>테스트코드 작성</h4><p>테스트코드는 간단히 e2e테스트코드만 작성하도록 하겠습니다.</p><p>개인적으로는 e2e테스트를 작성하고, 추가적으로 배포환경에서 이슈가 발생했을때 해당 케이스를 테스트코드로 작성 후 해결해나가며 테스트코드를 늘리는 쪽으로 작업합니다.</p><p>(물론 dev, staging phase에서 테스트를 진행하며 나오는 이슈도 테스트코드로 작성합니다.)</p><p>테스트코드를 작성하기 전, python답게 테스트코드를 효율적으로 작성할 수 있도록 도와주는 훌륭한 패키지를 추가해줍니다.</p><p>poetry add django-test-plus</p><p>이제 카테고리 crud(create/read/update/delete)에 대한 테스트코드를 추가해줍시다.</p><pre>from test_plus.test import APITestCase<br><br>from .models import Category<br><br>class CategoryApiTestCase(APITestCase):<br>    def setUp(self) -&gt; None:<br>        self.category = Category.objects.create(name=&#39;test category&#39;)<br>        self.category2 = Category.objects.create(name=&#39;test category 2&#39;)<br><br>    def test_get_categories(self):<br>        response = self.get(&#39;/reviews/categories/&#39;)<br>        self.assert_http_200_ok(response)<br>        self.assertEqual(len(response.data), 2)<br>        self.assertEqual(response.data[0][&#39;name&#39;], self.category.name)<br>        self.assertEqual(response.data[1][&#39;name&#39;], self.category2.name)<br><br>    def test_get_category(self):<br>        response = self.get(f&#39;/reviews/categories/{self.category.id}/&#39;)<br>        self.assert_http_200_ok(response)<br>        self.assertEqual(response.data[&#39;name&#39;], self.category.name)<br><br>    def test_create_category(self):<br>        data = {&#39;name&#39;: &#39;test category 3&#39;}<br>        response = self.post(&#39;/reviews/categories/&#39;, data=data)<br>        self.assert_http_201_created(response)<br>        self.assertEqual(response.data[&#39;name&#39;], data[&#39;name&#39;])<br>        self.assertEqual(Category.objects.count(), 3)<br>        self.assertEqual(Category.objects.last().name, data[&#39;name&#39;])<br><br>    def test_update_category(self):<br>        data = {&#39;name&#39;: &#39;test category 3&#39;}<br>        response = self.put(f&#39;/reviews/categories/{self.category.id}/&#39;, data=data)<br>        self.assert_http_200_ok(response)<br>        self.assertEqual(response.data[&#39;name&#39;], data[&#39;name&#39;])<br>        self.assertTrue(Category.objects.filter(name=data[&#39;name&#39;]).exists())<br>        self.assertFalse(Category.objects.filter(name=self.category.name).exists())<br><br>    def test_delete_category(self):<br>        response = self.delete(f&#39;/reviews/categories/{self.category.id}/&#39;)<br>        self.assert_http_204_no_content(response)<br>        self.assertFalse(Category.objects.filter(name=self.category.name).exists())<br>        self.assertEqual(Category.objects.count(), 1)</pre><p>아주 간단한 테스트코드를 작성했습니다.</p><p>테스트를 실행 python3 manage.py test 하게되면 <strong>당연히</strong> 실패합니다.</p><p>이제 api를 작성해 해당 테스트코드가 동작하도록 만들어주면 됩니다.</p><h4>API 작성</h4><p>drf의 구성 요소는 크게 model, serializer, view, url이 있으며, 필요하다면 filters, permissions등의 추가 요소가 사용됩니다.</p><p>model은 이미 작성했으니 serializer, view, url을 작성해 api를 작성해봅시다.</p><p>serializer는 django orm에서 쿼리의 리턴으로 사용하는 queryset , model instance등을 json등의 형식으로 변경해주고, 반대로 json형식을 model instance 등으로 변환해줍니다.</p><p>ModelSerializer를 상속한 serializer 클래스를 만들고, 사용할 모델을 model 에 넣어주면 해당 모델의 필드를 자동으로 인식해 serializer를 만들어줍니다.</p><pre>from rest_framework import serializers<br>from .models import Review, Category<br><br>class CategorySerializer(serializers.ModelSerializer):<br>    class Meta:<br>        model = Category<br>        fields = &quot;__all__&quot;<br><br></pre><p>이제 해당 serializer를 사용해 view를 만들어봅시다.</p><p>drf 문서를 보면 여러가지 view 종류가 있는데, 마트에서 쇼핑하듯 입맛에 맞는 클래스를 사용하면 됩니다.</p><p>저는 특별히 커스텀할 요소가 없으면 model에 맞게 기본적인 crud api를 만들어주는 ModelViewSet을 사용하도록 하겠습니다.</p><p>ModelView을 상속받고, queryset, serializer_class를 지정해주면 crud api가 생성됩니다.</p><pre>from rest_framework import viewsets<br>from .models import Category<br>from .serializers import CategorySerializer<br><br>class CategoryViewSet(viewsets.ModelViewSet):<br>    queryset = Category.objects.all()<br>    serializer_class = CategorySerializer</pre><p>이제 만든 view를 특정 path에서 사용할 수 있도록 path와 view를 연결해줍니다.</p><p>django에서는 보통 urls.py에서 해당 로직을 처리하게 됩니다.</p><p>방금 만든 viewset을 처리하는 router를 만들고, 이를 urlpatterns에 넣으면 django에서 인식하게 됩니다.</p><pre>from .views import CategoryViewSet<br>from rest_framework import routers<br>from django.urls import path, include<br><br>router = routers.DefaultRouter()<br>router.register(r&#39;categories&#39;, CategoryViewSet)<br><br>urlpatterns = [<br>    path(&#39;&#39;, include(router.urls)),<br>]</pre><p>만든 urls.py를 기본 앱(settings.py가 위치한 바로 그 앱)의 urls에서도 연결시켜줍니다.</p><pre>from django.contrib import admin<br>from django.urls import path, include<br><br>urlpatterns = [<br>    path(&#39;admin/&#39;, admin.site.urls),<br>    path(&#39;reviews/&#39;, include(&#39;reviews.urls&#39;)),<br>]</pre><p>이제 django -&gt; main urls.py -&gt; reviews urls.py로 연결되어 categories api가 생성됩니다.</p><p>테스트코드를 다시 실행시켜보면 모두 성공하는것을 확인할 수 있습니다.</p><p>또, python manage.py runserver를 실행한 뒤 <a href="http://127.0.0.1:8000/reviews/categories/">http://127.0.0.1:8000/reviews/categories/</a>에 접속하면 실제로 api를 테스트해볼 수 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nELCZU9OwUB32euyZcopbw.png" /></figure><h4>Django admin</h4><p>제게 Django의 최대 장점을 두가지 꼽으라면 높은 생산성과 자동 생성되는 어드민 페이지를 대답할 것 같습니다.</p><p>그만큼 Django admin은 매우 간편하게 사용할 수 있고, 사용하기에도 매우 용이합니다.</p><p>얼마나 간편하냐면, 단 1줄의 코드로 category의 어드민을 만들 수 있을 정도입니다.</p><p>이제 그 방법에 대해 알아보도록 하겠습니다.</p><p>python manage.py createsuperuser 명령어를 입력해 admin페이지에서 사용할 superuser를 생성합니다.</p><p><a href="http://127.0.0.1:8000/admin/">http://127.0.0.1:8000/admin/</a>에 접속해 아까 생성한 계정정보를 입력하면</p><p>admin page를 확인할 수 있습니다.</p><p>지금은 아무 코드를 작성하지 않아 user, group만 관리할 수 있는 admin page입니다.</p><p>다시 코드로 돌아와, reviews/admin.py에 category, item, review model을 등록해줍시다.</p><pre>from django.contrib import admin<br><br>from .models import Category, Item, Review<br><br>admin.site.register(Category)<br>admin.site.register(Item)<br>admin.site.register(Review)</pre><p>(총 7줄이지만, category 부분만 보면 1줄… 1줄입니다.)</p><p>다시 어드민 페이지에 접속하면 다음과 같이 각 모델에 대한 탭이 생성된걸 확인할 수 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*saN6gTMDrHzGbTrUGpo-_Q.png" /></figure><p>category의 crud를 작성했으니, 이를 조금 응용해서 Item, review의 crud를 작성해주시면됩니다.(귀찮아서 스킵..)</p><p>다음 시간엔 category, Item, review의 모델 연결관계를 api에서 표현하는 방법, permission을 적용하는 방법(일반유저가 items를 삭제/수정하지 못하도록 관리)에 대해 알아보도록 하겠습니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=81a55c49bfed" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-2-81a55c49bfed">하루만에 완성하는 Django+DRF 서비스(2)</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[하루만에 완성하는 Django+DRF 서비스(1)]]></title>
            <link>https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-1-e3be69177b02?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/e3be69177b02</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[django-rest-framework]]></category>
            <category><![CDATA[django]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Thu, 02 Mar 2023 04:40:18 GMT</pubDate>
            <atom:updated>2023-03-05T05:38:37.979Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*GobER4_Uaq8VGilO.png" /><figcaption>출처: <a href="https://www.django-rest-framework.org/">https://www.django-rest-framework.org/</a></figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>이번 시리즈에서는 django+drf 조합을 이용해 서비스를 빠르게 만드는 법에 대해 알아보도록 하겠습니다.</p><p>이번 시간엔 간단히 프로젝트의 생성, 모델의 생성만 진행할 예정입니다.</p><p>프로젝트를 진행한 코드는 깃허브에 올라가 있으니 참고하시기 바랍니다.</p><p><a href="https://github.com/Root-kjh/drf_practice">GitHub - Root-kjh/drf_practice</a></p><h4>주제 선정</h4><p>어쩌면 이번 시리즈에서 가장 어려울 수도 있는(?) 부분입니다.</p><p>저는 이전부터 만들고 싶었던 모든것을 리뷰해 정보의 비대칭성을 개선 하는 서비스를 주제로 잡았습니다.</p><p>서비스의 이름은 직관적으로 review_all 이라고 하겠습니다.</p><p>글을 읽으며 같은 서비스를 만들어도 좋고, 다른 서비스를 만들어가도 좋습니다.</p><h4>파이썬 설치</h4><p>자연어 처리를 하기 위해선 파이썬을 설치해야 합니다.</p><p>아래 사이트에 접속해 파이썬을 설치해줍니다.</p><p><a href="https://www.python.org/">Welcome to Python.org</a></p><h4>Django 프로젝트 생성</h4><p>django project를 생성하기 위해 python에 django를 설치해줍니다.</p><pre>$ python -m pip install Django</pre><p>django project를 생성해줍니다.</p><pre>$ django-admin startproject django_practice</pre><h4>패키지 버전 관리(Poetry)</h4><p>파이썬 패키지 버전 관리는 poetry를 사용해서 진행합니다.</p><p>아래 문서를 보고 poetry install을 진행합니다.</p><p><a href="https://python-poetry.org/docs/">Introduction</a></p><p>poetry install이 완료되었으면 poetry init 명령어를 실행해 pyproject.toml 파일을 생해줍니다.</p><p>새로 생성된 pyproject.toml에는 django package가 포함되어있지 않으니 django를 추가합니다.</p><pre>$ poetry add django</pre><p>django+drf 조합을 사용할 예정이기 때문에 drf도 미리 설치해줍니다.</p><p>poetry add djangorestframework</p><p><a href="https://www.django-rest-framework.org/">Home - Django REST framework</a></p><p>settings.py -&gt; INSTALLED_APPS에 rest_framework를 추가해 줍니다.</p><pre>INSTALLED_APPS = [<br>    &#39;django.contrib.admin&#39;,<br>    &#39;django.contrib.auth&#39;,<br>    &#39;django.contrib.contenttypes&#39;,<br>    &#39;django.contrib.sessions&#39;,<br>    &#39;django.contrib.messages&#39;,<br>    &#39;django.contrib.staticfiles&#39;,<br>    &#39;rest_framework&#39;,<br>]</pre><p>poetry의 python virtualenv에 들어가려면 다음 명령어를 입력하면 됩니다. poetry shell</p><h4>Reviews 앱 생성</h4><p>review_all은 크게 3가지 모델로 구성할 예정입니다.</p><p>모델: 일반적으로 database의 table과 연결되며, 데이터의 구조와 동작을 표현합니다.</p><ol><li>리뷰할 item들을 그룹화한 category</li><li>리뷰할 item들을 저장하는 item</li><li>리뷰를 저장하는 review</li></ol><p>먼저 reviews앱을 생성해줍니다.</p><p>python3 manage.py startapp reviews</p><p>settings.py -&gt; INSTALLED_APPS에 방금 생성한 review를 추가해 줍니다.</p><p>이제 reviews/models.py에 각 모델을 생성합니다.</p><pre>from django.db import models<br>from django.contrib.auth.models import User<br><br>class Category(models.Model):<br>    name = models.CharField(max_length=100)<br><br>    def __str__(self):<br>        return self.name<br><br><br>class Item(models.Model):<br>    name = models.CharField(max_length=100)<br>    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name=&#39;items&#39;)<br>    description = models.TextField(null=True, blank=True)<br>    image = models.ImageField(upload_to=&#39;images/&#39;, null=True)<br>    date = models.DateTimeField(auto_now_add=True)<br><br>    def __str__(self):<br>        return self.name<br><br><br>class Review(models.Model):<br>    item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name=&#39;reviews&#39;)<br>    context = models.TextField()<br>    created = models.DateTimeField(auto_now_add=True)<br>    updated = models.DateTimeField(auto_now=True)<br>    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name=&#39;reviews&#39;)<br><br>    def __str__(self):<br>        return self.name</pre><p>이렇게 django+drf 프로젝트의 생성, 모델 작성까지 빠르게 진행했습니다.</p><p>다음 시간엔 testcode를 작성하고 api를 생성해보도록 하겠습니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e3be69177b02" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/%ED%95%98%EB%A3%A8%EB%A7%8C%EC%97%90-%EC%99%84%EC%84%B1%ED%95%98%EB%8A%94-django-drf-%EC%84%9C%EB%B9%84%EC%8A%A4-1-e3be69177b02">하루만에 완성하는 Django+DRF 서비스(1)</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[처음부터 시작하는 Django 데이터 적재(5) — END]]></title>
            <link>https://medium.com/humanscape-tech/%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-django-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%81%EC%9E%AC-5-end-bf43b32a60f4?source=rss----53c6a2785a10---4</link>
            <guid isPermaLink="false">https://medium.com/p/bf43b32a60f4</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[django]]></category>
            <dc:creator><![CDATA[June]]></dc:creator>
            <pubDate>Thu, 02 Mar 2023 04:40:05 GMT</pubDate>
            <atom:updated>2023-03-02T04:40:05.817Z</atom:updated>
            <content:encoded><![CDATA[<h3>처음부터 시작하는 Django 데이터 적재(5) — END</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/0*iN5iZTK2Ybh6lnWl.jpg" /><figcaption>출처: <a href="https://www.atmmarketplace.com/articles/its-time-to-rethink-how-we-manage-the-cost-of-cash/">https://www.atmmarketplace.com/articles/its-time-to-rethink-how-we-manage-the-cost-of-cash/</a></figcaption></figure><p>안녕하세요. 휴먼스케이프 june입니다.</p><p>이번시간엔 임상연구 업데이트 시 변경 부분을 감지하고, 해당 부분만을 업데이트하는 방법에 대해 알아보겠습니다.</p><p>프로젝트를 진행한 코드는 깃허브에 올라가 있으니 참고하시기 바랍니다.</p><p><a href="https://github.com/Root-kjh/django_practice">GitHub - Root-kjh/django_practice</a></p><p>실제 서비스하는 임상연구는 유료 번역api를 사용해 번역의 퀄리티를 높이기 때문에 관련 비용이 굉장히 많이 나오게 되고, 번역 태스크에서의 병목현상이 많이 일어나기 때문에 이 부분을 최적화하는게 굉장히 중요합니다.</p><p>진행 과정은 다음과 같습니다.</p><ol><li><strong>convert태스크에서 변경된 부분을 확인합니다.</strong></li></ol><p>study model의 필드는 &lt;updated&gt;와 같은 값으로 채워 변경됨을 알리고, 연결된 모델은 인스턴스를 특정할 수 없는 고유 키가 없기 때문에(clinicaltrials에서 id값을 제공하지 않음) 모든 값이 같을 경우에만 유지, 하나라도 다를 경우 생성/제거합니다.</p><p><strong>2. translate태스크에서 변경된 부분(marked study model field, created related model instance)에 대한 번역을 새로 생성합니다.</strong></p><p>이 때 제거된 related model instance의 번역본의 경우, 이미 on_delete 규칙에 따라 convert 태스크에서 같이 제거되었기 때문에 신경쓰지 않아도 됩니다.</p><p>먼저, 간단하게 구현할 수 있는 study model의 필드 변경 감지 코드를 작성해보겠습니다.</p><p>번역필드만 확인하면 되기 때문에 TRANSLATE_FIELDS로 지정해두고 사용하도록 하겠습니다.</p><pre>def mark_updated_sutdy_field(study, convert_data):<br>    TRANSLATE_FIELDS = [&#39;title&#39;, &#39;overall_status&#39;, &#39;phase&#39;]<br><br>    translated_studies = Study.objects.filter(translate_from_study=study)<br>    for field in TRANSLATE_FIELDS:<br>        if getattr(study, field) != convert_data[field]:<br>            for translated_study in translated_studies:<br>                setattr(translated_study, field, &quot;&lt;updated&gt;&quot;)<br>    for translated_study in translated_studies:<br>        translated_study.save()</pre><p>이제 related model instance에서 기존 instance를 재활용하도록 수정해줍니다.</p><pre>def convert_conditions(condition_module):<br>    conditions = []<br>    condition_instances = list(Condition.objects.filter(name__in=condition_module))<br>    for condition in condition_module:<br>        condition_id = None<br>        for condition_instance in condition_instances:<br>            if condition_instance.name == condition:<br>                condition_id = condition_instance.id<br>                break<br>        conditions.append({<br>            &#39;id&#39;:  condition_id,<br>            &#39;name&#39;: condition,<br>            &#39;locale&#39;: &#39;en&#39;,<br>        })<br>    return conditions<br><br>def convert_interventions(study, intervention_module):<br>    interventions = []<br>    intervention_instances = list(Intervention.objects.filter(study=study))<br>    for intervention in intervention_module:<br>        intervention_id = None<br>        for intervention_instance in intervention_instances:<br>            if (<br>                intervention_instance.name == intervention.get(&#39;InterventionName&#39;, None) and \<br>                intervention_instance.intervention_type == intervention.get(&#39;InterventionType&#39;, None) and \<br>                intervention_instance.description == intervention.get(&#39;InterventionDescription&#39;, None)<br>            ):<br>                intervention_id = intervention_instance.pk<br>                intervention_instances.remove(intervention_instance)<br>                break<br>        interventions.append({<br>            &#39;id&#39;: intervention_id,<br>            &#39;intervention_type&#39;: intervention.get(&#39;InterventionType&#39;, None),<br>            &#39;name&#39;: intervention.get(&#39;InterventionName&#39;, None),<br>            &#39;description&#39;: intervention.get(&#39;InterventionDescription&#39;, None),<br>            &#39;locale&#39;: &#39;en&#39;,<br>        })<br>    return interventions<br><br>def convert_eligibilities(study, eligibility_module):<br>    if eligibility_module is None:<br>        return []<br>    eligibility_instances = list(Eligibility.objects.filter(study=study))<br>    eligibility_id = None<br>    for eligibility_instance in eligibility_instances:<br>        if (<br>            eligibility_instance.gender == eligibility_module.get(&#39;Gender&#39;, None) and \<br>            eligibility_instance.minimum_age == eligibility_module.get(&#39;MinimumAge&#39;, None) and \<br>            eligibility_instance.maximum_age == eligibility_module.get(&#39;MaximumAge&#39;, None) and \<br>            eligibility_instance.healthy_volunteers == eligibility_module.get(&#39;HealthyVolunteers&#39;, None) and \<br>            eligibility_instance.criteria == eligibility_module.get(&#39;EligibilityCriteria&#39;, None)<br>        ):<br>            eligibility_id = eligibility_instance.pk<br>            eligibility_instances.remove(eligibility_instance)<br>            break<br>    return [{<br>        &#39;id&#39;: eligibility_id,<br>        &#39;gender&#39;: eligibility_module.get(&#39;Gender&#39;, None),<br>        &#39;minimum_age&#39;: eligibility_module.get(&#39;MinimumAge&#39;, None),<br>        &#39;maximum_age&#39;: eligibility_module.get(&#39;MaximumAge&#39;, None),<br>        &#39;healthy_volunteers&#39;: eligibility_module.get(&#39;HealthyVolunteers&#39;, None),<br>        &#39;criteria&#39;: eligibility_module.get(&#39;EligibilityCriteria&#39;, None),<br>        &#39;locale&#39;: &#39;en&#39;,<br>    }]</pre><p>convert 태스크를 실행하면 변경사항이 없는 부분은 재활용하고, 변경된 부분만 인식함을 확인할 수 있습니다.</p><p>이제 translate 태스크를 수정합니다.</p><ol><li>trnaslated_studies를 제거하는 코드 삭제</li><li>related model instance중 translated instance가 없는 부분만 번역하도록 수정</li><li>&lt;updated&gt;마크 되어있는 study model컬럼만 다시 번역하도록 수정</li></ol><p>1번은 다들 금방 할 수 있기 때문에 2, 3번에 대해서만 설명하도록 하겠습니다.</p><ul><li>translated_study 인스턴스를 업데이트 하는 방식으로 진행하기 때문에, translate 태스크 진행 시 translated_study 인스턴스를 찾아 translate_study함수에 인자로 넣어줍니다.</li><li>translate_study함수는 인자로 받은 translated_study(default: None)에서 &lt;update&gt;마크가 되어있는 부분만 새로 번역하고, 나머지는 기존 데이터를 그대로 사용합니다.</li><li>intervention, condition 등의 연결된 모델들은 translated object가 있는지 확인하고, 없으면 생성해 사용합니다.</li></ul><p>코드를 보면 다음과 같습니다.</p><pre><br>def translate_study(study, translated_study=None):<br>    &quot;&quot;&quot;<br>    임상 연구 데이터를 번역하는 메소드<br>    &quot;&quot;&quot;<br>    translated_text_dict = {translate_field:None for translate_field in TRANSLATE_FIELDS}<br>    if translated_study is not None:<br>        for translate_field in TRANSLATE_FIELDS:<br>            if getattr(translated_study, translate_field) != &quot;&lt;updated&gt;&quot;:<br>                translated_text_dict[translate_field] = getattr(translated_study, translate_field)<br>    for translate_field in TRANSLATE_FIELDS:<br>        if translated_text_dict[translate_field] is None and getattr(study, translate_field) is not None:<br>            translated_text_dict[translate_field] = translate(getattr(study, translate_field))<br><br>    conditions = []<br>    for condition in study.conditions.all().prefetch_related(Prefetch(&quot;translated_conditions&quot;, queryset=Condition.objects.filter(locale=&#39;ko&#39;))):<br>        translated_condition = condition.translated_conditions.first()<br>        if translated_condition is not None:<br>            conditions.append({<br>                &#39;id&#39;: translated_condition.pk,<br>                &#39;name&#39;: translated_condition.name,<br>                &#39;locale&#39;: &#39;ko&#39;,<br>                &#39;translate_from_condition&#39;: condition.pk<br>            })<br>        else:<br>            conditions.append({<br>                &#39;name&#39;: translate(condition.name),<br>                &#39;locale&#39;: &#39;ko&#39;,<br>                &#39;translate_from_condition&#39;: condition.pk<br>            })<br><br>    interventions = []<br>    for intervention in Intervention.objects.filter(study=study).prefetch_related(Prefetch(&quot;translated_interventions&quot;, queryset=Intervention.objects.filter(locale=&#39;ko&#39;))):<br>        translated_intervention = intervention.translated_interventions.first()<br>        if translated_intervention is not None:<br>            interventions.append({<br>                &#39;id&#39;: translated_intervention.pk,<br>                &#39;intervention_type&#39;: translated_intervention.intervention_type,<br>                &#39;name&#39;: translated_intervention.name,<br>                &#39;description&#39;: translated_intervention.description,<br>                &#39;locale&#39;: &#39;ko&#39;,<br>                &#39;translate_from_intervention&#39;: intervention.pk<br>            })<br>        else:<br>            interventions.append({<br>                &#39;intervention_type&#39;: translate(intervention.intervention_type),<br>                &#39;name&#39;: translate(intervention.name),<br>                &#39;description&#39;: translate(intervention.description),<br>                &#39;locale&#39;: &#39;ko&#39;,<br>                &#39;translate_from_intervention&#39;: intervention.pk<br>            })<br><br>    eligibilities = []<br>    for eligibility in Eligibility.objects.filter(study=study).prefetch_related(Prefetch(&quot;translated_eligibilities&quot;, queryset=Eligibility.objects.filter(locale=&#39;ko&#39;))):<br>        translated_eligibility = eligibility.translated_eligibilities.first()<br>        if translated_eligibility is not None:<br>            eligibilities.append({<br>                &#39;id&#39;: translated_eligibility.pk,<br>                &#39;gender&#39;: translated_eligibility.gender,<br>                &#39;minimum_age&#39;: translated_eligibility.minimum_age,<br>                &#39;maximum_age&#39;: translated_eligibility.maximum_age,<br>                &#39;healthy_volunteers&#39;: translated_eligibility.healthy_volunteers,<br>                &#39;criteria&#39;: translated_eligibility.criteria,<br>                &#39;locale&#39;: &#39;ko&#39;,<br>                &#39;translate_from_eligibility&#39;: eligibility.pk<br>            })<br>        else:<br>            eligibilities.append({<br>                &#39;gender&#39;: translate(eligibility.gender),<br>                &#39;minimum_age&#39;: eligibility.minimum_age,<br>                &#39;maximum_age&#39;: eligibility.maximum_age,<br>                &#39;healthy_volunteers&#39;: translate(eligibility.healthy_volunteers),<br>                &#39;criteria&#39;: translate(eligibility.criteria),<br>                &#39;locale&#39;: &#39;ko&#39;,<br>                &#39;translate_from_eligibility&#39;: eligibility.pk<br>            })<br><br>    return {<br>        &#39;nct_id&#39;: study.nct_id,<br>        &#39;title&#39;: translated_text_dict[&#39;title&#39;],<br>        &#39;results_first_submitted_date&#39;: study.results_first_submitted_date,<br>        &#39;last_update_submitted_date&#39;: study.last_update_submitted_date,<br>        &#39;start_date&#39;: study.start_date,<br>        &#39;completion_date&#39;: study.completion_date,<br>        &#39;overall_status&#39;: translated_text_dict[&#39;overall_status&#39;],<br>        &#39;phase&#39;: translated_text_dict[&#39;phase&#39;],<br>        &#39;enrollment&#39;: study.enrollment,<br>        &#39;interventions&#39;: interventions,<br>        &#39;conditions&#39;: conditions,<br>        &#39;eligibilities&#39;: eligibilities,<br>        &#39;locale&#39;: &#39;ko&#39;,<br>        &#39;translate_from_study&#39;: study.pk,<br>        &#39;control_status_type&#39;: ControlStatusType.COMPLETED,<br>    }</pre><p>이렇게 임상연구 적재, 번역, 간단한 최적화까지 빠르게 진행해보았습니다.</p><p>여기서 조금만 더 응용한다면 다국어 번역, 여러 데이터 소스에서 임상연구 데이터 적재, 더 좋은 최적화 등 개선할 부분이 많으니 시도해보셔도 좋을듯 합니다.</p><p>Django 데이터 적재 시리즈는 여기서 마무리 짓고, 이후 하루만에 완성하는 Django+DRF 서비스시리즈로 찾아오도록 하겠습니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bf43b32a60f4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/humanscape-tech/%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-django-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%81%EC%9E%AC-5-end-bf43b32a60f4">처음부터 시작하는 Django 데이터 적재(5) — END</a> was originally published in <a href="https://medium.com/humanscape-tech">None</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>