상세 컨텐츠

본문 제목

깔끔한 파이썬 탄탄한 백엔드 예제 수정

Python

by toa-lee 2024. 1. 1. 18:59

본문

https://m.yes24.com/Goods/Detail/68713424

 

깔끔한 파이썬 탄탄한 백엔드 - 예스24

파이썬 개발 환경 구축부터 API 개발, HTTP, Database, Unit Test, AWS Deploy까지 백엔드 개발 입문의 모든 것!파이썬을 지식으로 아는 것뿐 아니라 파이썬을 응용하여 백엔드 시스템을 개발할 수 있도록

m.yes24.com

쉬는 동안 이 책을 읽으면서 되게 쉽게 읽히고 재밌어서 쭉 읽다가
재밌는 예제가 나와서 직접 타이핑을 해보고 수정을 해보았다.

 

기존 예제 코드(Python 3.7)

 

from flask import Flask, request, jsonify
from flask.json import JSONEncoder

class CustomJSONEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, set):
            return list(obj)
        
        return JSONEncoder.default(self, obj)

app = Flask(__name__)
app.users = {}
app.tweets = []
app.json_encoder = CustomJSONEncoder

app.id_count = 1

@app.route("/sign-up", methods=['POST'])
def sign_up():
    new_user    = request.json
    new_user["id"]  = app.id_count
    app.users[app.id_count] = new_user
    app.id_count    = app.id_count + 1

    return jsonify(new_user)

@app.route("/tweet", methods=['POST'])
def tweet():
    payload = request.json
    user_id = int(payload['id'])
    tweet = payload['tweet']

    if user_id not in app.users:
        return 'No User', 400
    if len(tweet) > 300:
        return '300 Over', 400
    
    user_id = int(payload['id'])

    app.tweets.append({
        'user_id' : user_id,
        'tweet' : tweet
    })

    return '', 200

@app.route("/follow", methods=['POST'])
def follow():
    payload = request.json
    user_id = int(payload['id'])
    user_id_to_follow = int(payload['follow'])

    if user_id not in app.users or user_id_to_follow not in app.users:
        return 'No User', 400
    
    user = app.users[user_id]
    user.setdefault('follow', set()).add(user_id_to_follow)

    return jsonify(user)

@app.route("/unfollow", methods=['POST'])
def unfollow():
    payload = request.json
    user_id = int(payload['id'])
    user_id_to_follow = int(payload['unfollow'])

    if user_id not in app.users or user_id_to_follow not in app.users:
        return 'No User', 400
    
    user = app.users[user_id]
    user.setdefault('follow', set()).discard(user_id_to_follow)

    return jsonify(user)

@app.route("/timeline/<int:user_id>", methods=['GET'])
def timeline(user_id):
    if user_id not in app.users:
        return 'No User', 400
    
    follow_list = app.users[user_id].get('follow', set())
    follow_list.add(user_id)
    timeline = [tweet for tweet in app.tweets if tweet['user_id'] in follow_list]

    return jsonify({
        'user_id' : user_id,
        'timeline' : timeline
    })

 

이게 기존 예제인데 아무래도 책이 오래 되다보니 python 3.7 버전으로 되어있어서

위와 같이 적용을 하면 에러가 발생한다.

 

이걸 바로 실행하면 아마 아래와 같은 에러가 발생 할 것이다.

ImportError: cannot import name 'JSONEncoder' from 'flask.json' (/opt/homebrew/lib/python3.11/site-packages/flask/json/__init__.py)

 

해결 방법은 import flask.json이 아닌 import json으로 수정 해야한다.

https://stackoverflow.com/questions/76107450/flask-attributeerror-module-flask-json-has-no-attribute-jsonencoder

 

Flask AttributeError: module 'flask.json' has no attribute 'JSONEncoder'

My flask app was working prior to upgrades. I was having trouble with sending email when there was a forgot-reset-password. To try and fix this I recently upgraded some modules for my flask app. The

stackoverflow.com

이 stackoverflow를 확인해서 해당 내용을 수정 했다.

 

위와 같이 수정하면 실행은 잘 될것이다.

다만, follow, unfollow 기능 사용 시에 아래와 같은 에러 문구가 발생 할 것이다.

    raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
TypeError: Object of type set is not JSON serializable

 

이는 set은 json이 지원하지 않아 json으로 변환할 수 없다.

책에도 위와 같은 내용이 나와 맨 위에 CustomEncoder를 사용하는데

여러 시도를 해봐도 CustomEncoder는 적용 되지가 않았다.

 

그래서 내가 내린 결론은 list를 사용한 것이었다.

저자는 set이 중복 제거를 해주기 때문에 사용 했다고 말했다.

 

list를 사용하는 나는 이 중복 제거 로직을 따로 넣어줬다.

아래는 수정된 follow, unfollow 엔드포인트이다.

 

