루나의 TIL 기술 블로그

파이썬 - 진수 변환 함수

|

진수를 변환하는게 코딩테스트에서 나와서 진수변환함수를 작성하고 인터넷에 있는 풀이들을 살펴보았다.

1번은 코딩테스트를 위해서 10진수를 N진수로, N진수를 10진수로 변환하라는 조건을 위해서 만든 클래스고
2번은 유투브에서 찾은 10진수를 N진수로 바꾸는 함수이다.
3번은 검색하다가 나온 N진수에서 N진수로 만드는 함수이다.
4번은 1번의 클래스 안의 함수를 사용해서 내가 만든 N진수에서 N진수로 만드는 함수이다.

1번 : 10진수를 N진수로, N진수를 10진수로 변환하는 클래스

class Transformer(object):
    #10진수로 변환할 경우 숫자들의 리스트 
    #This Transformaer class takes list of numbers that are being used in certain digit(진법) and hand it over to initializing method.
    decimal_digits = "0123456789"
    #For example, decimal digit takes 0 to 9, Binary digit takes 0 and 1.
    #digits는 n진수의 숫자리스트
    def __init__(self, digits):
        self.digits = digits

    #바꾸고자하는 십진수 수가 i에 들어가고 i값과 십진수 숫자리스트, 바꾸고자하는 진수의 숫자리스트를 반환한다.
    #This question is about converting digit from and to decimal digits so we make two methods. 
    def from_decimal(self, i):
        return self._convert(i, self.decimal_digits, self.digits)
    
    #바꾸고자하는 n진수 수가 s에 들어가고 s값과 바꾸고자하는 진수의 숫자리스트, 십진수 숫자리스트를 반환한다.
    #Order of decimal digits and digits matters since we are going to use convert method for both actions.
    def to_decimal(self, s):
        return int(self._convert(s, self.digits, self.decimal_digits))
    
    #convert method takes number that we'd likt to change, which digit we are going to change from and to.
    def _convert(self, number, fromdigits, todigits):
        # 입력받은 숫자, 바꾸기 전 진수의 숫자리스트, 바꾼 뒤의 진수의 숫자리스트를 받는다. 예(555,BASE10,BASE2)
        # 숫자리스트의 길이에 따라 어떤 진수로 바뀔지 정해진다.
        # length of these parameters decides which digits we're going to convert.
        fromdigits_len, todigits_len = len(fromdigits), len(todigits)
        # 해당 진수의 숫자리스트의 길이를 저장한다. 예(BASE10은 10진수, '0123456789' 이므로 10개)
        number = str(number)
        number_len = len(number)
        # 입력받은 숫자의 길이도 저장한다

        tmp_dec=0
        # 숫자의 갯수만큼 지수를 곱해서 더하여 십진수로 변환한 값을 저장한다. 
        # 예: 100(2진수)이면 (2의2승 * 1) + (2의1승 * 0) + (2의0승 * 0)
        # Here we change all number into decimal digits.
        for idx, n in enumerate(number):
            # 예를 들어 십진수 1234이면 idx는 0,1,2,3 n은 1,2,3,4
            print('idx :', idx)
            print('n : ', n)
            # By using base, exponent and digit of the number.
            tmp = fromdigits_len ** (number_len-idx-1) * fromdigits.index(n)
            #fromdigits_len(밑)이 십진수일 때 10이고 number_len-idx-1(지수)이 1234일때 각 3210으로 10의 지수
            #fromdigits.index(n)은 1,2,3,4로 '012..89'숫자리스트에서 n번째 숫자
            print('number_len-idx-1 :', number_len-idx-1)
            print('fromdigits.index(n):', fromdigits.index(n))
            #tmp에 각 자릿수를 십진수로 계산한 값을 더한다
            tmp_dec += tmp
        
        result=''
        while tmp_dec > 0:
            # 몫과 나머지를 구하는 파이썬의 내장함수 divmod사용
            # And from here we change it back to N base by dividing and saving remainders.
            tmp_dec, mod = divmod(tmp_dec, todigits_len)
            # 10진수로된 tmp_dec를 바꾸자하는 진수의 숫자리스트길이로 나누어서
            result += str(todigits[mod])
            # 나온 나머지들을 반대로 정렬 
        return result[::-1]

BASE2 = Transformer("01")
BASE10 = Transformer("0123456789")
BASE16 = Transformer("0123456789ABCDEF")
BASE20 = Transformer("0123456789abcdefghij")
BASE62 = Transformer("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz")

print(BASE20.from_decimal('1234'))
# print(BASE2.to_decimal('1000101011'))

2번 : 10진수를 N진수로 바꾸는 함수

출처 - https://www.youtube.com/watch?v=s3mxIcr7fOQ


from math import log

    #10진수에서 원하는 진수로 바꾸기(바꿀 숫자, 바꿀 숫자의 진수)
def convertFromBase10(num, base):
    #변경할 숫자의 리스트
    numToChar = {i : "0123456789ABCDEF"[i] for i in range(16)}
    #가장 큰 지수를 power에 저장 
    power = int(log(num, base))
    converted = ""
    #power숫자부터 0까지 거꾸로 돌리기
    for pow in range(power, -1, -1):
        # 진수의 가장 큰 지수(예 7의3승)로 주어진 숫자를 나누어서 몫을 converted에 더하고  
        converted += numToChar[num // (base**pow)]
        # 나머지를 다시 숫자의 자리에 넣음
        num %= base**pow
    return converted

print(convertFromBase10(429,7))
#1152

3번 : N진수에서 N진수로 만드는 함수

출처 - https://code.activestate.com/recipes/111286/


BASE2 = "01"
BASE10 = "0123456789"
BASE16 = "0123456789ABCDEF"
BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"

def baseconvert(number,fromdigits,todigits):
    #converts a "number" between two bases of arbitrary digits

    # The input number is assumed to be a string of digits from the
    # fromdigits string (which is in order of smallest to largest
    # digit). The return value is a string of elements from todigits
    # (ordered in the same way). The input and output bases are
    # determined from the lengths of the digit strings. Negative 
    # signs are passed through.

    # decimal to binary
    # >>> baseconvert(555,BASE10,BASE2)
    # '1000101011'

    # binary to decimal
    # >>> baseconvert('1000101011',BASE2,BASE10)
    # '555'

    # integer interpreted as binary and converted to decimal (!)
    # >>> baseconvert(1000101011,BASE2,BASE10)
    # '555'

    # base10 to base4
    # >>> baseconvert(99,BASE10,"0123")
    # '1203'

    # base4 to base5 (with alphabetic digits)
    # >>> baseconvert(1203,"0123","abcde")
    # 'dee'

    # base5, alpha digits back to base 10
    # >>> baseconvert('dee',"abcde",BASE10)
    # '99'

    # decimal to a base that uses A-Z0-9a-z for its digits
    # >>> baseconvert(257938572394L,BASE10,BASE62)
    # 'E78Lxik'

    # ..convert back
    # >>> baseconvert('E78Lxik',BASE62,BASE10)
    # '257938572394'

    # binary to a base with words for digits (the function cannot convert this back)
    # >>> baseconvert('1101',BASE2,('Zero','One'))
    # 'OneOneZeroOne'

    # make an integer out of the number
    x=long(0)
    for digit in str(number):
       x = x*len(fromdigits) + fromdigits.index(digit)
       
    # create the result in base 'len(todigits)'
    res=""
    while x>0:
        digit = x % len(todigits)
        res = todigits[digit] + res
        x /= len(todigits)
    return res

4번 : 1번을 이용한 N진수에서 N진수로 만드는 함수2

def nnconverter(number, fromdigits, todigits):
    fromdigits_len, todigits_len = len(fromdigits), len(todigits)
    number = str(number)
    number_len = len(number)

    tmp_dec=0
    for idx, n in enumerate(number):
        tmp = fromdigits_len ** (number_len-idx-1) * fromdigits.index(n)
        tmp_dec += tmp
    
    result=''
    while tmp_dec > 0:
        tmp_dec, mod = divmod(tmp_dec, todigits_len)
        result += str(todigits[mod])
    return result[::-1]

BASE2 = "01"
BASE10 = "0123456789"
BASE16 = "0123456789ABCDEF"
BASE20 = "0123456789abcdefghij"
BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"

print(nnconverter('31e', BASE20, BASE2))

원티드 x 위코드 프리온보딩 과제7 카닥(타이어정보 검색)

|

과제 설명 요약

  • 구현 기간 : 21.11.22(18시) ~ 21.11.28 (22시)
  • 자세한 내용은 Github 참고

과제 설명

1. 배경 및 공통 요구사항

  • 데이터베이스 환경은 별도로 제공하지 않습니다. RDB중 원하는 방식을 선택하면 되며, sqlite3 같은 별도의 설치없이 이용 가능한 in-memory DB도 좋으며, 가능하다면 Docker로 준비하셔도 됩니다.
  • 단, 결과 제출 시 README.md 파일에 실행 방법을 완벽히 서술하여 DB를 포함하여 전체적인 서버를 구동하는데 문제없도록 해야합니다.
  • 데이터베이스 관련처리는 raw query가 아닌 ORM을 이용하여 구현합니다.
  • Response Codes API를 성공적으로 호출할 경우 200번 코드를 반환하고, 그 외의 경우에는 아래의 코드로 반환합니다.

Copy of Code


2. 사용자 생성 API

🎁 요구사항

  • ID/Password로 사용자를 생성하는 API.
  • 인증 토큰을 발급하고 이후의 API는 인증된 사용자만 호출할 수 있다.
/* Request Body 예제 */

 { "id": "candycandy", "password": "ASdfdsf3232@" }

3. 사용자가 소유한 타이어 정보를 저장하는 API

🎁 요구사항

  • 자동차 차종 ID(trimID)를 이용하여 사용자가 소유한 자동차 정보를 저장한다.
  • 한 번에 최대 5명까지의 사용자에 대한 요청을 받을 수 있도록 해야한다. 즉 사용자 정보와 trimId 5쌍을 요청데이터로 하여금 API를 호출할 수 있다는 의미이다.
/* Request Body 예제 */
[
  {
    "id": "candycandy",
    "trimId": 5000
  },
  {
    "id": "mylovewolkswagen",
    "trimId": 9000
  },
  {
    "id": "bmwwow",
    "trimId": 11000
  },
  {
    "id": "dreamcar",
    "trimId": 15000
  }
]

🔍 상세구현 가이드


4. 사용자가 소유한 타이어 정보 조회 API

🎁 요구사항

  • 사용자 ID를 통해서 2번 API에서 저장한 타이어 정보를 조회할 수 있어야 한다.

모델링

모델링

사용한 기술 설명

이번 과제는 팀이 아닌 개인과제였어서 초기세팅부터 배포까지 혼자 진행하게되었다.
저번 과제때 했던 drf를 이용한 회원가입/로그인에 태우님이 작성하셨던 simpleJWT를 사용해서 refresh token/ access token을 발급하도록 했다.

실력이 부쳐서 4개 중 2번과제까지만 진행할 수 있었다.
함께 위워크에서 작업했던 현묵님의 도움을 받아서 해당 JSON을 디비에 업로드하였다.

위코드에서 제공하는 비디오 튜토리얼을 따라서 AWS EC2로 배포를 했는데 생각보다 어렵지 않았다.

EC2 배포 명령어

cd coding/aws-pem
ssh -i wantedxcardoc.pem ubuntu@3.38.150.162  
#public ip주소

wget https://repo.anaconda.com/miniconda/Miniconda3-py37_4.10.3-Linux-x86_64.sh

chmod +x Miniconda3-py37_4.10.3-Linux-x86_64.sh

./Miniconda3-py37_4.10.3-Linux-x86_64.sh

source .bashrc

#껏다 켜고, 가상환경 생성
conda create -n cardoc python=3.8
conda activate cardoc

sudo apt-get update
sudo apt-get upgrade

#gcc 설치, mysql 설치
sudo apt-get install gcc
sudo apt-get install libmysqlclient-dev

git clone 레포 주소
pip install -r requirements.txt

API

모델링

데이터셋

카닥에서 제공한 데이터는 이런 식으로 생겼었다. DB

내가 작성한 코드 / 기억에 남는 코드

#현묵님이 작성하신 디비 업로더 파일
import urllib.request
import json
import django
import os

from django.db import transaction

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cardoc.settings')
django.setup()

from tires.models import Tire

def DataUploader():
    
    trimid = ["5000", "9000", "11000", "15000"]
    
    for i in trimid:
        url = "https://dev.mycar.cardoc.co.kr/v1/trim/" + i

        response    = urllib.request.urlopen(url)
        json_str    = response.read().decode("utf-8")
        json_object = json.loads(json_str)
        data        = json_object
        frontTire   = data['spec']['driving']['frontTire']['value']
        rearTire    = data['spec']['driving']['rearTire']['value']

        Tire.objects.create(
            trimid          = int(i),
            f_section_width = int(frontTire.split('/')[0]),
            f_aspect_ratio  = int(frontTire.split('/')[1].split('R')[0]),
            f_rim_diameter  = int(frontTire.split('/')[1].split('R')[1]),
            r_section_width = int(rearTire.split('/')[0]),
            r_aspect_ratio  = int(rearTire.split('/')[1].split('R')[0]),
            r_rim_diameter  = int(rearTire.split('/')[1].split('R')[1]),
        )

DataUploader()

프로젝트 후기

혼자 진행하면서 모르겠는 부분이 너무 많아서 팀원분들의 소중함을 느꼈고, 팀장님의 코드를 보고 작성해보려다가 내 실력보다 너무 어려운 코드를 소화하려고 하는 것은 사상누각을 짓는 일이구나 단계별로 차근차근 소화해서 코드가 한 줄 한 줄 어떤 역할을 하는지 정확히 알고 사용해야겠다고 생각했다. 과제 4개 중 2개밖에 하지 못해서 아쉬움이 남는다.

온보딩 과정이 너무 어려워서 중간에 포기하고 싶은 마음도 있었고 한 달동안 많이 배우거나 팀에 기여하지 못한 것 같아서 안타까웠지만 이렇게 한 달이 흐른 뒤에 회고를 해보니 생각보다 많은 코드를 쳐봤고 배웠다는 생각이 든다. 회사에 한 달 일찍 입사하는 것보다 이 과정에 참여한 것이 좋은 결정이었다는 생각이 들고 한 달간 7개의 프로젝트를 진행하면서 힘들었던 만큼 많이 성장했다고 느낀다.

원티드 x 위코드 프리온보딩 과제6 디어(전동킥보드대여)

|

과제 설명 요약

  • 구현 기간 : 21.11.18(18시) ~ 21.11.21 (22시)
  • 자세한 내용은 Github 참고

과제 설명

디어는 사용자의 요금을 계산하기 위해 다양한 상황을 고려합니다.

  • 우선 지역별로 다양한 요금제를 적용하고 있습니다. 예를 들어 건대에서 이용하는 유저는 기본요금 790원에 분당요금 150원, 여수에서 이용하는 유저는 기본요금 300원에 분당요금 70원으로 적용됩니다.
  • 할인 조건도 있습니다. 사용자가 파킹존에서 반납하는 경우 요금의 30%를 할인해주며, 사용자가 마지막 이용으로부터 30분 이내에 다시 이용하면 기본요금을 면제해줍니다.
  • 벌금 조건도 있습니다. 사용자가 지역 바깥에 반납한 경우 얼마나 멀리 떨어져있는지 거리에 비례하는 벌금을 부과하며, 반납 금지로 지정된 구역에 반납하면 6,000원의 벌금을 요금에 추과로 부과합니다.
  • 예외도 있는데, 킥보드가 고장나서 정상적인 이용을 못하는 경우의 유저들을 배려하여 1분 이내의 이용에는 요금을 청구하지 않고 있습니다.

최근에 다양한 할인과 벌금을 사용하여 지자체와 협력하는 경우가 점점 많아지고 있어 요금제에 새로운 할인/벌금 조건을 추가하는 일을 쉽게 만드려고 합니다. 어떻게 하면 앞으로 발생할 수 있는 다양한 할인과 벌금 조건을 기존의 요금제에 쉽게 추가할 수 있는 소프트웨어를 만들 수 있을까요?

우선은 사용자의 이용에 관한 정보를 알려주면 현재의 요금 정책에 따라 요금을 계산해주는 API를 만들어주세요. 그 다음은, 기능을 유지한 채로 새로운 할인이나 벌금 조건이 쉽게 추가될 수 있게 코드를 개선하여 최종 코드를 만들어주세요.

다음과 같은 정보들이 도움이 될 것 같아요.


  • 요금제가 사용자 입장에서 합리적이고 이해가 쉬운 요금제라면 좋을 것 같아요.
  • 앞으로도 할인과 벌금 조건은 새로운 조건이 굉장히 많이 추가되거나 변경될 것 같아요.
  • 가장 최근의 할인/벌금 조건의 변경은 ‘특정 킥보드는 파킹존에 반납하면 무조건 무료’ 였습니다.

이용에는 다음과 같은 정보들이 있습니다.


use_deer_name (사용자가 이용한 킥보드의 이름)
use_end_lat, use_end_lng (사용자가 이용을 종료할 때 위도 경도)
use_start_at, use_end_at (사용자가 이용을 시작하고 종료한 시간)

데이터베이스에는 킥보드에 대해 다음과 같은 정보들이 있습니다.


deer_name (킥보드의 이름으로 고유한 값)
deer_area_id (킥보드가 현재 위치한 지역의 아이디)

데이터베이스에는 지역에 대해 다음과 같은 정보들이 있습니다.


area_id (지역 아이디로 고유한 값)
area_bounday (지역을 표시하는 MySQL spatial data로 POLYGON)
area_center (지역의 중심점)
area_coords (지역의 경계를 표시하는 위도, 경도로 이루어진 점의 리스트)

데이터베이스에는 파킹존에 대해 다음과 같은 정보들이 있습니다.


parkingzone_id (파킹존 아이디로 고유한 값)
parkingzone_center_lat, parkingzone_center_lng (파킹존 중심 위도, 경도)
parkingzone_radius (파킹존의 반지름)

데이터베이스에는 반납금지구역에 대해 다음과 같은 정보들이 있습니다.


forbidden_area_id (반납금지구역 아이디로 고유한 값)
forbidden_area_boundary (반납금지구역을 표시하는 MySQL spatial data로 POLYGON)
forbidden_area_coords (반납금지구역의 경계를 표시하는 위도, 경도로 이루어진 점의 리스트)

디어 서비스 화면

디어 서비스 화면

사용한 기술 설명

MySQL에서 point, polygon필드를 사용해서 킥보드의 경도 및 위치를 판단하여 전동킥보드의 대여, 반납 및 요금계산(할인 및 벌금 부과)을 하는 프로젝트였는데 새로운 개념이었고 여러가지 문제가 겹쳐서 추가 기능까지는 못하고 기본 기능인 대여, 반납, 기본 요금계산을 구현하게 되었다.

좌표를 이용하려면 GDAL이라는 것을 사용해야했는데 매우 새로운 개념이기도하고 gdal이 개인 개발환경에 따라 설치되지 않기도 해서 포기하는 팀들이 좀 있었다.

  • GDAL API GDAL 은 Geospatial Data Abstraction Library의 약자 이며 GIS 데이터 기능의 “스위스 군용 칼”입니다. GDAL의 하위 집합은 다양한 표준 형식의 벡터 지리 데이터를 읽고 쓰는 것을 전문으로 하는 OGR Simple Features Library입니다.

  • GeoDjango는 벡터 공간 데이터의 읽기 및 좌표 변환과 래스터 (이미지) 데이터에 대한 GDAL의 기능에 대한 최소 지원을 포함하여 OGR의 일부 기능을위한 고급 Python 인터페이스를 제공합니다.

우리 팀은 도커를 사용해서 개발환경을 설정하고 진행했다. 좌표를 다루는 기능구현에 있어서 팀장님의 블로그에 나와있어서 링크를 첨부한다. 태우님의 디어코퍼레이션 과제 블로그 글

모델링

디어 erd1 처음에 나는 이렇게 erd를 작성했다. 팀에서 최종적으로 사용한 모델링은 아래와 같다.

디어 erd2

킥보드의 사용을 시작하면 BoardingLog에 시작 시간과 함께 기록을 시작하고 in_use 필드를 True로 변경한다.

in_use필드를 deer 테이블에 넣을 것인지 boardingLog에 넣을 것인지에 대해서 고민했는데 deer와 user테이블 양쪽에서 참조를 통해서 in_use값을 사용하는 경우가 있을 것 같아서 중간에 있는 테이블인 boardingLog에 넣기로 했다.

내가 작성한 코드 / 기억에 남는 코드

#vehicle > views.py
from datetime                        import datetime

from rest_framework                  import status, viewsets, response
from rest_framework.decorators       import action
from rest_framework.permissions      import IsAuthenticated, AllowAny

from vehicle.models                  import Deer, BoardingLog
from user.serializers                import UserSerializer
from vehicle.serializers             import DeerSerializer, BoardingLogSerializer

class VehicleViewSet(viewsets.GenericViewSet):
    quertset           = Deer.objects.all()
    serializer_class   = DeerSerializer
    permission_classes = [IsAuthenticated]
    lookup_field       = 'deer_name'

    #킥보드를 대여하는 로직, 파라미터를 받는 경우에 detail=True를 적용한다. 
    @action(detail=True, methods=['POST'])
    def rent(self, request, deer_name):
        #사용자가 킥보드를 대여중인 경우
        if BoardingLog.objects.filter(user_id = request.user.id, in_use=True).exists():
            return response.Response("One Deer Allowed", status=status.HTTP_400_BAD_REQUEST)
        #킥보드가 사용중인 경우
        if BoardingLog.objects.filter(deer__name = deer_name, in_use=True).exists():
            return response.Response("Already In Use", status=status.HTTP_400_BAD_REQUEST)

        BoardingLog.objects.create(
            user_id = request.user.id,
            deer = Deer.objects.get(name = deer_name),
            in_use = True,
            use_start_at = datetime.now()
        )
        return response.Response("Start Using Deer", status=status.HTTP_200_OK)

원티드 x 위코드 프리온보딩 과제5 휴먼스케이프(임상실험정보 검색)

|

과제 설명 요약

  • 구현 기간 : 21.11.15(17시) ~ 21.11.17 (10시)
  • 자세한 내용은 Github 참고

사용한 기술 설명

임상정보인 공공데이터를 API로 받고 변경 내역이 있는 부분을 포함하여 검색할 수 있도록 하는 과제에서 상세 임상정보 조회 API, 스웨거를 담당했고 테스트케이스를 처음으로 작성했다.

가장 어려웠던 API의 실시간 DB동기화는 팀장님이 작성하셨고 아래의 회고 글에 들어가면 로깅을 포함한 구현 설명을 읽을 수 있다. 태우님의 휴먼스케이프과제 블로그 글

모델링

디비 API로 받게되는 공공데이터가 위와 같은 형태로 되어있었고, 이 형태 그대로 디비에 업로드하였다. 이 디비를 보고 과제번호와 과제이름, 학과(ex.소아과)와 담당기관, 연구종류와 임상시험단계 컬럼을 검색에 포함하는 것이 좋을 것 같다고 결정했다. 자세한 기준은 아래와 같다. 모델링

컬럼별 검색 기능 구현 내역

이름(필드명) 필드 타입 필터링 가능/필요 여부 lookup_expression
과제명(name) Char O icontains (입력된 값을 포함하는 경우)
과제번호(number) Char (unique) X (수집한 임상정보에 대한 API에서 처리) X
연구기간(period) Char X (데이터가 통일성이 없음) X
연구범위(range) Char O icontains (입력된 값을 포함하는 경우)
연구종류(code) Char O icontains (입력된 값을 포함하는 경우)
연구책임기관(institute) Char O icontains (입력된 값을 포함하는 경우)
임상시험단계(stage) Char O icontains (입력된 값을 포함하는 경우)
전체목표연구대상자수(target_number) Integer O lte, gte (최소/최대 대상자 수)
진료과(office) Char O icontains (입력된 값을 포함하는 경우)
생성일(created_at) Date X (우리 데이터베이스에 객체 생성된 시점 값이므로 필터링 필요없음) X
업데이트일(updated_at) Date X (수집한 임상정보 리스트 API에서 처리) X

rest_framework 라이브러리에서 제공하는 SearchFilter를 사용해 검색 기능을 구현했다.

내가 작성한 코드 / 기억에 남는 코드

#research > views.py
#import 부분 생략

#method_decorator를 사용하면 함수가 아닌 Class에 스웨거를 넣을 수 있다.
@method_decorator(name='get', decorator=swagger_auto_schema(
    operation_id ="연구 데이터 상세 조회",
    operation_description ="연구 번호를 입력하세요\n\n" +
                            "예시 : C160008",
    responses    = {
        "201": "SUCCESS",
        "404": "NOT_FOUND",
        "400": "BAD_REQUEST",
    }
))
class ResearchRetrieveView(RetrieveAPIView):
    queryset            = ResearchInformation.objects.all()
    serializer_class    = ResearchInformationSerializer
    lookup_field        = 'number'

#스웨거상에서 함수가 아닌 클래스에 예시를 넣는 방법을 시간상 못 찾아서 설명 부분에 같이 넣었다. 
@method_decorator(name='get', decorator=swagger_auto_schema(
    operation_id ="연구 데이터 검색",
    operation_description = "전체 연구 데이터를 필터 및 검색합니다.\n\n"
                            "필터 : 과제명(name), 연구기간(range), 연구종류(code), 연구책임기관(institute), 임상시험단계(연구모형)(stage), 연구책임기관(institute), 임상시험단계(stage), 전체목표연구대상자수(target_number), 진료과(office), 최고 연구기간(min_target_number), 최대 연구기간(max_target_number)의 필터 및 개별 검색 가능\n\n" +
                            "전체 검색 : search 값에 검색하고 싶은 단어 입력" +
                            "(과제명, 과제번호, 연구기간, 연구종류, 연구책임기관, 임상시험단계(연구모형), 진료과 중 검색 가능)\n\n" +
                            "정렬 : 업데이트된 날짜 역순으로 기본 정렬, 기준으로 정렬하고 싶은 컬럼의 키값을 ordering에 입력하세요\n\n" +
                            "예시 : \n" +
                            "{\n"+
                                "id: 10,\n"+
                                "name: 제2형 당뇨병 임상연구네트워크 구축사업\n"+
                                "number: C140014\n"+
                                "period: 120개월\n"+
                                "range: 국내다기관\n"+
                                "code: 관찰연구\n"+
                                "institute: 경희대학교병원\n"+
                                "stage: 코호트\n"+
                                "target_number: 700\n"+
                                "office: Endocrinology\n"+
                                "created_at: 2021-11-16\n"+
                                "updated_at: 2021-11-16\n"+
                            "}",
    responses    = {
        "201": "SUCCESS",
        "404": "NOT_FOUND",
        "400": "BAD_REQUEST",
    }
))
#이렇게 drf내부의 서치뷰를 사용하면 아래컬럼을 모두 포함하는 검색을 구현할 수 있다. 
class SearchResearchView(ListAPIView):
    queryset            = ResearchInformation.objects.all()
    serializer_class    = ResearchInformationSerializer
    filter_backends     = [DjangoFilterBackend, OrderingFilter, SearchFilter]
    filterset_class     = ResearchFilter
    ordering            = ['-updated_at']
    search_fields       = ['name', 'number', 'range', 'code', 'institute', 'stage', 'office']
#research > tests.py
from rest_framework         import status
from rest_framework.test    import APITestCase

from research.models        import ResearchInformation

from django.core import serializers


class ResearchRetrieveTests(APITestCase):
    @classmethod
    def setUpTestData(cls):
        ResearchInformation.objects.create(
            name="조직구증식증 임상연구 네트워크 구축 및 운영(HLH)",
            number="C130010",
            period="3년",
            range="국내다기관",
            code="관찰연구",
            institute="서울아산병원",
            stage="코호트",
            target_number="120",
            office="Pediatrics",
            created_at="2021-11-16",
            updated_at="2021-11-16"
        )
        ResearchInformation.objects.create(
            name="대한민국 쇼그렌 증후군 코호트 구축",
            number="C130011",
            period="6년",
            range="국내다기관",
            code="관찰연구",
            institute="가톨릭대 서울성모병원",
            stage="코호트",
            target_number="500",
            office="Rheumatology",
            created_at="2021-11-16",
            updated_at="2021-11-16",
        )
        ResearchInformation.objects.create(
            name="Lymphoma Study for Auto",
            number="C100002",
            period="1년",
            range="단일기관",
            code="국내연구",
            institute="가톨릭대 서울성모병원",
            stage="Case-only",
            target_number="200",
            office="Hematology",
            created_at="2021-11-16",
            updated_at="2021-11-16"
        )

    #처음에는 검색 결과가 문자열로 그대로 일치하도록 작성하려고했는데 여의치 않아서 객체로 비교하도록 했다.
    def test_detail_view_success(self):
        response = self.client.get('/researches/C100002')
        
        expected_data_id = ResearchInformation.objects.get(number='C100002').id

        self.assertEqual(response.json()['id'], expected_data_id)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
    
    def test_detail_view_fail(self):
        response = self.client.get(
            '/researches/C140111'
            )

        self.assertEqual(response.json(), {"detail" : "Not found."})
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

프로젝트 후기

나는 과제번호로 임상정보를 조회하는 기능을 담당했는데 쉬운 기능이라 빨리 끝나서 세원님이 필터를 구현하는 것을 옆에서 같이 보다가 필터기능을 작성했다면 아주 간단한 방법으로 여러개의 컬럼을 포함하는 검색 기능을 추가 구현할 수 있다는 것을 발견해서 알려드렸다. 저렇게 search_fields라는 한 줄로 원하는 컬럼을 전부 검색할 수 있다는 사실이 신기했고 저번 프로젝트에서 퓨어 장고로 구현했던 것보다 너무나 쉽고 간편해서 놀라웠다. 하지만 성능적인 측면에서 좋은 선택인지 알아봐야할 것 같다.

원티드 x 위코드 프리온보딩 과제4 8퍼센트(입출금거래내역)

|

과제 설명 요약

  • 구현 기간 : 21.11.01(17시) ~ 21.11.13 (10시)
  • 자세한 내용은 Github 참조

모델링

모델링

사용한 기술 설명

DRF와 sqlite로 입금, 출금, 거래내역 조회를 구현하는 프로젝트에서 로그인, 회원가임, 유저정보 조회와 배포환경설정을 맡아서 구현했다.

DRF에서 Class Based View를 처음 사용했는데 만들어져 있는 기능을 자동으로 가져다 쓸 수 있는 것이 편했다.

Docker로 배포해본 적이 없어서 막연히 어려울 것 같다고 생각하고 있었는데 팀장분께서 컴포즈파일의 구성요소를 자세히 설명해주셔서 금방 이해할 수 있었다.

내가 작성한 코드 / 기억에 남는 코드

#users > model.py
from django.db                  import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser


#BaseUserManager를 상속받아서 유저메니저 클래스가 유저를 생성하게된다
class UserManager(BaseUserManager):
    def create_user(self, email, name, password=None):
        if not email:
            raise ValueError('Users must have an email address')

        user = self.model(
            email = self.normalize_email(email),
            name  = name,
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

#AbstractBaseUser에서 기본으로 만들어져있는 모델의 유저필드들을 받은 뒤 커스텀해서 사용한다
class User(AbstractBaseUser):
    email      = models.EmailField(max_length=255, unique=True)
    name       = models.CharField(max_length=20)
    is_admin   = models.BooleanField(default=False)
    username   = models.CharField(max_length=10, default='', null=True, blank=True)

    objects = UserManager()

    #유저네임대신 이메일로 로그인한다
    USERNAME_FIELD  = 'email'
    #은행업무이기 때문에 실명을 꼭 받도록했다.
    REQUIRED_FIELDS = ['name']

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        return True

    def has_module_perms(self, app_label):
        return True

    @property
    def is_staff(self):
        return self.is_admin
    
    class Meta:
        db_table = 'users'
#users > view.py
from rest_framework             import generics
from rest_framework.generics    import CreateAPIView
from rest_framework.permissions import AllowAny
from django.contrib.auth        import get_user_model

from .permissions               import IsOwner
from users.models               import User
from .serializers               import RegisterUserSerializer, UserSerializer


class RegisterUserView(CreateAPIView):
    permission_classes = [AllowAny]
    serializer_class   = RegisterUserSerializer

#유저정보를 조회하는 코드, RetrieveAPIView를 사용해서 코드를 쓰지 않아도 자동으로 1개 객체의 정보를 조회하는 기능이 구현된다.  
class UserDetailAPIView(generics.RetrieveAPIView):
    queryset           = get_user_model().objects.all()
    serializer_class   = UserSerializer
    permission_classes = [IsOwner]
#docker-compose.yml 파일
version: "3"
services: 
  wantedlab-backend: #컨테이너
    container_name: wantedlab-backend #docker compose run 했을 때 뜨는 이름
    build: 
      context: .
      dockerfile: ./Dockerfile-deploy #직접 빌드해서 쓸 도커파일 지정
    depends_on:
      - wantedlab_deploy_db #밑에 나오는 
    restart: always #커맨드가 터지면 다시 올릴거냐
    environment: # 시크릿키 직접 안쓰고 여기서 가져오겠다 하기 위한 환경설정
      SQL_HOST: wantedlab_deploy_db # 이렇게 쓰면 이 컨테이너의 아이피로 됨 
      SQL_PORT: 5432
      DJANGO_SETTINGS_MODULE: wantedlab.settings.deploy
    env_file: 
      - .dockerenv.deploy.backend
    command: #컨테이너가 커지면 실행되는 명령어
      - bash #터미널
      - -c #이걸 써야 여러줄 쓸 수 있다
      - | #이걸 써야 여러줄 쓸 수 있다
        python manage.py wait_for_db_connected -t 120 
        python manage.py migrate
        python manage.py collectstatic
        gunicorn wantedlab.wsgi:application --bind 0.0.0.0:8000
    volumes: #위 : 필요한 명령어를 쭉 치고 구니콘은 진짜 서버 돌리는 용, wantedlab이 프로젝트명(폴더명) 나머지는 구니콘 명령어
      - .:/usr/src/app/ # 이 컴터의 위치와 : 도커의 위치 동기화(도커파일의 워킹 디렉토리 설정해놓은 것 WORKDIR) 

  wantedlab_deploy_db: #이 컨테이너가 실행될 때까지 기다린다
    container_name: wantedlab_deploy_db
    image: postgres # 도커 허브에서 받아서 쓰는 포스트그레스 이미지
    restart: always
    volumes: #포스트그레스 이미지대로 세팅한거, 이렇게 데이터를 저장해줘야 컨테이너가 다운되도 데이터가 안 날아감
      - ./postgresql/data:/var/lib/postgresql/data
    environment: #포스트그래스 아이디 비번
      - POSTGRES_INITDB_ARGS=--encoding=UTF-8
      - TZ="Asia/Seoul"
    env_file:
      - .dockerenv.deploy.db

  wantedlab_nginx: #컨테이너
    image: nginx #엔지닉스는 스태틱 웹서버 구니콘은 다이나믹 웹서버 
    container_name: wantedlab_nginx
    volumes:
      - ./config/nginx:/etc/nginx/conf.d
      - ./static:/static
    ports:
      - "8021:8021" #로컬 : 쏴주는 포트끼리 연결
    environment:
      - TZ="Asia/Seoul"
    depends_on:
      - wantedlab-backend

이렇게 도커 컴포즈 yml파일을 만든 뒤 아래 명령어로 실행시켜서 도커를 이용해 띄우는 서버와 디비를 연결시킨다.

docker-compose -f docker-compose-deploy.yml up

프로젝트 후기

DRF로 특정 기능을 혼자서 구현한 것은 처음이었는데 개념을 이해하거나 세팅하는데는 시간이 오래 걸렸는데 정작 코드를 쳐서 내용을 구현하는 것은 정말 금방 끝났다.

하지만 유저를 생성하는 클래스의 경우 내부에서 어떻게 작동하는지 모르겠어서 이후의 프로젝트에서 계속 drf의 유저생성에 관련해서 어려움을 겪었다.

공식문서 번역본의 예제 코드를 참고해서 작성했다. https://kimdoky.github.io/django/2018/07/06/drf-Generic-views/

원티드 x 위코드 프리온보딩 과제3 Wantedlab(다국어회사이름검색)

|

과제 설명 요약

  • 구현 기간 : 21.11.08(17시) ~ 21.11.10 (10시)
  • 자세한 내용은 Github 참조

아래와 같은 형태로 저장되어있는 데이터에서 헤더에서 받은 언어 설정값을 통해 회사를 검색하는 부분을 담당했다.

company_name : {
    'ko': '라인',
    'en': 'Line',
    'ja': 'ライン'
}

모델링

다른 언어도 추가될 수 있도록 만들라는 전제가 있어서 확장성을 고려해서 JSONField를 사용하기로 했다. JSON

사용한 기술 설명

팀에서 세 분은 DRF를 사용할 줄 알았고 나와 지원님은 몰랐어서 이번 과제를 통해서 DRF를 공부하고 적용해보기로 했다. DRF를 바로 공부하고 바로 적용하는 것이 어려웠지만 태우님께서 공부할 수 있게 단계별로 DRF를 적용하는 코드를 보여주시고 지원님도 도와주셔서 세팅을 할 수 있었다.

내가 작성한 코드 / 기억에 남는 코드

헤더에서 언어를 받는 부분에서 처음에는

if request.headers['X-Wanted-Language'] is False:
            language = 'ko'
        else :
            language = request.headers['X-Wanted-Language']

이렇게 작성했는데 쿼리스트링(검색값)이 아예 없는경우에는 if문 작동까지가지도 못하고 500에러가 발생해서 팀장분께서 아래와 같이 리팩토링해주셨다.

class CompanySearchAPIView(views.APIView):

    def get(self, request):
        search  = request.GET.get('query', None)
        results = []

        if search is None:
            return response.Response(results, status=status.HTTP_200_OK)

        language = request.headers.get('x-wanted-language', 'ko')

        #JSON구조에 따라 depth 안 쪽으로 들어가서 company_name : { ko : 여기를 검색 } 하도록 함    
        c = {f'company_name__{language}__icontains': search}

        companies = Company.objects.filter(**c)
        company_serializer = CompanySerializer(companies, many=True)

        results = [{
             "company_name" : company['company_name'][language]
        } for company in company_serializer.data]

        return response.Response(results, status=status.HTTP_200_OK)

프로젝트 후기

팀원분들에 비해서 내가 정말 많이 부족하다는걸 느꼈다..! 배우는 속도가 느려서 내가 개발자가 될 수 있을까 의구심이 들었지만 내가 공부를 더 하면 될 일이고, 일단 백엔드를 3년간 열심히 해봐야겠다고 생각한다. 함께 프로젝트를 하는 팀원 분들이 정말 잘하고 열심히 하셔서 배울 점이 많다.

원티드 x 위코드 프리온보딩 과제2 Mafia company(그래프DB연결검색)

|

과제 설명 요약

  • 구현 기간 : 21.11.04(18시) ~ 21.11.06 (10시)
  • 뮤지션 곡 앨범 3개가 있다고 가정하고 관계를 연결해서 관련된 정보를 얻는 로직을 구현 (단 앨범과 뮤지션은 연결이 안되어 있다고 가정한다)
  • db는 무조건 neo4j GraphDB를 사용해야된다.
  • CUD는 REST API로 구현
  • Read쪽은 Graph QL로 구현하면 가산점 있음 (Strawberry 라이브러리 추천)
  • 자세한 내용은 Github 참조

과제 설명

  • 마피아컴퍼니 선호 기술스택
    • Python/FastAPI
    • Javascript 사용시 선호 프레임워크 없음
  • 사용 필수 데이터 베이스
    • neo4j GraphDB
    • 개발 완료 시 리뷰어가 실행해볼 수 있도록 neo4j 디비를 csv 로 export해서 프로젝트 루트 경로에 포함해주세요.
  • API 구성은 Restful API 형태로 구성하시면 됩니다.
    • GraphQL로 구현하면 가산점이 있습니다.
      • Strawberry graphql 라이브러리 추천
      • CUD는 GraphQL Mutation 으로 만들지 않고, Restful로 만들어주세요.

[필수 포함 사항]

  • READ.ME 작성
    • 프로젝트 빌드, 자세한 실행 방법 명시
    • 구현 방법과 이유에 대한 간략한 설명
    • 완료된 시스템이 배포된 서버의 주소
    • Swagger나 Postman을 통한 API 테스트할때 필요한 상세 방법
    • 해당 과제를 진행하면서 회고 내용 블로그 포스팅
  • Swagger나 Postman을 이용하여 API 테스트 가능하도록 구현

[과제 안내]

  • 음악 스트리밍 서비스에는 3가지 요소 뮤지션 앨범 이 존재합니다.

  • 앨범 페이지, 뮤지션 페이지, 곡 페이지에 인접 정보들 (ex, 곡의 뮤지션, 곡의 앨범) 을 표현할 수 있도록 CRUD API를 구성해주세요.

  • 이 페이지들에 대한 DB를 구성할 때 - 뮤지션 연결과

  • 앨범 연결은 내부 운영팀에서 직접 연결 가능하지만, 뮤지션 - 앨범 정보까지 태깅하기엔 내부 운영 리소스가 부족한 상황으로 가정해보겠습니다.

  • 이 때, 뮤지션 - 이 연결되어있고 - 앨범 이 연결되어있다면 뮤지션 - [곡*] - 앨범 연결되어있다고 판단할 수 있는데요. 이 특성을 이용해서 뮤지션앨범 을 보여주는 Read API, 특정 앨범 의 뮤지션 목록을 보여주는 Read API를 만들어주세요.

각 요구사항을 아래에 명시해두었습니다.

  • 화면별 Read API 요구사항
    • 페이지
      • 해당 이 속한 앨범을 가져오는 API
      • 해당 을 쓴 뮤지션 목록을 가져오는 API
    • 앨범 페이지
      • 해당 앨범을 쓴 뮤지션 목록 가져오는 API
      • 해당 앨범 목록을 가져오는 API
    • 뮤지션 페이지
      • 해당 뮤지션의 모든 앨범 API
      • 해당 뮤지션 목록 가져오는 API
  • Create, Update, Delete API 요구사항
    • 생성 API
    • 앨범 생성 API
    • 뮤지션 생성 API
    • 뮤지션 - 연결/연결해제 API
    • - 앨범 연결/연결해제 API
    • 뮤지션 - 앨범 연결/연결해제 API 는 필요하지 않습니다. (구현 X)
      • 뮤지션 - 연결과 - 앨범 연결이 되어있으면 GraphDB (neo4j) 에서 뮤지션 - [*] - 앨범 연결 여부를 뽑을 수 있습니다. 이 특성을 Read API에서 활용해주세요.
  • Neo4j DB 테이블 요구사항
    • 뮤지션, , 앨범은 각각의 테이블 (musician, song, album)로 구성되어야합니다.
    • 앨범 안에는 여러 이 속해있을 수 있습니다.
    • 에는 여러 뮤지션이 참여할 수 있습니다.
    • 앨범 1개에만 들어가있습니다.
    • 뮤지션은 여러 앨범을 갖고 있을 수 있습니다.
    • 뮤지션, 앨범, 데이터는 위 relation을 테스트할 수 있을만큼 임의로 생성해주시면 좋습니다.

사용한 기술 설명

  • GraphDB 기업에서 요구 조건 중에 neo4j디비는 NoSQL인 GraphDB이다. 아래 사진과 같이 각 객체와의 연결로 나타내어진다.

image

처음 프로젝트를 할 때는 GraphDB에 대한 이해가 없었는데 아래 영상을 보고, SQL과 NoSQL은 한국음식과 No한국음식과 같은 개념이라는 니꼬의 설명이 짧은 시간 안에 이해하는데 도움이 많이 되었다.

뮤지션과 앨범이 연결되지 않는다는 전제를 가지고 생각만으로 팀원들과 소통하고 코드를 짜기가 불편해서 아래와 같이 그림을 그려서 얘기했는데 한결 편했다.

image image

  • GraphQL Restful API가 아닌 GraphQL이라는 방식으로 클라이언트와 연결을 하는 것을 처음 해봤는데 얄팍한 코딩사전의 병맛 피자가게 만화가 이해에 많은 도움이 되었다.

기억에 남는 코드

뷰셋을 처음 써봤는데 다른 팀원분이 작성해주신 동일한 구조의 코드에서 앨범만 뮤지션으로 바뀌는거라서 괜찮았지만 neo4j 디비에 연결해서 코드를 실행해보는 부분이 처음 보는 것이라서 어려웠다. 특정 뮤지션의 앨범들을 조회하는 부분은 팀장님과 다른 분 2분께서 2시간여를 씨름하다가 작성해주셨다.

class MusicianViewSet(viewsets.GenericViewSet):
    permission_classes = [AllowAny]

    #뮤지션 CRUD
    def create(self, request):
        name = request.data.get('name')
        if name is None:
            return Response({'error': 'name field is required.'},status=status.HTTP_400_BAD_REQUEST)
        musician = Musician(name=name).save()
        rtn = MusicianSerializer(musician).data
        return Response(rtn, status=status.HTTP_201_CREATED)

    def list(self, request):
        musicians = Musician.nodes.all()
        rtn = MusicianSerializer(musicians, many=True).data
        return Response(rtn, status=status.HTTP_200_OK)

    def retrieve(self, request, pk):
        musician = Musician.nodes.get_or_none(uuid=pk)
        if musician is None:
            return Response({'error': 'DoesNotExist'})
        rtn = MusicianSerializer(musician).data
        return Response(rtn, status=status.HTTP_200_OK)

    def destroy(self, request, pk):
        musician = Musician.nodes.get_or_none(uuid=pk)
        musician.delete()
        return Response(status=status.HTTP_200_OK)
    
    #특정 뮤지션의 곡들을 조회 
    @action(detail=True, methods=['GET'])
    def songs(self, request, pk):
        musician = Musician.nodes.get_or_none(uuid=pk)
        if musician is None:
            return Response({'error': 'DoesNotExist'})
        songs = musician.song.all()
        rtn = SongSerializer(songs, many=True).data
        return Response(rtn, status=status.HTTP_200_OK)

프로젝트 후기

위워크 여의도점에서 18시간 정도 진행을 했는데 새로운 기술스택에 대한 이해가 낮고 새로운 디비의 세팅을 할 수가 없어서 처음에 대화에 참여하기가 어려웠다. 우리 팀은 운이 좋게도 5명 중에 세 분이 전공자인데 정말 잘 하신다. 나와 비전공자 한 분은 팀장님이 셋팅해준 구조 위에서 각각 뮤지션과 곡의 API를 작성했는데 기능 구현을 다 하고나서 11시쯤에 테스트케이스를 작성하기 시작할 때 너무 피곤해서 막차를 타러가서 남아서 첫차 탈 때까지 작업하신 두 분께 미안했다.

개발자로 가는 길

|
  • 개발자가 되기로 결심한 계기/과정

대학교 다닐때 영어토론 동아리를 하면서 알던 선배 분께서 학교 창업센터에서 만든 강아지 산책 앱이 정부지원사업에 당선이 되서 보스톤/뉴욕에 피칭을 하러 가게되었는데 그 때 처음 창업가정신과 스타트업, IT문화 등에 대해서 알게 되면서 엄청 흥미가 생겼었다.

SendBird, AIM, 셀잇, Between, 마이리얼트립 등등 정말 쟁쟁한 기업의 대표님 및 창업가분들과 코데카데미, 위워크, 눔 본사에 가서 대표분들도 만나고 많은 얘기를 들으면서 함께 여러가지를 배울 수 있는 소중한 경험이었다.

그 때부터 코데카데미를 사용하기 시작했는데 주변에 개발하는 사람이 없는 상황에서 혼자서 영어로 공부하기가 쉽지 않았다. 이후에 제약상사와 보험회사에서 일하면서 혼자 C언어 책으로 기본적인걸 공부하고 친척오빠분이 친구를 소개시켜주셔서 개발의 기본을 배웠다.

여유가 좀 되었을 때 웹사이트 만드는 수업을 3개월 듣고 웹사이트 만드는 일을 하게되었는데 개발 업무는 아니었고 실력이 늘지 않는 것 같아서 국비지원 자바 프론트엔드 개발자 과정을 6개월 수강했다. 선생님이 3번이나 바뀌고 프로젝트를 하는 와중에 깃 브랜칭을 안 하겠다는 조원과 갈등을 빚는 등 힘들었던 6개월이 지나고, 많이 부족한 점을 느끼고 대학교 학비가 무료인 독일에서 공대를 다녀야겠다는 생각이 들어서 무작정 베를린으로 향했다.

처음에는 공대를 갈 생각이었는데 도착한 첫 주 주말에 친구의 친구를 통해서 UX디자이너 인턴으로 일 할 기회가 있어서 일하다가 3개월 후 정직원 오퍼를 받아 1년 반동안 일하게 되었다. 사내 음악 관리용 웹서비스를 앵귤러로 만드느라 자바스크립트를 사용했지만 일하다 보니 공부를 좀 소홀히 하게 된 것 같다. 그러다가 코로나에 걸리게 되었고 회사 측에서도 UX디자이너로 일하지 않고 개발직군으로 일하려면 공부를 조금 더 하는게 좋을 것 같다고해서 부트캠프를 들으러 한국에 들어오게 되었다.

원래는 실력이 좀 부족한 것 같으니 프론트엔드를 할까도 생각했었는데 부트캠프 사전스터디 중에 파이썬으로 알고리즘을 푸는 것이 재미있었고 자바스크립트보다 파이썬이 직관적이고 논리적이라는 생각이 들었고, 백엔드를 선택해서 공부하면 더 어렵고 많이 배울 수 있을 것 같아서 백엔드를 선택하게 되었다.

  • 내가 위코드 x 원티드 프리온보딩에 참여하게 된 동기/이유.

위코드 부트캠프를 3개월간 수강했는데 정말 좋았고, 취직 오퍼는 받았지만 좀 더 공부해서 더 실력을 쌓는게 우선이라는 생각이 들어서 참여하게 되었다. 위코드 대표이신 은우님과 예리님께서 진행하는 과정이라면 실력과 경험이 늘지 않을 이유가 없다고 생각했다.

  • 나는 앞으로 어떤 개발자가 되고 싶은가?

여태까지 내 인생을 살펴보면 여기저기 넓고 얕게 발을 담갔던 것 같다. 앞으로 3년동안은 아무것도 생각하지 않고 백엔드 개발자로서 제 몫을 다하는데 집중할 것이고 앞으로는 한 가지 분야의 전문가가 되어서 나중에 컨퍼런스 같은 곳에서 내 이름을 걸고 발표도 하고 지식을 공유하고 싶다.

원티드 x 위코드 프리온보딩 과제1 Aimmo(몽고디비대댓글)

|

과제 설명 요약

  • 구현 기간 : 21.11.01(17시) ~ 21.11.03 (10시)
  • 기본적인 게시판 CRUD 구현
  • 댓글과 대댓글 기능 필요하며, pagination 구현 해야된다.
  • db는 무조건 mongodb를 사용해야된다.
  • 성능 테스트 필요(1000만건의 data가 db에 있는 상태에서)
  • 자세한 내용은 Github 참조

과제 설명

  • 에이모 선호 기술스택: python flask, mashmallow, mongoengine
  • 필수 사용 데이터베이스: mongodb

[필수 포함 사항]

  • Swagger나 Postman을 이용하여 API 테스트 가능하도록 구현
  • READ.ME 작성
    • 프로젝트 빌드, 자세한 실행 방법 명시
    • 구현 방법과 이유에 대한 간략한 설명
    • 완료된 시스템이 배포된 서버의 주소
    • Swagger나 Postman을 통한 API 테스트할때 필요한 상세 방법
    • 해당 과제를 진행하면서 회고 내용 블로그 포스팅

[개발 요구사항]

  • 원티드 지원 과제 내용 포함
  • 게시글 카테고리
  • 게시글 검색
  • 대댓글(1 depth)
    • 대댓글 pagination
  • 게시글 읽힘 수
    • 같은 User가 게시글을 읽는 경우 count 수 증가하면 안 됨
  • Rest API 설계
  • Unit Test
  • 1000만건 이상의 데이터를 넣고 성능테스트 진행 결과 필요

사용한 기술 설명

Djongo 사용

Django에서 기본적으로 제공하는 Database는 Mongodb가 포함되어 있지 않아서 Mongodb기반의 ORM을 작성할 수 있도록 해주는 Djongo를 사용하여 Mongodb와 연결했다.

Docker 사용

팀원들의 빠른 개발환경 셋팅을 위해서 로컬 개발용과 배포용 docker-compose 파일을 만들어서 적용했다.

docker-compose-deploy.yml 파일 펼쳐서 보기
version: "3"
services:
  aimmo_deploy_db:
    image: mongo
    container_name: aimmo_deploy_db
    environment:
      - PUID=1000
      - PGID=1000
    volumes:
      - ./mongodb/database:/data/db
    ports:
      - 27017:27017
    restart: unless-stopped
  aimmo_deploy_backend:
    build:
      context: .
      dockerfile: ./Dockerfile-deploy
    container_name: aimmo_deploy_backend
    ports:
      - 8000:8000
    depends_on:
      - aimmo_deploy_db
    restart: always
    environment:
      DB_HOST: aimmo_deploy_db
      DJANGO_SETTINGS_MODULE: aimmo.settings.deploy
    env_file:
      - .dockerenv.deploy
    command:
      - bash
      - -c
      - |
        python manage.py migrate
        gunicorn --bind 0.0.0.0:8000 aimmo.wsgi:application
    volumes:
      - .:/usr/src/app/

기억에 남는 코드

대댓글을 보여주는 api

class CommentView(View):
    @login_decorator
    def get(self, request, post_id):
        parent_id = int(request.GET.get("parent_id","0"))
        #parent_id를 받아와서 저장한다. 없는 경우에는 0.

        if parent_id == 0:
            all_comments = Comment.objects.filter(post_id=post_id, parent_comment__isnull=True).select_related('user')
        #parent_id가 0인 경우(대댓글이 아닌 댓글)에는 parent_comment_id 컬럼이 null인 코멘트들(댓글)을 불러온다. 'parent_id' 라는 변수에 0이 입력되면 댓글을 조회
        
        else:
            all_comments = Comment.objects.filter(post_id=post_id, parent_comment_id=parent_id).select_related('user')
        #대댓글일 경우에는 parent_comment_id가 해당 id인 코멘트들(대댓글)을 불러온다. 'parent_id' 라는 변수에 *이 입력되면 *번 댓글의 대댓글을 조회

        limit = int(request.GET.get("limit","10"))
        offset = int(request.GET.get("offset","0"))
        offset=offset*limit
        
        comments  = all_comments[offset:offset+limit]
        #대댓글의 페이지네이션을 짤 때 어떤 순서로 댓글을 보여주어야할 지 고민했었는데 

        comment_list = [{   
                'comment_id' : comment.id,
                'user_id'    : comment.user_id,
                'email'      : comment.user.email,
                'content'    : comment.content,
                'created_at' : comment.created_at,
                'updated_at' : comment.updated_at,
                'parent_id'  : comment.parent_comment_id,
                } for comment in comments
            ]
        return JsonResponse({'comments':comment_list}, status=200)

댓글과 대댓글을 보여주는 url

{server_url}/posts/1/comments?parent_id=1&offset=0&limit=5 -> 1번 게시물 / 1번 댓글의 대댓글을 조회 / 5개의 대댓글을 조회

데이터 천만건 넣기?

우리 팀은 시간이 없어서 성능테스트를 하지 못했는데 온보딩 참여하신 분 중에 성능테스트를 하고 블로그를 아주 잘 작성하신 분이 계셔서 링크를 첨부한다. 김태희 - [WEEK1] Aimmo 기술과제를 마치고..

프로젝트 후기

처음 만나는 사람 세명이서 거의 하루만에 하는 프로젝트였음에도 불구하고 능력이 엄청 좋으면서 사려깊은 팀장님과 정말 열심히 공부하시고 밝고 친절한 분을 팀원으로 만나서 잘 끝낼 수 있었다. 나는 밤 12시쯤에 전사했지만 나머지 두 분께서 새벽 4시반까지 마무리작업을 해주셨는데 리드미와 커밋메세지가 좋은 사례로 뽑혀서 온보딩을 진행하는 위코드 대표 은우님께서 세션을 하면서 다른 분들께 보여드렸다.

처음 써보는 몽고디비와 도커 개발환경으로 초기 세팅을 하다보니 태우님이 해주셨는데 좀 시간이 걸려서 그 동안 모델링을 하고 오후 5시정도부터 뷰를 쓰기 시작했다. 댓글 CRUD는 원래 하던 것에서 가져와서 금방했는데 대댓글을 구현하는 부분이 어려웠어서 태우님께서 저 기억에 남는 코드를 작성해주셨다.

처음하는 프로젝트라서 시간이 더 걸렸던 것 같고 후기를 쓰는 지금은 세번째 프로젝트가 진행중인데 서로 기술 수준도 잘 알고 패턴도 잘 알고, 미리 시작하고 위워크에서 오프라인으로 만나고해서 훨씬 수월해진 것 같다.

위코드 수료 후 백엔드 면접 후기 및 FAQ1 - 기술면접

|

이력서를 60군데(링크드인 8, 원티드 16, 사람인 11, 로켓펀치 20, 이외 5)정도 넣고 면접을 10군데 제의 받고 현재 6개째 면접을 봤고 한 군데에서 3500으로 오퍼를 받았는데 위코드 x 원티드 온보딩 코스를 듣기로 결정해서 나머지 면접은 보러가지 않았다.

파이썬, 장고 기술 스택인 회사 위주로 넣었는데 면접을 본 6군데 중에 클라우드 데이터를 다루는 곳은 Go를 써야한다고 했고 음악 스트리밍 앱을 만드는 곳에서는 nodeJS를 써야한다고 했어서 파이썬, 장고는 배우는 수단으로서 익힌 것이고 개발자로 일하려면 다른 언어와 프레임워크들도 익혀야되겠구나 생각이 들었다. 의료영상 처리하는 회사에서는 내가 Angular로 일한 경험이 있는 점이 마음에 든다고 했는데 그럼 풀스택으로 일을 시키려고 하는건지 싶은 생각이 들었고 두 군데에서 교포인지 물어봤다. 독일에 가서 일했어서 그런가,,??

예리님께서 가이드해주신대로 사수가 없이 혼자 배워서 혼자 책임지는 개발을 하기를 원하는 곳들이 많았고 사수가 없는 곳도 있어서 다시 한 번 열공의 필요성을 느꼈다. 위코드 수료 이후에 기수 분들과 함께 했던 스터디가 많은 도움이 되었고 팀원들과 내 이력서를 기준으로 면접을 보는 동안 자주 나온 질문들과 예상 질문과 대답을 추려보았다.

JWT 관련

JWT는 무엇인가? (인증/ 인가를 위해서 사용하는 여러 정보를 담은 JSON web token, 1. header에는 토큰을 생성하는signing 알고리즘 2.body에는 유저아이디와 유저권한과 같은 페이로드 3. 마지막 변조 확인하는 시그니쳐로 이루어져있다.)
JWT를 왜 사용하였는지? (보안을 위해서, JWT말고 Oauth를 사용할 수도 있다)
JWT의 변조 알고리즘에는 무엇이 있는지? (256, base64)
JWT가 어떤 방식의 해킹을 당할 수 있는지 그리고 그걸 예방하기 위해서 어떻게 해야하는지? (토큰의 탈취가 이루어질 수 있다, refresh token을 이용해서 access token을 재발급하도록 해서 예방할 수 있다)

인증 / 인가, 로그인이 이루어지는 방식

인증은 회원가입, 로그인하는 것 / 인가는 토큰을 제시하고 접근하는 것.

ORM 관련

ORM은 무엇인가? (장고에서 DB의 데이터를 객체로서 원하는 형태로 가공하기 위한 object relation manager)
ORM장,단점은? (장고에서 쉽고 빠르게 디비를 셋업할 수 있지만 쿼리문을 직접 작성하지 않아서 복잡한 쿼리를 쓰기가 더 번거롭다) Eager Loading은 무엇인가? (가져와야하는 데이터를 먼저 가져와놓아서 쿼리를 실행했을 때 원하는 정보를 빠르게 가져올 수 있도록 하는 것)

RESTful API는?

URI가 그 자체로 리소스와 기능이 무엇인지를 설명해주는 api를 말한다. 예시를 들자면 /post 인 경우 get method라면 게시글 불러오기가 되고 post method라면 게시글 등록하기가 될 것이고 post/{post_id} 인 경우 patch 혹은 delete method라면 해당 게시글을 수정, 삭제하는 기능과 연결시키는 것이 restful한 api가 될 것이다.

REST(representational state transfer)는 HTTP의 URL과 HTTP method(GET, POST, PUT, DELETE)를 사용하여 API 사용 가독성을 높인 구조화된 시스템 아키텍쳐(프레임워크)이고

REST의 설계 원칙으로는 서버와 클라이언트의 존재, Stateless, Uniform Interface등 다양한 조건이 존재하지만 현대 HTTP통신에서는 JSON 형식으로 데이터를 주고받기 때문에 self-descriptive의 조건을 만족하지 못해서 REST의 의도를 벗어난다고 한다.

API(Application Program Interface)는 request, response로 오가는 구조화된 데이터를 의미하고 클라이언트와 서버 간의 메신저, 매개체 역할을 통해 서로간의 데이터를 특정 형식에 맞게 전달하는 역할을 한다.

RESTful API는 이러한 RESTful의 개념과 API를 합쳐서 REST 설계 원칙을 따르는 API를 의미하며, 우리는 RESTful API를 통해 HTTP로 CRUD 등의 기능을 수행하는 API를 개발할 수 있다.

데코레이터

데코레이터는 무엇이고 왜, 어디서 사용하는지?
(분리, 재사용성, 권한관리 및 쿼리 디버깅)

장고의 MVT 패턴

MVT

컴파일러(C)와 스크립터(Python)언어의 차이?

컴파일러는 런타임 이전에 전체를 스캔하고 실행파일을 만들어서 실행하므로 속도가 빠르다. C/C++, Java가 그렇다.

인터프리터는 런타임 이후에 코드를 줄단위로 해석해서 구동한다. Python, Javascript가 이에 해당한다. 실행파일을 만들지 않으므로 메모리 효율이 좋다.

AWS

AWS 관련해서는 많이 물어보지는 않고 사용했다는 점이 중요한 것 같다.

캐싱

캐시 (cache)

  • 원래 ‘은닉처’ 라는 의미. 컴퓨터 분야에서는 ‘고속 기억장치’를 의미

캐싱

  • 자주 사용하는 데이터를 RAM과 같이 빠르게 액세스할 수 있는 곳에 저장하는 작업
  • 데이터 검색 성능을 높이는 것이 목적
  • ex) 속도가 느린 디스크의 데이터를 속도가 빠른 메모리로 가져와서 메모리 상에서 읽고 쓰는 작업

장고에서의 캐싱

  • django-redis 라이브러리 설치를 통해, Redis로 cache 사용이 가능하다

Redis란? (REmote Dictionary Server)

메모리 기반(= In-Memory) 의 key-value구조 비관계형 데이터 관리 시스템이고 별도 쿼리없이 데이터를 가져올 수 있다. 5가지 자료구조를 지원한다. < String, Set, Sorted Set, Hash, List > Redis의 위 특징에 따라 cache 역할을 할 수 있다.

프로세스와 쓰레드의 차이

프로그램이란 어떤 작업을 위해 실행할 수 있는 파일이다.
프로세스는 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램이고 Code, Data, Stack, Heap의 구조로 되어 있는 독립된 메모리 영역이다. 프로세스당 최소 1개의 스레드(메인 스레드)를 가지고 있다.
쓰레드는 프로세스 내에서 실행되는 여러 흐름의 단위이고 스레드는 프로세스 내에서 각각 Stack만 따로 할당받고 Code, Data, Heap 영역은 공유한다. 출처 - [OS] 프로세스와 스레드의 차이

DB Indexing

pk id 이외에 여러가지 자료구조를 활용하여 열에 번호를 붙여주는 것이다. insert해야하는 경우를 생각해보면 번호를 하나씩 밀어내지 않아도 되므로 시간이 덜 걸린다. 빠른 검색을 위해서도 필요하고 5배에서 10배정도 빠를 수 있다.
출처 - 안돌의 Index

인터뷰 질문 리스트

다음은 스터디 팀원들과 공유한 인터뷰 질문 리스트이다.

자기소개
개발자 전향 이유
전 직장에 관한 설명
개발자 되어서 겪었던 어려운 점
개발자로써의 목표
인간으로써의 목표
MVC 패턴
jwt 설명
restful api 설명

자기소개
개발자가 된 이유
전 직업에 관한 질문
코딩테스트 풀이 설명
틀린 이유 설명 및 다른 해결 방법
프로젝트 때 토큰 인증 방식을 채택한 이유
acess token과 fresh token의 차이
웹 개발자와 다른 개발자의 차이
MVC 패턴이 무엇인지
위코드에서 배운 것이 무엇인지
넥스클라우드에서 얻은 것은 무엇인지
현재 개인적으로 공부하고 있는 것은 무엇인지
drf serializer 에 대한 설명 및 기본적인 코드 구현 방식을 설명(함수까지…)
웹 개발자로써 발전하기 위해 노력하고 있는 부분
github repo들중에 requirement file이 없는 것에 대한 해명
넥스클라우드에서 경험한 좋은 개발문화란 무엇인지?

자기소개
개발자 전향한 이유
status code 200은 뭔가
인턴쉽에서 무엇을 했는지
프로젝트 2주정도면 짧은데 어떻게 했는지
WAAB 커뮤니티 무엇인가(스터디 이름)
회사 지원동기
어떤 회사인지 알고 있나

stack, que, decque 설명
장고 모듈화해보았나
장고 환경설정 어떻게 했나, settings에서만 했나
jwt가 무엇인가
jwt로 암호화해도 해킹당하는데 어떻게 대처할 것인가
jwt 시간제한설정은 안했나
로그인 과정 설명
HTTP 메소드 설명
REST API 설명
API 문서화는 어떻게 했나
서버다운이나 플랫폼 작동이 안되는 경우에 대해 효율적으로 방지할 수 있는 방법
클라우드에 관심이 많은가

개발자로 전향한 이유
인턴쉽 기간동안 본인이 담당한 기능은
프로젝트 짧은데 얼마나 구현한것인지
파이썬 장고로 한 이유와 자바나 자바스크립트 쓰게되면 어떻게 할 것인지
프로젝트에서 본인의 역할과 힘들때 어떻게 했는지
비즈니스 방향과 개발자로서의 가치관이 부딪힐 때 어떻게 할것인가
본인이 생각하는 근로의 의미는
배포 문제해결을 위해 어떻게 할것인가

프론트에서 왜 백엔드로 넘어왔나?
CRUD설명
Restful api 예 그려보기(put patch차이?)
ORM 장단점?
파이썬에 접근제한자있는지? __init
컴파일러/스크립터 언어 차이
ERD그려봤는지? (포폴에 사진 넣어야겠음)
SQL 쿼리문 쓸 줄 아는지?
데코레이터 왜 쓰는지?(분리, 재사용)
생성자란?
웹서버 작동 설명, 캐시서버(redis)?
AWS 어떻게 사용했는지?

자기소개
프로젝트에서 본인의 역할
인턴쉽에서 본인의 역할
위코드에서 힘들었던 점
인증과 인가란
Jwt가 무엇이고 왜 사용했는지
Open api spec이 무엇인가
Open api는 무엇인가
Restful api란
도커 어느정도로 사용해봤는지
CI/CD 경험해봤는가
CI/CD를 한다면 이를 위해 무엇을 노력할것인지
위코드 커리큘럼 관련: 프로젝트에서 백엔드만 선택한 이유
개발을 시작한 이유
개발이 재미있는지

개발자가 된 이유
개발환경구축과정 (1차부터 3차 전부) 설명
api 란? restful api?
jwt와 Oauth의 차이점 / Jwt 사용이유
serverless란? / 대표적인 serverless 제품
자료구조란? / 가장자신있는자료구조 / 구현한것들
로드밸런서란?
굳이 백엔드가 된 이유?
인턴십 후 채용되지 않은 이유

인증/인가
Jwt토큰 구성요소, 무슨 알고리즘, 단점? 어떤 해킹 당할 수 있는지?
ORM이란?
Eager loading이란?
본인이 생각하는 리팩토링이란?
유닛테스트 설명
리팩토링하고 유닛테스트했는지?
프로젝트할 때 어려웠던 점?

자료구조 데이터스트럭쳐 얼마나 아는지?
그럼 해시 트리 그런걸로 직접 코드 짜봤는지?
자바로 제일 길게 짠 코드가 몇 줄인지?
ERD짜고 설계같은거 생각해봤는지?
파이썬 느린 이유 컴파일러언어 개념?

깃헙레포 리드미하고 기술블로그를 다같이 쭉 보면서 물어보셨습니다
개발자 왜 하고싶은지?
전에 회사 짧게 그만두었던 이유?
백엔드 공부가 프론트에 비해서 어려운데 잘 하려면 어떻게 해야할지?
본인이 개발을 잘 하는 것 같은가? 해보니까 적성에 맞는지?