티스토리 뷰

Apps/Sada9

싸다9 개발이야기... API서버편

정선생 2015. 12. 10. 10:37

내가 담당했던 업무는 데이터 수집과 클라이언트에 데이터를 전달하는 RESTful 형태의 API개발이다.

데이터를 공급해주는 공급책(?) 역할이라고 생각하면 쉽다.


이제부터 다룰내용은 개발에 관심있는 사람이 아니라면 외계어로 들릴수 있을테니 관심있는분만 읽어주면 된다. 내가 사용한 언어, 주요 라이브러리는 아래와 같다.


<개발언어>

python 2.7.x


<라이브러리>

flask (마이크로 웹프레임워크)

flask-cache (flask에 캐쉬붙이는용도)

Beautifulsoup4 (html파서)

pymongo (mongodb 컨넥터)


<스토리지>

mongodb




1. Python을 개발언어로 선택한 이유 ?


내가 주로쓰는 언어는 사실 java이다. jsp를 안썼던 이유는 배포를 할때 빌드와 패키징하는게 상당히 귀찮았기 때문이다. 하지만 그것보다 더 큰 이유는 java에서 collection에 대한 데이터 처리할때 입력해야할 코딩량이 많다는것도 제외한 이유이긴하다.



예를 들어 자바에서는 몇가지 하드코딩된 map을 생서하려면 이런 뻘짓을 해야한다.

Map<String, String> m = new HashMap<String, String>();

m.put("code1", "코드1");

m.put("code2", "코드2");

m.put("code3", "코드3");


파이썬이라면 쉽게 표현이 가능하다.(node.js나 ruby도 마찬가지지만)

자바의 불친절함(?)에 비해 적은 코드량으로 많은일을 할 수 있다.

m = {'code1': '코드1', 'code2': '코드2', 'code3': '코드3'}


스크립트 언어중에서 Node.js도 있고 Ruby도 있는데 왜 python을 썼냐고 묻는다면,

Node.js도 고려했지만, event loop방식의 call back형태의 익숙하지 않은 코딩스타일이라 탈락

ruby는 성능이 python보다 떨어진다는 말도 있고, 과거에 ROR로 개발할때 세팅삽질을 많이해서 왠지 정이 가지 않았다 ㄱ-)


뭐... 가볍고 개인적으로 만들려고 했는데 굳이 새로운길이나 어려운길을 가지 말자고 판단했다.

즐기려는 건데 내가 나를 압박해서 별로 좋을것 같지 않았다.




2. MongoDB와 Flask를 사용한 이유 ?


python을 사용하고 있으면 왜 django를 안쓰고 이런 조합을 썼어요? 이런 질문을 했을것 같다.

아마도 java에서는 ibatis와 같이 쿼리문을 직접 컨트롤해서 쓰는 ORM구조를 주로 사용했는데, 내부적으로 알수없게 완벽히 묶인 구조로 되어있어 뭔가 이질감을 느꼈고 견고한 코드를 만들기 보다는 최대한 빨리 결과 json방식으로 만들어 주는 방법이 필요했다. (html도 결과 확인을 위해 디자인도 없는 결과로 볼테니)





MongoDB는 ORM을 굳이 안써도 사용하는데 불편이 없다. 모든건 json으로 요청하고 json으로 결과를 받아온다. flask는 복잡한 규칙을 알지 않아도 바로 web의 결과를 요청받고 처리할 수 있다. 둘의 조합으로는 python문법만 알고 있다면, 학습비용이 거의 제로에 가깝게 바로 개발을 할수 있다.


from flask import Flask

app = Flask(__name__)

@app.route("/") def hello(): return "Hello World!" if __name__ == "__main__": app.run()


위 코드를 실행하면 http://127.0.0.1:5000/ 으로 접속하면 Hello World!라는 결과를 볼수 있다.

너무 심플하지 않은가? (물론 실서비스에서는 개발용 내장컨테이너를 쓰진 않는다)


여기에 Pymongo를 이용해서 데이터를 조회하는것도 그리 복잡하진 않다.

