Project4 - About Boards & Articles Project

Heechul Yoon·2020년 4월 30일
0

LOG

목록 보기
49/62

프로젝트 소개(Project Description)

주제(Topic)

  • CRUD 게시판 만들기 프로젝트

구성원(Member)

  • 1인 프로젝트

기간(Developing Period)

  • 5일(20200427 ~ 20200501)

적용 기술(Skill Applied)

Python 3.8.0 : language
Flask 1.1.2 : web framework
Git : cooperation and version management tool
Redis : Caching database
PostgreSQL : Database
Alembic : migration tool
SQLalchemy : ORM(Object Relational Mapping)
Bcrypt : password hashing
JWT : token generating

깃허브(Github)

프로젝트 초기 설계

  • MVC layered pattern에 맞게 view, service, model(dao)로 구분
  • flask의 blueprint를 사용하여 app.py에서 등록된 app에 대하여 일괄 routing
  • sqlalchemy의 declarative_base를 사용하여 모델링(postgresql database 사용)
  • alembic 라이브러리를 사용한 migration 관리
  • 데이터베이스에 alembic migrations를 통해서 auth_types, users, boards, articles 테이블 생성
  • users앱, boards앱 생성
  • sqlalchemy의 sessionmaker를 사용하여 데이터베이스와 통신
    • reqeust당 한번의 세션이 열리고 닫힘
    • 세션이 열리고 닫힐 때 애러처리
  • database 정보 모듈화
  • .gitignore 생성
  • freeze를 통해서 프로젝트 환경 공유파일(requirements.txt) 생성

모델링(ERD)

  • 유저테이블의 auth_types_id 컬럼이 auth_types 테이블을 외래키로 가지면서 유저에게 권한 타입이 부여되고 권한의 종류에 따라 취할 수 있는 action이 달라진다

  • 로그인, 회원가입에 성공하면 토큰이 발행되는데, 토큰은 클라이언트에게 바로 직접적으로 리턴되지 않고 session storage역할을 하는 redis 저장공간에 key-value형태로 발행된다. 따라서 redis 저장공간에 발행된 토큰의 key를 데이터베이스 random_keys 테이블에 저장한다

  • 유저 권한 타입이 master인 유저는 게시판을 생성/수정/삭제 할 수 있다. 게시판의 작성자의 권한타입은 uploader 컬럼을 통해서 확인할 수 있다.

  • 게시판의 이름을 수정하거나 게시판을 삭제하면 작성자(modifier) 컬럼에 값이 추가되고, updated_at컬럼에 업데이트 날짜가 기록된다. 게시판의 작성자(uploader)는 수장자(modifier)과 다를 수 있다. 마스터권한을 가진 유저라면 게시판을 생성/수정/삭제할 수 있기 때문이다

  • 게시물은 특정 게시판 아래에서 생성되기 때문에 게시판 테이블의 id를 외래키로 가지며 게시물 또한 작성자(uploader)와 수정자(modifier)의 id를 가진다. 게시물의 수정자(modifier)는 게시물을 작성한 유저만 될 수 있다. 따라서 게시물은 작성자(uploader)와 수정자(modifier)가 일치한다

기능(API document)

로그인 데코레이터
  • request로 들어온 토큰을 decode해서 토큰 유효성 검사
  • 토큰 확인 후 성공 시 유저정보를 flask g 객체에 저장
회원가입
  • flask_request_validator를 통한 request body 유효성 검사(이메일 중복체크, 이메일 형식체크 등)

  • bcrypt 비밀번호 암호화 : 단방향 암호화, 로그인 시 해쉬값을 비교

  • 회원가입 성공 시 jwt 토큰(유효기간 6일)을 redis에 uuid를 키(key)로 하여 저장

   "8b3b9a0b-5fbc-47d6-a113-5479d33aec7f":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6OCwiZXhwIjoxNTg4NzY0MTU3fQ.W_gWd9sXPpLaY6teppDYgCKRW8rFz8O5-_vMcfzS6jM"
  • redis에 저장된 uuid 키(key)를 데이터베이스에 random_key테이블에 저장
로그인
  • flask_request_validator를 통한 이메일, 패스워드 유효성검사

  • bcrypt checkpw를 통해서 해쉬된 패스워드 비교

  • 로그인 성공 시 jwt 토큰(유효기간 6일)을 redis에 uuid를 키(key)로 하여 저장

   "8b3b9a0b-5fbc-47d6-a113-5479d33aec7f":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6OCwiZXhwIjoxNTg4NzY0MTU3fQ.W_gWd9sXPpLaY6teppDYgCKRW8rFz8O5-_vMcfzS6jM"
  • redis에 저장된 uuid 키(key)를 데이터베이스에 random_key테이블에 저장
