Django 내 SMTP를 활용한 이메일 발송 기능을 사용하고 있었는 데, 어느 날부터 발송이 안 되었다.  


 물론 단순히 진짜 계정 정보랑 비밀번호가 안 맞아서 생기는 문제는 아닌 걸 확인했다고 가정하고 쓰겠다. 이 문제가 왜 생겼나 설명하기 전에, 아마 90% 이상의 사람들은 파이썬을 이용하여 gmail을 보낼 수 있도록 세팅을 하다가, 이 옵션을 만졌을 것이다.

 

 

이 옵션을 켜야 파이썬 안에서 구글 이메일/비밀번호만 가지고 로그인해서 이메일을 보낼 수 있었다. 그런데, 이 옵션이 흥미로운 게 무려 2단계 인증을 꺼야 활성화가 되고, 구글에서도 사용을 권장하지 않는 꺼림칙한 옵션이다. 그래서 원인은 간단하다.

 

 

 

이 옵션이 22년 5월 30일자로 막혔기 때문이다. 이메일이랑 비번 가지고 접속하는 행위가 안 통한다.

 

 

그러면 어떻게 해야 하느냐? 안타깝게도 이 '보안 수준이 낮은 앱 액세스'라는 방법의 대안을 설명한 글은 쉽게 눈에 띄지 않았다. 약간의 구글링 끝 방법을 찾을 수 있었다.

https://levelup.gitconnected.com/an-alternative-way-to-send-emails-in-python-5630a7efbe84

 

An Alternative Way to Send Emails in Python

Stop using your Google password to programmatically log in and send emails in Python.

levelup.gitconnected.com

 

 이 방법에 따르면, 우선 계정에 2단계 인증(SMS 인증 등등)을 건다. 그러면, 

 

 

 이제 보안 탭에서 '앱 비밀번호' 메뉴가 눈에 띌 것이다.

 

앱 선택에선 '기타'를 고르고, 이름은 원하는 거 아무거나 입력한다. 사실 이 부분은 뭘 선택하든 크게 상관 없어 보이긴 하는데, 그래도 '메일' '유튜브' 이런 이름보단 좀 더 명확한 이름이 나을 것 같다.

 

 

 

 그러면 이제 여기 앱 비밀번호 16글자가 나온다. 이제 이걸 기존 비밀번호 대용으로 쓰면 된다.

 

 

 

 이게 끝이다.

생각보다 엄청 간단한데, 구글의 안내 페이지에서 그냥 옵션을 막기만 하고 이걸 제대로 설명해주지 않아서 당황스러웠다. ㅡㅡ;

 

 

 결론

 아래와 같이 입력하면 된다. 윈도우 기준이고 다른 os에서는 시도해 보진 않았다.

easy_install pip

 

대충 보충설명

 아마 저 증상 있으면 프롬프트에 관리자 권한 줘도 마찬가지고, python -m pip install --upgrade pip 이 명령어도 안 먹히고 있을 것이다. 이게 딴 라이브러리는 install이나 uninstall이 자유로운데 pip 자기 자신만 건드려고 하면 항상 저런 이슈가 있었다.

easy_install은 pip로 대체된, 현재는 deprecate 된 파이썬의 패키지 다운로더이다. easy_install도 결국은 pip와 똑같은 역할을 하는 프로그램이니, 다운로드를 받을 수 있는 것이다.

 

 

 사실 이 문제가 왜 생기냐가 궁금한데, 대부분 이렇게 해라라는 말만 있고 원인에 대해서는 명쾌하게 설명한 글을 찾지 못했다. 그래서 내 생각엔, 다른 패키지 설치나 제거가 자유로운데 pip 스스로에 대해서만 권한 문제 운운하는 걸로 봐서는 실제 폴더 권한 문제는 아닌거 같다. 사실은, pip 프로그램이 열린 상태로 자기 자신을 삭제하려고 해서 접근이 안 되고, 그래서 권한 문제처럼 보이지 않을까 한다. 엑셀 파일을 열어둔 채로, 해당 파일 삭제를 시도했을 때 안 되는 것과 똑같은 이치이다. easy_install은 pip와 별개의 프로그램이니까 접근이 자유로워서 업그레이드(=삭제 후 재설치)가 가능하다고 생각된다.

 

 

pip install korean-geocoding

사용방법 : https://github.com/RE-A/korean-geocoding

 

GitHub - RE-A/korean-geocoding: 한국 지역별 위도/경도 좌표 조회 라이브러리

한국 지역별 위도/경도 좌표 조회 라이브러리. Contribute to RE-A/korean-geocoding development by creating an account on GitHub.

github.com


공식적인 사용방법 등은 위의 링크에서 확인할 수 있습니다.

 

혹시 그냥 시군구별 위경도 자료가 필요하다 -> https://skyseven73.tistory.com/23

 

대한민국 행정구역별 위경도 좌표 파일

다 필요없고 파일부터 내놔  잘가세요. ㅂㅂ 이 파일이 뭐고 왜 필요한데?  말 그대로 대한민국의 행정구역별로 위도/경도 좌표를 모아 놓은 엑셀이다. 모두 합치면 2만개쯤 되는데 지역마다 시

skyseven73.tistory.com

 

위 라이브러리의 기술적인 이슈나 개선 사항이 궁금하다 -> 작성 예정

 

이렇게 스타 0개짜리 라이브러리에 진심인 이유는 -> 내가 만들었으니까...

 

 

감사합니다. 꾸벅

다 필요없고 파일부터 내놔

행정구역별_위경도_좌표.xlsx
0.90MB

 

잘가세요. ㅂㅂ
(22.04.11 파일 수정되었습니다.)

 

 

 

이 파일이 뭐고 왜 필요한데?

 말 그대로 대한민국의 행정구역별로 위도/경도 좌표를 모아 놓은 엑셀이다. 모두 합치면 2만개쯤 되는데 지역마다 시트로 구분해 놨고, 맨 뒤의 숫자는 차례대로 '위도'/'경도'이다. 지역별로 시트가 구분되어 있어서 찾기 쉬울 것이다.

 

 

 기존에도 행정구역별로 위도/경도를 정리해 둔 자료가 몇개 있긴 했는데, 좀 오래된 자료거나 계층화가 되어 있지 않은 자료들도 많았다. 무엇보다 거의 다 주소 DB에서, 각 지역구별 도로명 주소들의 중점을 계산하여 계산한 자료들이었다. 물론 이 방식도 솔직히 내가 할 줄 몰라서 생각하지 못했을 뿐인 좋은 방식이고, 밑에서 설명할 내가 했던 방식이랑 차이도 크게 나지 않는다.

 

 

 그렇지만, 나는 단순히 주소들의 중심을 계산한다는 점에선, 해당 행정구역이 길쭉하거나 반달 모양 등을 취하는 지역이라면 이 방식이 정확하지 않을 것 같다는 생각이 들었다. 시골 행정구역에선 이런 모양의 행정구역을 가끔 볼 수 있고, 당장 경기도만 봐도 가운데가 뻥 뚫린 도넛 모양이기 때문이다.

 

 

 그래서, 나는 Naver의 Geocoding API를 이용해서 해당 행정구역의 좌표를 받아오고, 이 데이터를 구조적으로 정리했다. 네이버에서 해당 행정구역으로 검색하면, 대부분 '시청' , '구청', '동사무소' 등 각 구역의 행정 중심지의 좌표를 찍어 주는 것에 착안했다. 동사무소는 그렇다 쳐도 보통 '시청'이나 '구청'은, 해당 지역에서 지리적으로 중앙은 아닐지라도 행정 혹은 경제의 중심에 위치하는 게 대부분이라고 생각해서 단순히 지역 내 주소들의 중점보다 조금 더 유용하지 않을까라고 생각하게 되었다. 

 

해당 좌표를 그대로 네이버 지도에 다시 검색한다.
네이버 지도에 35.10295, 128.9769을 검색했을 때 결과. 행정복지센터(구 동사무소)의 위치를 찍어준다.

 

 

 

 혹시 문제가 있으면 댓글로 써주면 많은 도움이 될 것 같다.

거의 다 문제가 없었지만, 아주 간혹 가다 좌표 정보가 비어있는 데도 있을 수 있다. 그건 행정구역 목록엔 있었는데 네이버 API에선 검색이 안 되는 경우라서 어떻게 처리할 지 고민 중이다.

 

 

이 자료를 Python에서 쓰고 싶어요

 

 그런 당신을 위해 오픈소스 라이브러리로 만들어 두었다. 헤헤

사실 오픈소스 라이브러리로 먼저 만들었고, 그걸 이용해서 위의 엑셀을 만든 것이다.

 

 

Thanks To

https://torrms.tistory.com/55  행정구역 목록은 여기서 받아서 데이터를 정제했다. 공공 머시기 포탈에서 받은 자료는 따로 전처리가 필요했었는데 이미 누군가가 어느 정도 정리해 둔 게 있었다. 감사합니다~

 

 

 인터넷에 뒤져보면 python의 reduce 함수에 대해서는 많이 있는데, 정확한 명세를 알려 주는 글은 잘 없는 것 같아서 직접 정리해 본다.

 

 

reduce(func,  iterable , (optional)value)

from functools import reduce

# 케이스 1
test_list = [20, 5, 4, 3]
result = reduce(lambda a, b: a - b, test_list)
print(result)
# 결과: 8

# 케이스 2
test_list = [20, 5, 4, 3]
result = reduce(lambda a, b: a - b, test_list, 100)
print(result)
# 결과: 68

 

func:

 reduce 과정이 진행되면서 실행될 함수로, 람다 함수든 일반 함수든 상관없지만, func(a,b) 형태로 파라미터 두 개를 받아야 한다.

 a는 reduce를 진행하면서 누적된 값, b는 iterable 객체에서 가져온 원소이다.

 당연히 파라미터 순서가 상관 있으니, 유의해야 한다. 대부분의 인터넷 예시에선 +를 사용하여 이 점이 잘 드러나지 않아서, 일부러 -를 사용해 보았다. 만약 파라미터 순서가 바뀐다면...

from functools import reduce

test_list = [20, 5, 4, 3]

# 케이스 1에서 a-b가 b-a로 바뀐 상태
result = reduce(lambda a, b: b - a, test_list)
print(result)

# 3 - (4 - (5-20))
# 출력 : -16

 

iterable 

 list, tuple, .. 등등 순회 가능한 요소는 모두 가능하다.

 

value

 위 케이스 1,2의 차이가 초기값의 유무이다. 이 값이 주어지지 않았다면 iterable 객체의 첫번째 원소를 초기값으로 사용하여, reduce 과정에서 맨 처음에 첫번쨰 원소와 두번째 원소에 대해 fucn을 실행한다. 위의 케이스 1에선 20 -5 가 첫번째로 수행되었다.

 초기값이 주어지면, 맨 처음에 value와 첫번쨰 원소에 대해 fucn를 실행한다. 위의 케이스 2에선 100 -20이 첫번째로 수행되었다.

 즉, 초기값 유무에 따라 func 함수의 실행 횟수가 1번 차이가 난다. 아래의 예시를 보면 이해가 빠를 것이다.

from functools import reduce

test_list = [20, 5, 4, 3]

def reduce_fun(a, b):
    print("함수 실행")
    return a - b

result = reduce(reduce_fun, test_list)
print(result)

result = reduce(reduce_fun, test_list, 100)
print(result)

""" 출력:
함수 실행
함수 실행
함수 실행
8
함수 실행
함수 실행
함수 실행
함수 실행
68
"""

 

reduce 코드 살펴보기

_initial_missing = object()

def reduce(function, sequence, initial=_initial_missing):

    it = iter(sequence)

    if initial is _initial_missing:
        try:
            value = next(it)
        except StopIteration:
            raise TypeError("reduce() of empty sequence with no initial value") from None
    else:
        value = initial

    for element in it:
        value = function(value, element)

    return value

 

 functools.py에 정의된 reduce 함수의 내용이다. 내용은 크게 어렵지 않으니 천천히 살펴보자. initial 파라미터의 유무에 따라 value=initial 또는 value =next(it)를 통해 시작값을 정의한다. 시작값이 주어지지 않았다면 이미 본격적인 reducing 과정 전에 원소를 하나 순회하므로, 순회 횟수에 차이가 생기는 것이다. 아래엔 value = function(value, element)를 통해 값을 계속 갱신한다. 이 부분을 통해 왜 fucntion의 파라미터 순서가 상관 있는지, 꼭 파라미터 2개만 들어가야 하는지 알 수 있을 것이다.

 

 

DynamoDB는 기존의 RDB와 달리 비정형화된 데이터를 담을 수 있는 noSQL 데이터베이스이며, 아마존에서 서비스하고 있따. 여기서는 DynamoDB를 python에서 다루는 방법에 대해 다룬다. 물론 AWS 공식가이드가 있긴 한데, 자세한 설명보다는 말 그대로 "따라해 보세요" 수준이라 감 잡는 용도 이상으로는 쓰기가 어려워서 따로 사용법을 정리해 본다.그래도 아래 공식 튜토리얼을 훝어보고 오면 도움이 될 것이다. https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/GettingStarted.Python.01.html

 

1단계: Python으로 테이블 생성 - Amazon DynamoDB

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

 

 

0. 기본 세팅

 DynamoDB는 개발용으로 로컬 서버를 지원한다. 그래서 https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html

이 링크대로 로컬 서버를 세팅하면 된다. 실행 인자에서 -port 8888을 통해 사용 포트를 8888로 바꾼 채로 사용했다.

 

 

 이 글은 다음 링크들을 많이 참고했다.

https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/GettingStarted.Python.05.html

 https://www.dynamodbguide.com/what-is-dynamo-db

 자세한 정보, 특히 복잡한 쿼리문에 대한 정보를 원한다면 이 사이트들을 방문하면 편할 것이다. 아래의 내용 역시 위 사이트를 대부분 참고하였다.

 

1. DynamoDB 기본 