@app.route("/follow", methods=['POST'])
def follow():
    payload = request.json
    user_id = int(payload['id'])
    user_id_to_follow = int(payload['follow'])

    if user_id not in app.users or user_id_to_follow not in app.users:
        return 'No User', 400

    user = app.users[user_id]
    if 'follow' not in user:
        user['follow'] = []

    if user_id_to_follow not in user['follow']:
        user['follow'].append(user_id_to_follow)

    return jsonify(user)




@app.route("/unfollow", methods=['POST'])
def unfollow():
    payload = request.json
    user_id = int(payload['id'])
    user_id_to_unfollow = int(payload['unfollow'])

    if user_id not in app.users or user_id_to_unfollow not in app.users:
        return 'No User', 400

    user = app.users[user_id]
    if 'follow' not in user:
        user['follow'] = []

    if user_id_to_unfollow in user['follow']:
        user['follow'].remove(user_id_to_unfollow)

    return jsonify(user)

 

앞에서 말햇듯이 처음부터 list로 받고 유저 딕셔너리에 follow 키 값이 없으면 빈 list를 주는 if문, follow에 해당 유저가 없다면 append를 해주는 if문 이렇게 2가지 로직을 넣어줬다.

 

추가적으로 타임라인 엔드포인트의 경우에도 set에 맞춰 코드가 짜여져 있기 때문에 아래와 같이 수정했다.

@app.route("/timeline/<int:user_id>", methods=['GET'])
def timeline(user_id):
    if user_id not in app.users:
        return 'No User', 400
    
    # 사용자가 팔로우하는 사람들의 목록을 가져옴
    follow_list = app.users[user_id].get('follow', [])

    # 현재 사용자의 ID를 추가하지만 중복 방지
    if user_id not in follow_list:
        follow_list.append(user_id)

    # 타임라인 생성: 팔로우하는 사용자들의 트윗을 포함
    timeline = [tweet for tweet in app.tweets if tweet['user_id'] in follow_list]

    return jsonify({
        'user_id' : user_id,
        'timeline' : timeline
    })

 

follow list를 set이 아닌 배열로 가져오고 follow, unfollow 엔드포인트와 마찬가지로 현재 ID 추가 시에 중복 방지 로직을 추가했다.

생성 시에 add가 아닌 append로 변경 후 마무리 했다.

 

그래서 전체 코드를 보자면 아래와 같다.

(Python 3.11)

from flask import Flask, request, jsonify

app = Flask(__name__)
app.users = {}
app.tweets = []

app.id_count = 1

@app.route("/sign-up", methods=['POST'])
def sign_up():
    new_user    = request.json
    new_user["id"]  = app.id_count
    app.users[app.id_count] = new_user
    app.id_count    = app.id_count + 1

    return jsonify(new_user)

@app.route("/tweet", methods=['POST'])
def tweet():
    payload = request.json
    user_id = int(payload['id'])
    tweet = payload['tweet']

    if user_id not in app.users:
        return 'No User', 400
    if len(tweet) > 300:
        return '300 Over', 400
    
    user_id = int(payload['id'])

    app.tweets.append({
        'user_id' : user_id,
        'tweet' : tweet
    })

    return '', 200

@app.route("/follow", methods=['POST'])
def follow():
    payload = request.json
    user_id = int(payload['id'])
    user_id_to_follow = int(payload['follow'])

    if user_id not in app.users or user_id_to_follow not in app.users:
        return 'No User', 400

    user = app.users[user_id]
    if 'follow' not in user:
        user['follow'] = []

    if user_id_to_follow not in user['follow']:
        user['follow'].append(user_id_to_follow)

    return jsonify(user)




@app.route("/unfollow", methods=['POST'])
def unfollow():
    payload = request.json
    user_id = int(payload['id'])
    user_id_to_unfollow = int(payload['unfollow'])

    if user_id not in app.users or user_id_to_unfollow not in app.users:
        return 'No User', 400

    user = app.users[user_id]
    if 'follow' not in user:
        user['follow'] = []

    if user_id_to_unfollow in user['follow']:
        user['follow'].remove(user_id_to_unfollow)

    return jsonify(user)



@app.route("/timeline/<int:user_id>", methods=['GET'])
def timeline(user_id):
    if user_id not in app.users:
        return 'No User', 400
    
    # 사용자가 팔로우하는 사람들의 목록을 가져옴
    follow_list = app.users[user_id].get('follow', [])

    # 현재 사용자의 ID를 추가하지만 중복 방지
    if user_id not in follow_list:
        follow_list.append(user_id)

    # 타임라인 생성: 팔로우하는 사용자들의 트윗을 포함
    timeline = [tweet for tweet in app.tweets if tweet['user_id'] in follow_list]

    return jsonify({
        'user_id' : user_id,
        'timeline' : timeline
    })

 

이 코드로 실행 해보니 문제가 없었다.

대부분의 도움은 내 머리가 아닌 GPT의 도움을 받았다.

아무래도 초보이다 보니 이런 생각 자체를 못 했는데 막상 완성되니 내가 한 것 같아서 기분이 묘하다.