로그아웃
  • 로그인 할 때 리턴 한 redis 리소스 접근 key를 request body로 받음
  • 토큰 접근 key를 redis에서 삭제해서 다음 api 요청 시 토큰을 찾을 수 없도록 함
게시판 생성
  • 유효성검사 : 마스터권한이 아닐 시 400 리턴
  • 유효성검사 : 생성하려는 게시판 이름이 중복된 이름이면 400리턴
게시판 리스트 표출
  • 삭제되지 않은 게시판의 리스트를 표출함
  • 검색 기능 : 게시판 이름을 통한 검색. like를 통해서 해당 글자가 포함되면 검색하는 기능
  • pagination : offset과 limit을 쿼리 파라미터로 받아서 페이지네이션 구현
게시판 이름 수정
  • 유효성검사 : 마스터권한이 아닐시 400리턴
  • path parameter로 수정 대상 게시판 아이디를 받음
  • 게시판 테이블에 수정자 컬럼 업데이트
게시판 삭제
  • 유효성검사 : 마스터권한이 아닐시 400리턴
  • path parameter로 게시판 아이디를 받고 body로 게시판 삭제 True를 받음, 삭제 api이기 때문에 True이외의 액션은 400애러 처리
  • boards 테이블의 is_deleted 컬럼의 값을 False에서 True로 바꿔주는 soft delete
  • 삭제하고자 하는 게시판이 이미 삭제되어있으면 400 리턴
  • 게시판 테이블에 수정자 컬럼 업데이트
  • 게시판을 삭제하고 해당 게시판에 작성된 모든 게시물도 삭제함
게시물 생성
  • path 파라미터로 게시판 아이디를 받고 해당 게시판에 게시물 생성
  • flask validator를 통해서 게시물 제목, 게시물 내용 유효성 검사
  • 이미 삭제된 게시판에 게시물을 생성하려는 경우 404 애러 리턴
게시물 리스트 표출
  • path 파라미터로 게시판 아이디를 받고 해당 게시판의 게시물 리스트 표출
  • 이미 삭제된 게시판의 게시물 리스트를 가져오려는 경우 404리턴
  • 삭제되지 않은(is_deleted==false) 게시물을 리스트 형식으로 최근 생성된 순으로 정렬해서 가져옴
  • 검색기능 : 게시물 제목과 작성자를 통한 검색. like를 통해서 해당 글자가 포함되면 검색하는 기능
  • pagination : offset과 limit을 쿼리 파라미터로 받아서 페이지네이션 구현
게시물 상세 정보 표출
  • path파라미터로 게시판아이디와 게시물 아이디를 같이 받아서 게시물 상세정보 표출
  • 이미 삭제된 게시판 또는 게시물의 정보를 가져오려는 경우 404 리턴
게시물 수정
  • path 파라미터로 게시판 아이디와 게시물 아이디를 받아서 해당 게시물의 내용을 수정
  • flask validator를 통해서 게시물 제목과 게시물 내용의 유효성검사
  • 게시물의 생성자가 게시물의 수정하려는 경우가 아닌 경우 403리턴
  • 이미 삭제된 게시판 또는 게시물을 수정하려는 경우 404 리턴
  • 게시물 제목 또는 게시물 내용을 업데이트 하고, 게시물 수정자를 업데이트함.
게시물 삭제
  • path 파라미터로 게시판 아이디와 게시물 아이디를 받고, body로 삭제여부 True를 받아서 해당 게시물의 내용을 삭제
  • 섹제 api이기 때문에 True이외의 액션은 400리턴
  • 게시물의 생성자가 아닌데 게시물을 삭제하려는 경우 403리턴
  • 이미 삭제된 게시판 또는 게시물을 삭제하려는 경우 404 리턴
  • is_deleted를 True로 바꿔주고 수정자 업데이트
대시보드 표출
  • 게시판 리스트 표출 함수, 게시물 리스트 표출 함수 재활용
  • 존재하는 게시판에 가장 최근의 게시물리스트 N개를 가져온다.
  • 게시판 리스트표출 함수를 호출해서 게시판 리스트를 가져옴.
  • 게시물 리스트표출 함수를 호출해서 게시판 게시판 id를 parameter로 넣어서 해당 게시판의 게시물 리스트를 가져옴.

기억에 남는 기능

게시판 삭제시 해당 게시판안에 있는 게시물도 전부 삭제

[1] Session = sessionmaker(bind=engine)
            session = Session()

