루나의 TIL 기술 블로그

위코드 수료 후 백엔드 면접 후기 및 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짜고 설계같은거 생각해봤는지?
파이썬 느린 이유 컴파일러언어 개념?

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

장고 게시판 CRUD 복습

|

게시판 CRUD하는 것을 다시 해보았다. 모든 코드는 깃 레포에서 확인할 수 있다. 게시판CRUD

장고 프로젝트 시작

  • Miniconda 가상환경 생성 및 가상환경 activate
conda create -n 가상환경이름 python=3.8
conda activate 가상환경이름
conda info --envs #설정한 가상환경 리스트 확인

데이터베이스 생성

mysql -u root -p #마이sql시작
show databases; --mysql의 모든 db들 보여주기
use [데이터베이스이름]; --사용할 db골라주기
create database [데이터베이스이름] character set utf8mb4 collate utf8mb4_general_ci; --데이터베이스 생성 및 utf8설정(한중일)
show tables; --db안의 모든 테이블들 보여주기

mysql 나가기 키는 \q 컴퓨터에 mysql응용프로그램이 켜져있으면 터미널로 열리지 않을 수 있다.

프로젝트 시작을 위한 python package 설치

pip install 패키지이름 #파이썬 패키지 설치

pip install Django #장고 설치
pip install mysqlclient # mysqlclient설치 (mysql 먼저 설치 필요!!!)
pip install django-cors-headers #CORS 해결을 위한 패키지

pip freeze #가상환경 패키지 리스트 확인

장고 프로젝트 생성

django-admin startproject 프로젝트이름 . #장고 프로젝트 생성
cd 프로젝트이름 #프로젝트 디렉토리로 들어감
python manage.py startapp 앱이름 #앱 생성(manage.py가 존재하는 디렉토리에서)

이때 프로젝트 다음에 점 기호(.)가 있음에 주의하자. 점 기호는 현재 디렉터리를 의미한다. 위 명령의 의미는 현재 디렉터리를 기준으로 프로젝트를 생성하겠다는 의미이다.

.을 안 쓰면 현재 디렉터리 밑에 같은 이름의 앱 디렉터리가 생성되어 mysite/mysite와 같은 구조가 되어 버린다.

settings.py 설정

from pathlib        import Path #기존에 settings.py 에 있는 코드
from my_settings   import DATABASES, SECRET_KEY #my_settings.py에서 가져와야한다

#시크릿 키와 데이터베이스 변수는 my_settings파일을 만들어서 갈음한다.
SECRET_KEY = SECRET_KEY # 기존의 시크릿키 변수 삭제 후 대체
DATABASES = DATABASES # 기존의 데이터베이스 변수 삭제 필수!

ALLOWED_HOSTS = ['*'] #수정 : 모두 접속 가능

APPEND_SLASH = False #추가 : 슬래시 자동으로 삽입하지 않음

INSTALLED_APPS = [
    # 'django.contrib.admin', #admin도 
    # 'django.contrib.auth', #login auth도 직접 만들어쓸 예정이다
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders', # corsheaders 추가
    'products', #앱을 새로 만들면 여기에 추가해야한다
]
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware', csrf 주석처리
    # 'django.contrib.auth.middleware.AuthenticationMiddleware', auth 주석처리
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'corsheaders.middleware.CorsMiddleware', #corsheaders middleware 추가
]

# 데이터베이스 부분 삭제
# 뒷부분 생략

##CORS 부분 추가 
CORS_ORIGIN_ALLOW_ALL=True
CORS_ALLOW_CREDENTIALS = True

CORS_ALLOW_METHODS = (
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
)

CORS_ALLOW_HEADERS = (
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'x-requested-with',
	#만약 허용해야할 추가적인 헤더키가 있다면?(사용자정의 키) 여기에 추가하면 됩니다.
)

위와 같이 settings.py를 추가 및 수정한다.

my_setting.py 파일 추가

시크릿 키와 디비정보가 깃에 공개적으로 노출되지 않도록 빼기 위해서
manage.py있는 디렉토리에 my_setting.py라는 새 파일을 만들어서 추가한다.

#클라우드에 올리지 않는, 키를 보관하는 파일
DATABASES = {
    'default' : {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'westarbucks_db',
        'USER': 'root',
        'PASSWORD': '', #원하는 db 비밀번호
        'HOST': '127.0.0.1', #데이터베이스의 IP주소, 이건 각자의 컴퓨터
        'PORT': '3306',
    }
}
SECRET_KEY ='_9^=58g2r*r(6@q7ugn!fxg-!fo48$7af9i4yn9-1$+...'
#setting.py에 있던 시크릿 키를 붙여넣음

urls.py수정

from django.urls import path

urlpatterns = [
]

gitignore추가 (manage.py가 존재하는 디렉토리에)

소스를 공유하기 위해 깃을 사용하지만 올리고 싶은것 올리고 싶지 않은것, 올려서는 안되는 것들이 존재하고 이를 구분하기 위해 깃이 설치된 디렉토리에 .gitignore파일을 생성해서 관리해야 한다.

gitignore.io

위의 사이트에서 사용하는 환경에 해당하는 키워드를 선택하면, 자동으로 .gitignore 파일에 정의할 요소들을 생성해준다.

python,pycharm,visualstudiocode,vim,macos,linux,zsh

이 키워드들을 추가해서 파일을 만들고 마지막에 my_settings.py도 추가해준다.

보안관련파일과 크롤링파일

my_settings.py (보안 관련 파일은 github에 업로드되면 안된다.)

git ignore 펼쳐서 보기/접기

#보안관련파일과 크롤링파일을 위해서 추가하는 부분
my_settings.py
*.csv 
#아래부터 끝까지는 자동생성된 부분

# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode,vim,macos,linux,zsh
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,visualstudiocode,vim,macos,linux,zsh

### Linux

\*~

# temporary files which can be created if a process still has a handle open of a deleted file

.fuse_hidden\*

# KDE directory preferences

.directory

# Linux trash folder which might appear on any partition or disk

.Trash-\*

# .nfs files are created when an open file is removed but is still being accessed

.nfs\*

### macOS

# General

.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r

Icon

# Thumbnails

.\_\*

# Files that might appear in the root of a volume

.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share

.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### PyCharm

# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider

# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff

.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/\*\*/shelf

# AWS User-specific

.idea/\*\*/aws.xml

# Generated files

.idea/\*\*/contentModel.xml

# Sensitive or high-churn files

.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/\*\*/dbnavigator.xml

# Gradle

.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import

# When using Gradle or Maven with auto-import, you should exclude module files,

# since they will be recreated, and may cause churn. Uncomment if using

# auto-import.

# .idea/artifacts

# .idea/compiler.xml

# .idea/jarRepositories.xml

# .idea/modules.xml

# .idea/\*.iml

# .idea/modules

# \*.iml

# \*.ipr

# CMake

cmake-build-\*/

# Mongo Explorer plugin

.idea/\*\*/mongoSettings.xml

# File-based project format

\*.iws

# IntelliJ

out/

# mpeltonen/sbt-idea plugin

.idea_modules/

# JIRA plugin

atlassian-ide-plugin.xml

# Cursive Clojure plugin

.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)

com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client

.idea/httpRequests

# Android studio 3.1+ serialized cache file

.idea/caches/build_file_checksums.ser

### PyCharm Patch

# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# \*.iml

# modules.xml

# .idea/misc.xml

# \*.ipr

# Sonarlint plugin

# https://plugins.jetbrains.com/plugin/7973-sonarlint

.idea/\*\*/sonarlint/

# SonarQube Plugin

# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin

.idea/\*\*/sonarIssues.xml

# Markdown Navigator plugin

# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced

.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/\*\*/markdown-navigator/

# Cache file creation bug

# See https://youtrack.jetbrains.com/issue/JBR-2257

.idea/$CACHE_FILE$

# CodeStream plugin

# https://plugins.jetbrains.com/plugin/12206-codestream

.idea/codestream.xml

### Python

# Byte-compiled / optimized / DLL files

**pycache**/
_.py[cod]
_$py.class

# C extensions

\*.so

# Distribution / packaging

.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
_.egg-info/
.installed.cfg
_.egg
MANIFEST

# PyInstaller

# Usually these files are written by a python script from a template

# before PyInstaller builds the exe, so as to inject date/other infos into it.

_.manifest
_.spec

# Installer logs

pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports

htmlcov/
.tox/
.nox/
.coverage
.coverage._
.cache
nosetests.xml
coverage.xml
_.cover
\*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations

_.mo
_.pot

# Django stuff:

\*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:

instance/
.webassets-cache

# Scrapy stuff:

.scrapy

# Sphinx documentation

docs/\_build/

# PyBuilder

.pybuilder/
target/

# Jupyter Notebook

.ipynb_checkpoints

# IPython

profile_default/
ipython_config.py

# pyenv

# For a library or package, you might want to ignore these files since the code is

# intended to run in multiple environments; otherwise, check them in:

# .python-version

# pipenv

# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.

# However, in case of collaboration, if having platform-specific dependencies or dependencies

# having no cross-platform support, pipenv may install dependencies that don't work, or not

# install all needed dependencies.

#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow

**pypackages**/

# Celery stuff

celerybeat-schedule
celerybeat.pid

# SageMath parsed files

\*.sage.py

# Environments

.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings

.spyderproject
.spyproject

# Rope project settings

.ropeproject

# mkdocs documentation

/site

# mypy

.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker

.pyre/

# pytype static type analyzer

.pytype/

# Cython debug symbols

cython_debug/

### Vim

# Swap

[._]_.s[a-v][a-z]
!_.svg # comment out if you don't need vector files
[._]\*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]

# Session

Session.vim
Sessionx.vim

# Temporary

.netrwhist

# Auto-generated tag files

tags

# Persistent undo

[._]\*.un~

### VisualStudioCode

.vscode/_
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
_.code-workspace

# Local History for Visual Studio Code

.history/

### VisualStudioCode Patch

# Ignore all local history of files

.history
.ionide

### Zsh

# Zsh compiled script + zrecompile backup

_.zwc
_.zwc.old

# Zsh completion-optimization dumpfile

_zcompdump_

# Zsh zcalc history

.zcalc_history

# A popular plugin manager's files

.\_zinit
.zinit_lstupd

# zdharma/zshelldoc tool's files

zsdoc/data

# robbyrussell/oh-my-zsh/plugins/per-directory-history plugin's files

# (when set-up to store the history in the local directory)

.directory_history

# MichaelAquilina/zsh-autoswitch-virtualenv plugin's files

# (for Zsh plugins using Python)

# Zunit tests' output

/tests/\_output/\*
!/tests/\_output/.gitkeep

# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode,vim,macos,linux,zsh

실행

pythong manage.py runserver

requirements.txt 추가

pip freeze > requirements.txt

이렇게 manage.py가 있는 디렉토리에 설치된 라이브러리의 버전을 명시해준다.

Django==3.2.6
django-cors-headers==3.7.0
mysql-client==0.0.1
PyMySQL==1.0.2 #맥 M1의 경우 설치한 파일

장고에서 자동으로 설치되는 것을 제외하고 직접 설치한 것들만 남겨주면 좋다.

디렉토리 구조

(참고) 프로젝트 디렉토리 구조 구조
└── wantedxwecode(mysite)
    ├── manage.py
    ├── my_settings.py
    ├── READEME.md
    ├── requirements.txt
    └── wantedxwecode(mysite)
        ├── \__init__.py
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py
    └── wanted(myapp)
        ├── \__init__.py
        ├── admin.py
        ├── apps.py
        ├── models.py
        ├── urls.py(추가)
        ├── decorator.py(추가)
        ├── tests.py
        └── views.py

Model.py

필요한 게시판의 데이터 관계도를 만들고 새로 생성된 앱의 models.py에 아래 migration을 넣어서 데이터베이스 토대를 만들어준다.

#project > wanted > models.py
from django.db import models

class User(models.Model):
    name         = models.CharField(max_length=40, null=True)
    email        = models.EmailField(max_length=200, unique=True)
    password     = models.CharField(max_length=200)

    class Meta:
        db_table = 'users'

class Post(models.Model): 
    user        = models.ForeignKey(User, on_delete=models.CASCADE, db_column='user_id')
    text        = models.CharField(max_length=300)
    created_at  = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        db_table = 'posts'

migration 만들고 migrate 실행하기

python manage.py makemigrations 
#모델의 변경사항을 파악, 설계도 작성(하고나서 만들어진 파일을 잘 살펴본다)
python manage.py showmigrations 
#현재 migrations가 어떤 상태인지 살펴보기
python manage.py sqlmigrate [앱이름] [마이그레이션번호]
#실제 데이터베이스에 전달되는 SQL 쿼리문을 확인
python manage.py migrate 
#자동으로 migration을 실행