db명은 db, 콜렉션명이 table인 데이터에서 name이 홍길동인 모든 결과를 json형태로 리턴하는건 저런식으로 코딩하고 "http://127.0.0.1:5000/mongo_test/홍길동" 형태로 요청하면 끝이다.

(하지만 나는 가변되는 많은 값의 처리는 기냥  post get방식으로 썼다)



from pymongo import MongoClient

from bson.json_uril import dumps


client = MongoClient('mongodb://id:pass@localhost:port/db')

collect = client["db"]["table"]


@app.route("/mongo_test/<name>")

def select(name):

   my_json = collect.find( {"name" : name} )

   dumps( my_json, ensure_ascii=False )




3. 개발 할때 이런저런 이야기


- 무한반복되어 수집하면서 나타나는 성능문제

데이터 수집을 할때, 정식 API가 제공되지 않기때문에 html파싱을 하는데 조금 애매한 상황이 많다.

우선, 카테고리 분류는 "형태소분석+사전"을 통한 1차분류와, link에서 카테고리관련값을 추출해서 사용한다.

이게 상품별로 처리해야하다보니 성능이 썩 좋지 않지만, 새로운 데이터가 언제 올지 모르기 때문에 주기적으로 수집이 무한반복 요청되어야 하는 구조이다. 성능을 위해서 이미 처리된 데이터에 대해서는 제외되는 로직이 처리되어야 하는데, 수집된 URL을 메모리에 로딩해서 중복체크하면서 이미 수집되어 저장된 데이터는 제외하는 방식으로 개선했다. 정식제공되지 않는(야메)로 데이터를 땡겨올땐 이런 번거로움은 항상 생기는것 같다.


- 성능테스트를 할때 힘겨워 하는 서버

프로토타입을 구현할때는 스토리지 없이 메모리에 올리는 구조로 구현했었다.

아주 간단히 테스트해볼 생각이라 ngrinder 데모 에서 가상유저 2을 두고 테스트 해봤다. 서버성능이 안좋은걸 고려하면 20TPS수준이 나왔다. 1차 오픈에 이정도면 큰 문제 없겠다 싶었다.

그리고 mongodb에 데이터를 저장해서 가져오도록 수정해봤다. (설치가 귀찮아서 mongolab 을 활용)

하지만 결과는 충격적이었다. TPS가 0.4 이건 서비스를 할 수 없는 수준이었다. 초당 1건도 처리를 못하다니...



아마 네트워크 속도의 문제라고 판단하고, 서버내에 직접 mongodb를 설치했다. 평균 TPS는 3.4

생각보다 심각한 수준이었다. 이걸로는 실제 오픈하기 힘든 경악스러운 수치였다.


네트워크 전송량을 줄이기 위해 flask.ext.compress 를 적용하고,캐쉬를 넣기위해 flask.ext.cache 를 도입했다.

처음에는 캐쉬를 결과를 가져온 python의 콜렉션을 캐싱했는데, 테스트결과 json으로 변환하는 작업도 CPU를 많이 먹는걸 보고 json으로 변경한 결과 자체를 flask 캐쉬에 적용하기로 했다.

이런저런 삽질을 통해 TPS평균 34.7 수준까지 올려두었다. 뭐 이 수치도 엄청난건 아니지만, 일단 서버가 클라우드 서버이니까 성능이 부족한 경우 스펙을 몇단계 올리면 해결될테고 이정도면 1차오픈수준은 충분히 버틸꺼라고 예상해서 일단 마무리 짓고 코드 개발을 계속 했었다.


(추가글: 14.08.16 , 성능관련이야기는 나중에 더 모니터링하면서 경험을 2차 오픈에 정리해볼까 한다)



4. 실제 적용기


현재 배포된지 1주일밖에 안되서, 다운로드수 300명에 설치유지가 220명수준이라 부하테스트를 논할 단계는 아니다. 자체 테스트 결과 1만명 수준은 지금 서버스펙으로도 커버가 될수 있지 않을까 생각된다.

이유는 데이터 특성상 3일~1주일 수준의 데이터만 조회되고 있고  자체 부하테스트를 해보면서 간단하게 캐쉬도 적용해둔 상태이다. 아마 2차 개발 오픈까지 성능문제는 일단 없다고 볼수 있다.


