티스토리 뷰

Flask-RESTful 라이브러리에는 Output Fields 라는 기능을 제공하여 response를 만드는 것에 대한 편의를 제공하고 있다. API의 response로는 `application/json`포맷을 자주 사용한다. 일단 flask의 기능을 사용한다고 하면, dictionary를 `jsonify` 함수의 인자로 넣어주면 적절한 response 객체가 만들어지긴 한다. (참고: Flask-RESTful Resource에서는 그냥 dictionary를 return 해도 json 포맷으로 response를 준다.) 그러나 출력하고자 하는 것을 dictionary로 만드는 것부터가 일단 일이다. 


flask 환경에서는 SQLAlchemy를 이용하는 경우가 많다. 이 때, SQLAlchemy를 어떻게 사용하느냐에 따라 모델 클래스를 얻기도 하고, SQLAlchemy의 커스텀 클래스를 얻기도 한다. 일반적인 사용 방법인 모델 클래스에 바로 query 함수를 호출하여 데이터를 로드하는 경우에는 해당 모델 클래스의 인스턴스를 얻고, `with_entities`와 같은 메소드를 쓰는 경우에는 `sqlalchemy.util._collections.result` 타입의 인스턴스를 얻게 된다. 후자의 경우엔 `keys` 같은 편리한 메소드가 있어서 `dict(zip(data.keys(), data))`와 같이 호출하면 dictionary가 잘 만들어지긴 한다. 하지만 일반적인 클래스(SQLAlchemy 모델 클래스 등)의 경우 `keys` 메소드 같은 것이 없기 때문에 dictionary를 쉽게 만들어내기 어렵다. 따라서 SQLAlchemy 모델 클래스를 상속받은, serialize를 쉽게 해주도록 다듬은 클래스를 쓰도록 한다던가 해야 한다. 말이 길어졌는데, 하여튼 귀찮은 일들을 해야한다. 이 정도까지만 언급해도 뭔가 좋은 해법이 필요하다는 설득이 어느 정도 전달되었으리라 믿는다.


"출력에 대한 부분만 깔끔하게 별도 로직으로 분리시키고 싶다." 이 부분을 나름대로 깔끔하게 해소해주는 기능이 바로 Output Fields 이다. 일단 공식 문서를 통해 간단한 사용법은 알 수 있지만, 공식 문서는 상당히 설명이 빈약함을 매번 느낀다. 영어라서 진입 장벽을 느끼는 분들도 계시고, 몇 몇 유용한 기능들을 눈에 잘 보이지 않게 써놔서 놓치는 부분도 있고 해서 이번 기회에 몇 가지를 언급한 가이드를 써봐야겠다고 생각하였다.


기본적인 API를 먼저 살펴보자. 예제들은 일단 모두 하드코딩 된 데이터지만, 이 글을 끝까지 읽어보면 SQLAlchemy를 이용하여 데이터베이스에서 로드한 데이터여도 문제가 없음을 알 수 있을 것이다.



게시글 목록을 조회하는 매우 간단한 API이다. 이 형태에서 조금씩 변화를 줘보자. 만약 IP 주소 전체가 보이는 것이 부담스러워 중간의 일부를 `*` 기호로 변경하여 보여주고 싶다고 해보자. 이 정도 쯤은 직접 API 함수에서 해줄 수 있다. 



ip attribute를 얻어와 직접 수정하는 코드를 추가하였다. 하지만 댓글 목록 조회 API에서도 이런 식으로 추가 처리를 해줘야 한다면? 이는 분명히 중복을 일으킨다. 게다가 일단 나 같은 경우 저런 코드를 API 코드에 함께 둔다는 사실 자체가 맘에 들지 않는다! 이 때 Flask-RESTful의 Output Fields를 쓸 수 있다. 



코드가 갑자기 좀 많아져서 약간 설명을 부연한다. 기본적인 `fields.Integer`, `fields.String` 같은 경우 별 설명이 없어도 쉽게 이해될텐데, 자신에게 주어진 객체로부터 `attribute`에 해당되는 이름의 필드를 찾아오고, `default` 값을 설정해둘 수도 있다. `fields.Nested` 이 녀석이 매우 특이한 녀석이다. depth가 더 깊어지게 되는 경우에 활용될 수 있는 필드 클래스인데, 특정 객체 속에서의 필드를 찾을 때나, 위 예제에서처럼 리스트 형태로 결과를 마샬링할 때 유용하게 쓰인다. 특이한 것은 `fields.Nested`의 `attribute`를 지정해주지 않는 경우, 자신에게 할당된 필드명으로부터 자동으로 객체를 찾은 뒤, 그것을 사용한다는 것이다. 그 차이를 보여주는 예가 바로 다음의 예이다.



API 함수에서는 `item-list` 라는 필드명으로 객체를 넘겨줬다. 하지만 `fields.Nested`은 `items`라는 이름의 필드로 쓰이고 있기 때문에, 정상적으로 객체를 찾아오지 못한다. 이를 올바르게 찾아 쓸 수 있도록 하기 위해 `attribute`에 `item-list` 라고 명시를 해주었다.


