3월 27, 2022

도커 배포하며 겪은 사소한 에러들

 

에러


이미지를 만들려고 할때마다 이 에러가 뜹니다

해결책으로 이 링크 https://dev.to/shriekdj/python-failed-building-wheel-for-backportszoneinfo-on-linux-2mo8 

의 tzdata 를 설치했더니( 두번째 해결책)

이젠 다른 에러가 뜹니다 


문제 해결

저는 pip freeze > requirements.txt로 pip freeze 항목을 그대로 다 requirements에 옮겨줬는데,

그러다 보니 제가 손수 깐 것이 아닌 장고 등을 설치하면서 깔린 라이브러리 등이 있다고 생각합니다.

그래서 backports.zoneinfo라는 제가 깔지 않은것이 pip freeze에 들어가지 않았나 생각이 들었습니다.

그 부분 requirments.txt에서 지워주고 다시 이미지 만드니 이미지가 만들어집니다.

requirements.txt는 pip로 모듈을 설치할때마다 성실하게 업데이트를 해줘야 한다는 것을 느꼈습니다.


문제는 requirments에 gunicorn 관련 내역이 없어서 이미지로 컨테이너를 실행하려니컨테이너가 실행 안됩니다,

가상환경 키고(구니콘이 있는), pip freeze로 gunicorn부분을 확인한 후 이를 requirements에 복사붙여넣기 해줍니다. 이를 바탕으로 이미지를 다시 만들어줍니다.

이미지를 다시 만들었습니다.

근데 도커콘테이너 실행하려니.


이 에러가 뜹니다.

docker ps -a 로 꺼진 콘테이너 확인해보니

api란 이름의 콘테이너가 꺼진채로 있습니다

이 콘테이너를 지워줍니다.



내 컴퓨터 - ec2 - docker 이렇게 연결되는 것이 아니라 ,

내 컴퓨터 → docker

아마존이 가진 ec2 → docker 이런식으로 별개의 컴퓨터 두개에서 도커로 서버를 돌린다고 생각하면 될듯 싶습니다.


여태껏 도커로 서버를 키려할 때 해당 명령어만을 사용했었는데

이 명령어로 키면 wecode/projcet 이미지로 api란 이름을 가진 새로운 컨테이너를 만들고 그거로 서버를 여는것과 같습니다

그래서 매번 이름이 안겹치게 새로운 이름을 가진 애들을 열어줬어야 했습니다. 

docker run -p 8000:8000 wecode/project:0.1.0

그러고 싶지 않으면 이렇게 —name api 부분을 없애주면 됩니다 , 이러면 새로운 이름의 컨테이너 안 열고 기존 컨테이너를 이용하여 그냥 바로 서버열기 가능합니다.

3월 27, 2022

SOLID 원칙

 객체지향 프로그래밍을 하며 지켜야 할 5가지의 원칙을 SOLID 원칙이라 합니다.

A. SRP(Single Responsibility Principle) - 단일 책임의 원칙 

클래스는 오직 하나의 기능만 가지며, 클래스의 모든 작업은 그 단일 책임에 종속돼야 합니다.

이에 맞추어  https://github.com/wecode-bootcamp-korea/30-2nd-WantU-backend/tree/main/applications 해당 링크의 applications view를 살펴보겠습니다.

우선 리팩토링 이전의 StatusView 코드입니다.


해당 view는 지원자의  지원 상태와, 그것들의 개수를 데이터베이스에서 가져오는 역할을 하고 있습니다.

34번째 줄에서 application.status.status는db에서 직접적으로 상태값을 가져오는 역할을 합니다. 

StatusView는 지원상태를 return 해줄 뿐 아니라, DB에서 직접 상태값들을 가져와 return할 값에 직접적으로 투입하고 있습니다. 

하나의 클래스가 여러 기능을 하고 있다고 보여지기에, enum클래스를 따로 만들어주어 상태값을 enum클래스를 통해 불러올 수 있도록 하겠습니다. 