DynamoDB가 왜 필요하게 되었는가

 

 간단하게, DynamoDB가 왜 세상에 나타났는지부터 살펴보자. noSQL류 데이터베이스를 사용하지 않고 RDB를 사용하던 시절, 아마존의 엔지니어들이 자사에서 데이터베이스를 활용하는 패턴을 분석했다. 놀랍게도 무려 90%의 연산이 JOIN을 사용하지 않았다. 또, 약 70%의  연산이 단순히 기본 키를 이용하여 하나의 행을 가져오는 key-value 형식의 연산이었다. JOIN은 매우 코스트가 큰 연산이라 JOIN을 피하기 위해, 한 마디로 성능을 위해 정규화를 포기하는 경우도 있었다. 이렇게  엔지니어들은 관계형 데이터베이스 모델의 필요성에 대해 의문을 가지게 되었다.

 

 

 

  이와 더불어, 대부분의 RDB는 강한 일관성을 주요 특징으로 내세운다. 이 말은, 특정 연산이 실행된 이후, 모든 유저들이 해당 부분에 대해서 동일한 결과를 받아야 한다는 뜻이다. 이 점이 현실의 상황에선 어떤 문제를 갖는지 살펴보자. A는 서울에 살고, B는 브라질에 살고 있다. A가 트위터에 'hello'라는 게시물을 올렸다. RDB를 사용하는 상황이라면,  A가 게시글을 올린 순간 트위터 본사가 있는 미국 서버 내 데이터베이스에 'hello'가 커밋되고, 모든 트위터 유저들은 A의 타임라인에 들어가면 'hello'가 보여야 한다. 하지만, 물리적인 거리를 고려하면 A가 글을 작성한 이후, 서울에서 미국까지 갔다가 브라질까지 가는 데 필연적으로 (컴퓨터의 세계에선) 적지 않은 시간이 소요된다. A는 글을 썼지만, B는 해당 글을 조회할 수 없어 일관성에 어긋나게 된다.

 

 

 

  물론, 이를 해결하기 위한 방법이 있다. 서울과 브라질 각각에 데이터베이스 인스탄스를 두고, 서로 완전히 복제하도록 하는 것이다. 이 경우, 일관성을 유지하기 위해선 A가 작성한 글은 데이터베이스에 바로 커밋되지 않고, 우선 A의 글을 전 세계의 데이터베이스로 퍼뜨린 후, 동시에 커밋해야 한다. 'hello' 라는 글이 업로드됨과 동시에, 전 세계의 유저들이 저 글을 볼 수 있다. 하지만, 이 시나리오를 실현하려면 우선 여러 대의 데이터베이스를 한꺼번에 관리할 수 있는 복잡한 시스템이 필요하고, 무엇보다 A의 요청엔 불필요한 지연 시간이 생긴다. 일관성은 지켰지만, 이번엔 가용성에 문제가 생기게 된다.

 

 

 

  이러한 문제를 해결하기 위해 역시 아마존의 엔지니어들이 고민한 결과, 강한 일관성은 지키지 않아도 된다는 결론이 나왔다. 정확히는, 은행 거래와 같이 신뢰성이 생명인 부분에선 강한 일관성이 중요하지만, 위의 트위터의 사례와 같은 대부분의 일상적 상황에선 일관성보단 가용성과 속도가 더 중요하다는 것이다. 즉, 트위터에 올린 글이 다른 사람들에게 몇 초 늦게 보여져도 크게 문제가 생기지 않는다는 점이다. 이러한 점들에서, 기본의 관계형 DB보다 일관성, 관계성을 약화하고 대신 가용성 및 확장성에 초점을 둔 DynamoDB가 탄생하게 되었다.

 

 

Table의 구조

  각 테이블 안에는 item이라고 불리는 항목들이 있고, 각 항목 안에는 RDB의 column 같은 개념의 attribute(속성)들이 들어 있다. 위 그림에선 People이라는 테이블 안에 item 3개가 있고, 각 아이템 안에는 여러 가지 속성을 가지고 있다. noSQL이라는 특성상 정형화된 구조가 없기에, 각각의 아이템은 저마다 다른 구조를 가질 수 있다.

 

 

  각 아이템들을 구분하는 용도로, partition key가 있다. 필수적으로 테이블에 존재해야 하며 흔히 말하는 기본 키에 해당하며, 해시 함수를 통해 각 아이템의 저장공간이 지정되며 서로 다른 값을 가져야 한다. 위의 예제에선 PersonID가 파티션 키이다. 또, 필수는 아니지만 sort key라는 테이블 내 아이템의 정렬에 사용되는 키가 있다. 이 파티션 키와 정렬 키가 합쳐서 복합 기본 키(composite primary key)로, 두 개의 속성을 합쳐서 기본 키처럼 사용할 수 있다. 이 경우, 기본 키와 정렬 키 중 하나만 달라도 서로 다른 아이템으로 인식이 될 수 있다.

 

 

  또 키와 별개로 인덱스를 생성할 수 있는데, 두 가지의 인덱스가 있다. local secondary index(로컬 보조 인덱스)는 기존의 파티션 키와 함께, 지정했던 정렬 키 이외에 다른 속성을 정렬 키처럼 이용하여 테이블에 접근하고 싶을 때 사용할 수 있다.global secondary index(전역 보조 인덱스)는 아예 기존의 키와 관련없는, 새로운 구조의 키(단순 파티션 키or복합 키)를 사용하고 싶을 때 쓸 수 있다. 전역 보조 인덱스가 더 넓은 개념이라고 보면 된다.

 

 

2. DynamoDB 테이블 다루기

 

테이블 불러오기

import boto3

ENDPOINT_URL = "http://localhost:8888"


resource = boto3.resource('dynamodb', endpoint_url=ENDPOINT_URL)
client = boto3.client('dynamodb', endpoint_url=ENDPOINT_URL)

table = resource.Table("TestTable")

  우선, boto3.resource와 boto3.client를 불러왔다. 둘 다 AWS의 서비스를 이용하기 위한 SDK이다. 차이점이라면 resource는 client를 래핑한 고수준의 서비스이다. 따라서, client는 비교적 로우 레벨에 접근이 가능하며, resource를 이용하여 다룰 수 없는 부분을 위해 사용한다. 테이블을 부르는 건 resource.Table(테이블명)으로 간단하게 불러올 수 있지만, 처음 실행한 상태라면 아직 아무 테이블도 존재하지 않을 것이고, 따라서 맨 밑의 부분에선 오류가 뜰 것이다. 우선 테이블을 만들어 보자.

 

 

테이블 생성하기

 테이블을 생성하는 문법은 다음과 같다. 상세한 문법은 다음 사이트를 참고하면 된다.

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client.create_table

import boto3

ENDPOINT_URL = "http://localhost:8888"

resource = boto3.resource('dynamodb', endpoint_url=ENDPOINT_URL)
client = boto3.client('dynamodb', endpoint_url=ENDPOINT_URL)

