Instagram Clon-Backend

bcj0114·2022년 2월 16일
0

개요

프로젝트 목적

  • django 기반 RESTful 설계 연습

교재 선정

설명

  • 이 글은 프로젝트를 작성하며 공부하거나 고민한 내용을 기록했다.
  • 작성한 코드는 깃헙에 업로드 해두었다.



개발 노트

1. SignUp

1) json.loads

json형식을 python의 dictionary로 바꾸어 return한다.

2) nullblank

Django 공식문서를 통해 nullblanck의 차이 *를 이해하고 User모델 중 CharField에서 null를 단독으로 사용하지 않고 blank와 함께 사용하였다.
* : null과 empty string 간에 모호성의 문제가 존재하는데, string-based fields에서 empty string은 blank=True로 한다 왜냐하면 empty string을 가능한 입력값으로 고려하고 null과 구분을 두는 것이다.

3) makemigrations & migrate

자세한 migration 방법과 순서는 정리해둔 것을 참고

  • python manage.py makemigrations <app>
    <app>을 지정하는 이유는 모델 간 참조 관계가 있을 때 순서가 중요하기 때문
  • python manage.py sqlmigrate <app> 0001
    실제로 어떤 sql문이 적용되는지 확인하는 습관

4) 정규표현식

(?=.*[a-z])는 영문 소문자 조건을 의미한다.

5) 상수

상수는 함수 바깥 혹은 임포트 아래에 선언해주는 것이 좋다.

6) 409 Conflict

mozilla 참고하니 이미 있는 파일보다 오래된 파일을 업로드할 때 주로 사용된다고 하는데, 이것이 이미 존재하는 email을 기입했을 때 반환하는 status로 알맞은 것인지 햇갈렸다. 구글링해보니 무엇을 사용하는가에 대한 논란이 꽤 있었던 내용이고, 409를 활용하는 사람들이 있는 편인 것 같아서 그대로 409 status를 활용하기로 했다.

7) Q 객체

filter()안에 여러개의 kwargs를 다룰 때, 각 kwarg를 Q object로 캡슐화하여 연산자 &, |와 같이 활용한다.
django 공식 문서 참고


2. LogIn

1) 401 Unauthorized

없는 ID 입력시 인증(Authentication: 누구인지 확인)되지 않았다는 status로 응답한다. 로그인에 실패하거나 비회원이 회원의 기능을 요구할때 사용된다고 한다. 가입되지 않은 ID나 잘못된 패스워드를 입력하는 것이 클라이언트가 잘못된 요청을 보내는 경우 사용하는 400 Bad Request status를 사용하는 것이 아닌가 고민했다. 하지만 입력된 값들이 ID pattern이나 password pattern에 맞지 않는, 즉 API 스펙에 맞지 않는 것은 아니니 401 status가 더 바람직해 보인다.

2) POST method

post method를 이용하여 구현하는 것이 의문이었다. 이와 관련된 블로그 글 찾아 읽고 정리해보면, 캐싱을 사용하며 쿼리문에 사용자 정보가 들어나는 GET보단 캐싱을 사용하지 않는 POST가 나으며, https를 함께 사용하는 것이 더욱 안전하다는 결론. 캐싱을 사용하지 않으므로 동일한 상태를 보여주지 않고 다수의 로그인 실패 카운트를 고려한 처리를 할 수 있다.

3) id 보단 log_in_id로 Manager를 사용하자

모델 객체가 생성될때마다 django는 자동으로 id를 카운팅하여 저장한다고 알고 있다.
모호성을 방지하고자 login_id = data.get("id", None)보단 login_id = data.get("login_id", None)라고 코딩하였다.

3. bcrypt & JWT

1) bcrypt

  • 인증(Authentication) 과정에서 비밀번호를 암호화해준다. 따라서 보안을 강화해줄 것으로 예상된다.
  • 단방향 해쉬 함수로 암호화할 때 사용되는 파이썬 라이브러리이며 SaltingKey Stretching 구현을 편히 해준다.
    - Salting이란 password에 랜덤 데이터(Salt)를 더해주는 것이며, 이를 통해 동일 password를 사용하는 회원들이어도 동일한 다이제스트(패스워스를 해쉬 함수로 대입하여 얻는 암호화된 메시지)나오는 것을 방지할 수 있다.
    -Key Stretching이란 해쉬 값을 여러번 반복 해싱하는 행위이다. 무차별 대입 공격을 막을 수 있어서 보안을 강화할 수 있다고 한다.