[2]         if session.query(Board.is_deleted).filter(Board.id == board_info['board_id']).one()[0]:
                return  jsonify({'message': 'BOARD_NOT_EXISTS'}), 404

[3]         (session
             .query(Board)
             .filter(Board.id == board_info['board_id'])
             .update({'is_deleted': board_info['is_deleted'], 'modifier': board_info['modifier']}))

[4]         (session
             .query(Article)
             .filter(Article.board_id == board_info['board_id'])
             .update({'is_deleted': board_info['is_deleted'], 'modifier': board_info['modifier']}))

[5]         session.commit()
            return jsonify({'message': 'SUCCESS'}), 200

UI에서 게시판이 삭제되면 해당 게시판이 보이지 않기 때문에 게시물로 접근할 수 없겠지만, 서버를통해서 삭제된 게시판에 있는 게시물로 접근할 수 있기 때문에 삭제된 게시판의 게시물도 함께 삭제해준다

[1] 우선 데이터베이스에 접근하는 세션을 열어준다
[2] path파라미터로 들어온 게시판 아이디가 데이터베이스에 없으면 애러를 리턴한다
[3] 삭제하고자 하는 게시판의 is_deleted 컬럼의 값을 True로 바꿔준다
[4] 삭제하고자 하는 게시판의 게시물의 is_deleted 컬럼의 값을 모두 True로 바꿔준다
[5] 데이터베이스 변경사항을 session commit을 통해서 확정한다

redis를 세션스토리지로 활용한 로그인과 로그아웃(소스코드)

클라이언트의 세션스토리지를 redis로 대체하여 로그아웃을 백엔드 서버 쪽에서 구현했던 것이 새로운 개념이었다. 소스코드는 여기를 통해서 확인하자

데이터베이스에 redis 접근 key를 저장하는 이유는 로그아웃 기능 때문이다. 로그아웃 기능을 살펴보자.

로그아웃 기능을 알기 위해서는 토큰의 역할을 분명히 알아야한다.

  1. 로그인 시 서버는 클라이언트에게 토큰을 발행 한다
  2. 클라이언트는 그 토큰을 세션스토리지에 저장한다.
  3. 토큰이 필요한 api를 호출할 때 마다 세션스토리지에서 토큰을 가져와 request headers에 실어보낸다
  4. 로그아웃 시 세션스토리지에서 토큰을 삭제한다

세션스토리지를 활용하면 프론트엔드 서버에서 로그아웃을 세션스토리지에서 토큰 삭제를 통해서 구현한다.

백앤드서버에서 로그아웃을 구현하려면 어떻게 해야할까?
정답은 세션스토리지를 백앤드 서버가 접근할 수 있는 곳으로 옮기는 것이고 대표적인 key-value nosql 데이터베이스인 redis를 통해서 구현가능하다.

redis는 value에 해당하는 key를 할당해서 key를 통해서 value에 접근할 수 있다. 이제 프론트엔드는 세션스토리지 대신 자바스크립트 객체를 redis에 저장한다. redis를 통한 로그인 로그아웃 과정을 알아보자

  1. 로그인에 필요한 데이터를 백앤드 서버로 보낸다
  2. 백엔드 서버로 들어온 로그인정보를 확인해서 데이터베이스 정보와 일치하면 토큰을 생성한다
  3. redis에 랜덤한 key를 만들고, 그 key에 토큰을 붙혀서 저장시킨다
  4. 로그인에 성공해서 토큰을 발급할 때 직접 클라이언트에게 토큰을 주지않고 세션스토리지 대용으로 사용하는 redis에 랜덤이름의 key(uuid) + 토큰을 넣고, uuid와 토큰을 리턴한다. 클라이언트는 받은 토큰을 세션스토리지에 저장하지 않고 redis에서 꺼내쓴다.
  5. 클라이언트는 권한이 필요한 api를 요청할 때는 redis에 로그인 할 때 받은 key를 통해서 토큰을 가져와서 사용한다
  6. 그리고 로그아웃 api를 호출할 때 request body로 로그인 시 받은 key(uuid)와 토큰을 보낸다
  7. 백앤드 서버에선느 토큰을 decode해서 expiration date를 현시각으로 바꿔서 만료토큰을 만든다
  8. 클라이언트에게서 받은 key로 redis에 있는 로그인 토큰을 만료토큰으로 치환한다

이제 클라이언트가 로그아웃을 한 상태에서 redis에서 토큰을 가져와서 권한이 필요한 api를 호출하면 만료토큰을 사용할 것이고, 백앤드서버에서 만료토큰은 유효성검사에서 걸러지게 된다.

