2020년 4월 16일 목요일

[Python] 재시도 로직이 추가된 requests 모듈 사용 예제

파이썬엔 간단하게 HTTP 요청을 할 수 있게 해주는 requests 모듈이 있다. 렌더링을 위한 데이터 획득이 아니라면 굳이 브라우저를 이용할 필요가 없기 때문에 json 데이터 등을 가져오는게(API 호출) 목적이라면 requests 모듈을 이용하는 것이 보다 합리적이다. 여기서 한가지 주의할 점은 네트워크 환경이 불안정한 경우 일시적으로 요청 작업이 실패할 수 있다는 부분이다. 따라서 사용자는 요청 실패에 대한 로직 흐름까지 고려할 필요가 있다.

아주 단순하게 문제를 해결하자면 루프 구조 내에서 원하는 결과가 나올 때까지 응답을 계속하는 방법이 있을 수 있다.

# 방법 1
def get(url):
    try:
        return requests.get(url)
    except Exception:
        time.sleep(1)
        return get(url) # 재시도

# 방법 2
while True:
    response = get(url)
    if response.status_code != 500: # 500, server error
        break
    else:
        time.sleep(1)

방법 1의 문제점은 url 문자열이 잘못되어있을 경우 무한정 요청 작업을 수행한다는 것이고, 방법 2의 문제점은 다양한 네트워크 에러를 로직이 올바르게 처리해주지 못한다는 점이다.

제안되는 솔루션은 다음과 같다.

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

