웹 어플리케이션을 개발하는 과정이 생각보다 오래 걸려서, 다른 챕터들에 비해 업로드의 텀이 매우 넓게 잡혀버린 것에 죄송한 마음을 전합니다. 이 컨텐츠를 진행하기 위한 시간이 그렇게 많지 않다는 걸 확실히 인지하고 있었더라면 범위를 좀 적게 잡을걸 싶기도 했는데, '시간이 나서' 하는 것보다 '시간을 내서' 하는 게 맞는 것 같으니 이제 좀 더 열심히 글을 써봐야겠습니다. 저번 챕터 업로드와 이번 챕터 업로드 사이에 벨로그 트렌딩에 올라간 것, 코딩 교육 부트캠프인 Code States의 페이스북 페이지에 글이 공유된 것, 잘 읽고 있다는 댓글 모두 잘 보고 있습니다. 감사합니다.
13챕터의 회고다. 웹 어플리케이션 개발 과정에 무슨 일이 있었고, 어떤 생각을 가지고 진행했는지를 공유하고자 한다. 이번 챕터의 목표는 이 두 가지다.
이 '회고'라는 과정은 실제 개발 조직들에서(내가 일하고 있는 ab180의 백엔드 팀에서도!) 자주 하는 것이고, 서로의 상황을 공유하고 개발 환경을 개선하기 위한 의견을 공유하는 등 여러 이점들이 있으니, 독자 여러분도 팀 프로젝트를 진행한다면 몇 주마다 한 번씩 회고를 해 보는 것도 좋은 경험이 될 것 같다.
이번 챕터에서 진행하고자 하는 회고는 위에서 말했던 대로 13챕터에서 시간을 끌었던 웹 어플리케이션 개발 과정에 대한 회고다. 시간 순서대로 이슈를 나열해서 섹션을 나누고, 이슈별로 커밋을 나열하고, 해당되는 pull request를 링크하고, 얘기하고 싶은 부분이 있는 곳에는 글로 열심히 떠들어 보겠다.
참고로, '이 코드 이상한데?' 싶은 것들은 대부분 다 나중에 글쓸 각을 잡기 위해 노린 거니까, '여기에 이거 들어가야 할 것 같아요'하는 의견은 정중히 거절하겠다.
.gitignore
는 git이 관리하지 않았으면 하는 파일을 명시하기 위한 파일이다(What is .gitignore exactly? - Stack Overflow). 굳이 git을 통해 공유하지 않아도 되는 것을 넣어두면 되는데, PyCharm에서 workspace와 관련된 상태 정보를 저장하는 .idea/
하위 파일, Python 캐시(.pyc)가 저장되는 __pycache__/
하위 파일 등이 그 예다. Documentation을 보고 하나하나 손수 작성할 수도 있지만, 운영체제/개발 환경/프로그래밍 언어별로 gitignore 내용을 만들어주는 gitignore.io라는 좋은 서비스가 있어서 이걸 사용했다.
뭐 포함시킬지 한 1분 생각하고, 바로 뽑아서 올렸다. 작업 착수부터 이슈 close까지 따지면 5분 정도 걸렸다.
특별한 것 없었다. 의존성 관리 툴도 이미 의사결정이 끝난 상태였고, 의존성 목록도 이미 12. 어플리케이션 레벨 의사결정 - (2)#사용할 라이브러리에서 결정해 두었기 때문이다. 여기서 무슨 라이브러리가 Python 3를 지원하지 않는다거나 하는 삑사리가 있었으면 좀 헤맸을텐데 그런 것도 없었다.
리스팅해둔 거 따라서 pipenv install schematics sqlalchemy mysqlclient ...
실행시켜 두고, 설치 다 된 후에 run.py
실행해 보고 문제 없길래 바로 커밋했다. lock이 오래 걸려서 한 10분 걸렸다.
zappa_settings.json
을 변경했던 577f83b 커밋을 push한 후 PR을 한 번 더 올렸다 : #12
Flask Large Application Example 레포 받아서 싹 붙여넣고, 의사결정의 범위에서 벗어나는 것들이 있길래 그것들 제거하고, zappa_settings.json
의 app_functions
값을 변경하는 작업을 진행했다.
30분 정도 걸렸다.
예전에 validation 용도로 schematics를 써봤었던 적이 있어서 완전 처음 구현해보는 건 아니었는데, validate_with_schematics
함수 하나 달랑 만들고 나서 커밋하고 보니까 너무 부실한 것 같아서 reset시키고 아래의 작업들을 더 진행했었다.
데코레이터 인자들에 type hinting
PayloadLocation
이라는 enum을 추가하고 데코레이터에 인자를 추가해서, query string에 대한 validation도 가능하도록 만듦
_ContextPropety
클래스에 request_payload_object
프로퍼티를 만들어서, view function 단에서는 context property 객체에 접근해서 명시해둔 스키마의 객체를 얻어갈 수 있도록 만듦 → API 구현 커밋들에서 확인해볼 수 있다.
딴짓 하면서 하느라 무려 2시간 걸렸다. 딴짓 안하고 했으면 3~40분 언저리로 끝냈을 것 같다.
SQLAlchemy의 engine 객체를 생성하기 위한 설정을 추가하고, session에 관한 부분을 Flask extension 라이브러리처럼 추상화시키는 데에 시간을 들였다. - app/models/init.py::DB
이에 대해서는 'SQLAlchemy Transaction 관리 Practice 공유'라는 글이 많은 영감을 주었는데, 도메인이 만료되었는지 더이상 연결되지 않아서 내용을 대충 요약하자면,
with
문으로 세션을 생성하지 말고, 데코레이팅된 함수 단위로 세션을 발급하는 방식으로 트랜잭션을 관리하는 practice를 공유한다.이 내용을 조금 더 확장해서, with
문이고 데코레이터고 보일러플레이팅할 것 없이, DB 객체의 session
프로퍼티에 접근해서 세션을 가져가고, 이는 Flask의 한 request context에서 하나씩만 생성되도록 했다.
Python, Flask, SQLAlchemy에 대해 전체적으로 이해도가 낮다면 설명을 들어도 코드가 잘 이해되지 않을 수 있을텐데, 컨텐츠 자체가 코드 레벨의 이해가 무조건 필요한 게 아니니까 이해를 위해 너무 많은 시간을 들이지 않기 바란다. 모르면 일단 넘어가고 나중에 다시 찾아오자. 시험 볼때 어려운 문제는 별표 쳐놓고 다른 문제 풀던 것처럼.
설정에서 사용한 MAIN_DB_URL이라는 이름을 지금 보니, MAIN_DB_CONNECTION_STRING
이 더 맞는 네이밍이었을 것 같다.
고민하는 시간, 구현 단에서 삽질하는 시간까지 해서 3~4시간 걸렸다.
SQLAlchemy connection string에서 DB connector를 mysqlclient
에서 mysqldb
로 변경하는, 이슈랑 관계 없이 갑자기 끼어든 커밋이 하나 있다. 일단 이걸 설명하자면, mysqlclient로 두고 로컬에서 서버를 켰더니 SQLAlchemy에서 dialect 관련 에러가 떴고, mysqlclient가 mysqldb를 fork해서 만들어진 것이라 그런지 mysqldb로 두니 이를 잘 인식하길래 변경한 것이다. 원래 이런 문제가 발생하면, 이슈를 따로 만들어서 진행하는 것이 맞다.
이슈 자체의 작업은 별 게 없다. Flask-Large-Application-Example에 이미 extension 관련 코드를 쉽게 붙일 수 있도록 준비되어 있었기 때문이다.
15분 정도 걸렸다.
스키마를 정의하는 것은 사실 따로 챕터를 빼서 다뤘어도 될 정도로 꽤 중요한 주제지만, 애초에 내가 DB에 자신있는 사람이 아니어서 ERD같은 거 그리면서 난리 피워봤자 퀄리티가 별로 나오지 않을 것 같았다. 그냥 커밋에 설명 조금만 붙이고 넘어가는 것으로 결정했다. 스키마를 정의하며 생각했던 것은,
무조건 Primary Key로 Auto Increment ID를 사용하지 않으려고 했다.
Foreign Key로 타 테이블을 참조하는 경우, relationship을 정의해 ORM 단에서의 도움을 받도록 했다.
컬럼 사이즈를 이유 없이 막 정하려고 하진 않았다. 비밀번호 해시 생성을 위해 사용할 werkzeug.security.generate_password_hash()
의 반환값 길이는 항상 93자리인 것처럼, 스펙을 참고할 수 있는 것들은 당연히 그 스펙을 따르도록 했고, 닉네임, 게시글, 댓글 등의 최대 길이같은 것들은 기획 단에서 정의되어야 하는 것이라 대충 잡았다.
이런 내용들에 관심이 많다면 'SQL AntiPattern'이라는 책을 읽어보는 것을 추천한다.
1시간 30분 정도 걸렸다.
일단 JWT에 관련된 부분들은 따로 챕터를 진행한 것이 아니기 때문에 아직 의사결정이 모두 되지 않아서, 일단은 flask-jwt-extended가 가이드하는 Basic Usage대로 구현했다.
테스트 코드를 작성하는 챕터가 진행되지 않았기 때문에, HTTP Client 툴인 Insomnia를 통해 API들을 테스트했다. PostMan을 쓰던 cURL을 쓰던 뭐 상관없는 부분이다.
로그아웃 API는 JWT 관련 챕터를 진행하며 이야기해볼 예정이다.
Schematics를 거의 처음 써봤을 때라 삽질 시간이 좀 있었고, 남들 보여준다고 코드 다듬느라 시간을 끌었다. 2시간 정도 걸렸다.
schematics validation을 위한 BaseModel에서 추상 메소드를 표현하기 위해 NotImplementedError
를 raise하는 옛날 파이썬 방식을 사용했더니 추상 메소드를 구현하지 않았을 때 에러가 발생하길래, 이를 제거하고 pass
로 바꾼 뒤, abc.abstractmethod
를 데코레이팅하는 방식으로 변경했다. 객체지향적으로 바라보면 추상 메소드 구현을 강제해야 되는 것이 맞을 수 있겠지만, optional한 추상 메소드를 표현하고자 했기에 이렇게 했다.
생산성을 위해 프로퍼티 하나를 추가하고 헬퍼를 조금 수정한 것을 제외한 나머지는 단순한 API 작업이다.
30분 정도 걸렸다.
241줄의 코드가 새로 생성되었으니 여태껏 작업했던 것들 중에 아마도 가장 많은 양의 변경사항이 있었던 것 같다. 사실 Category API의 schematics model 필드를 수정하는 것이나, exception handler를 건드리는 등 이 이슈와 관련 없는 커밋들이 끼워져서 생긴 변경들이 꽤 있다. 얘네들은 issue/17
브랜치에서 커밋하지 말고, 따로 이슈를 만들거나 최소한 별도의 브랜치를 만들어 작업했어야 더 맞다. 그래도 지금은 얼른 개발하고 글 써야 하는 입장이어서, 이 정도는 봐주는 걸로 하자는 생각에 그냥 커밋했다고 보면 된다. 서비스가 실제로 돌아가고 있는 상태라면, 커밋을 이렇게 대충 관리하지 말아야 한다. 별거 아닌 것 같고 아무도 안 볼 것 같지만, 나중에 가면 잘 관리된 커밋 히스토리가 도움이 될 때가 종종 있다.
요새 밀고 있는, '데이터 액세스와 관련된 코드는 따로 관리하자'는 컨셉을 조금 적용해 봤다. TblPosts
에 @classmethod
로 데코레이팅되어 있는 몇몇 메소드들이 그 결과물이다. 디자인 패턴에서 이야기하는 repository pattern을 조금 표방한 것인데, 공부를 하루 정도밖에 안 해서 실험적인 코드니까 아무 비판과 여과 없이 이 컨셉을 가져다 쓰지 않았으면 좋겠다.
1시간 가까이 걸렸다.
Item 하나 추가하고, Item 여러개 내려주고 하는 게 17번 이슈에서 진행했던 Post API와 비슷해서, 붙여넣고 다듬었다.
이미 작업해둔 것 붙여넣고 조금만 수정하면 되는 일이었어서, 10분 정도 걸렸다.
오