migration을 만드는 것과 migrate는 각각 클래스에 맞게 설계도를 만들고 설계한대로 데이터베이스를 건설하겠다는 뜻이라고 할 수 있다.

SQL

select * from django_migrations; --장고 마이그레이션 보여주기
desc posts; --포스트 테이블이 잘 만들어졌는지 보여준다

View.py 회원가입, 로그인 구현

#mysite > users > views.py
import json, re, bcrypt, jwt

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

from users.models         import User  

from my_settings          import SECRET_KEY, const_algorithm

class SignUp(View):
    def post(self, request):
        try:
            data            = json.loads(request.body)
            email           = data['email']
            password        = data['password']
            hashed_password = bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt())

            if User.objects.filter(email=email).exists():
                return JsonResponse({"MESSAGE": "EMAIL_ALREADY_EXIST"}, status=400)

            if not re.match(r"^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", email):
                return JsonResponse({"MESSAGE": "INVALID_FORMAT"}, status=400)

            if not re.match(r"^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$", password):
                return JsonResponse({"MESSAGE": "INVALID_FORMAT"}, status=400)

            User.objects.create(
                name     =   data.get('name'), #선택적으로 입력받을 때
                email    =   email,
                password =   hashed_password.decode('UTF-8'),
            )
            return JsonResponse({"MESSAGE": "SUCCESS"}, status=201)

        except KeyError:
            return JsonResponse({"MESSAGE": "KEY_ERROR"}, status=400)

class SignIn(View):
    def post(self, request):
        try:
            data     = json.loads(request.body)      
            email    = data['email']
            password = data['password']        

            if not User.objects.filter(email = email).exists():
                return JsonResponse({'MESSAGE':'INVALID_VALUE'}, status = 401)

            if bcrypt.checkpw(password.encode('utf-8'),User.objects.get(email=email).password.encode('utf-8')):
                token = jwt.encode({'id':User.objects.get(email=email).id}, SECRET_KEY)
            
                return JsonResponse({'TOKEN': token}, status = 200)

            return JsonResponse({'MESSAGE':'INVALID_USER'}, status=401)

        except KeyError:
            return JsonResponse({'MESSAGE':'KEY_ERROR'}, status = 400)

View.py 게시글 기능 구현

import json, re, bcrypt, jwt

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

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

from .models               import User
from .models               import Post as PostModel

from my_settings           import SECRET_KEY

from wanted.decorator      import login_decorator

class Post(View):
    @login_decorator
    def post(self, request):
        try:
            data = json.loads(request.body)
            user = request.user

            PostModel.objects.create( #디비에 값을 추가
                user_id  = user.id, #요청을 수행하는 유저의 아이디 
                text     = data["text"]#입력받은 값
            )
            return JsonResponse({"message": "SUCCESS"}, status=201)

        except KeyError:
            return JsonResponse({"message": "KEY_ERROR"}, status=400)

    def get(self, request):
            post_list = PostModel.objects.all().order_by('id')
            paginator = Paginator(post_list, 3)
            # 한 페이지당 오브젝트 3개씩 나오게 설정
            page      = int(request.GET.get('page',1))
            # page라는 값으로 받을거고, 없으면 첫번째 페이지로

            try:
                posts = paginator.page(page)
            except PageNotAnInteger:
                posts = paginator.page(1)
            except EmptyPage:
                posts = paginator.page(paginator.num_pages)

            results = []

            results.append([{
                        "post_id"    : post.id,
                        "user_id"    : post.user_id,#글 객체의 유저아이디
                        "text"       : post.text,
                        "created_at" : post.created_at,
                    } for post in posts ])
            return JsonResponse({"page" : page, "results": results}, status=200)

class PostModify(View):
    @login_decorator
    def patch(self,request, post_id):
        try:
            data = json.loads(request.body)
            post = PostModel.objects.get(id=post_id)
            
            if post.user_id == request.user.id : #요청하는 유저가 글 쓴 사람이라면
                PostModel.objects.filter(id=post_id).update( 
                    text     = data["text"]
                )
                return JsonResponse({"message": "SUCCESS"}, status=201)
            else:
                return JsonResponse({"message": "NOT_AUTHORIZED"}, status=403)
        except KeyError:
            return JsonResponse({"message": "KEY_ERROR"}, status=400)
    
    @login_decorator
    def delete(self,request, post_id):
        try:
            post = PostModel.objects.get(id=post_id)
            if post.user_id == request.user.id:
                post.delete()
                return JsonResponse({"message": "SUCCESS"}, status=201)
            else:
                return JsonResponse({"message": "NOT_AUTHORIZED"}, status=403)
        except KeyError:
            return JsonResponse({"message": "KEY_ERROR"}, status=400)

로그인 데코레이터

import jwt

from django.http            import JsonResponse
from django.core.exceptions import ObjectDoesNotExist

from my_settings            import SECRET_KEY
from .models                import User

def login_decorator(func):
    def wrapper(self, request, *args, **kwargs):
        #들어올 수 있는 인자를 모두 받도록 한다
        try:
            token         = request.headers.get("Authorization", None)
            #헤더에서 Authorization(헤더에 있는 속성)을 가져와서 토큰에 저장한다.
            user          = jwt.decode(token, SECRET_KEY, algorithms='HS256')
            #토큰을 시크릿키를 이용해 디코드해서 유저 아이디를 알아내서 유저에 저장한다.
            request.user  = User.objects.get(id = user['id'])
            #유저의 아이디에 해당하는 유저객체를 리퀘스트.유저에 저장한다.

            return func(self, request, *args, **kwargs)
            #받은 인자들을 모두 전달해준다(예를 들어 이미지, 텍스트 등등)

        except jwt.exceptions.DecodeError:
            return JsonResponse({"message" : "INVALID_TOKEN"}, status=400)

        except ObjectDoesNotExist:
            return JsonResponse({"message" : "INVALID_USER"}, status=400)

    return wrapper

4. Urls.py 작성

클라이언트의 요청을 받아서 게시판 뷰를 호출할 수 있도록 urls.py 를 작성해야합니다.

#상위 프로젝트 폴더의 urls.py
from django.urls import path,include

urlpatterns = [
    path("",include("wanted.urls")),
]

#하위 post앱 폴더의 urls.py
from django.urls import path
from .views      import Post, PostModify, SignUp, SignIn

urlpatterns = [
    path("post", Post.as_view()),
    path("post/<int:post_id>", PostModify.as_view()),
    path("signup", SignUp.as_view()),
    path("signin", SignIn.as_view()),
]

