본문 바로가기

FastAPI 채팅 개발 일지

21. 05. 12 친구 관계 설정 API, Query logging, jwt 로그아웃

728x90

친구 관계 설정 API, Query logging, jwt 로그아웃

  1. 친구 관계 설정 API

    • 이 부분을 하면서 뭔가 다양한 것을 찾아본 것 같다. 쿼리 로깅을 달고 쿼리 횟수를 보면서 쿼리 횟수를 최대한 줄이기 위해서 orm 로딩 전략도 찾아보고 많은 것을 공부해볼 수 있었다.
    • api 작성 자체는 어렵지 않았다. 하지만 쿼리를 어떻게 날릴 건지에 대한 많은 고민을 했다.
      • 친구 상세 조회
        • 현재 로그인 된 유저와 친구 관계인지 먼저 확인하는 것이 필요했다. 그렇다면 현재 로그인 된 유저의 친구 목록을 모두 순회를 하면서 찾는 것은 비효율적이다. 그래서 조인으로 뭔가 해보려고 했다.
        • 중간 관계 테이블과 유저 테이블을 조인해서 로그인 유저-목적 유저가 존재할 때만 목적 유저의 객체만 가져오도록 했다.
      • 친구 목록 조회
        • 현재 로그인된 유저 객체를 찾고 해당 유저.friends를 하니 쿼리가 2번 이상 날라갔다. 이게 그 N+1 문제였던가...
        • 그래서 어차피 모든 친구들의 객체를 가져와야했기에 joinedload를 사용하여 쿼리 1번으로 끝낼 수 있었다.
      • 친구 목록 추가/삭제
        • 이 부분은 위 부분을 구현해놓으니 상대적으로 쉬웠다. 똑같은 방법으로 구현했고, orm으로 친구 관계를 넣어줘야 되어서 로그인 유저, 목적 유저를 조회하여 1개 객체를 꺼내서 서로의 친구 관계에 넣어주었다.
        • 하지만 삭제에서는 좀 생각해볼 것이 남겨져 있었다. 지금은 friends 배열에서 remove 함수로 제거하고 있는데, 만약 친구 목록이 많아지면 비효율적이기 때문에... 이건 어떻게 해야될지...
  2. Query logging

    • 회사에서 다른 팀 장고 프로젝트 하는 것을 보며, 단위 테스트를 돌리면서 쿼리 찍히는 것을 보며 쿼리 최적화를 진행하는 것을 보았다. 나는 여태까지 직접 쿼리를 보며 분석하지는 않았기에, 진짜 쿼리를 최적화 하려면 orm이 어떻게 실행되는 지 봐야겠다고 생각했다.
    • 로깅 자체는 어렵지 않았다. 그냥 파이썬 내장 로깅 가져와서 sqlalchemy 엔진을 찍어주면 되었다.
    • 파일이나 다른 방법으로 저장하는 것은 프로젝트가 좀 커지면 생각해봐야겠다.
  3. jwt 로그아웃

    • 사용자 로그아웃은 어떻게 처리해야되는지 궁금했다. jwt가 아닐땐 세션에서 제거해주고 했어야 했는데, 현재는 세션을 안쓰기 때문이었다. 그래서 찾아보았더니 단순히 클라이언트에서 해당 토큰을 브라우저에서 지워주면 된다고 한다.

    • 그런데 만약에 어떠한 이유로 access 토큰이 남아있어 사용이 된다면 문제가 된다. 그래서 로그아웃 할 때 redis에 username 값으로 토큰을 저장해주고 해당 토큰이 조회되는지 확인해주었다. → 마치 블랙리스트 느낌으로, 존재한다면 그건 이미 destory 된 토큰이니깐 인증을 안해주면 된다.

    • 근데 refresh 토큰도 해줘야하나....?

      Invalidating JSON Web Tokens