2) JWT

  • 인가(Authorization) 구현과 관련이 있다.
  • 인증 절차를 거치면 서버는 유저 정보가 담긴 access token을 생성하여 유저에게 보내며, 이후 유저는 request에 이 token을 담아보낸다. 서버는 다시 이 token을 복호화하여 User id를 얻고 DB에서 권한을 확인 후 인가 결과를 처리한다.
  • JWT(JSON WEB Token)가 가장 널리 사용되는 access token 중 하나이다.

3) httpie, CSRF 비활성화

httpie 는 python 으로 개발된 콘솔용 http client 유틸리티로 터미널에서 클라이언트의 회원가입과 로그인 request를 실행해볼 수 있었다.

4) REST와 CSRF protection

httpie을 통해 post request를 할 시 CSRF verification failed.메시지와 함께 403 Forbidden 승인 거부 오류가 발생한다. 따라서 settings.py에서 CSRF protection을 비활성화했다. 이렇게 되면 보안상으로 취약해질 수 있다고 생각할 수 있으나, 충분히 RESTful하다면 문제될 것 없다고 한다.

  • 위 링크의 내용을 정리하자면,
    - CSRF공격은 쿠키를 이용한 것이므로 쿠키를 사용하지 않는 API라면 CSRF protection은 필요하지 않다.
    - 주요 의견의 필자는 REST의 stateless의 특성을 충분히 따른다면 쿠키는 필요하지 않다고 한다.
    - 그러나 로그인 인증 정보를 토큰을 사용하여 로컬에 저장하는 경우 XSS 공격에 취약하며 이럴 땐 RESTful해도 쿠키가 활용될 수 있다고 한다.

현재까지 프로젝트에서 쿠키를 사용하지 않으므로 CSRF protection을 잠시 배제하는 것으로 하겠다. 이후 쿠키를 추가하게 된다면, 테스트 때만 잠시 CSRF protection을 배제하겠다.

4. Posting

1)models.ForeignKey

모델 간 One-to-many(1:N)관계가 있다면 N에 해당하는 모델에서 models.ForeignKey를 사용하여 1에 해당하는 모델에 연결한다.

  • 예시
# 1:N = User:Posting
class Posting(models.Model):
    user = models.ForeignKey('user.User', on_delete=models.CASCADE)

# 1:N = Posting:Image
class Image(models.Model):
    posting = models.ForeignKey('Posting', on_delete=models.CASCADE)
  • migration시 models.ForeignKey로 연결된 다른 모델은 DB에 자동으로 <모델이름>_id라는 데이터가 추가된다.
 % python manage.py sqlmigrate posting 0001
BEGIN;
--
-- Create model Posting
--
CREATE TABLE "postings" (
(...생략...)
"user_id" bigint NOT NULL REFERENCES "users" ("id") DEFERRABLE INITIALLY DEFERRED
);

2) 다른 앱의 모델을 참조하기

위 예시에서 Postinguser 앱의 User모델을 참조하고 있기에 다음과 같은 방법으로 참조하였다.

# import 하기
from user.models import User

# 'user.User'로 명시하기
class Posting(models.Model):
    user = models.ForeignKey('user.User', on_delete=models.CASCADE)

여기서 from user.models import User로 import해 ForeignKey에 앱의 이름을 반드시 명시해야한다. 만약 아래와 같이 앱의 이름을 밝혀주지 않으면 에러가 발생한다.

class Posting(models.Model):
    user = models.ForeignKey('User', on_delete=models.CASCADE)
% python manage.py check
SystemCheckError: System check identified some issues:

ERRORS:
posting.Posting.user: (fields.E300) Field defines a relation with model 'User', which is either not installed, or is abstract.
posting.Posting.user: (fields.E307) The field posting.Posting.user was declared with a lazy reference to 'posting.user', but app 'posting' doesn't provide model 'user'.

System check identified 2 issues (0 silenced).

5. Login Authentication Decorator