POSTMAN에서 회원가입으로 아이디를 하나 만들고 (비밀번호 유효성 검사 8자이상, 알파벳, 숫자 포함 필요) 글을 작성해보면 맨 위 그림과 같이 잘 작동되는 것을 볼 수 있다.

프로그래머스 level 2 타겟넘버 BFS

|

문제 설명

n개의 음이 아닌 정수가 있습니다. 이 수를 적절히 더하거나 빼서 타겟 넘버를 만들려고 합니다. 예를 들어 [1, 1, 1, 1, 1]로 숫자 3을 만들려면 다음 다섯 방법을 쓸 수 있습니다.

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

사용할 수 있는 숫자가 담긴 배열 numbers, 타겟 넘버 target이 매개변수로 주어질 때 숫자를 적절히 더하고 빼서 타겟 넘버를 만드는 방법의 수를 return 하도록 solution 함수를 작성해주세요.

제한 조건

주어지는 숫자의 개수는 2개 이상 20개 이하입니다.
각 숫자는 1 이상 50 이하인 자연수입니다.
타겟 넘버는 1 이상 1000 이하인 자연수입니다.

입출력 예

numbers target return
[1,1,1,1,1] 3 5

사고 과정

타겟넘버

0으로 시작해서 다음 수를 더하거나 빼는 경우의 수를 가지 2개로 뻗어서 계속 가지를 뻗어나가는 결과를 리스트에 보관했다가 마지막에 count로 원하는 결과값의 갯수를 세어준다.

제출 코드

def solution(numbers, target):
    sup = [0]
    #전체 노드의 합을 더해준 리스트를 super라고 하고 초기화해준다
    for i in numbers:
        # i가 numbers의 원소 길이만큼 반복하면서 sub이라는 배열을 생성한다
        sub = []
        for j in sup:
            # j가 sup의 원소만큼 반복한다. 그러니까 +인 경우, -인 경우에 대해서 반복한다  
            sub.append(j+i)
            sub.append(j-i)
            #그 경우에 대한 값을 sub에 넣어둔다 
        sup = sub
        #하나의 노드에 대한 탐색이 끝나면 반복이 끝나서 sub에서 계산한 값을 sup으로 덮어둔다
        #이렇게 2중 반복을 마치게되면 sup에는 모든 경우에 대해서 +인 경우, -인 경우를 조합한 합을 가지게 된다
    return sup.count(target)

queue 답안

from collections import deque
#collections 모듈의 deque는 double-ended queue의 약자로 데이터를 양방향에서 추가하고 제거할 수 있는 자료 구조이다.
 
def solution(numbers, target):
    answer = 0
    queue = deque() #queue 생성
    
    length = len(numbers)
    queue.append([-numbers[0], 0])
    queue.append([+numbers[0], 0])
    
    while queue :
        num, i = queue.popleft()
        #popleft()는 리스트의 첫 번째 데이터를 제거
        if i+1 == length :
            if num == target : answer += 1
        else :
            queue.append([num - numbers[i + 1], i + 1])
            queue.append([num + numbers[i + 1], i + 1])
    
    return answer

참고

배우고 정리하기 - level 2 - 타겟 넘버 ( python ) by 아뜨으츄 아뜨으츄

그럼에도 불구하고 - 코딩테스트 고득점 Kit DFS/BFS1 타겟넘버

파이썬에서 큐(queue) 자료 구조 사용하기

BFS/DFS 설명하는 예전 내 블로그 글

프로그래머스 level 2 가장 큰 수, 정렬

|

문제 설명

0 또는 양의 정수가 주어졌을 때, 정수를 이어 붙여 만들 수 있는 가장 큰 수를 알아내 주세요.

예를 들어, 주어진 정수가 [6, 10, 2]라면 [6102, 6210, 1062, 1026, 2610, 2106]를 만들 수 있고, 이중 가장 큰 수는 6210입니다.

0 또는 양의 정수가 담긴 배열 numbers가 매개변수로 주어질 때, 순서를 재배치하여 만들 수 있는 가장 큰 수를 문자열로 바꾸어 return 하도록 solution 함수를 작성해주세요.

제한 조건

numbers의 길이는 1 이상 100,000 이하입니다.
numbers의 원소는 0 이상 1,000 이하입니다.
정답이 너무 클 수 있으니 문자열로 바꾸어 return 합니다.

입출력 예

numbers return
[6,10,2] [3,30,34,5,9]
"6210" "9534330"

사고 과정

앞자리를 비교해서 큰 순서대로 새 리스트에 넣어주고 앞자리가 같은 경우에는 전체 숫자를 비교해서 큰 순서대로 넣어준다.

def solution(numbers):
    arr={}
    answer = []
    #맨 앞 숫자를 따서 큰 순서대로 답에 넣어주고 후보리스트에서 제거
    for number in numbers:
        arr.append(number,(str(number)[0]))
        answer.append(max(arr))
        numbers.remove(int(max(arr)))
        #숫자가 같으면 나머지 전체 숫자를 크기대로 정렬해서 답에 넣어주기
        if numbers.count(str(number[0])) > 1:
            numbers = sorted(numbers, reverse=True)
            answer.append(max(arr))
            numbers.remove(int(max(arr)))
    answer.join('')
    return answer

solution([3,30,34,5,9])

이렇게 짜니까 어떤 수가 어떤 수의 앞자리인 지 알 수 있어야해서 {맨 앞자리수 : 원래 숫자} 이런 식으로 딕셔너리로 리스트에 넣어줘야할 것 같았다.

그렇게 하지 않고 숫자의 크기를 비교할 수 없을까 생각하다가 검색을 해봤는데 다들 숫자가 최대 3자리 수라는 점을 이용해서 원래 숫자를 세 번 반복한 뒤에 문자열끼리 비교하면 아스키코드값에서 첫번째 인덱스 값으로 비교한다는 점을 이용해서 숫자의 크기를 비교하는 방법을 사용한 것을 볼 수 있었다.

def solution(numbers):
    answer = []
    new_nums =[]
    for num in numbers:
        new_nums.append(str(num)*3)
    new_nums = sorted(new_nums, reverse=True)
    print(new_nums) # ['999', '555', '343434', '333', '303030']

    for number in new_nums:
        answer.append(str(number)/3)

    return answer

solution([3,30,34,5,9])

그렇게 혼자 코드를 짜보려고 했는데 3번 반복해서 썼던 수를 다시 원래대로 돌릴 수가 없었다..!

제출 답안

def solution(num): 
    num = list(map(str, num)) 
    num.sort(key = lambda x : x*3, reverse = True) 
    return str(int(''.join(num)))

그래서 다른 블로그에 쓰인대로 람다를 사용했다.
개발개발 울었다에 나와있는 설명을 참고하자면 아래와 같다.

  • int형의 list를 map을 사용하여 string으로 치환한 뒤, list로 변환한다.
  • 변환된 num을 sort()를 사용하여 key 조건에 맞게 정렬한다.
  • lambda x : x3은 num 인자 각각의 문자열을 3번 반복한다는 뜻이다. x3을 하는 이유? -> num의 인수값이 1000 이하이므로 3자리수로 맞춘 뒤, 비교하겠다는 뜻. 이 문제의 핵심이라고 할 수 있다.
  • 문자열 비교는 ASCII 값으로 치환되어 정렬된다. 따라서 666, 101010, 222의 첫번째 인덱스 값으로 비교한다. 6 = 86, 1 = 81, 2 = 82 이므로 6 > 2 > 1순으로 크다.
  • sort()의 기본 정렬 기준은 오름차순이다. reverse = True 전의 sort된 결과값은 10, 2, 6이다.
  • 이를 reverse = True를 통해 내림차순 해주면 6,2,10이 된다. 이것을 ‘‘.join(num)을 통해 문자열을 합쳐주면 된다.
  • int로 변환한 뒤, 또 str로 변환해주는 이유? 모든 값이 0일 때(즉, ‘000’을 처리하기 위해) int로 변환한 뒤, 다시 str로 변환한다. 출처 : 개발개발 울었다

Django에서 API문서 관리 Swagger 사용법

|

스웨거 예시

스웨거

스웨거 설치

django-rest-swagger 패키지는 더이상 관리안해서 drf-yasg를 쓰도록 추천한다고 한다.
출처: https://hyeonyeee.tistory.com/66 [hyeoneee’s blog]

pip install -U drf-yasg

파일 설정

INSTALLED_APPS = [
   ...
   'drf_yasg',
   ...
]

‘django.contrib.auth’ 부분 주석처리했으면 풀어주기

urls.py

#urls.py
from django.urls import path, re_path, include

from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg       import openapi, generators

class BothHttpAndHttpsSchemaGenerator(generators.OpenAPISchemaGenerator):
    def get_schema(self, request=None, public=False):
        schema = super().get_schema(request, public)
        schema.schemes = ["http", "https"]
        return schema

urlpatterns = [
    path('users', include('users.urls')),
    path('applications', include('applications.urls')),
    path('recruits', include('recruits.urls')),
    path('', include('helloworld.urls')),
]

schema_view = get_schema_view(
    openapi.Info(
        title            = "******** API", #타이틀
        default_version  = "v1", #버전
        description      = "******** API 문서", #설명
        license          = openapi.License(name=""),
    ),
    public             = True,
    permission_classes = (permissions.AllowAny,),
    generator_class    = BothHttpAndHttpsSchemaGenerator,
)