def requests_retry_session(
    retries=3,
    backoff_factor=0.3,
    status_forcelist=(500, 502, 504),
    session=None,
):
    session = session or requests.Session()
    retry = Retry(
        total=retries,
        read=retries,
        connect=retries,
        backoff_factor=backoff_factor,
        status_forcelist=status_forcelist,
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount('http://', adapter)
    session.mount('https://', adapter)
    return session

사용은 아래와 같이 한다. session 객체 전달은 선택 사양으로, 사용자가 session 객체를 따로 인자로 넘겨주지 않으면 requests_retry_session() 함수 내부에서 session 객체를 새로 생성하는 것을 위 코드에서 볼 수 있다. 그리고 status_forcelist엔 요청 실패로 간주할 응답 코드를 넣어줄 수 있다.


# 단순 get
response = requests_retry_session().get(url)

# 단순 get, timeout이 있는 경우.
response = requests_retry_session().get(url, timeout=5)

# 헤더 정보가 필요한 경우
s = requests.Session()
s.auth = ('user', 'pass')
s.headers.update({'x-test': 'true'})

response = requests_retry_session(session=s).get(url)

Retry 모듈과 HTTPAdapter 모듈을 이용해 요청을 할 때 재시도 정보를 포함시키는 것이다. backoff 팩터는 retries 값과 연계해 요청과 요청 사이에 쉬어줄 시간을 정해주게 된다.

쉬어줄 시간에 대한 로직은 다음과 같다.
슬립 = backoff factor * 2^(재시도 횟수 - 1)

따라서 위 함수의 기본 값으로 지정된 3과 0.3을 그대로 쓰는 경우
• 1회 재시도 sleep : 0초 (0.3 * 0)
• 2회 재시도 sleep : 0.6초 (0.3 * 2)
• 3회 재시도 sleep : 1.2초 (0.3 * 4)
값을 갖게 된다.

따라서 3회 요청에 실패하는 경우 사용자는 최대 1.8초 + @(통신 비용)을 기다리게 되는 셈이다.
위 함수는 당연히 post() 호출 시에도 유효하다.

출처: https://www.peterbe.com/plog/best-practice-with-retries-with-requests

아래는 유사 한글 포스팅 참고.
https://knight76.tistory.com/entry/python-%EC%9B%B9-%EC%9A%94%EC%B2%AD-%EC%98%88%EC%8B%9C-requests-HTTPAdapter-Retry

2020년 4월 8일 수요일

모두 제자리 - 도미니크 로로

'심플하게 산다'로 대 히트를 쳤던 도미니크 로로의 또 다른 책으로, 짧은 글들을 모아 엮어 낸 책이다. 다른 책 심플하게 산다나 다시 쓰는 내 인생의 리스트에 나온 글귀와 비슷한 부분이 많아서 읽는데 좀 지루했는데 한번 붙잡은 책은 끝까지 읽는 편이라 어쨌든 끝까지 읽긴 했다. 밀리의 서재를 통해 읽었는데 구매해서 읽었다면 조금 후회했을 만한 책.

글쓴이는 오랫동안 일본에 거주하며 선불교와 동양 철학에 큰 영향을 받았다고 한다. 그래서인지 책 곳곳에 일본 문학에서 가져온 글귀나 일본인의 생활 방식에 감탄하는 구문들이 있다. 웹 서비스를 통해 읽은 책이라 '소장'할 순 없으니 개인적으로 기억에 남는 문장을 옮겨 적어 보기로 한다.

대도시에서 한 평의 가격이 얼마인지 계산하면 요즘 시대에 공간을 갖는다는 것이 얼마나 큰 사치에 속하는지 알게 된다.
• 많이 갖지 않으면 공간도 벌 수 있다.
• 소유하고 저장하고 계속 갖고 있을지 아니면 버릴지 결정하는 것은 삶의 질을 높이는 일이기도 하다.
• 좁은 공간에 사는 사람은 깔끔하게 정리할 수밖에 없다. 공간이 귀하다는 것을 알기에 편하게 살기 위해 좁은 공간을 잘 활용한다.
• 쓸모 있을까 생각했을 때 머뭇거린다면 별로 필요하지 않은 물건이다. 필요한 물건이라면 생각조차 할 필요가 없기 때문이다.
목표를 이루려면 단계를 세분화해야 한다. 너무나 당연하다 보니 많은 사람들이 그러한 점을 잊거나 과대평가한다.
• 정리에서, 추억의 물건은 맨 마지막에 다뤄야 한다. 이러한 물건은 감정이 깃들어 있어서 분류, 정리하기가 가장 힘들다.
• 조금 깊이 생각해 보면 미학은 일치를 추구하려는 본능일 뿐이다.
• 평범한 것들이 유지하고 원칙을 따르며 이대로 좋다는 확신으로 가득할 때 일상의 평범한 옷을 입고 있어도 위대해진다.
• 너무 완벽하게 질서 정연하면 서정적인 면이 없다.
• 몸은 훌륭한 도구다. 경험을 통해 습관을 들이기 때문이다.
• 일본 속담에서는 작은 것을 하찮게 여기는 사람은 절대 큰일을 할 수 없다는 말이 있다.
• 정리를 하면 물질의 영향력에서 자유로워져서 건강하고 자연스럽게 살 수 있으며 가볍게 살 수 있다.

뭐 이런 책이다.
꼭 필요한 물건만 소유하면서 삶에 집중하자는 책. 확실히 주변에 불필요한 물건이 많으면 물건에 일상이 눌리는 느낌이 들곤 한다. 가족이 생기고 식구가 늘다보니 한번 읽을 책을 계속 갖고 있기에도 어딘가 부담이 가는 요즘이다.

밀리의 서재를 1년 결재해서 책을 읽고 있는데, 쨍한 컴퓨터 디스플레이나 모바일 디스플레이로 읽어야 해서 눈이 좀 아프긴 한데 꽤나 만족스럽다. 원하는 시간, 원하는 장소에서 내가 원하는 형태로 책을 읽을 수 있으니 말이다. 아쉬운 건 내가 꼭 읽고 싶은 책은 정작 서비스 목록에 없다는 점. 그리고 '종이 냄새'가 나지 않으니 책을 읽을 때의 만족감이 좀 덜하다.

아, 배우 박정민이 쓴 전설의 책(?) 쓸만한 인간도 여기서 읽을 수 있었다. 마포구청 도서관에선 예약이 밀려 있어서 도저히 읽을 수가 없었는데 밀리의 서재에 딱 있길래 반갑게 읽었다. 책이 막 좋진 않다. 연예인 등 유명인이 쓴 신변잡기 책 중에서 조금 나은 정도다. 그래도 뭐 나는 배우 박정민을 좋아하니까 이만하면 되었다.

2020년 3월 16일 월요일

[C++] 암시적 링크와 명시적 링크

동적 라이브러리 (DLL)을 컴파일 하면 *.lib 파일과 *.dll 파일이 생성된다. 여기서 *.lib 파일(가져오기 라이브러리)은 라이브러리의 전체 코드를 포함하고 있는 정적 라이브러리의 *.lib와 달리 DLL에서 제공하고자 하는 외부 함수 참조 정보가 담겨져 있다. 이렇게 생성된 DLL을 다른 실행 파일에서 연결하려면 다음의 두가지 방식 중 하나를 선택해야 한다.

  • 암시적 링크 : 정적 로드 방식. 함수가 정적으로 연결되고 실행 파일 내에 포함된 것과 동일한 방식으로 DLL에서 내보낸 함수를 호출할 수 있다. *.lib 파일을 이용해 연결된다. *.lib에 담겨진 정보를 토대로 런타임에 DLL의 함수 코드를 참조할 수 있게 된다.

  • 명시적 링크 : 런타임에 필요시 DLL을 로드한다. DLL에의 각 함수에 접근할 함수 포인터를 설정해 함수를 호출해야 한다. 다 사용하고 나면 DLL을 언로드 해주어야 한다. 각각 LoadLibrary(), GetProcAddress(), FreeLibrary()의 세가지 함수를 통해 구현된다.

대부분의 응용 프로그램에선 간편하고 쓰기 쉽기 때문에 암시적 링크를 사용하지만 명시적 링크 방식을 쓸 때의 장점도 있다. 명시적 링크 방식을 쓰면 프로그램 실행 중에 동적으로 DLL을 교체할 수 있다. (플러그인) 그리고 프로그램 실행 전에 필요한 모든 DLL을 메모리에 로딩하는 암시적 링크 방식에 비해서 빠른 실행 시간을 갖는다.

https://docs.microsoft.com/ko-kr/cpp/build/linking-an-executable-to-a-dll?view=vs-2019

[Java] BufferedReader

BufferedReader는 입력 스트림(파일, 소켓, ...) 에서 텍스트를 쉽게 읽을 수 있는 클래스이다. 텍스트 데이터를 효율적으로 읽을 수 있도록 문자를 버퍼링한다. (입력된 데이터가 바로 전달되지 않고 버퍼링 된 후 전달된다.)

BufferedReader는 데코레이터 패턴을 적용한 좋은 예다. (* 데코레이터 패턴은 객체에 동적으로 새로운 책임을 추가해준다. 서브클래스를 만드는 것보다 더 합리적이다.)
여기서 데코레이터는 말 그대로 장식자로, 특정 객체를 감싼 뒤 중간에 위치해서 오고 가는 요청과 응답을 더 매끄럽게 처리해 준다. 당연히 데코레이터는 자신이 감싼 객체의 동작을 잘 알고 있어야 한다.

reader = new BufferedReader(new FileReader("sample.txt"));
reader = new BufferedReader(new InputStreamReader(System.in));
reader = new BufferedReader(new InputStreamReader(socket.getInputStream());

위 구문에서 보듯 Reader를 구현하는 인스턴스를 감싸, 유연하게 버퍼링 기능을 추가(확장) 해줄 수 있다. 저렇게 한번 감싸주면 readLine() 같은 메서드를 이용해 데이터를 쉽고 간편하게 읽어들일 수 있게 된다.

마지막으로 BufferedReader는 Scanner와 자주 비교된다. 비슷한 일을 해주는 클래스를 굳이 따로 구분한 까닭이 있을 것이다. 주요 차이점은 다음과 같다.

• BufferedReader는 thread-safe하지만 Scanner는 그렇지 않다.
• Scanner는 정규 표현식을 써서 구문 분석을 할 수 있다. (파싱 같은 작업을 하는데 편리하다.)
• Scanner는 IOException 을 숨기고, BufferedReader는 이를 드러낸다.
• BufferedReader의 기본 버퍼 크기가 더 크고, 데이터를 읽기만 하는 경우 Scanner 보다 빠르게 처리할 수 있다.

Scanner 사용법 : Text Parsing Made Easy
https://dzone.com/articles/java-scanner-text-parsing-made-easy

2020년 3월 10일 화요일

[Django] no such table: main.auth_user__old

해당 오류는 SQLite 3.26.0에서 ALTER TABLE RENAME 구문에 대한 사양 변경으로 발생한 오류라고 한다. 기존의 모델(models.py) 또는 어드민(admin.py)을 수정하는 과정에서 이 오류가 발생할 수 있다. 오류는 Django 2.1.5 이하 버전에서 발생하며 Django의 버전을 2.1.5 이상으로 올린뒤 다시 마이그레이션(makemigrations, migrate) 명령을 호출해주면 문제는 해결된다. 사용하는 버전에 따라 SQLite를 다시 설치해야 하는 경우도 있다고 하는데 내 경우엔 필요한 작업이 아니었기 때문에 확실한 정보는 아니다.

어쨌든 조치를 하기 전에 데이터 백업은 필수.

참고로 contrib.auth.views.login(), logout() 함수는 Django 2.1에서 삭제되었으므로 해당 함수를 사용하는 Django 버전을 쓴다면 LoginView.as_view(template_name='...') 방식으로 수정이 필요하다.

//

위와 같이 작업한 후 마이그레이션은 되었는데, 다시 사용자 데이터를 수정할 때 동일한 문제가 생긴다면 직접 db.sqlite3에 접속해 문제를 해결하는 방법이 있다. (기존 DB를 날리고 새 DB 파일을 생성하는 건 차마 못하겠다..)

커맨드 라인에서 Django 프로젝트 폴더로 이동해 sqlite3 db.sqlite3를 입력하면 Django가 바라보는 SQLite DB에 접근할 수 있다.

테이블 목록을 확인하는 명령어는 .tables 인데 입력해보면
사용자 데이터는 auth_user
그룹 데이터는 auth_group
사용자와 그룹에 대한 관계 데이터는 auth_user_groups
에 저장되어 있는 것을 확인할 수 있다.

수정하고 싶은 데이터의 ID를 알아내 스키마에 맞게 SQL을 작성하면 원하는 작업을 할 수 있는 것이다.

문제가 되는(?) auth_user_groups의 테이블 생성 구문을 살펴보면 아래와 같이 user_id 컬럼이 존재하지 않는 auth_user__old 테이블의 id를 참조하고 있는 것으로 확인된다.

CREATE TABLE "auth_user_groups" (
"id" integer NOT NULL,
"user_id" integer NOT NULL,
"group_id" integer NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("group_id") REFERENCES "auth_group"("id"),
FOREIGN KEY("user_id") REFERENCES "auth_user__old"("id")
);

아마도. 다른 트러블 슈팅 문서대로 조치하였는데 고쳐지지 않는다면 이 부분을 해결하면 될 것이다. 방법은 아래와 같이 외래키가 올바르게 지정된 테이블을 생성한 뒤 교체해 주는 작업을 진행하면 된다.

PRAGMA foreign_keys = OFF;

CREATE TABLE auth_user_groups_temp (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "user_id" integer NOT NULL REFERENCES "auth_user" ("id"), 
    "group_id" integer NOT NULL REFERENCES "auth_group" ("id")
);

INSERT INTO auth_user_groups_temp SELECT * FROM auth_user_groups;

DROP TABLE auth_user_groups;

ALTER TABLE auth_user_groups_temp RENAME TO auth_user_groups;

PRAGMA foreign_keys = ON;


2020년 3월 4일 수요일

[Java] 프로토콜 버퍼 (Protocol Buffers) 기초

아래의 내용은 Java 언어를 이용해 프로토콜 버퍼를 사용하는 것에 대한 내용이다.

자세한 내용은 아래 링크를 참고하시면 됨.
https://developers.google.com/protocol-buffers/docs/javatutorial

프로토콜 버퍼는 직렬화 라이브러리로 프로그래밍 언어를 통해 만들어진 데이터를 bytes로 변환해 준다. 프로토콜 버퍼는 XML, JSON 방식보다 더 작고(smaller data) 빠르게 동작한다. 데이터를 텍스트가 아니라 bytes로 변환하기 때문이다. 알려진 내용에선 프로토콜 버퍼가 텍스트 포맷 송수신 대비 10배 적은 용량을 사용하면서도 100배 빠르게 동작한다고 한다.

게다가 사용자가 하기 싫어하는 지저분한 작업들을 프로토콜 버퍼가 대신 해준다. 직렬화/역직렬화는 물론이고 enum과 문자열 조작에 대한 것들이 이러한 작업이다. 또한 서로 다른 언어간 데이터를 주고 받을 때 발생하는 문제점들도 프로토콜 버퍼가 해결해준다. 사실 이게 가장 강력한 기능일 것이다.

튜토리얼에 따르면 프로토콜 버퍼를 사용하기 위해선 아래의 3가지를 알아야 한다고 되어 있다.

• .proto 파일을 정의하는 방법.
• 프로토콜 버퍼 컴파일러를 이용해 .proto 파일을 .java 파일로 변환하는 방법
• 프로토콜 버퍼의 Java API로 메시지를 읽고 쓰는 방법.

프로토콜 버퍼를 사용하는 이유는 다음과 같다.

• 직접 직렬화 라이브러리를 작성해야 하는 경우 또는 빌트-인 직렬화 기능보다 더 나은 솔루션이 필요한 경우
• 직렬화된 객체를 각기 다른 언어로 작성된 프로그램에서 읽어서 사용해야 하는 경우

* 물론 "단순한 객체를 단일 언어에서" 간단하게 객체를 읽고 쓰기 위함이라면 (임시로) 자체적인 로직을 작성하거나 XML/JSON 등의 텍스트 형식으로 이를 처리할 수 있을 것이다.


□ .proto 파일을 정의하는 방법.


프로토콜 버퍼의 프로토콜 포맷은 .proto 확장자를 이용한다.

C의 struct 정의와 유사한 문법으로 메시지 형식을 정의하는데 message 키워드를 이용해 각각의 데이터 스트럭쳐를 정의할 수 있다. 이렇게 만들어진 .proto 파일은 프로토콜 버퍼 컴파일러를 통해 타겟 언어의 소스 코드로 변환된다. 그리고 각자 작성하는 프로그래밍 언어에서 이 파일을 참조해 목적에 맞는 작업을 하면 된다.

message A { message B { ... } } 식의 nested 방식을 지원하며 bool, int32, float, double, string등의 익숙한 자료형을 쓸 수 있다. enum을 지원하며 repeated 키워드를 이용해 리스트 형식을 표현할 수 있다.

* Java 언어인 경우 package, java_package 옵션을 이용해 이름 충돌이 발생하지 않도록 패키지 이름을 정의해주어야 한다.

필드엔 반드시 값이 지정되어야 한다는 의미의 required와 값이 지정되지 않아도 된다는 의미의 optional이 위치하게 된다. 튜토리얼에 의하면 required는 가급적 사용하지 않는 것이 좋다고 한다. 메시지 구조의 유연함을 떨어뜨리는 요인으로 나중에 가서 하위 호환성을 지키기 어려워지기 때문이다.

마지막으로 한가지, 모든 필드엔 " = 1", " = 2" 식의 숫자 태그를 붙여주어야 한다. 필드를 직렬화할 때 정보를 컴파일러에게 알려주는 일을 하는데 하위 호환성을 제공하려면 한번 부여한 숫자는 절대로 변경해선 안된다. 태그가 변경되면 바이너리 포맷이 틀어지기 때문이다.


□ 프로토콜 버퍼 컴파일러를 이용해 .proto 파일을 .java 파일로 변환하는 방법


protoc.exe가 제공되는데 이를 가지고 타겟 프로그래밍 언어로 프로토콜 포맷에 대한 소스코드를 생성할 수 있다.
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/프로토콜포맷파일명


□ 프로토콜 버퍼의 Java API로 메시지를 읽고 쓰는 방법.


필드에 대한 getter(), setter()는 물론이고 빌더-패턴을 제공하기 때문에 객체에 값을 지정하고 읽는 것엔 큰 어려움이 없을 것이다.

Person john =
  Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .addPhones(
      Person.PhoneNumber.newBuilder()
        .setNumber("555-4321")
        .setType(Person.PhoneType.HOME))
    .build();

객체를 읽고 쓰는덴 아래의 4가지 API가 제공된다.

byte[] 버전과,
  • byte[] toByteArray();
  • static Person parseFrom(byte[] data);

스트림 버전이 있다.
  • void writeTo(OutputStream output);
  • static Person parseFrom(InputStream input);

추가로, 통신으로 객체를 주고 받을 수도 있는데 Netty와 궁합이 좋다.
디코딩 단계에선 ProtobufVarint32FrameDecoderProtobufDecoder를,
인코딩 단계에선 ProtobufVarint32LengthFieldPrependerProtobufEncoder를 사용하면 된다.
사용자의 입장에선 데이터 프레임의 길이만 앞에 붙여서 서로 주고 받으면 되는 것이다.


2020년 3월 3일 화요일

[Netty] Netty의 데이터 처리 순서

Netty에서의 데이터 처리 순서를 이해하려면 Channel과 Pipeline에 대한 이해가 먼저 되어야 한다. Netty에서 Channel은 읽기, 쓰기, 연결 등의 I/O 작업이 가능한 통로이고, Channel에 연결된 Pipeline엔 인바운드, 아웃바운드, 인아웃바운드 핸들러가 쭉 연결되어 있다. 각 핸들러는 서블릿 필터와 유사한 역할을 수행한다.

서버 입장에서 보면 Channel은 연결된 클라이언트마다 생성되고, Channel당 하나의 Pipeline을 갖게 된다. Pipeline에 올라가는 핸들러는 등록된 순서에 따라 연결되는데 인바운드, 아웃바운드, 그리고 인아웃바운드 핸들러를 구분하지 않고 하나의 double-linked list에 추가된다. 개인적으론 인바운드와 아웃바운드에 대한 체인이 따로 구분되었다면 이해하고 사용하기에 더 편했을 것이란 아쉬움이 있다.

인바운드 핸들러는 읽기 이벤트를 처리하는데 ByteBuf를 데이터 스트럭쳐로 변환하는 일을 하므로 디코더라고 하고,
아웃바운드 핸들러는 쓰기 이벤트를 처리하는데 데이터 스트럭쳐를 ByteBuf로 변환하는 일을 하므로 엔코더라고 한다.
* 인아웃바운드 핸들러는 읽기와 쓰기 이벤트 모두에 관여할 것이다.

엔코더와 디코더는 처리 순서를 고려하여 Pipeline의 시작 부분에 위치시키는 것이 일반적이다. 그리고 비즈니스 핸들러는 가장 나중에 위치시켜야 한다. 알아두어야 할 것은 모든 핸들러는 하나의 파이프라인에 연결된다는 점이다.


@ChannelInitializer

ChannelPipeline p = ch.pipeline();
p.addLast(new MyEncoder());
p.addLast(new MyDecoder());
p.addLast(new MyBusiness()); // 비즈니스 핸들러는 가장 마지막에 위치.

내가 작성한 프로그램으로 데이터가 입력된다면 처리 순서는 다음과 같을 것이다. 입력 데이터에 대해 엔코더는 관여하지 않는다.
MyDecoder (ByteBuf를 데이터 스트럭쳐로 변환) -> MyBusiness


반대로 내가 작성한 프로그램이 데이터를 다른 곳으로 출력한다면 처리 순서는 다음과 같아진다. 위와 마찬가지로 출력 데이터에 대해 디코더는 관여하지 않는다.
MyBusiness -> MyEncoder (데이터 스트럭쳐를 ByteBuf로 변환)