schema = {
    'TableName': 'TestTable',
    'KeySchema': [
        {
            'AttributeName': 'name',
            'KeyType': 'HASH'  # Partition key
        },
        {
            'AttributeName': 'key1',
            'KeyType': 'RANGE'  # Sort Key
        }
    ],
    'AttributeDefinitions': [
        {
            'AttributeName': 'name',
            'AttributeType': 'S'
        },
        {
            'AttributeName': 'key1',
            'AttributeType': 'S'
        },
        {
            'AttributeName': 'key2',
            'AttributeType': 'S'
        }
    ],
    'LocalSecondaryIndexes': [
        {
            'IndexName': 'idx',
            'KeySchema': [
                {
                    'AttributeName': 'name',
                    'KeyType': 'HASH'
                },
                {
                    'AttributeName': 'key2',
                    'KeyType': 'RANGE'
                }
            ],
            'Projection': {
                'ProjectionType': 'INCLUDE',
                'NonKeyAttributes': [
                    'key2',
                ]

            }
        },
    ],
    'ProvisionedThroughput': {
        'ReadCapacityUnits': 10,
        'WriteCapacityUnits': 10
    }
}

retu = client.create_table(**schema)
print(retu)

 문법이 굉장히 복잡하다. 이걸 create_table에 파라미터로 그냥 박으면 가독성이 답이 없으므로 위처럼 일단 딕셔너리 형태의 변수로 뺀 후에 넣는게 제일 편할 것이다. 

 

간단하게 스키마에 대해 설명하자면,

- TableName : 말 그대로 테이블 이름이다.

 

- KeySchema : 테이블에서 key로 사용할 속성들에 대해 정의해야 한다. KeyType은 'HASH'는 파티션 키, 'RANGE'는 소트 키를 나타낸다.

 

- AttributeDefinitions : 쉽게 말해 여기 스키마에서 언급된 키와 인덱스에 대해선 전부 여기 명시되어 있어야 한다. 키뿐만 아니라 인덱스를 쓴다면 인덱스에 명시된 속성들도 있어야 한다는 점에 유의해야 한다.

여기 쓰이는 type엔 S, N ,B 세가지가 있는데 각각 String, Number, Binary 이다. 각각의 속성에 맞춰서 넣으면 된다.

 

- LocalSecondaryIndexes : 위에서 언급한 로컬 보조 인덱스이다. 인덱스 이름과 키 schema를 입력한다. 위에서 봤듯이 로컬 보조 인덱스는 파티션 키를 그대로 사용하기에, 우선 기존 파티션 키를 그대로 쓰고 다음에 인덱스로 사용할 키를 정의한다. Projection은 인덱스로 복사할 속성 및 옵션을 정하는 부분이다.

GlobalSecondaryIndexes는 위의 로컬 보조 인덱스와 거의 유사하게 사용할수 있기에, 따로 언급하지 않았다.

 

- ProvisionedThroughput : 각각 읽기, 쓰기에 사용할 유닛(=동시에 처리 가능한 트랜잭션의 수)의 수를 정하는 옵션으로, 데이터베이스의 구조와는 관련이 없지만 트래픽에 따라 유연하게 조정할 수 있다.

 

 

 

 이렇게 열심히 언급했는데, 솔직히 쓰면서도 좀 많이 복잡하다.

그냥 키-값 store로 사용하고자 한다면 간단하게 아래의 코드를 통해 테이블을 생성할 수 있다.

 

schema = {
    'TableName': 'TestTable',
    'KeySchema': [
        {
            'AttributeName': 'name',
            'KeyType': 'HASH'  # Partition key
        },
    ],
    'AttributeDefinitions': [
        {
            'AttributeName': 'name',
            'AttributeType': 'S'
        },
    ],
    'ProvisionedThroughput': {
        'ReadCapacityUnits': 10,
        'WriteCapacityUnits': 10
    }
}

client.create_table(**schema)

 

 

   현재 dynamoDB에 있는 테이블의 목록을 가져오는 방법은 아래와 같다.

# boto3.client 활용
client.list_tables()

이렇게 요청하면, 결과는 다음과 같이 HTTP Response와 형태로 온다. 처음 써보는 거라면 아마 TableName엔 아무것도 없을 것이다.