1) Decorator

  • 코딩도장에 데코레이터 개념이 잘 정리되어 있다.
  • 보통 Django에서는 데코레이터를 utils.py라는 파일에 별도로 저장하여 사용한다.

2) 401 Unauthorized

login_decorator에서 INVALIE_ERROR에 대해 블로그에선 클라이언트가 잘못된 요청을 보냈다는 400 Bad Request로 처리하고 있다. 하지만LogInView에서 INVALID_ERROR를 클라이언트가 해당 리소스에 대한 인증이 필요하다는 401 Unauthorized로 처리했기에, 통일감과 의미를 고려하여 status 401로 처리하였다.

6. Modify and Delete

1) 403 Forbidden

서버가 요청을 이해했지만 승인을 거부한 경우에 사용하는 status다. 주로 인증 자격 증명은 있지만, 접근 권한이 불충분한 경우 사용한다.
블로그에선 posting의 userid와 일치하지 않는 id가 삭제 요청하는 경우 _401 Unauthorized를 return하고 있으나 403이 더 적합하다고 판단했다.

2) PUT method

posting 수정엔 POST 또는 PUT Method를 사용해도 되나, PUT을 사용하기로 했다. PUTPOST의 차이점은 크게 두 가지일 것이다.

  • PUT은 클라이언트가 request시 리소스의 uri를 함께 포함시켜야 한다.
  • PUT은 기존 리소스를 완전히 대체하며 멱등성을 만족시킨다.

단순한 게시물 수정이기 때문에 위 성질들이 크게 중요하진 않아보인다. 무엇을 선택하든 무방하기에 써보지 않은 PUT을 사용해보기로 했다.

3) Image url 삭제

실제 인스타그램 포스팅 수정 기능에선 이미지를 삭제하는 기능이 있으나, 블로그에서 코드엔 이러한 기능이 구현되지 않았다. 그래서 직접 구현해보았다.

class PostingDetailView(View):
    @login_decorator
    def put(self, request, posting_id):
        try:
            # ...(생략)...

            old_image_url_list = [image.image_url for image in Image.objects.filter(posting_id=posting_id)]
            image_url_list = data.get("image_url", old_image_url_list)

            # KEY_ERROR
            if image_url_list is None:
                return JsonResponse({"message": "KEY_ERROR"}, status=400)

            if image_url_list != old_image_url_list:
                for image_url in set(old_image_url_list) - set(image_url_list):
                    Image.objects.get(image_url=image_url, posting_id=posting_id).delete()
			# ...(생략)...

7. MySQL 연동

1) MySQL 설치

MySQL을 공식 홈페이지에 들어가서 직접 설치하였으나, database connector를 설치하는 것에서 ERROR가 발생하였다. 찾아보니 파이썬 mac에선 homebrew를 이용하여 설치하는 것이 깔끔하다고 하여 homebrew를 이용하였다.
터미널을 실행시키고brew install mysql 을 사용하여 설치를 완료...하려 했으나 이번엔 brew에서 ERROR가 났다. 이 글을 참고하여 그대로 따라하니 해결되었다.

brew doctor

brew에서 문제가 생기면 위 명령어를 자주 애용하자!

2) MySQL 초기설정

이 글을 참고하여 초기설정을 하였다.

mysql 서버 Start, Stop 하는 법
터미널에서
mysql.server start
mysql.server stop

당연하지만 장고에서 python manage.py migrate를 하기위해선 MySQL 서버가 동작하고 있어야한다.

3) database connector 설치

파이썬에 mysql을 연동하긱 위해선 database connector를 설치해야하며, 유명한 것이 pysqlmysqlclient였다. pysql은 파이썬 기반이며, mysqlclient는 C기반이어서 속도가 빠르다고 한다. mysqlclient를 권장하는 글들이 많아서 일단 mysqlclient를 사용하기로 했다.
프로젝트 가상환경에서 $ pip install mysqlclient 입력하여 설치에 성공했다. MySQL을 공식 홈페이지를 통해 다운 받았을 때는 mysql_config not found라는 문구를 포함한 ERROR가 발생하여 실패했었다. 때문에 해결방법을 서칭하여 위에서 언급한 단계를 시행해보았고, 설치에 성공할 수 있었다.

4) Django와 MySQL 연동

이 글 참고하였다.

0개의 댓글