MongoDB를 사용중인 느낌

일반적인 dbms가 아닌 mongodb를 쓰면서 겪는 몇가지 사소한 문제가 있었다.

우 선 쿼리문을 만들때 조금 번거로웠다. 특히 or 조건을 추가하려면 { "$OR" : [{"title": "제목"}, {"title": "동의어"}] } 형태로 OR의 list로 묶어주고 여기에 또 like검색을 하려면 정규식형태로 넣어줘야하고 범위검색을 하면 또 dict형태로 묶어주는게 생각보다 조금 쿼리가 복잡한 패턴으로 가게될 경우 골치아픈 상황이 생겼다. 2차 작업을 하면서 쿼리를 만드는 보조코드를 보강해서 만들어서 해결해야 할것 같다.

또 귀찮았던점이, 타입을 명시적으로 정하지 않다보니 크롤링한 데이터의 날짜값이 문자형으로 저장이 되었는데, 조건은 숫자형으로 범위검색을 하면 검색이 되지 않는 문제도 있었다. 

DB에서는 검색조건에서 타입이 다른 경우 내부적으로 형변환을 해서 비교하는데 mongodb에서는 그런 기능이 없는건 아닐까 싶기도 한다.


2차에서 추가할 기능때문에 스키마 설계에서도 고민이다.

mongodb 에서는 join이 지원되지 않아서, 임베디드된 데이터를 list형태로 넣거나, objectid를 참조키로 이용해서 저장하는 방식을 취해야 하는데, 뭔가 document기반의 스키마 설계를 어떻게 해야 좋을지 명확한 해답을 얻기가 어려웠다. 임베디드형태가 성능상 유리하지만 json document의 용량제한과 잦은 update로 인한 파편화가 고민되었고, 참조키 처럼 구현하면 성능이나 구조가 맞게 설계하는지 고민이 되었다.

특히 table 단위의 lock을 한다고 들어서 고민이 되기도 하는데 그정도 사용이 되는 수준이라면 mongodb의 샤딩으로 해결될것 같기도 하고 결론은 아직 나지 않았다.

아마 2차 개발 스펙을 정해서 개발후 사용자가 늘어났을 경우 고민해도 늦지 않을꺼 같다.


Flask를 사용중인 느낌

데이터를 수동으로 보정해야 할때가 생긴다. 카테고리 분석이 잘못되었을때 변경을 한다거나.

마감된 상품이나 잘못 분류된 데이터를 관리자가 데이터를 수정하면 좋을텐데 flask에서는 관리페이지를 만들어야 한다. flask.ext.admin 이라는 확장 모듈이 있어서 사용해 볼까 하는데 뭔가 거저먹는 기술은 아닌거 같다.

django를 선택했다면 admin기능이 잘되어있어서 이런 고민은 안했을지 모르겠다.



5. 앞으로의 추가개발


지금 2주정도 고민을 해본 결론은 MongoDB+Flask기반의 "api + admin"기능을 구현해야 겠다는 생각이었다.

mongodb로 한정해서 개발한다면 충분히 가능할것 같다는 생각이 들었다.

모티브가 된건 아이러니하게도 document db 기반의 아파치제단의 couchDB(http://couchdb.apache.org/)이다. 몽고디비와 다른점이 자체적으로 Http기반으로 요청과 리턴이 된다는 점이다. (이게 장점이자 단점이다. 그래서 사용자가 요청할때 curl같은걸 통해 call하는 구조이다)


2차개발에서는 부가적인 콜렉션 생성이 많이 필요하고 수동으로 데이터 보정을 하기위해서는 CMS수준은 아니더라도 약간의 관리기능과 컨텐츠 작성을 손쉽게 할 필요가 있긴하다. 그래서 현재 api구조로 데이터를 저장하고 간단한 관리기능을 가진 dmango 라고 명명한 서브프로젝트를 진행중이다. 2차개발때는 구조를 보완하여 컨텐츠관리를 좀더 수월하게 할수 있지 않을까 싶다.

댓글
최근에 올라온 글
싸다구
최근에 달린 댓글