urlpatterns += [
    re_path(r'swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name="schema-json"),
    re_path(r'swagger', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
    re_path(r'redoc', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]

뷰에 데코레이터 작성하기

class SuperAdminView(APIView):
    parameter_token = openapi.Parameter (
                                        "Authorization", 
                                        openapi.IN_HEADER, 
                                        description = "access_token", 
                                        type        = openapi.
                                        TYPE_STRING
    )
    supaeradmin_get_response = openapi.Response("result", SuperadminGetSerializer)
    #이 부분에 대한 설명 : https://swagger.io/docs/specification/describing-parameters/

    @swagger_auto_schema (
            manual_parameters = [parameter_token],
            responses = {
                "200": supaeradmin_get_response,
                "400": "BAD_REQUEST",
                "401": "INVALID_TOKEN"
            },
            operation_id = "(슈퍼관리자 전용)어드민 정보 조회",
            operation_description = "header에 토큰이 필요합니다."
        )
    #http://localhost:8000/swagger-ui 상에서 보여질 부분

    @superadmin_only
    def get(self, request):    

        result = [{
            "id"        : admin.id,
            "email"     : admin.email,
            "name"      : admin.name if admin.name else admin.email.split('@')[0],
            "created_at": admin.created_at,
            "updated_at": admin.updated_at,
        } for admin in User.objects.filter(role='admin') ]

        return JsonResponse({"result": result}, status=200)
    #실제 함수

이렇게 하면 서버를 구동했을 때 스웨거 페이지(http://localhost:8000/swagger-ui) 상에 표시가 되고 페이지 상에서 데이터를 입출력!!!해볼 수 있다.

스웨거

배포가 되면 스웨거가 이렇게 오픈이 되서 서버구동 없이도 url을 통해서 아무나 접근할 수도 있다.
https://api-we.stockfolio.ai/swagger

로그인 - jwt, Redis, stateless vs stateful, cookie vs session

|

회원가입/로그인이 이루어질 때 jwt를 발급하고 이를 통해 유저를 알아본다는 것까지 학원에서 실습 및 구현을 했었다. 기업협업 중에 admin/superadmin 데코레이터를 만드는데 pay_load에 넣은 role이 반영되지 않아서 하루동안 고민했는데 사수분께서 pay_load에 뭔가 새로 추가했으면 엑세스토큰을 다시 발급받았어야된다면서 jwt의 구성요소에 대해서 설명해주고 아래 개념들에 대해서 공부해보라고 하셨다.

참고로 첨부하는 admin_only 데코레이터

def admin_only(func):
    def wrapper(self, request, *args, **kwargs):
        try:
            access_token = request.headers.get('Authorization')
            pay_load     = jwt.decode(access_token, SECRET_KEY, algorithms=[ALGORITHM])
            role         = pay_load['role']
            user         = User.objects.get(id=pay_load['user_id'])
            request.user = user
            
            if not role == 'admin':
                return JsonResponse({'message': 'UNAUTHORIZED'}, status=401)

            return func(self, request, *args, **kwargs)

        except jwt.InvalidTokenError:
            return JsonResponse({'message': 'INVALID_TOKEN'}, status=401)
        except jwt.exceptions.DecodeError:
            return JsonResponse({'message': 'DECODE_ERROR'}, status=400)
        except jwt.ExpiredSignatureError:
            return JsonResponse({'message': 'EXPIRED_TOKEN'}, status=401)
        except User.DoesNotExist:
            return JsonResponse({'message': 'USER_DOES_NOT_EXISTS'}, status=401)
        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)

jwt

JWT에 대해서 알고자 한다면 Auth0에서 만든 JWT 사이트가 참고하기 제일 좋다. 이 사이트에서 JWT 토큰을 테스트해보거나 구조도 파악해 볼 수 있고 언어별로 추천 라이브러리와 지원상태를 한눈에 볼 수 있어서 믿고 쓸 수 있고 JWT에 대한 보안 취약점도 잘 보고되고 있다.

jwt구성요소
https://jwt.io/

마침표를 구분자로 세 부분으로 나뉘어져있고 header에는 타입과 jwt signing 알고리즘(이 방법으로 토큰을 생성하겠다), 페이로드에는 담아서 전달하고 싶은 데이터가 들어있고 마지막으로 세트가 변조되었는지 확인하는 부분인 Signature이 있다.

JOSE 헤더와 JWT Claim Set를 base64로 인코딩해서 만든 두 값을 마침표(.)로 이어 붙이고 지정한 알고리즘 HS256으로 인코딩한 것이 JWT 토큰의 세 번째 부분인 Signature이다. 출처 : 아웃사이더의 데브 스토리

헤더와 JWT Claim Set은 암호화를 한 것이 아니라 단순히 JSON문자열을 base64로 인코딩한 것뿐이다. 그래서 누구나 이 값을 다시 디코딩하면 JSON에 어떤 내용이 들어있는지 확인할 수 있다. base64를 디코딩해보고 싶을 때는 아래에서 해봐도 된다. https://www.base64decode.org/

stateless vs stateful

서버가 클라이언트(사용자)의 정보를 저장하느냐 아니냐에 따라 stateful(상태유지)와 stateless(무상태)로 나눌 수 있다. 이 블로그의 자전거를 구매하는 대화의 예시를 통해 무상태와 상태유지의 차이를 쉽게 알 수 있다!

예를 들어서 stateless(무상태)는 서버가 아무것도 기억하지 않고 stateful(상태유지)는 클라이언트가 전에 했던 요청을 서버가 다 기억하고 있는 것이다. 무상태는 클라이언트의 요청으로 더 많은 데이터를 소모하지만 서버의 확장성이 높다. 상대적으로 특별한 일이 없다면 무상태를 지향해야한다.

HTTP는 statelss, connectless 즉 클라이언트의 요청을 저장하지 않고 연결된 상태로 있지 않는다. 무상태를 디폴트로 로그인을 유지하는 등의 기능이 필요할 때 쿠키와 세션을 사용한다.

헨젤과 그레텔이 쿠키를 조금씩 떨어뜨려서 길을 찾았다는 이야기로부터 이름을 따온 쿠키는 클라이언트에 저장된 사용자의 정보를 의미한다. 크롬의 자물쇠 표시를 누르면 저장된 쿠키들을 볼 수 있다. + 크롬의 시크릿모드는 창을 끄는 순간 쿠키가 지워진다.

2000년대 초반까지도 쿠키정보가 암호화되지 않아서 해킹에 취약했다고 한다. 그래서 생겨난 것이 정보를 웹 서버에 저장하는 세션이다. + 브라우저가 종료되면 세션도 만료된다.

쿠키

쿠키

  • 이름, 값, 만료일(저장 기간 설정), 경로 정보로 구성되어 있다.
  • 클라이언트에 총 300개의 쿠키를 저장할 수 있다, 도메인 당 20개의 쿠키를 가질 수 있다
  • 하나의 쿠키는 4KB(=4096byte)까지 저장 가능하다.

쿠키 사용 예시로는 방문했던 사이트에 다시 방문 하였을 때 아이디와 비밀번호 자동 입력, “오늘 이 창을 다시 보지 않기” 체크가 있다.

브라우저가 Request를 보낼 때 함께 보내는 쿠키의 값들은 장고에서 request.COOKIE 객체를 통해 접근 가능하다.

쿠키와 세션의 작동 방식

쿠키와 세션

세션

  • 웹 서버에 웹 컨테이너의 상태를 유지하기 위한 정보를 디비나 메모리에 저장한다.
  • 브라우저를 닫거나, 서버에서 세션을 삭제했을때만 삭제가 되므로 쿠키보다 보안이 좋다. 서버의 리소스를 쓰므로 비용이 있다.
  • 각 클라이언트 고유 Session ID를 부여한다, Session ID로 클라이언트를 구분하여 각 클라이언트 요구에 맞는 서비스 제공한다.

화면이 이동해도 로그인이 풀리지 않고 로그아웃하기 전까지 유지하는 것이 세션의 예시이다.

해당 브라우저와의 연결 상태를 기억하고 있는 세션 저장소는 장고에서 request.session객체를 통해 접근 가능하다. 장고에 SessionMiddleware라는 기본 미들웨어가 있는데 이는 클라이언트가 보내는 요청마다 request.session 속성에 이미 생성되어있는 세션 객체를 연결해준다. 세션이 생성되어있지 않은 상태라면 빈 껍데기를 연결해주고 session_key필드값이 None이다. 장고의 세션 프레임워크 튜토리얼

장고의 세션엔진 대용으로 Redis를 사용할 수 있고 Redis를 사용하면 성능이 향상된다. 보통 장고에서는 memcached 등을 사용해서 캐시를 설정하는데 Redis를 사용할 경우 memcached를 사용했을 때와 비교하여 손색이 없을정도라고 한다.

Redis

한 마디로 메모리를 이용하여 고속으로 <key, value> 스타일의 데이터를 저장하고 불러올 수 있는 시스템이라고 할 수 있다. 2009년 살바토르 산필리포가 개발한 오픈 소스 기반 비관계형 데이터베이스 관리 시스템으로 데이터베이스를 쓸 때 입출력에 시간이 걸리기 때문에 메모리 기반의 저장소에 캐시처럼 정보를 저장해놓고 빠르게 입출력할 수 있도록 만든 것이다.

Redis를 이용하면 MySQL보다 10배 빠를 수 있지만 메모리는 휘발성이기때문에 시스템이 꺼지면 모든 데이터는 날아가므로 Redis는 임시 데이터를 저장하는데 사용한다. -> 09.07수정 Redis에서 비휘발성으로 설정할 수도 있다.redis를 장고 세션 저장으로 사용하기(영문)

Django REST framework 1 Serializer, ApiView

|

위코드 22기 분들이 한 달동안 작성한 채용 관리 사이트를 drf로 리팩토링하라는 업무를 받음으로서 Django REST framework, 장고 레스트 프레임워크를 처음 접하게 됐다. 코드를 읽어보고 공식 문서와 블로그를 둘러보면서 알아보려고 하다가 모르겠어서 위코드의 멘토님께 강의를 추천받아서 들었더니 첫 기본 개념이 좀 잡혔다.

동기의 말에 따르면 학원에서 만들었던 퓨어장고는 레고와 같고 DRF는 철근과 같다고 한다. 멘토님의 말에 따르면 DRF를 사용하면 더 쉽고 빠르게 기능을 구현할 수 있고 틀이 정해져있어서 대부분의 개발자가 같은 형식을 공유하게 되고 에러가 날 여지가 더 적다고 한다.

코드를 뜯어보면 상속과 클래스를 통해 장고에서 만들어놓은 기본 기능을 가져다씀으로써 라우팅과 같은 기본 기능보다 상태별 모델링이나 API의 상호작용에 더 집중할 수 있도록 하는 것 같다.

Django Rest Framework

웹에서 모바일로 사용환경이 변화함에 따라서 HTML뿐만 아니라 JSON으로 데이터를 처리하는 것이 필요해졌다.

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를 개발할 수 있다.

Django REST framework(이하 DRF)는 장고안에서 Restful API 서버를 쉽게 구축할 수 있도록 도와주는 오픈소스 라이브러리이다.

추천해주신 강의를 보고 개념을 이해하고 여러가지 블로그를 보고 정리했다.

내가 여태까지 이해한 바로 DRF의 주요 기능에는 다음과 같은 것들이 있다.

  • Serializer : data를 json으로 직렬화해준다. (API 디버깅을 쉽게 만들어주며 코드를 정리해서 보안 이슈를 해결하기도 하며 validation도 검증해준다.)
  • ModelSerializer : 모델에 serializer를 적용할 때 사용한다.
  • Api View : View를 기반으로 Restful API를 더 쉽게 작성할 수 있게 해준다. 함수기반 Function based View, 클래스기반 Class based View가 있다.
  • GenericAPIView, Mixins : CRUD기능이 미리 만들어져있어서 가져다 쓸수 있다.
  • ViewSet : Class Based View를 더 간결하고 쉽게 사용하지 위한 추상클래스를 이용해 확장된 버전이다.

뷰셋은 유용한 추상화를 제공하며 일관된 API 접근, 코드 작성의 최소화, 일일이 URL conf를 작성하는 대신 상호작용과 표현에 더 집중할 수 있도록 도와주지만 마치 클래스 기반 뷰와 함수 기반 뷰를 선택할 때와 비슷한 트레이드 오프가 있으며, 직접 뷰를 작성할 때보다 덜 명료하다. 그러니 뷰셋이 항상 좋은 선택이 아니라 상황에 따라 판단을 해야한다. - drf 공식 문서

Serializer

data를 json으로 직렬화 해준다. 직렬화에 대한 자세한 설명 반대로 parser는 json을 data로 다시 만들어준다.

ModelSerializer

모델의 인스턴스들을 serialize하려면 아래와 같이 길게 작성해야하는데 modelserializer1 Model Serializer를 이용하면 이렇게 짧게 작성할 수 있다. modelserializer2

ApiView

django 에서는 view 를 통해서 HTTP 요청을 처리하는데 Api view는 RESTful한 API를 만들 때 사용하는 것으로

  1. 클래스를 기반으로 사용하면 뷰 클래스를 APIView를 사용하여 클래스를 만들고 예시: class PostListAPIView(APIView)
  2. 함수를 기반으로 사용하면 @api_view([‘GET’,’POST’])이런 식으로 함수 위에 데코레이터를 사용하는 방식으로 쓸 수 있다.

1번 클래스기반 뷰


from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer

# 포스팅 목록 및 새 포스팅 작성
class PostListAPIView(APIView):
    def get(self, request):
        serializer = PostSerializer(Post.objects.all(), many=True)
        return Response(serializer.data)
    def post(self, request):
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
          	serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)  
      
from django.shortcuts import get_object_or_404

# 포스팅 내용, 수정, 삭제
class PostDetailAPIView(APIView):
    def get_object(self, pk):
        return get_object_or_404(Post, pk=pk)
      
    def get(self, request, pk, format=None):
        post = self.get_object(pk)
        serializer = PostSerializer(post)
        return Response(serializer.data)
    
    def put(self, request, pk):
      	post = self.get_object(pk)
        serializer = PostSerializer(post, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      
    def delete(self, request, pk):
        post = self.get_object(pk)
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

2번 함수기반 뷰

from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer
from rest_framework.decorators import api_view

@api_view(['GET','POST'])
def post_list(request):
    if request.method == 'GET':
        qs = Post.objects.all()
        serializer = PostSerializer(qs, many=True)
        return Response(serializer.data)
    else:
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

@api_view(['GET','PUT','DELETE'])
def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == 'GET':
        serializer = PostSerializer(post)
        return Response(serializer.data)
    elif request.method == 'PUT':
        serializer = PostSerializer(post, data=reqeust.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    else:
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

코드 출처 : 수학과의 좌충우돌 프로그래밍

프로그래머스 level 1 문자열 내 마음대로 정렬하기

|

문제 설명

문자열로 구성된 리스트 strings와, 정수 n이 주어졌을 때, 각 문자열의 인덱스 n번째 글자를 기준으로 오름차순 정렬하려 합니다. 예를 들어 strings가 [“sun”, “bed”, “car”]이고 n이 1이면 각 단어의 인덱스 1의 문자 “u”, “e”, “a”로 strings를 정렬합니다.

제한 조건

strings는 길이 1 이상, 50이하인 배열입니다. strings의 원소는 소문자 알파벳으로 이루어져 있습니다. strings의 원소는 길이 1 이상, 100이하인 문자열입니다. 모든 strings의 원소의 길이는 n보다 큽니다. 인덱스 1의 문자가 같은 문자열이 여럿 일 경우, 사전순으로 앞선 문자열이 앞쪽에 위치합니다.

입출력 예

strings n return
["sun", "bed", "car"] 1 ["car", "bed", "sun"]
["abce", "abcd", "cdx"] 2 ["abcd", "abce", "cdx"]

입출력 예 설명

입출력 예 1
“sun”, “bed”, “car”의 1번째 인덱스 값은 각각 “u”, “e”, “a” 입니다. 이를 기준으로 strings를 정렬하면 [“car”, “bed”, “sun”] 입니다.

입출력 예 2
“abce”와 “abcd”, “cdx”의 2번째 인덱스 값은 “c”, “c”, “x”입니다. 따라서 정렬 후에는 “cdx”가 가장 뒤에 위치합니다. “abce”와 “abcd”는 사전순으로 정렬하면 “abcd”가 우선하므로, 답은 [“abcd”, “abce”, “cdx”] 입니다.

사고 과정

def solution(strings, n):
    dict = {}
    strings = sorted(strings)
    #딕셔너리로 [문자값 : 해당 문자[n]값] 만들고 밸류값에 따라 정렬
    dict = {x:y for x,y in zip(strings,strings[n])}
    #이렇게 하니까 c,c,x 일 때 abcd, abce의 사전순서대로의 정렬이 일어나지 않았다.
    return sorted(dict, key=lambda x: x[1])
def solution(strings, n):
    answer = []
    dict_a = {}
    n_list= []
    for string in strings:
        n_list.append(string[n])
    #딕셔너리로 [문자값 : 해당 문자[n]값] 만들고
    dict_a = {x:y for x,y in zip(strings,n_list)}
    #키값에 따라 한 번 정렬한 뒤
    dict_a = dict(sorted(dict_a.items()))
    #밸류값에 따라 한 번 더 정렬
    answer = sorted(dict_a, key=lambda x: x[1])
    print(answer)
    #정답 딕셔너리에서 키값만 출력
    return answer

이렇게 하면 3번만 맞고 다 틀린다

모범 답안

def solution(strings, n):
  answer = []
  for i in range(len(Strings)):
    strings[i] = strings[i][n] + strings[i]
    #n번째 수를 글자의 앞에다 붙이고
  strings.sort()
  #정렬
  print(strings)

  for j in range(len(strings)):
    answer.append(strings[j][1:])
    #맨 앞 n을 뺀 부분을 answer에 넣기 
  retur answer

n번째 수를 글자의 앞에다 붙인 다음, 바로 정렬하면 끝!!

프로그래머스 level 1 같은 숫자는 싫어

|

문제 설명

배열 arr가 주어집니다. 배열 arr의 각 원소는 숫자 0부터 9까지로 이루어져 있습니다. 이때, 배열 arr에서 연속적으로 나타나는 숫자는 하나만 남기고 전부 제거하려고 합니다. 단, 제거된 후 남은 수들을 반환할 때는 배열 arr의 원소들의 순서를 유지해야 합니다. 예를 들면,

arr = [1, 1, 3, 3, 0, 1, 1] 이면 [1, 3, 0, 1] 을 return 합니다. arr = [4, 4, 4, 3, 3] 이면 [4, 3] 을 return 합니다. 배열 arr에서 연속적으로 나타나는 숫자는 제거하고 남은 수들을 return 하는 solution 함수를 완성해 주세요.

제한사항

배열 arr의 크기 : 1,000,000 이하의 자연수 배열 arr의 원소의 크기 : 0보다 크거나 같고 9보다 작거나 같은 정수

입출력 예

arr answer
[1,1,3,3,0,1,1] [1,3,0,1]
[4,4,4,3,3] [4,3]

제출 답안

def solution(arr):
    answer = []
    for i in range(0,len(arr)-1):
        if arr[i] != arr[i+1]:
            answer.append(arr[i])
    answer.append(arr[-1])
    return answer

모범 답안

def no_continuous(s):
    a = []
    for i in s:
        if a[-1:] == [i]: continue
        a.append(i)
    return a

a[-1:]는 a배열에서 마지막 1개 값을 뺀 나머지 리스트를 의미한다.

위코드 두번째 프로젝트 험블벅 3 카카오 소셜로그인, 최종 구현 영상

|

험블벅 최종 구현 영상

카카오 소셜로그인

먼저 공식문서에 나와있는대로 카카오디벨로퍼스에 웹 플랫폼 및 도메인 정보를 등록한다.

소셜로그인

프론트에서 카카오 로그인을 통해 토큰을 받을 수 있는 인증코드를 받고, 이 인증코드를 통해서 API를 호출 할 수 있는 사용자 토큰(Access Token, Refresh Token)을 카카오로부터 받아서 header에 담아 백엔드에 보내준다.

Request

GET/POST /v2/user/me HTTP/1.1  
Host: kapi.kakao.com  
Authorization: Bearer {access_token}  
Content-type: application/x-www-form-urlencoded;charset=utf-8  

백엔드 코드

class KakaologinView(View):
    def get(self, request):
        try:
            access_token = request.headers.get("Authorization")
            #프론트에서 헤더스에 담아보낸 토큰 받기
            if not access_token:
                return JsonResponse({"message" : "INVALID_TOKEN"}, status = 400)
                
            response = requests.get(
                    "https://kapi.kakao.com/v2/user/me", 
                    headers = {"Authorization" : f"Bearer {access_token}"}
                    )
            #{Authorization: bearer+access_token}의 양식으로 kakao에 사용자 정보 reqeust를 보낸다
                    
            if not response:
                    return JsonResponse({"message" : "NOT_FOUND_IN_KAKAO"}, status = 404)
                    
            profile_json  = response.json()
                
            kakao_id      = profile_json.get("id")
            kakao_account = profile_json.get("kakao_account")
            name          = kakao_account["profile"]["nickname"]
            #카카오에서 kakao_id, kakao_account, kakao_account.profile.nickname 받기
            
            if not User.objects.filter(kakao = kakao_id).exists():
                User.objects.create(
                            kakao     = kakao_id,
                            nickname      = name,
                )
            #험블벅 db와 유저정보 비교 후 존재하지 않으면 새로운 유저정보 생성 
            user           = User.objects.get(kakao = kakao_id)
            #험블벅 db와 유저정보 비교 후 존재하면 유저를 가져와서
            user.name      = name
            #이름을 넣고
            user.save()
            #저장

            token = jwt.encode({"id" : user.id}, SECRET_KEY, algorithm = "HS256")
            #토큰 발행
			
		    return JsonResponse({"message" : "SUCCESS", "acess_token" : token}, status = 200)
		except KeyError:
		    return JsonResponse({"message" : "KEY_ERROR"}, status = 400)

카카오에서 받게 되는 profile_json데이터는 아래와 같이 생겼다.

{
    'id': 1855599935,
    'connected_at': '2021-08-18T08:54:05Z',
    'properties':
        {
        'nickname': '한효주'
        },
    'kakao_account':
        {'profile_nickname_needs_agreement': False,
         'profile':
            {'nickname': '한효주'},
         'has_email': True,
         'email_needs_agreement': False,
         'is_email_valid': True,
         'is_email_verified': True,
         'email': 'hyojoo@gmail.com'
        }
}