enum 클래스를 분리해준 이후 , 이 클래스의 value값과 상태값의 id를 맞추어서 개수를 셀 때 Status 클래스를 사용해줍니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class StatusView(View):
    @login_decorator
    def get(self, request):
        applicant = Applicant.objects.filter(user=request.user).prefetch_related('application_set')
 
        if not applicant.exists():
            return JsonResponse({'result':"NO valid application"}, status = 404)
 
        applications_in_applicant = applicant[0].application_set.all().select_related('status')          
        status_list               = [application.status.id for application in applications_in_applicant]
 
        result = { 
            "applied"         : status_list.count(Status.APPLIED.value), 
            "document_passed" : status_list.count(Status.DOCUMENT_PASSED.value),
            "accepted"        : status_list.count(Status.ACCEPTED.value),
            "rejection"       : status_list.count(Status.REJECTION.value)
            }
 
        result["total"= len(status_list)
 
        return JsonResponse({'result':result}, status = 200
cs


B. OCP(Open Close Principle) - 개방폐쇄의 원칙

클래스는 확장에 대해서는 개방적이어야 하고, 변경에 대해서는 폐쇄적이어야 합니다.

클래스의 코드 변경은 이 클래스를 사용하는 모든 시스템에 영향을 미치게 됩니다. 따라서 클래스의 기능을 추가하려면, 기존 기능의 변경이 아닌 새로운 함수의 추가가 이상적입니다.


C. LSP(The Liskov Substitution Principle) - 리스코브 치환의 원칙

부모클래스 T의 서브타입인 자식 클래스 S의 객체는 어떠한 문제도 없이 부모 클래스의 객체를 대체할 수 있습니다.

문제없는 대체를 위해서, 자식 클래스는 부모클래스의 기능을 오버라이드 하지 않아야합니다. 자식 클래스는 부모의 기존 기능을 건드리지 않은 채, 확장만 수행하여야 LSP를 만족하게 됩니다

D. ISP(Interface Segregation Principle) - 인터페이스 분리의 원칙

바꿔 말하면, 인터페이스의 단일 책임 원칙입니다. 구체적인 책임을 지는 여러 인터페이스를 분리하여, 사용하지 않는 인터페이스가 존재하지 않게 해야 됩니다.


E. DIP(Dependency Inversion Principle) - 의존성 역전의 원칙

고차원의 모듈과 클래스는 저차원의 모듈과 클래스에 의존해서는 안됩니다. 고차원, 저차원 모두 구체에 의존적이지 않고, 추상에 의존해야 합니다. 구체는 추상에 의존적입니다.

고차원 모듈 또는 클래스는 저차원 모듈 또는 클래스를 도구로 하여 동작하는 클래스입니다. 추상은 두 클래스를 연결하는 인터페이스이고, 구체는 도구가 동작하는 방법입니다. 

자동차가 타이어를 사용할때, 한국타이어, 금호타이어등은 "구체"이고, 단순 "타이어"는 "추상"이라 할 수 있습니다. 자동차라는 고차원 클래스는 타이어라는 추상적 "인터페이스"에 의존하고 있습니다. 이때 타이어들은 모두 인터페이스 입니다.




출처 https://server-engineer.tistory.com/228

인터페이스를 통해 고수준 클래스가 저수준 클래스에 의존성을 가지지 않게 할 수 있습니다.

앞서 Enum클래스로 상태값을 분리해준 과정에서, StatusView는 고차원 클래스, Enum을 상속받은 Status는 저차원 클래스라고 생각됩니다.

3월 26, 2022

[리팩토링] SQL문과 쿼리 개수를 확인하여 prefetch와 select_related의 성능 확인하기

 



해당 erd의 applicants에서 application_status까지 타고 들어가려 합니다.




Get 요청 보냈을 시 


저는 SQL문을 알지 못합니다. 쿼리문을 보고 직관적으로 해석해보았습니다. 틀린 해석일 가능성이 큽니다.

1. id가 8 인 유저를 user테이블로부터 불러옵니다

2. applicants 중에서 user_id가 8인 것을 찾습니다. -> 7번 applicant

3. applications에서 applicant_id가 7 인것을 찾습니다

4. inner join을 이용하여 application_status 중 applications.status_id 와 같은 id를 가진 행을 엮은 뒤, applicant_id가 7 인 것을 찾습니다 



이 부분에서 prefetch_related를 빼주었더니 


오히려 쿼리의 숫자가 하나 줄어들었습니다




이 부분이 없어졌군요

그니까 위의 3번 부분이 없어진 것입니다.


반면에 select_related를 주석처리 해주었더니

쿼리가 21개로 급증했습니다

inner join으로 미리 데이터를 엮어오지 않아서 매번 for문이 돌때마다 데이터를 db에서 가져오는 것을 볼 수 있습니다.


왜 prefetch를 썼더니 쿼리 개수가 하나 더 늘어났을까요??

 https://stackoverflow.com/questions/31237042/whats-the-difference-between-select-related-and-prefetch-related-in-django-orm

이 링크의 답변을 보시면, select_related는 SQL join을 하여 데이터를 SQL서버의 테이블 일부분으로 받는 반면, prefetch_related는 또 다른 쿼리를 발생시켜서 기존 object의 불필요한 컬럼을 줄여준다(inner join을 하여 테이블의 컬럼을 늘리지 않는다는 뜻으로 받아들였습니다) 고 합니다. 

다시 한번 코드를 보면,

applicant에서 status로 가는 과정에서 역참조로 application_set을 사용하는 것은 맞습니다. 다만 prefetch를 사용하지 않아도, 그 밑의 코드에서 application_set.all() 로 역참조한 application의 쿼리셋을 가져오고, 거기에 select_related로 status를 가져오고 있습니다 

저는 prefetch를 사용하면 미리 application을 db에서 join으로 긁어와서 추후에 application_set을 사용할때 db를 다시한번 hit할 일이 없다고 생각했습니다. 그러나 prefetch_related는 "파이썬" 상에서 join을 실시하며(db에서 미리 join을 일으키지 않으며), join될 각 테이블에 대한 쿼리를 발생시킵니다.

따라서 prefecth_related를 사용했을 시 이 부분이 추가되지 않았나 생각이 듭니다


 


한번 더 실행해보니 


쿼리 숫자가 5개로 또 한번 줄어들었습니다.
그런데 화면상에 뜨는 쿼리문은 그대로이네요.. 이 부분은 알게되면 추후 포스팅하겠습니다


밑의 CompanyView를 봅시다.

이 코드의 경우 

applicant에서 application으로 가서 job_position으로 한번 더 이동 한 뒤 , 거기서 각각 company의 name / job_category의 name / 해당 테이블의 created_at을 뽑아와야 하는 구조입니다


쿼리가 무려 31개나 날아갑니다.

자세히보면 job_category, company, application_status를 가져오는 쿼리가 중복됩니다.

job_position만 select_related로 join할 것이 아니라 job_category와 company는 select_related로,  status는 prefetch로 가져옵니다. 

쿼리의 개수가 획기적으로 줄어들었습니다.




application 쿼리셋에서 시작해서, 어떻게 두다리 너머의 company와 category 까지 가져올 수 있을지 고민했는데, 그냥 job_position에서 __ 로 넘어가면 되는거였습니다.

다시한번 prefetch와 select의 차이를 알기 위하여 

prefetch 를 select_related로 바꿔주었습니다.

쿼리가 하나 줄어들었습니다.

마지막의 이 부분이 없어졌네요 



앞서 첨부한 스택오버플로우 답변글에 이런 답변이 있습니다. select_related와 prefetch_related의 쿼리 개수가 딱 하나 차이나는것이 이 답변을 증명하네요.

"prefetch_related run the query on the first model , collects all the IDs it needs to prefetch and then runs a query with an IN clause in the WHERE with all the IDs that it needs. "

이 답변에 해당하는 쿼리가 이것인듯 하네요




3월 20, 2022

AWS로 배포하기: RDS를 사용하기 위해 로컬 mysql dump

 프로젝트를 진행하면서 원격 통신을 위해 ngrok을 사용하려다가 mysql이 2002 에러로 맛이 갔습니다.

ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)

ngrok을 설정하기 위해 snap으로 ngrok을 설치하려다가, 이 명령어가 듣지 않아서 해결책을 찾다가 

$ sudo daemonize /usr/bin/unshare --fork --pid --mount-proc /lib/systemd/systemd --system-unit=basic.target


해당 명령어를 실행해주면 된다는 포스팅을 보고 덜컥 따라한 이유로 mysql이 켜지지 않습니다. 
sudo service mysql restart 로 mysql을 껐다 키려해봐도

Warning! D-Bus connection terminated. mysql

이 에러가 계속 나서 발이 묶였습니다. 결국 임시방편의 해결책을 찾긴 했습니다. wsl --shutdown으로 wsl을 껐다가 다시 켜주면 mysql에 접속이 됩니다.

이는 어쨌든 임시방편일 뿐이고... 후에 프로젝트가 끝나고 시간적 여유가 생기면 꼭 linux공부를 해야겠습니다. wsl을 사용하면서 수많은 고난을 겪으니 운영체제를 꼭 공부해야겠다는 생각이 듭니다(오히려 좋아)

아무튼 ngrok 사용을 포기하고 이참에 aws로 그냥 배포를 하기로 했습니다. 

이때 RDS를 사용하여 클라우드에 데이터베이스를 만든 이후, 이를 EC2와 연동해줘야 합니다. 이 과정에서 기존 Mysql(로컬)을 dump를 뜬 이후, 나의 RDS에 밀어넣어(?)줍니다.

dump가 뭘까요??
데이터베이스 덤프는 테이블 구조와 데이터에 대한 기록입니다. 이는 SQL 선언문의 리스트입니다. 보통 database dump는 데이터 손실에 대비하여 DB를 백업하는 용도로 사용합니다. 

mysqldump -u root -p wantu > wantu.sql

해당 명령어로 wantu db의 스키마가 담긴 wantu.sql 파일을 생성합니다 
(스키마란 자료의 구조, 자료의 표현 방법, 자료간 관계를 형식 언어로 정의한 구조입니다)

그리고 이를 

mysql -h 데이터베이스명 -u root -p wantu < wantu.sql
명령어로 밀어넣어 줍니다

이후 RDS 상의 데이터베이스를 확인해보면 나의 로컬 데이터베이스가 그대로 RDS에 옮겨간 것을 확인할 수 있습니다. 


3월 20, 2022

aws - django를 통해 s3에 파일 업로드, 삭제 기능 구현하기

 S3는 Simple Storage Service의 약자입니다. 아마존에서 제공하는 파일 서버 서비스입니다.


"버킷(Bucket)" 이라는 디렉토리에 데이터 하나하나(객체)를 저장합니다. 


파이썬을 이용하여 S3 저장소를 이용하기 위해서는 boto3 를 사용해야합니다.

pip로 boto3를 설치해준 뒤, 업로드를 진행하고자 하는 클래스(뷰)에서 boto3.client부터 만들어줍니다. 


s3를 사용할 것이기에 s3를 명시해주고, 그 뒤의 kwargs로 access key와 secret access key를 줍니다. 이때 이 둘은 보안과 관련된 사항이기에, my_settings 또는 환경 변수 등으로 따로 관리해주는 것이 좋습니다.

이를 통해 서비스 클라이언트를 명시해준 이후, upload_fileobj를 통해 s3에 파일 업로드를 진행해줍니다. 


인자는 순서대로 파일 객체, 업로드하려는 버킷 명, 키값(str), 추가인자입니다. 

이때, key 값은 uuid를 사용하여 관리해줍니다. (file_uuid는 uuid값을 담은 변수입니다)

만약 파일이름을 key 값으로 사용한다면, 기존의 객체와 같은 이름을 가진 파일이 추가될때, 버킷에 새로운 파일이 추가되는 것이 아니라, 기존에 있던 동명의 파일이 업데이트 되기에 uuid로 관리를 해주는 것입니다.


uuid는 universally unique identifier를 뜻합니다. 네트워크 상에서 고유성이 보장되는 ID를 만들기 위한 표준 규약입니다. 저는 uuid4 버전을 활용했습니다. 1과 4버전을 많이 이용하는데, 1버전의 경우 보안상 허점이 있을 수 있다하여 4버전을 선택했습니다.


ExtraArgs를 통해 파일의 컨텐츠 타입도 설정해줍니다.


내친김에 삭제 기능도 구현해 줍니다.

삭제는 delete_object 메소드 를 사용하면 됩니다.


이렇게 버킷 이름과, 키를(DB에 저장해두었던 UUID)를 알려주면 삭제가 됩니다.


이상입니다. S3가 생소한 개념이어서 그렇지 구현은 그렇게 어렵지 않았습니다. 세팅을 하는데 시간이 더 많이 걸렸던것 같습니다. 그러나 단순히 업로드/ 다운로드/ 삭제가 가능하게 하는것이 다가 아니라, AWS 전반에 대한 이해가 부족하기에 이를 늘려야 할 듯합니다.




3월 20, 2022

JSONDecodeError - form data란 ?

 

해당 코드에서 


계속해서 JSONDecodeError가 발생합니다.

포스트맨을 이용해서 계속 값을 보내주는데 


뭐가 문제인지 파악하지 못하고 있습니다.

json.loads()는 json형식을 python의 dictionary 형식으로 변경(decode)해줍니다


그러다 문득 체크박스에서 raw를 선택하고 직접 json을 입력해보았습니다


작성해보면 계속 빨간줄이 뜨는데, json형식에 맞게 인덴트까지 세심히 조절해주면 빨간줄이 더이상뜨지 않습니다

이렇게 post 요청을 보내니 잘 됩니다!


코드의 문제가 아니라, 포스트맨을 통한 요청 방식에 따라 차이가 있는 것이었습니다 


HTTP request body는 bytestring 형식으로 오기때문에 decode를 거쳐 str로 만들어준 이후 각각 방식을 프린트해보겠습니다


이것이 form-data로 보낸것의 결과입니다

이것이 raw로 보낸 것을 프린트 찍어본 결과입니다


그렇다면 포스트맨에서 form-data로 보낼때와 raw로 보낼때의 차이점이 정확히 무엇일까요?


웹사이트 form에서 API로 mulipart/form-data 형식으로 값을 보낼 수 있습니다. 이 형식을 모방한 것이 포스트맨의 form-data 입니다. form-data를 통해 키-값 쌍을 content type을 특정해서 보낼 수 있습니다.
파일을 보낼때에는 form-data를 사용할 수 있습니다. 

body로 data를 보낼때, header에 알맞는 cotent type을 지정하여서 API가 값을 적절하게 받을 수 있게 해줘야 합니다. form-data를 사용하면 이 content-type헤더를 포스트맨이 자동으로 지정합니다.(수동으로 지정해주지 않을 시)

반면에 raw로 body에 값을 실을때에는, postman은 제가 고른 type에 맞게 헤더를 고르게 됩니다.

여기서 고를 수 있습니다.


프론트로부터 post로 요청을 받을때에도, 알맞은 type으로 데이터를 보내주지 않는 한 계속 Key Error가 발생하는 것을 확인할 수 있었습니다. 
분명히 키 값을 다 맞췄음에도 계속 키에러가 나서 보내는 형식을 확인해보니, 파일을 post하는데 일반 application/json 형식으로 보내고 있었기에 키에러가 나는 것을 확인할 수 있었습니다.
 이를 form-data형식으로 변경하니 키에러가 나지 않고 알맞게 데이터를 받을 수 있었습니다.






3월 13, 2022

Path Variable VS Query Parameter

Path Variable 

경로를 변수로써 사용합니다.

포스트맨에서는 이런식으로 path parameter에 해당하는 변수(:id)를 정의하고, params에서 값을 줄 수 있습니다




여러개의 path parameter가 올 수 있습니다. 




어떠한 resource를 식별하고 싶으면 Path Variable을 사용합니다.

이유는 다음과 같습니다. 위의 예시의 경우, 존재하지 않는 boards의 id가 들어온다면 path variable은 404 not found 에러를 발생시킬 것입니다. 

/drinks [GET] # Fetch a list of drinks
/drinks [POST] # Create new drink
/drinks/123 [PUT] # Update drink id 123
/drinks/123 [DELETE] # remove drink id 123


리소스를 식별한 이후, HTTP 메소드를 변경함으로써 추가적인 endpoint (drinks/create) 나 쿼리 파라미터(drink?action=create) 없이 원하는 동작을 수행할 수 있습니다.

반면 Query Parameter를 사용한다면, 서버로 데이터가 넘어가 쿼리를 날린 이후, 해당하는 데이터가 없을 경우에 에러가 발생할 것입니다




Query Parameter

경로 "뒤에" 입력 데이터를 함께 제공하는 형태입니다. 

따라서 경로의 일부가 아니고, resource를 호출할때 받는 추가적인 정보에 해당합니다


Key,value의 쌍으로 이루어집니다, &로 연결 가능합니다


? 뒤에 오는 모든 것은 쿼리 파라미터입니다.

 
Optional 하기에 없더라도 문제가 되지 않습니다.



정렬이나 필터링을 할 시 Query Parameter를 사용합니다 

앞서 Path Variable의 경우와 달리, 정렬이나 필터링을 할시 404 not found가 발생하는 상황은 부적절합니다. 따라서 이러한 경우엔 Query Param을 사용해줍니다.



사진에서 보이듯이 query params는 체크박스 해제가 가능하지만, path Variable은 체크 해제가 불가능합니다. 이는 path parameter가 없을 시 path가 달라지기 때문입니다(경로의 일부분이기에)