여기서 멈출 수 없다. 왜 굳이 리스트를 dictionary로 만들어서 반환해줘야 하는가? 그냥 리스트를 넘겨주면 자동으로 `items`는 필드명으로 넣어서 response를 만들어주면 편하지 않을까? 당연히 제공되는 기능이다. `marshal_with`에서 `envelop`를 명시해주면 된다.



`document_list_fields`의 내용물이 이전의 `fields.Nested`의 안의 것과 같이 된 것을 알 수 있다. `envelop`을 명시하면 API 함수의 return 값이 리스트일 경우 각 리스트의 내용물을 주어진 marshal rule에 따라 마샬링한 후 `envelop` 이라는 이름을 가지는 리스트 타입의 필드로 추가한다. 리스트가 아닌 경우에는 그냥 json 을 추가한 형태가 된다. (차이를 확인해보려면 API 함수의 return 값을 `items[0]`으로 바꿔보라.)


더 나아가보자. 각 게시글에 글쓴이 필드를 추가한다고 생각해보자. 이 때 `fields.Nested`를 쓰고 싶지 않다면? `attribute`에 dot notation을 활용할 수 있다. 



주어진 객체에서 `writer` 객체를 찾고, 그 객체의 `id` 값을 가져오기 위해 `attribute`를 `writer.id`로 명시한 것을 확인할 수 있을 것이다. 사실 위 예제로는 왜 굳이 이런식으로 달리 표현해야 하는지 알기 어렵다. 이 방법이 꼭 필요한 경우는 내부 객체에서 외부 객체의 값을 참조해야 하는 경우이다. 만약 게시글이 익명 모드일 경우 이름을 별도로 변경해줘야 한다고 생각해보자. 이 때는 `fields.Nested`로 내부 객체에 들어간 상태에서 외부 객체의 값을 참조할 수가 없다. 이 문제를 해결한 예는 다음과 같다. 



`attribute`에는 lambda 함수를 쓸 수도 있다. lambda 함수에서 외부 객체(게시글)의 `is_anonymous` 값을 확인하여 글쓴이의 이름을 `anonymous`로, `id`는 기본 값으로 변경하여 출력하도록 하였다. 필드 클래스를 구현하기에는 간단한 것일 때, 이렇게 lambda 함수를 쓸 수 있다. 하지만 lambda 함수에서는 복잡한 코드를 넣을 수 없기 때문에 한계가 존재한다. 따라서 필요한 경우에는 필드 클래스를 구현해야 한다.


현재 주어진 객체에서의 적절한 출력값을 만들어내기 좋은 방법은 커스텀 필드 클래스의 `output` 메소드를 활용하는 것이다. 어떤 필드명으로 쓰였는지와 주어진 객체가 무엇인지를 함수 인자로 받을 수 있다. 위에서 lambda 함수로 구현한 것을 커스텀 필드 클래스를 활용한 방법으로 다시 구현한 예가 아래의 코드이다.



`fields.get_value`라는 새로운 함수를 사용하고 있다. 이는 문서에서 알려주지 않는 유용한 함수 중 하나인데, dot notation을 이용하여 내부 객체를 쉽게 찾아 쓸 수 있도록 해준다. (실제로 직접 만들기에는 귀찮은 기능이다.) `writer` 필드에는 `fields.Nested`를 쓸 수 없다. `fields.Nested`를 쓰면 `AnonymousFields.output` 함수에서 전달받은 `obj`에서 외부 객체의 `is_anonymous`를 참조할 수 없기 때문이다. 


`fields.get_value`가 정말 유용한 또 다른 이유는 바로 dictionary 뿐만 아니라 다양한 형태의 객체에 사용할 수 있다는 점이다. 현재 API 함수에서는 그냥 dictionary를 넘겨줬기 때문에 일반적인 dictionary 사용 방법으로 값을 찾아 썼다. 하지만, 클래스일 경우에는 그렇게 하면 당연히 에러가 난다. 나 같은 경우 API 함수에서는 그냥 SQLAlchemy 모델 클래스를 찾아서 return 시켜주면 그냥 잘 마샬링이 되길 원했다. 그렇다고 클래스일 때랑 dictionary일 때를 구분하도록 추가적인 코드를 쓰는 것도 보기 좋지 않고.... `fields.get_value` 함수를 쓰면 이 문제가 그냥 해결된다. 다음의 코드가 그 예이다.



저 `Document`, `User` 클래스가 SQLAlchemy 모델 클래스여도 별 문제 없이 동작한다. 클래스는 subscriptable 객체가 아니라서 대괄호와 key를 이용한 접근시 에러가 난다. 따라서 이전의 `is_anonymous` 값을 참조하던 부분도 `fields.get_value` 함수로 대체해줘야 한다. 


Flask-RESTful의 Output Fields를 이용함으로써 출력에 대한 로직을 분리하는 예를 살펴보았다. 개인적으로 SQLAlchemy의 relationship과 위에서 소개한 Flask-RESTful의 Output Fields 기능을 이용함으로써 API들의 코드가 훨씬 더 간결해져서 나름 만족스러웠다. API 함수와의 로직 분리를 얼마나 할 것인가는 각자 판단의 몫이니 잘 생각해야 할 것이다. 이 글을 통해 위 기능의 유용함을 많은 이들이 쉽게 사용할 수 있게 되길 바란다.


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday