루나의 TIL 기술 블로그

프로그래머스 level 1 숫자 문자열과 영단어

|

걸린 시간 : 1시간

문제 설명

네오와 프로도가 숫자놀이를 하고 있습니다. 네오가 프로도에게 숫자를 건넬 때 일부 자릿수를 영단어로 바꾼 카드를 건네주면 프로도는 원래 숫자를 찾는 게임입니다.

다음은 숫자의 일부 자릿수를 영단어로 바꾸는 예시입니다.

1478 → “one4seveneight”
234567 → “23four5six7”
10203 → “1zerotwozero3”
이렇게 숫자의 일부 자릿수가 영단어로 바뀌어졌거나, 혹은 바뀌지 않고 그대로인 문자열 s가 매개변수로 주어집니다. s가 의미하는 원래 숫자를 return 하도록 solution 함수를 완성해주세요.

입출력 예

s result
"one4seveneight" 1478
"23four5six7" 234567
"2three45sixseven" 234567

사고 과정

해당 문자열이 있다면 숫자로 바꾸어준다.

제출 코드

def solution(s):
    words = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
    for word in words:
        if word in s:
            s = s.replace(word, str(words.index(word)))
            #문자 : 숫자를 딕셔너리로 짝지으려고 했는데 문자열이 의미하는 숫자가 인덱스값과 같아서 그냥 인덱스값을 넣었다.
    return int(s)

모범 답안

num_dic = {"zero":"0", "one":"1", "two":"2", "three":"3", "four":"4", "five":"5", "six":"6", "seven":"7", "eight":"8", "nine":"9"}

def solution(s):
    answer = s
    for key, value in num_dic.items():
        answer = answer.replace(key, value)
    return int(answer)

주요 포인트 및 생각해볼 점

모범답안은 포문을 한 번만 돌아서 빠르고, 어떤 문자열을 어떤 문자열로 바꿀지 지정할 수 있어서 바람직하다.
딕셔너리를 사용해서 반복문은 줄이는 것을 익혀야겠다.

파이썬 - 진수 변환 함수

|

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

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는 원래 하던 것에서 가져와서 금방했는데 대댓글을 구현하는 부분이 어려웠어서 태우님께서 저 기억에 남는 코드를 작성해주셨다.

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