{'TableNames': ['Book', 'TestTable', 'name'], 
'ResponseMetadata': {'RequestId': 'a637b852-986f-4d41-9454-0e1d15241986', 
'HTTPStatusCode': 200, 
'HTTPHeaders': {'date': 'Tue, 13 Jul 2021 03:53:01 GMT', 
'content-type': 'application/x-amz-json-1.0', 
'x-amz-crc32': '3717359480', 
'x-amzn-requestid': 'a637b852-986f-4d41-9454-0e1d15241986', 
'content-length': '42', '
server': 'Jetty(9.4.18.v20190429)'}, 
'RetryAttempts': 0}}

 응답 내용이 많지만, TableNames와 처리 상태를 체크할 수 있는 HTTPStatusCode 정도만 사용해도 충분할 것이다.

 

 

 특정 테이블의 존재여부를 확인할 수 있는 다른 방법으론 describe_table()이 있다.

client.describe_table(TableName="TestTable")

 

 만약, 해당 이름의 테이블이 존재하지 않을 경우 botocore.errorfactory.ResourceNotFoundException이 나기 떄문에, 테이블의 존재 여부를 판단하고자 할 경우 try ~ catch로 처리하면 될 것이다. 응답은 아래와 같다.

{
    'Table': {
        'AttributeDefinitions': [
            {
                'AttributeName': 'name',
                'AttributeType': 'S'
            }
        ],
        'TableName': 'TestTable',
        'KeySchema': [
            {
                'AttributeName': 'name',
                'KeyType': 'HASH'
            }
        ],
        'TableStatus': 'ACTIVE',
        'CreationDateTime': datetime.datetime(2021,
                                              7, 6, 16, 53, 37, 417000, tzinfo=tzlocal()), 'ProvisionedThroughput': {
            'LastIncreaseDateTime': datetime.datetime(1970,
                                                      1, 1, 9, 0, tzinfo=tzlocal()),
            'LastDecreaseDateTime': datetime.datetime(1970,
                                                      1, 1, 9, 0, tzinfo=tzlocal()), 'NumberOfDecreasesToday': 0,
            'ReadCapacityUnits': 10,
            'WriteCapacityUnits': 10
        },
        'TableSizeBytes': 0,
        'ItemCount': 0,
        'TableArn': 'arn:aws:dynamodb:ddblocal:000000000000:table/TestTable'
    },
    'ResponseMetadata': {
        ...
}

 크게 테이블의 구조와 테이블 생성일자 등의 정보, 그리고 역시 해당 응답에 대한 HTTP 정보(위 응답에선 임의로 생략함)가 담겨 있다.

 

 

테이블 내 항목 읽기(Read)

 

table = resource.Table("TestTable")
resp = table.scan()


items = resp['Items']
count = resp['Count']

 table.scan()을 통해 전체 테이블의 항목 및 아이템의 수를 위와 같이 가져올 수 있다... 인줄 알았는데 안 된다. AWS 문서에 따르면, 1MB 크기 내에서만 결과를 반환하기 때문에,  큰 데이터를 가져오려면 쿼리를 계속 불러서 페이지네이션처럼 처리해야 한다. 이는 count도 마찬가지라 정확한 테이블 내 아이템 개수를 반환한다고 보장하지 않는다.

따라서, 테이블 내 전체 item 수를 얻는 바른 방법은 다음과 같다.

 

client.describe_table(TableName="TableName")['Table']['ItemCount']

 

 이제, DynamoDB에서 쿼리를 날리는 방법에 대해 알아보자.

from boto3.dynamodb.conditions import Key

table = resource.Table("TestTable")
query = {"KeyConditionExpression": Key("name").eq("키값")}

print(table.query(**query))

# 결과물은 위와 동일하게 table.query(**query)['Items']와 ['Count']로 각각 아이템과 아이템 수 조회 가능

 

 

위에서 사용한 scan을 이용해서도 쿼리를 조회할 수 있다.

from boto3.dynamodb.conditions import Attr

query = {"FilterExpression": Attr('att1').eq('원하는 값')}
response = table.scan(**query)
print(response['Items'])

Key나 Attr에 eq뿐만 아니라 lt, gt, between 등등 많은 조건을 붙일 수 있다.

 

scan과 query를 사용한 방법을 제시했는데, query는 key를 이용하여 결과를 조회하는 거라면, scan은 그냥 말 그대로 테이블 전체를 훑는 거라 큰 테이블에서 사용하면 심각한 성능 문제를 야기할 수 있다. 심지어 scan은 필터 조건을 걸어도 일단 싹 긁은 다음에 필터링 하는 방식이라, 큰 테이블에서 쓰기 힘들다. 그래서, 작은 테이블이 아닌 이상 가급적 query를 쓰는 게 좋다.

 

 

테이블의 아이템 수정하기

query = {'Key': {'name': 'name1111'},
         'UpdateExpression': 'set att1=:new_data',
         'ExpressionAttributeValues': {
             ':new_data': 'new_attttt'
         }
         }
resp = table.update_item(**query)
print(resp)

 아이템을 수정하는 방식은 특이하다. 수정 대상인 키를 지정하고, UpdateExpression을 통해  자체적인 문법을 통해 업데이트 명령을 지정한다. 여기서 new_data는 임의로 정한 값으로, 변수명 앞에 콜론(:)을 붙여 내부적으로 사용하는 변수임을 나타낸다. 마지막으로, ExpressionAttributeValues를 통해 내부 변수가 어떤 값을 가지고 있는지 표현한다. 나는 새로 업데이트될 값으로 썡 문자열 'new_attttt'를 지정했는데, 값을 바꿀 속성인 att1이 인덱스나 키에 속하지 않은 이상, 당연히 여기엔 어떤 파이썬의 기본 오브젝트(숫자, 문자, 리스트, 딕셔너리 등)이 와도 괜찮다.

 

추가적인 파라미터인 ConditionExpression를 통해 WHERE절처럼 조건부 삭제를 할 수도 있다.

 

 

테이블에 쓰기, 조회하기, 삭제하기

 

# 아이템 삽입
item = {'name': 'name1112', 'att1': 'attttt'}
resp = table.put_item(Item=item)
print(resp)

# 아이템 조회
resp = table.get_item(Key={'name': 'name1112'})
print(resp['Item'])

# 아이템 삭제
resp = table.delete_item(
    Key={
        'name': 'name1112'
    }
)
print(resp)

# 데이블 삭제
teble.delete()

 

 R과 U는 상세하게 설명했는데 나머지는 이렇게 한번에 설명해도 될 거 같다. 왜냐하면, 위에서 다룬 문법들을 그대로 활용하면 되기 때문이다. 예를 들어 조건부 삭제를 하고 싶으면 update의 조건부 업데이트와 똑같이 조건을 지정하면 된다. 나머지 명령들도 위에서 사용한 문법과 거의 유사하다.

 

get_item()은 단 하나의 아이템만 읽어 오는 함수로, 여러 개를 읽어 와야 할땐 위에서 본 scan()이나 query()를 사용하면 된다.

테이블 삭제는 워낙 간단해서 저 한줄이면 충분하다.

 

 

 

 전체적으로 파이썬 문법보다는 aws cli를 활용하는 능력이 더 중요한 것 같다. 사실 ConditionExpression, FilterExpression과 같이 파라미터들이 이름이나 문법이나 무지하게 복잡한 이유가, 그냥 boto3 자체가 aws cli를 파이썬에서 활용할 수 있게 만든 라이브러리라, 거기서 쓰던 변수명이나 방식을 그대로 가져와서 그런 것 같다. 그래서, 어느정도 깊게 다루려면 python, boto3보다는 오히려 aws cli에 더 익숙해져야 할 것 같다.

 결제 기능이라는게 개발자 입장에선 생각 이상으로 매우 까다롭다. 그래서 장고 서버에서 아임포트로 결제 모듈을 구현한 경험을 공유하고자 한다. 여기서는 가장 일반적인 1회성 결제에 대해서만 구현을 했다.

 

 

  일단 용어부터 정리하자.

 

PG사 : 결제대행사. 내가 어떤 상품에 대해 XX카드로 5만원 결제를 원한다고 요청을 보내면, 온라인 상에서 실제로 XX카드를 들고 XX카드사에 결제를 해 주는 회사이다. 

가맹점 : 이 PG사들과 계약을 맺고 PG사를 통해 결제를 맡기는 점포. 즉 우리의 웹 서버이다.

 

 

 구현 방법이나 코드를 원한다면 다음 포스팅(업로드 예정)으로 가면 되지만, 아임포트가 왜 필요하고 결제과정에선 무슨 일이 일어나는지 아는게 구현이나 오류 해결에 큰 도움이 될 것이다. 

 

 

0. 결제 프로세스의 이론적 설명

 이 부분에 대한 자세한 내용은 https://github.com/iamport/iamport-manual/tree/master/%EC%9D%B8%EC%A6%9D%EA%B2%B0%EC%A0%9C 여기를 보면 된다.

아래의 사진이나 내용도 모두 여기서 참고했음을 밝힌다.

 

 

 

  일단 이론적으로는 이런 구조로 결제가 이루어진다. 눈에 띄는 특징은 카드정보가 우리의 웹 서버를 거치지 않고 다이렉트로 카드사 서버로 전달되는데, 이게 법적 제한으로 일부 카드사나 PG사 등을 제외하고는 카드정보를 저장할 수 없다. 그 외에는 가맹점 서버에서 PG 서버로 결제 요청을 전달하고, PG 서버에서는 카드사와 실제 결제를 수행하고, 그 결과(결제성공/실패)를 가맹점 서버에 돌려주는 구조이다. 그래서, 실제 결제 프로세스는 아래와 같이 이루어진다.

 

 

 

 

 

 위의 그림과 사실 큰 차이 없다. 대신, 구매자 브라우저에서 전달되는 카드정보(카드번호, CVC 등)이 가맹점 서버에서는 알 수 없도록 전달된다는 사실만 기억하면 될 것이다. 위 그림에서, 우리는 3,4,7,8만 신경쓰면 되고 PG 서버로 데이터가 넘어간 이후 다시 응답을 받을 때까지는 신경쓸 필요가 없다.

 

 

 PG사들도 결제 모듈을 모두 지원하고 있고, 개발자들에게 제공하고 있다. 그래서, 여기까지만 해도 결제에는 아무 문제 없다. 그렇지만 아임포트를 도입한다고 가정하자. 아임포트는 바로 가맹점 서버와 PG 서버의 사이에 있다. 그림판으로 조악하게나마 그림을 다시 그려보면 이렇게 될 것이다.

 

 

 즉 아임포트의 도입을 통해, PG사와의 통신 과정도 래핑을 하게 된다. 그러면, 이제 이런 의문이 생길 것이다. 왜 굳이 아임포트가 중간에 들어가야 하는가? 바로 이게 결제에 아임포트를 사용해야 하는 이유가 될 것이다. 그래서 내가 생각한 이유들을 아래에 정리해 놓았다.

 

 

1. PG사와의 복잡한 연동과정

 나는 처음으로 사용한 PG사가 KCP인데, 처음에는 아임포트 없이 직접 결제를 구현하고자 호기롭게 매뉴얼을 다운받아 열어봤다. 일단 메뉴얼도 수십 페이지인데, 그야말로 읽다가 정신이 나갈 것 같았다. 일단 서버에다가도 PG사와의 소켓 통신을 위한 모듈을 이것저것 깔고, 서버 설정도 만져야하고 뭐뭐 깔아야하고 할일이 수두룩했다. 주고받는 데이터도 양식이 복잡했다. 예제는 PHP랑 JSP밖에 없어서 별 도움도 안 됐다. 이러한 귀찮은 과정들을 미리 해 놓은 서버가 있어서, 내가 거기다가 결제에 필요한 정보만 보내면 알아서 다 해주는 서버가 있다면 어떨까? 그게 바로 아임포트 서버이다. 그냥 결제에만 신경쓸 수 있게 해 주는 것이다.

 

 

2. 복수 PG사 사용시 편리함

   당장 배달의 민족만 봐도, 배달음식을 결제할 때 카카오페이, 스마일페이, 네이버페이, 신용카드 등 여러 가지 결제 방식을 지원한다. 카카오톡을 안 쓰는 사람은 거의 없지만 카카오페이를 안 쓰는 사람은 많듯이, 하나의 결제 방식으로 모든 고객들을 커버할 순 없다. 그래서 여러 개의 결제방식을 지원하는 게 중요한데, 당연히 PG사들마다 결제 모듈이 제각각이다. 즉 1번의 저 귀찮은 과정들을 전부 다 따로 해야 한다. 아임포트는 PG사와 상관없이 결제에 정형화된 API 형식을 사용한다. 그래서 가맹점 서버에서 결제 모듈을 재활용 하기 용이하다... 사실 용이함을 넘어 그냥 결제 파라미터 하나만 바꾸면 거의 모든 PG사에 돌려 쓸 수 있다.

 

 

2-2. 매출 관리의 용이함

 이건 개발과는 크게 상관없는 내용이지만, 여러 개의 PG사를 운용할 경우 각 PG사마다 결제 내역이 있을 것이다. 이 결제 내역들을 아임포트 관리자 콘솔에서 한꺼번에 보고 관리할 수 있다. 

 

 

 

3. 환불이 쉬움

 이게 대체 장점이 맞나 하겠지만.... 개발자의 관점에서 내가 제일 사랑하는 기능이다.

 아임포트 결제로그 페이지에서, 성공한 결제에는 취소하기 버튼이 생긴다. 이거 누르면 바로 환불이 된다. 결제 기능 테스트할때, 처음에는 돈 깨질거 각오하고 1000원짜리 디버그용 상품(참고로 PG사마다 결제 최소금액이 있어서 그 이하로는 결제가 안 된다. KCP는 천원이다.) 만들어서 해볼까 했는데, 환불이 너무 쉬워서 결제 기능 테스트할떄 요긴하게 써 먹었다. PG사마다 '테스트 결제'라는 기능이 있긴 한데, 이거는 그냥 잘 돌아가는지 정도 확인하는데만 쓰고, 반드시 실제 결제로 테스트 하는 걸 권장한다. 아래에서 설명하겠지만 결제 모듈은 상상이상으로 까칠해서, 생각 이상으로 안될 때가 많다...

 

 

 

 대충 이런 이유로 아임포트를 도입하는게 개발 코스트 절약과 개발자의 정신건강을 위해 좋다. 일단 PG사 하나만 쓰는건 무료고, 여러개의 PG사를 쓸 때만 한번 돈을 지불하고 계정을 업그레이드 하면 된다. 

 

 

 

1. 아임포트 세팅 및 PG사 계약

 

 참고 :

https://docs.iamport.kr/

 

아임포트 docs

본 매뉴얼은 기존의 아임포트 매뉴얼의 리뉴얼 버전입니다. 아직 모든 컨텐츠가 준비되지 않은 베타 버전이며 지속적으로 업데이트 될 예정입니다. 따라서 결제 연동 시 기존의 아임포트 매뉴

docs.iamport.kr

 

https://www.iamport.kr/getstarted

 

개발자를 위한 무료 결제연동 API, 아임포트

아임포트를 import하세요. 아임포트의 풍부한 REST API 기능을 이용하면 결제연동이 훨씬 쉬워집니다. 지금 시작하세요.

www.iamport.kr

 

 과정도 쉽고 한글로 되어있어서 따로 설명은 필요 없을 것 같다. 회원가입하고 쓰면 된다.

 아임포트 docs의 경우엔 '가맹점 정보 확인하기', '일반결제 연동하기', 'IMP.request_pay - param, rsp 객체' 이 세 페이지만 참고해도 기본적인 결제를 구현하는 덴 지장 없을 것이다.

 

 

  이 과정에서 주의할 점은, 단순히 테스트 결제라면 상관없지만, 실제 결제기능을 위해선 PG사와 계약을 하고 PG사로부터 가맹점 id를 발급받아야 한다. 이 때, 직접 PG사와 계약하면 안되고 아임포트 내의 PG사 가입 페이지를 통해, 아임포트를 거쳐서 계약을 해야 한다. 구조상 아임포트 서버에서 결제가 이루어지기 때문이다. 물론 아예 가맹점 id를 아임포트에서 소유하는 건 아니고, PG사 페이지에서도 정상적으로 가맹점 취급을 받는다.

 

 

  이걸 모르고 직접 PG사와 계약을 했다가 결제가 안 돼서 아임포트 측에 문의를 하고, 최종적으론 아임포트를 통해 재계약을 하는 등 많은 삽질을 했었다... 참고로, PG사와 계약하는 것도 사업자등록증을 보내거나 쇼핑몰 심사 등의 과정이 있기 때문에 주 단위로 걸린다. 나는 개발 외적인 비즈니스 업무는 맡지 않았지만, 제3자의 눈으로 봐도 수많은 메일과 서류가 오고가는 등 많이 피곤해 보이는 작업이었다.

 

 

 이게 짧으면 1주일, 길면 2주일 정도고 중간에 뭔가 문제가 있으면 당연히 수정요청을 받는다. 생각보다 심사 조건이 까다롭다. 사이트에 연락처나 계좌 등이 있어야 하고, 상품 갯수도 일정 이상이여야 한다. 사실 상품 갯수도 제한이 있다는 것이 의외였는데, "이걸 누가 사?" 가 절로 나오는 상품을 사이트에 넣으며 불편한 진실을 어렴풋이 깨달았다...

 

 

 

 다음 편에선 실제 코드를 통해 아임포트를 세팅하고, 검증하는 과정에 대해 다뤄보겠다.

 

 

 

 

 

 실제 서버를 배포하면, 불행하게도 항상 따라오는 게 버그다. 물론 철저한 코딩과 테스트, 경우의 수 계산으로 미연에 방지하는 게 가장 좋겠지만 현실적으로 불가능하니, 차선책으론 에러가 발생했을 때, 문제 해결을 위한 데이터를 최대한 모으는 게 중요하다고 생각했다.

 

 

 

 물론 Django에서 아래와 같이 자체적으로 로깅을 지원하긴 하는데, 특히 이용자가 많아질 수록 텍스트로 이루어진 로그는 보기도 불편하고 무엇보다 내가 원하는 특정 상황(=특정 세션)에 대한 부분만 추출하기 너무 까다로웠다. 그렇다고 설정을 만져 Error 이외의 로그를 거르기엔 뭔가 찝찝했다.

 

 

 그래서 무료로 이용할 수 있는 에러 모니터링 툴에 대해서 찾아보던 중, Sentry에 대해 알게 되었다. 그래서 실제 서비스에 적용해서 사용해 보니, 매우 간편하면서 강력해서 큰 도움이 되었다. 그래서 센트리의 설치나 간단한 활용법에 대해서 공유하고자 한다.

 

 

1. 설치하기

https://sentry.io/

 

Application Monitoring and Error Tracking Software

Self-hosted and cloud-based error monitoring that helps software teams discover, triage, and prioritize errors in real-time.

sentry.io

  간단히 회원가입하고, 바로 프로젝트를 하나 만들면 된다. 프로젝트에서 모니터링을 진행할 프로그램을 지정할 것이다.  자기가 사용하는 언어/프레임워크를 고르면 그에 맞춰서 코드를 만들어 준다. Python -> Django를 고르면 된다. 그러면 아래와 같이 하라는 튜토리얼 페이지가 뜬다.

 

 

 

 

  sentry-sdk 라이브러리를 설치하고 Settings.py에 넣으면 된다. 물론 위 정보는 프로덕션 서버의 설정 파일에 따로 넣는 것을 추천한다. 필요하지 않다면 개발 서버에서는 굳이 에러 트레킹을 할 필요가 없기 때문이다.

 

 

  그 아래는 0으로 나누기 에러를 발생시키고 테스트 해보는 부분이 있는데, 간단한 테스트이므로 가볍게 한번 해 보면 될 것이다.

 

 

2. 활용하기 - 프로젝트

  프로젝트 세팅을 마친 후, 화면을 살펴 보자. 왼쪽의 목록에서 여러 가지 메뉴들이 있는데, 차례대로 Projects, Issues, Performance를 살펴볼 예정이다. 우선 Projects 메뉴에 가서, 만든 프로젝트를 눌러 보자.

 

  이 화면은 프로젝트 상세 페이지이다. 내가 돌리고 있는 현재 서버의 상태를 전체적으로 조망할 수 있다.

 

  Crash Free Sessions는 에러 없이 정상적으로 진행된 요청의 비율을 나타낸다. 당연히 이 비율이 높아야 서버가 정상적으로 돌아가고 있다고 간주할 수 있을 것이다. 내 서버는 약 99.9%의 신뢰도를 나타내고 있다. 이 0.1%가 왜 생겼는지는 아래에서 설명하도록 하겠다.

 

  Number of Releases는 이 프로젝트가 가진 릴리즈의 수인데, 쉽게 말해서 웹 서버의 버젼이 몇 개가 있었는지, 즉 몇 번의 변화가 있었는지를 뜻한다. 나는 따로 관련 설정을 안 해서 0이라고 나왔다. 아마 설정을 하면 특정 버젼에선 에러가 많았고 특정 버젼에선 적었다 식으로 모니터링이 가능할 텐데, 굳이 이 기능이 필요할 것 같진 않아서 나는 쓰지 않았으니 관심 있다면 사용해 봐도 될 것이다.

 

  Apdex는 성능 관련 지표인데, 지정된 시간 안에 요청에 대한 응답이 왔는지에 대한 비율이다. 이건 Settings->Perfomance에 가면 해당 값이 있는데, 기본값으론 300ms가 설정되는 것 같다. 성능 트래킹을 원한다면 이 부분도 활용할 수 있을 것이다.

 

 

3. 활용하기 - 이슈

 

 

 Issues 페이지이다. 사실 센트리를 즐겨찾기에 설정해 놨는데, 들어가면 자동으로 이 페이지로 뜬다. 그만큼 사실상 센트리에서 가장 중요한 부분 중 하나이다. 여기서 서버에서 발생한 에러들을 관리할 수 있다. 발생 시간이나 에러 내용, 어떤 URL로 접속했을 때 에러가 발생했는지 등이 적혀 있다. 여러번 발생하는 에러는 아래와 같이 이벤트(=요청 횟수)와 유저를 구분하여 중첩되어 기록된다. 즉, 여러 명의 유저가 이런 문제를 겪은 건지, 아니면 한 명이 똑같은 에러를 여러 번 발생시킨 건지 구분할 수 있다. 이 점이 Sentry가 에러를 보기 편하게 해주는 강점 중 하나라고 생각한다.

 

 

 

 참고로, 에러들이 꽤 많이 떠 있는데, 장고 서버를 운영하면서 몇 가지 발생할 수 있는 에러들이 있다.

 

 

  우선 Invalid HTTP_HOST header: 'www.blockfinex.com'. You may need to add 'www.blockfinex.com'... 등등 HTTP_HOST 에러가 대부분인데, 이건 유저가 아니라 그냥 인터넷 사이트들을 무차별적으로 취약점 스캔하는 봇들이다. 자세히 보면 접근하는 URL도 정상적이지 않다. 아무나 걸려라 하는 식으로 스캔하고 다니는 거라, 에러가 나면서 차단당하는 게 맞다. 해당 에러는 설정을 통해 이  유형의 에러는 무시하도록 할 수 있는데, 혹시 진짜 관찰이 필요한 에러들도 무시될까봐 따로 건들지 않았다.

 

 

 참고로 settings.py의 ALLOWED_HOST를 설정하지 않거나 *(모든 호스트 허용)으로 해 두면 이 에러는 안 뜰 것이다. 보안에 좋지 않으니 *보다는 실제 사용하는 호스트 이름으로 설정해 두자... 잘 모르겠으면 [{서버의 공인 아이피 주소}, '127.0.0.1', {서버의 도메인 주소}] 정도로만 해 둬도 될 것이다.

 

 

인터넷에 공개된 서버라면 반드시 받을 수밖에 없는 공격이다...

 

 

 

 그리고, OSError 같은 경우엔 개발 환경에서는 발생하지 않았지만 프로덕션 환경에서는 주기적으로 발생하던 에러인데, 이 에러는 다른 에러들과 다르게 Django의 세션 관련 데이터가 누락되어 있다.

 

 장고와 관련된 정보가 아예 누락되는 걸로 봐선 장고 밖에서 발생하는 에러로 추정되는데, 구글링 결과 장고의 문제는 아니고 wsgi로 사용하는 프레임워크인 uwsgi 관련 버그였다.

 

stackoverflow.com/questions/59026602/uwsgi-oserror-write-error-during-get-request

 

uwsgi: OSError: write error during GET request

Here is the error log that I received when I put my application in the long run. Oct 22 11:41:18 uwsgi[4613]: OSError: write error Oct 22 11:41:48 uwsgi[4613]: Tue Oct 22 11:41:48 2019 -

stackoverflow.com

 쉽게 말해서, 클라이언트와 서버가 요청을 주고받는 중 클라이언트는 어떤 이유로던 통신을 끊었는데, 서버(정확히는 uwsgi)에서는 요청을 계속 처리하고 있어서 생기는 오류이다. 이건 서버 설정을 만지면 해결될 수 있는 오류이다. 사실 이 에러 때문에 서비스에 문제가 생긴 적은 없었던 것 같다...

 

 

4. 활용하기 - 이슈 - 에러 상세보기

 

 위에 뜬 에러들 중 하나를 눌러 보면 에러의 상세한 내용을 볼 수 있다. 아래는 실제로 발생한 에러를 캡처한 것이다.

 

 

 

 우선 우측 상단의 이벤트, 유저를 통해 1명의 유저에게 이 에러가 12번 발생했다는 것을 알 수 있다. 또한, 해당 유저가 사용했던 브라우저와 운영 체제 역시 확인할 수 있다. 현재 보고 있는 건 가장 최근에 발생한 에러 정보로, 이전에 발생한 에러 정보를 보고 싶다면 events 아래의 숫자를 클릭하면 지금까지 발생한 동일한 에러 리스트가 뜬다. 각각을 클릭하면 각 에러에 대한 정보를 알 수 있다.

 

 

 

 또한, users 밑의 숫자(1)을 클릭해도 해당 에러를 겪은 유저들의 목록과 비율이 표시된다.

 

 

 

 

  센트리에서 에러가 발생한 상황의 정보를 수집하여 보여주는데, 우선 실행시킨 코드에서 에러가 발생한 부분을 표시해 준다. 여기는 기존 파이썬에서 보여주는 StackTrace와 같다.

 

 

 

 

  

 

 다음으로, breadcrumbs라 해서 사용자가 사이트에서 어떤 행동을 했는지 보여준다. 이 부분이 가장 핵심인데, Django 로거의 info 로그는 물론, 해당 사용자의 요청으로 인해 발생한 추가적인 HTTP 요청(심지어 타 사이트에 대한 API 요청이다)이나 DB 조작도 모두 보여준다. 이 부분을 보안상 가려야 하는 것이 유감일 정도로 상세하게 나온다. '카테고리(CATEGORY)'에서 httplib은 HTTP 요청, query는 DB 쿼리, django는 Django 로거를 나타낸다. 아마 더 많은 종류가 있을 것이다.

 

 

 그리고, 해당 view는 body에 데이터를 담은 POST 요청을 받아 특정한 동작을 수행하는데, 이 POST 요청에 대한 정보 역시 body의 내용을 포함해 쿠키, 헤더 등 모든 정보가 기록된다. 이외에도, 현재 유저가 django의 auth 시스템을 이용해 로그인 된 상태라면 해당 유저의 정보 역시 기록된다.

 

 

 

 

 

  Details 옆에 Activity 탭에서는, 해당 에러에 대해 기록을 할 수 있다, 깃헙의 issue와 비슷하다. 아마 여러 명이서 협업을 할 때 담당자를 배정하고, 의견 등을 교환하는 데 사용할 수 있을 것이다. 물론 아래처럼 그냥 개인 메모용으로 써도 된다. 

 

 

  마지막으로, 특정 에러에 대해 Resolve나 Ignore 등으로 체크를 할 수 있다. Resolve는 해결됨, Ignore는 무시이다. 체크할 시 해당 에러에 대해서 빨리 해결하라고 굵은 글씨로 강조하지 않게 된다. 단, 이그노어의 경우엔 이미 발생한 해당 에러에 대해서만 무시이고, 아예 관련 에러를 전체적으로 무시하려면 설정을 만져야 한다.

 

 

 

  이외에도 특정 에러 발생시 이메일 등으로 알림을 날리는 기능도 있지만 아마 간단히 활용하는 데는 이 정도만 해도 충분할 것이다.

 

 

 

 

 

5. 성능 분석

 

 이건 에러 모니터링과는 크게 관련은 없는 기능인데, 성능 측정엔 도움이 되는 부분이다. Perfomance에 들어가면 볼 수 있다.

 

 

  서버의 성능과 관련된 각종 지표들을 볼 수 있는 곳이다. 만약 데이터가 표시되지 않는다면 오른쪽 위의 기간을 좀 더 넓게 만져보자. Duration p75는 75%의 요청이 해당 시간보다 빠르게 처리되었다는 뜻이고, throughput은 분당 해당 수치만큼의 요청이 들어왔다는 뜻이고, Failure rate는 말 그대로 에러가 난 비율(HTTP 코드 4XX), apdex는 위에서 설명한 대로 제한 시간 내에 요청이 처리되었는지의 비율이다. 아래에는 관련 그래프들이 나타나 있다.

 

 

 

  Failure Rate가 꽤 높다. 대략 5명 중에 한 명이 오류가 난다는 것인데, 사실 정상이다. 왜냐하면 위에서 말한 취약점 스캔 봇들이 상상 이상으로 엄청나게 긁어가기 때문이다. 어떤 요청들이 있었는지 이 성능 페이지에서 바로 확인할 수 있는데, 한번 살펴보자.

 

 

 php를 사용하지도 않는 사이트인데 관련 URL로 많이 들어온다. 저게 워드프레스 쪽에서 사용되는 관리자 페이지나 설정 파일인데, 아무나 걸려라식 무차별 대입 공격이고 내 서버에는 저런 파일들이 존재하지 않으므로 전부 404 Not Found로 표시되는 것이다. 이게 바로 20%의 failure rate의 정체이다.

 

 

 

 

 위에서 소개한 센트리의 기능들은 극히 일부이다. 일단 무료 버젼에 있는 기능들만 소개한 거고 아마 유료 버젼엔 더 많은 기능들이 있을 것이다. 무료 기능으로도 꽤나 유용하게 사용할 수 있고 무엇보다  설치가 매우 간편하다는게 마음에 들었다. 장고 서버가 있다면 에러 모니터링을 공부해 보는 용도로도 유용할 것 같다.

+ Recent posts