게시판 리스트 표출, 게시물 리스트 표출 함수를 재활용한 대시보드

게시판 리스트 표출 API, 게시물 리스트 표출 API를 재활용해서 가장 각각의 게시판에 가장 최근 게시물을 가져오는 대시보드 API를 만들었다

    @board_app.route("/recent-articles", methods=["GET"], endpoint='get_recent_board_article')
    @login_required
    @validate_params(
[1]     Param('offset', GET, int, required=False),
[2]     Param('limit', GET, int, required=False),
    )
    def get_recent_board_article(*args):
[3]     board_info = {
            'offset': 0,
            'limit': None
        }
[4]     board_service = BoardService()
[5]     board_list = board_service.get_board_list(board_info)

[6]     for board in board_list:
            board_service = BoardService()
[7]         article_info = {
                'board_id': board['id'],
                'offset': args[0] if args[0] else 0,
                'limit': args[1] if args[1] else 2
            }
[8]         article_list = board_service.get_article_list(article_info)
            board['recent_article_list'] = article_list

        return jsonify({'dashboard': board_list})

[1][2] 기존에 만든 게시판 리스트 표출, 게시물 리스트 표출 함수는 전부 offset과 limit 값이 인자로 필요하기 때문에 기준을 쿼리파라미터로 받는다
[3] 게시판의 경우 offset과 limit을 통해서 페이지네이션을 할 필요가 없다고 판단하여 값을 처음부터 지정해주도록 한다
[4][5] 만들어 두었던 게시판 리스트 표출 함수를 호출해서 값을 가져온다
[6] 하나의 게시판안에 있는 게시물을 가져와야 하기 때문에 for문을 돌린다
[7] 게시물을 가져오기 위해서 필요한 파라미터를 게시물 리스트 표출 함수의 파라미터로 넘긴다(게시물을 가져오기 위해서 게시판 아이디, offset, limit이 필요함)
[8] 각각의 게시판에 게시물 리스트를 가져와서 게시판 정보에 새로운 key를 부여해서 value를 넣는다

스스로에 대한 피드백

성장

  • 클라이언트의 세션스토리지를 백엔드 서버에서 접근할 수 있는 nosql 데이터베이스를 사용한다는 개념을 실험을 통해서 이해하고 이해한 즉시 기능으로 구현 한 점이다. 이제 새로운 기술을 사용할 때 실험을 통해서 결과를 토출하는 것이 익숙해졌다. 스스로 문제를 해결하는 능력이 점점 상승하고 있는 것 같다.
  • 백앤드 서버에서 발생하지 말아야 할 예외에 대해서 더 많이 생각할 수 있게 되었다. 예를들면 게시판을 삭제하고 해당 게시판에 있는 게시물을 삭제하지 않았을 때 클라이언트 직접적으로 접근할 수 있는 삭제된 게시판의 게시물을 백엔드 서버에 직접 접근해서 crud 할 수 있는 위험이 있기 때문에 게시판을 삭제할 때는 게시판 삭제와 동시에 해당 게시판의 게시물도 같이 삭제해야 한다. 즉, UI에서 구현되지 않기 때문에 클라이언트가 접근할 수 없는 리소스이지만(클라이언트는 UI를 통해서 백엔드 서버에 접근한다는 전제) 백엔드 서버에 직접 요청을 걸어와서 접근 할 수 있는 리소스들 또는 그로인해서 발생할 애러들을 고려 할 수 있을 정도로 생각의 폭이 확장되었다는 점에서 발전이 있었다고 생각한다.

아쉬운점

  • 존재하지 않는 게시판이나 게시물을 호출 할 경우 에러가 쿼리를 실행하는 위치에서 발생하는 것이 아닌 로그인 데코레이터에서 발생하며 그 이유를 발견하지 못한 점이 아쉬웠다. 그래서 로그인 데코레이터 위치에서 try-except를 걸어서 에러를 잡아줬다. 데이터베이스 session이 열리면서 쿼리로 보낸 게시판 아이디가 없으면 그 위치에서 애러가 날 것으로 예상했지만 로그인 데코레이터에서 에러가 발생했기 때문에 원인을 알 수 없었다

개선방법

  • 데이터베이스 session이 열리고 sqlalchemy가 어떤식으로 데이터베이스에 접근해서 쿼리를 하는지 이해하고 sqlalchemy가 쿼리를 했을 때 쿼리 한 데이터가 없으면 어떻게 에러를 리턴하는지 실험을 해서 알아 볼 필요가 있다고 생각했다.
profile
Quit talking, Begin doing

0개의 댓글