발생한 문제

  1. request.user를 가져와서 해당 친구 관계 가져올 시 lazy loading 관련 오류
    • Parent object <User> is not bound to a Session; lazy load operation of attribute cannot proceedrequets.user.friends로 접근할 때 문제가 발생했다.
    • Auth 미들웨어에서 db 세션을 엶 → 유저 객체를 꺼냄 → 해당 세션을 종료 → API 실제 처리 하는 곳에서 꺼내 씀 (여기서 db 세션을 새로 만듦) → 오류 발생
    • 리서치 해 본 결과, 위의 과정에서 db 세션이 다르기에 기본적으로 lazy loading 전략으로 User 객체를 가져와서 세션이 끊겨버리므로 더이상 해당 객체에서 chaining(?)을 할 수 없어서 발생하는 오류
      • 그럼 lazy loading 말고 join이나 다른 전략을 사용하면...? → 이건 매번 요청이 들어올때마다 유저에 연결된 모든 것을 가져오므로 효율적이지 않을 것 같음
    • 그렇다면 미들웨어에서 사용하는 db 세션을 계속 쓰면 되겠다 라고 생각했다. → 다만 redis에 더 이상 user를 저장하지 못한다.
    • 찾아본 결과 FastAPI에서 사용하는 starlette.request에서 request.state.some_value와 같이 원하는 값을 저장할 수 있었다. 그래서 request.state.db에 넣어서 본 API 처리 함수에서 꺼내서 썼다.
    • 해당 문제는 해결
  2. Authentication 미들웨어 효율성에 관한 의문
    • 위의 문제를 해결했더니 또 다른 문제가 발생했다. 친구 목록을 가져오는데 시간이 꽤 오래걸렸다.
    • 혹시나 해서 쿼리 로그 기록을 봐보았다.
    BEGIN (implicit)

    SELECT users.id AS users_id, users.login_id AS users_login_id, users.username AS users_username, users.password AS users_password, users.nickname AS users_nickname, users.create_dt AS users_create_dt, users.update_dt AS users_update_dt
    FROM users
    WHERE users.id = %(pk_1)s

    [generated in 0.00014s] {'pk_1': 1}

    BEGIN (implicit)
    SELECT users.id AS users_id, users.login_id AS users_login_id, users.username AS users_username, users.password AS users_password, users.nickname AS users_nickname, users.create_dt AS users_create_dt, users.update_dt AS users_update_dt
    FROM users
    WHERE users.id = %(pk_1)s

    [cached since 1.909s ago] {'pk_1': 1}

    SELECT users.id AS users_id, users.login_id AS users_login_id, users.username AS users_username, users.password AS users_password, users.nickname AS users_nickname, users.create_dt AS users_create_dt, users.update_dt AS users_update_dt
    FROM users, friendships
    WHERE %(param_1)s = friendships.user_id AND users.id = friendships.friend_id

    [generated in 0.00011s] {'param_1': 1}
- 쿼리 자체는 2번 보낸 것 같다. 두번째꺼는 캐시된 것을 가져온 것 같다. → 캐시된 걸 가져오는데 꽤 오래걸린다... postman에서 2.72s가 떴다.
- 그럼 미들웨어를 안쓰고 그냥 API 함수에서 직접 꺼내서 쓰면 어떻게 되나 궁금했다.
    BEGIN (implicit)

    SELECT users.id AS users_id, users.login_id AS users_login_id, users.username AS users_username, users.password AS users_password, users.nickname AS users_nickname, users.create_dt AS users_create_dt, users.update_dt AS users_update_dt
    FROM users
    WHERE users.id = %(pk_1)s

    [generated in 0.00016s] {'pk_1': 1}

    SELECT users.id AS users_id, users.login_id AS users_login_id, users.username AS users_username, users.password AS users_password, users.nickname AS users_nickname, users.create_dt AS users_create_dt, users.update_dt AS users_update_dt
    FROM users, friendships
    WHERE %(param_1)s = friendships.user_id AND users.id = friendships.friend_id

    [generated in 0.00018s] {'param_1': 1}
- 우선 쿼리는 똑같이 2번인데 캐시된걸 가져오는 것이 빠졌다 (당연히 바로 사용하니깐) → postman에서 0.611s 걸렸다.
- 위에서 캐시된 것을 가져오는 것은 API함수에서 `request.user`를 사용할 때 발생한다. 해당 쿼리를 사용하지 않게 하는 방법은 아직까지 못찾았다.
- 이번에 미들웨어를 써보는 이유가 장고처럼 편하게 현재 로그인 된 유저를 가져오고 싶어서 그랬는데, 이렇게까지 느리면 사용하지 않고, 그냥 필요할때마다 직접 해당 함수에서 사용하는 것이 나을 것 같다.
728x90