no image
게으른 로깅
로깅은 프로그래밍에서 가장 중요한 것 중 하나입니다. 기록을 하는 것은 비단 프로그래밍 뿐 아니라 다른 작업에서도 중요하지만 휘발성으로 계속해서 새로운 작업이 발생하는 프로그래밍에선 그 의미가 더욱 남다릅니다. 해당 글에서는 파이썬에 로그를 기록하는 방법을 간단히 소개하며 파이썬을 사용한다면 일상적으로 쉽게 구현할 수 있는 게으른 로깅 (Lazy logging)이라는 모범 사례를 소개합니다. 파이썬 로깅 본격적으로 게으른 로깅을 설명하기 앞서 간단하게 파이썬에서의 로깅을 이해해보겠습니다. 하단의 코드는 로깅을 수행하는 가장 간단한 코드입니다. warning이라는 로그 레벨과 함께 함께 출력될 메세지를 전달하고 있습니다. >>> import logging >>> logging.warning("Hello l..
2023.07.29
[pytest] @pytest.fixture
import pytest import tensorflow as tf from PIL import Image import utils @pytest.fixture def pil_image(): return Image.new('RGB', (32, 32), color='red') def test_convert_pil_to_tensor(pil_image): tensor = utils.convert_pil_to_tensor(pil_image) assert tensor.shape == (32, 32, 3) assert tensor.dtype == tf.float32@pytest.fixture의 역할 Dummy 테스트 환경을 구성 위의 예시에서는 PIL.Image 객체를 생성하여 test_..
2023.05.02
logging 사용법과 관련 이슈
기본적인 사용방법 해당 글에서는 python의 logging 기본적인 사용방법을 다룬다. 관련해서 마주할 수 있는 에러 상황이나, 좀 더 다양한 상황을 다룰 수 있는 logging 사용방법을 기술해 보았다. 추가적으로 해당 글에서는 특정 메시지를 어떤 레벨로 로깅해야 하는지와 같은 철학은 다루지 않음을 감안해 주시면 좋겠다. 짧은 글이기에 바로 본론으로 들어가보자. 코드는 짧고 간결하다. import logging logging.basicConfig(filename='../logs/crawl.log', format='%(asctime)s %(levelname)s %(message)s') logging.getLogger().setLevel(logging.INFO) # Optional logging.info..
2023.03.27
no image
grpc 소개
gRPC는 Google에서 개발한 RPC(Remote Procedure Call) 시스템입니다. 전송을 위해 TCP/IP 프로토콜과 HTTP 2.0 프로토콜을 사용하고 IDL(Interface Definition language)로 protocol buffer를 사용합니다. gRPC에 대해 이해하기 위해선 다음에 대한 배경지식이 필요합니다. RPC(Remote Communication Mechanism) RPC(원격 프로시저 호출)는 한 프로그램이 네트워크의 세부 정보를 이해하지 않고도 네트워크 안의 다른 컴퓨터에 있는 프로그램에서 서비스를 요청하는 프로토콜입니다. RPC는 client-server 모델을 사용합니다. 클라이언트에서 서비스를 요청(function call)하면, 서버에서 서비스를 제공합니..
2022.11.15
no image
protocol 버퍼에 대한 소개
What are Protocol Buffers? Protocol buffer는 structured data(구조화 데이터)를 seriailize(직렬화)하기위한 Google의 언어 중립적, 플랫폼 중립적이며 확장 가능한 메커니즘입니다. XML보다 작지만, XML보다 빠르고 단순합니다. IDL(Interface Definition Language)로서 data structure를 정의한 다음, .proto 파일을 protocol buffer compiler(protoc)를 이용해 compile 합니다. Complie된 소스 코드를 사용하여 다양한 데이터 스트림에서 다양한 언어로 다양한 구조의 데이터를 쉽게 읽고 쓸 수 있습니다. 프로토콜 버퍼는 현재 Java, Python, Objective-C 및 C +..
2022.11.15
no image
[go 모듈 소개 - 2. logrus] 더 자세한 로깅 모듈
목차 1. logrus란? 2. logrus의 특징 3. logrus 사용 예시 Logrus란? logrus는 golang에서 사용할 수 있는 로깅 모듈 중의 하나로, docker와 Prometheus와 같은 많은 유명 오픈 소스 프로젝트가 이를 사용하여 로그를 기록하는 것으로 알려졌다. Logrus의 특징 logrus는 다음과 같은 특징이 있다. golang 표준 라이브러리의 로깅 모듈과 완벽하게 호환된다. logrus에는 6가지 로그 레벨이 존재하며, 이는 golang 표준 라이브러리의 로그 모듈 API의 상위 집합이다. 프로젝트에서 표준 라이브러리의 log 모듈을 사용하는 경우 저렴한 비용으로 logrus로 마이그레이션할 수 있다. 확장 가능한 hook 매커니즘: 사용자가 hook을 통해 로컬 파일..
2022.11.15
no image
[go 모듈 소개 - 1. gorm] Golang을 위한 ORM 라이브러리
ORM(Object Relation Mapping)이란? 객체와 RMDB의 데이터를 자동으로 매핑해주는 것을 말한다. 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용한다. (golang은 struct로 클래스를 대신한다) 객체 모델과 관계형 모델 간의 불일치가 존재한다. ORM을 통해 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 불일치를 해결한다. 매핑된 정보를 바탕으로 자동으로 SQL을 작성해준다. Object를 통해 간접적으로 데이터베이스를 다룰 수 있다. ORM의 장단점 장점 객체 지향적인 코드이기에 비즈니스 로직에 더 집중할 수 있다. ORM을 사용하면 SQL Query가 아닌 작성 중인 언어로 DB에 접근하여 데이터를 접근할 수 있다. 각종 객체에 대한 코..
2022.11.15

로깅은 프로그래밍에서 가장 중요한 것 중 하나입니다. 기록을 하는 것은 비단 프로그래밍 뿐 아니라 다른 작업에서도 중요하지만 휘발성으로 계속해서 새로운 작업이 발생하는 프로그래밍에선 그 의미가 더욱 남다릅니다.

 

 

해당 글에서는 파이썬에 로그를 기록하는 방법을 간단히 소개하며 파이썬을 사용한다면 일상적으로 쉽게 구현할 수 있는 게으른 로깅 (Lazy logging)이라는 모범 사례를 소개합니다. 

 

 

파이썬 로깅

 

본격적으로 게으른 로깅을 설명하기 앞서 간단하게 파이썬에서의 로깅을 이해해보겠습니다.

 

 

하단의 코드는 로깅을 수행하는 가장 간단한 코드입니다. warning이라는 로그 레벨과 함께 함께 출력될 메세지를 전달하고 있습니다.

 

 

>>> import logging
>>> logging.warning("Hello logging")
WARNING:root:Hello logging

 

 

warning을 로그 레벨로 설정한 후 지정된 "Hello logging"이라는 에러메세지를 출력하고 있습니다. Python의 기본 로깅 레벨은 warning이며 이는 해당 로그 레벨 이상의 로그들만 출력 혹은 파일에 기록됨을 의미합니다. 때문에 다음과 같은 실행에서는 아무런 결과를 얻을 수 없습니다.

 

 

>>> logging.info(“Hello logging”)

 

 

info 레벨은 warning 레벨보다 아래 단계에 위치하며 앞서 설명한대로 해당 코드는 로그를 기록하지 않습니다.

 

 

참고: 파이썬 로깅 레벨

 

 

우리는 파이썬을 통해 로그를 기록하며 자연스럽게 logging 모듈을 가져옵니다. 우리가 가져오는 logging 모듈은 기본으로 제공되는 로깅 모듈이므로 별도의 설정을 통해 로그 레벨을 바꿀 수 있습니다. 

 

 

로그 레벨을 변경하는 방법에는 2가지 방법이 있습니다. 이를 이해해기 위해서는 logger라는 존재에 대해 이해해야 합니다. 

 

 

logging 모듈 내의 getLogger라는 메서드는 logger를 반환합니다. 

 

 

logger = logging.getLogger("my_logger")

 

 

이때 logger는 로깅을 하는데 있어 설정 가능한 다양한 구성들을 담고 있는 하나의 인스턴스입니다. 우리가 설정하고자 하는 로그 레벨도 바로 logger를 통해 설정할 수 있구요.

 

 

logger를 이해하셨다면 로깅을 할 수 있는 첫번째 방법을 이해하실 수 있습니다.

 

 

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("my_logger")

 

 

가장 기본적인 설정 방법이며 basicConfig가 수행된다면 그 이후에 만들어지는 모든 logger의 로깅 레벨이 basicConfig에 명시된 대로 설정되는 것을 알 수 있습니다.

 

 

>>> logging.basicConfig(level=logging.DEBUG)
>>> logger = logging.getLogger("my_logger")
>>> logger.info("Hello logging")
INFO:my_logger:Hello logging

 

 

basicConfig는 편리하게 루트 Logger를 설정하는 방법을 제공하지만 한 번 설정된 루트 Logger의 설정은 변경되지 않습니다. 쉽지만 그만큼 자유도가 떨어지기에 사용방식이 다소 제한적입니다. (한 번 적용이 된다면 해당 런타임에 영구적입니다)

 

 

>>> logging.basicConfig(level=logging.DEBUG)
>>> logger = logging.getLogger("my_logger")
>>> logger.info("Hello logging")
INFO:my_logger:Hello logging
>>> logging.basicConfig(level=logging.ERROR)
>>> logger.info("Hello logging")
INFO:my_logger:Hello logging

 

 

두 번째 방식은 setLevel 메소드를 통한 설정 변경입니다.

 

 

앞서 설명한 문제는 setLevel이라는 메소드를 통해 해결할 수 있습니다. 먼저 basicConfig의 방식과의 가장 근본적인 차이는 로깅 설정을 일괄적으로 적용시키냐(basicConfig), 개별 logger에 적용시키냐의 차이입니다. 코드로 확인해보시죠.

 

 

# 1. basicConfig
>>> logging.basicConfig(level=logging.INFO)
>>> logger.info("Hello logging")
INFO:my_logger:Hello logging

# 2. logger 생성
>>> logger1 = logging.getLogger('my_logger_1')
>>> logger2 = logging.getLogger('my_logger_2')
>>> logger1.info('This is an informational message from logger1')
INFO:my_logger_1:This is an informational message from logger1
>>> logger2.info('This is an informational message from logger2')
INFO:my_logger_2:This is an informational message from logger2

# 3. logger1에 대해서 로깅 레벨 변경
>>> logger1.setLevel(logging.ERROR)
>>> logger1.info('This is an informational message from logger1 - it will not be logged')
>>> logger1.error('This is an error message from logger1')
ERROR:my_logger_1:This is an error message from logger1

# 4. logger2는 로깅 레벨 유지
>>> logger2.info('This is another informational message from logger2')
INFO:my_logger_2:This is another informational message from logger2

 

 

먼저 코드에서는 기본적으로 모든 logger에 적용될 기본 레벨을 basicConfig를 통해 명시합니다. 때문에 해당 라인 이후의 모든 logger는 error 로깅 레벨로 설정될 것입니다.

 

 

생성된 2개의 logger 중에서 logger1의 경우 중간에 설정을 변경해보았습니다. info 단계에 대한 출력이 이뤄지지 않았으며 설정이 성공적으로 변경되었음을 확인할 수 있습니다. logger2의 경우에는 여전히 Info 단계에 대한 출력이 이뤄짐을 확인할 수 있습니다.

 

 

로깅 레벨외에도 로깅을 통해 출력을 할지, 파일에 기록할지, 로그 메세지는 어떻게 구성할지 등 다양한 설정을 logger에 담을 수 있습니다.

 

 

게으른 로깅

 

로깅을 수행할 때 활용하는 warning 함수는 다음의 구조를 갖습니다 (다른 로깅 레벨도 동일합니다).

 

 

def warning(msg, *args, **kwargs):

 

 

warning에서는 기본적으로 msg를 첫 번째 인수로 요구하는 구조이며 추가적으로 더 많은 인수를 추가할 수 있도록 구성되어 있습니다.

 

 

이번에는 별도의 설명없이 코드를 먼저 보여드리면서 글을 이어나가보겠습니다.

 

 

>>> import logging
>>> class RaiseWhenPrinted:
...     def __str__(self):
...         raise Exception(“I was printed”)
>>> raise_when_printed = RaiseWhenPrinted()

 

 

현재 로깅 레벨은 warning 단계입니다. 앞서 설명한 별도의 로깅을 설정하지 않았기에 루트 logger의 기본설정을 따른 결과입니다.  

 

 

이어서 클래스의 __str__ 함수를 재정의합니다. 해당 클래스에서는 별도로 __str__ 에 접근하는 경우 에러를 발생시킬 것입니다.

 

 

>>> logging.info(“Logging {}”.format(raise_when_printed))

 

 

해당 코드를 실행하면 다음과 같은 결과를 얻습니다.

 

 

Traceback (most recent call last):
File “<input>”, line 1, in <module>
File “<input>”, line 3, in __str__
Exception: I was printed

 

 

앞서 설명하였듯 루트 logger의 레벨은 warning입니다. 그러나 info 레벨의 로깅에도 __str__ 함수에 접근하여 예외가 발생한 것을 확인할 수 있습니다.

 

 

이어서 게으른(Lazy) 방식으로 문자열을 전달해보겠습니다.

 

 

>>> logging.info(“Logging %s”, raise_when_printed)

 

 

해당 코드에서는 예외가 발생하지 않았습니다.

 

 

이는 __str__ 메소드가 호출되지 않았다는 것을 의미함과 동시에 게으른 로깅을 수행하는 경우 실제로 __str__이 요구되지 전까지는 해당 메소드가 사용되지 않음을 의미합니다.

 

 

만약 로깅 레벨이 warning으로 설정된 로깅을 수행하면 다음과 같은 결과를 얻을 수 있습니다.

 

 

>>> logging.warning("Logging %s", raise_when_printed)
--- Logging error ---
Traceback (most recent call last):
  File "/Users/mook/anaconda3/lib/python3.11/logging/__init__.py", line 1110, in emit
    msg = self.format(record)
          ^^^^^^^^^^^^^^^^^^^
  File "/Users/mook/anaconda3/lib/python3.11/logging/__init__.py", line 953, in format
    return fmt.format(record)
           ^^^^^^^^^^^^^^^^^^
  File "/Users/mook/anaconda3/lib/python3.11/logging/__init__.py", line 687, in format
    record.message = record.getMessage()
                     ^^^^^^^^^^^^^^^^^^^
  File "/Users/mook/anaconda3/lib/python3.11/logging/__init__.py", line 377, in getMessage
    msg = msg % self.args
          ~~~~^~~~~~~~~~~
  File "<stdin>", line 3, in __str__
Exception: I was printed
Call stack:
  File "<stdin>", line 1, in <module>
Message: 'Logging %s'
Arguments: (<__main__.RaiseWhenPrinted object at 0x10a891ed0>,)

 

 

실행 결과를 통해 예외가 발생하였으며 실제 __str__에 접근이 이뤄짐을 확인할 수 있습니다.

 

 

게으른 로깅의 원리

 

로그 레벨 함수의 정의를 다시 확인해보겠습니다.

 

 

def warning(msg, *args, **kwargs):

 

 

만약 우리가 f-string이나 format 메소드를 통해 문자열을 작성하는 경우 인터프리터는 msg 인수를 런타임에 즉시 해석하여 결과를 내놓게 됩니다.

 

 

logging.info(f"Logging {raise_when_printed}")

 

 

때문에 다음과 같은 호출은 사용여부와 관계없이 즉시 평가되어 메모리에 올라가게 될 것입니다.

 

 

여기서 대신 게으른(Lazy) 방식으로 문자열을 처리한다면

 

 

logging.info("Logging %s", raise_when_printed)

 

 

인터프리터는 해당 코드를 런타임 실행 당시에 바로 해석하지 않고 이후에 등장하는 인수가 인스턴스임을 인터프리터에가 알리기만 합니다. 때문에 해당 시점에서는 __str__ 메소드는 실행되지 않습니다.

 

 

프로그램이 실제 해당 라인을 실제로 표준 출력으로 내보야하는 경우 내부적으로 로그 수준이 어떻게 구성되어있는지를 기반으로 호출 여부가 결정됩니다. 만약 호출이 일어나지 않았다면 아무 일도 일어나지 않습니다. 호출이 발생했다면 그제서야 __str__ 메소드에 접근하여 예외가 발생할 것입니다.

 

 

퍼포먼스 테스트

 

퍼포먼스를 확인하기 위해  timeit 라이브러리를 사용합니다. 해당 라이브러리는 명령을 실행하는데 걸리는 시간을 계산하는데 활용됩니다.

 

 

>>> import timeit
>>> lazy = “logging.info(‘lazy %s’, ‘logging’)”
>>> not_lazy = “logging.info(‘not lazy {}’.format(‘logging’))”
>>> setup = “import logging”

 

 

두 개의 로깅 명령을 생성하고 추가적으로 로깅에 필요한 환경인 logging 모듈을 환경으로 구성합니다.

 

 

이를 통해 다음의 테스트를 수행하였습니다.

 

 

>>> timeit.timeit(not_lazy, setup=setup)
1.3170759610000005
>>> lazy = "logging.info('lazy %s', 'logging')"
>>> timeit.timeit(lazy, setup=setup)
1.0643428150000034

 

 

간단한 로깅 작업이지만 약 30%의 속도 차이가 발생하는 것을 확인할 수 있습니다. 그리 크게 느껴지는 단위가 아닐 수도 있지만, 구현에 비해 얻을 수 있는 이점이 상당하다는 점에 주목해볼 수 있겠습니다.

 

 

결론

 

지연 로깅 자체는 공식 문서에 기록은 되어있지만, 그와 별개로 파이썬의 일반적인 관행이라고 보기에는 모두가 동의하지 않을 수도 있는 방식입니다.

 

 

앞서 살펴보았듯 구현은 쉽고 이점은 확실한 방식이라고 할 수 있습니다. 미세한 차이이기에 속도에 민감하지 않다면 수많은 로깅 메소드 호출에 대한 리펙토링을 굳이 수행해야 할까는 의문입니다. 다만 새로 작성될 로깅에서는 지연된 로깅을 쓰는 것이 대부분의 상황에서 옳을 것으로 예상됩니다.  다른 부분에서의 Trade-Off가 있을 수도 있겠지만 어떠한 단점이 있을지에 대해서는 아직 떠오르는 바는 없으며 대부분의 상황에서 모범 사례로 동작할 것이라고 생각합니다.

'Programming Language > Python' 카테고리의 다른 글

[pytest] @pytest.fixture  (0) 2023.05.02
logging 사용법과 관련 이슈  (0) 2023.03.27
import pytest
import tensorflow as tf
from PIL import Image
import utils

@pytest.fixture
def pil_image():
    return Image.new('RGB', (32, 32), color='red')

def test_convert_pil_to_tensor(pil_image):
    tensor = utils.convert_pil_to_tensor(pil_image)

    assert tensor.shape == (32, 32, 3)
    assert tensor.dtype == tf.float32

@pytest.fixture의 역할

Dummy 테스트 환경을 구성

위의 예시에서는 PIL.Image 객체를 생성하여 test_conver_pil_to_tensor 함수에 넘겨줌

@pytest.fixture의 흐름

테스트 함수에서 parameter로 fixture 함수를 명시

이후 pytest에서 자동으로 fixture 함수를 호출하여 return을 input parameter로 넘겨줌

fixture 함수를 사용함으로써 테스트를 위해 간단한 dummy 데이터 생성 가능

'Programming Language > Python' 카테고리의 다른 글

게으른 로깅  (0) 2023.07.29
logging 사용법과 관련 이슈  (0) 2023.03.27

기본적인 사용방법

해당 글에서는 python의 logging 기본적인 사용방법을 다룬다. 관련해서 마주할 수 있는 에러 상황이나, 좀 더 다양한 상황을 다룰 수 있는 logging 사용방법을 기술해 보았다.

 

 

추가적으로 해당 글에서는 특정 메시지를 어떤 레벨로 로깅해야 하는지와 같은 철학은 다루지 않음을 감안해 주시면 좋겠다.

 

 

짧은 글이기에 바로 본론으로 들어가보자. 코드는 짧고 간결하다.

 

 

import logging

logging.basicConfig(filename='../logs/crawl.log', format='%(asctime)s %(levelname)s %(message)s')
logging.getLogger().setLevel(logging.INFO) # Optional

logging.info("This message is logged with INFO level")

logging.warn("This message is logged with WARNING level")

 

 

이 정도의 코드만으로도 대부분의 파이썬 프로그램은 로깅이 가능하다.

 

 

logging.basicConfig()

 

 

로깅을 기록하기 위한 기본적인 설정이 이뤄진다. 위의 코드에서는 파일 이름과 로깅 시 메시지에 앞서 기록될 템플릿이 명시되어 있다.

 

 

주의해야 할 점은 명시한 파일의 경우, logging이 자동으로 생성해주지 않는다는 것이다. 직접 touch나 vi를 통해 생성해야 한다.

 

 

logging.getLogger().setLevel(logging.INFO)

logging의 기본 로깅 레벨은 WARNING으로 INFO의 레벨로 설정된 메시지는 로그에 남지 않는다. 해당 코드에서는 INFO까지도 기록하기 위해 해당 라인을 추가해 보았다.

 

 

logging.info("This message is logged with INFO level")

INFO 레벨의 로깅이다.

 

 

logging.warn("This message is logged with WARN level")

WARN 레벨의 로깅이다.

 

 

다만 이렇게 했는데도, 해당 파일에 로깅이 되지 않는 문제가 생길 수 있다.

경험상 순서대로 3가지 정도를 점검해 볼 수 있다

1. 경로가 정확한지, 또 파일은 정상적으로 생성이 된 상태인지

2. 로깅 레벨이 당초 의도한 대로 설정되어 있는지 (WARN인 경우 INFO는 당연히 기록되지 않는다.)

3. 권한이 충분한지, py 파일이 로그 파일에 접근하여 쓰기 작업을 진행할 수 있는 권한이 있는지

 

 

이 정도 문법만으로도 사실 충분하지만 handler라는 친구를 통해 더욱 유연하게 상황에 맞는 로그를 작성할 수 있다.

 

logging.FileHandler

import logging

# logging 객체를 생성한다
logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)

# 디버그 레벨 핸들러를 생성한다
debug_handler = logging.FileHandler('../logging/debug.log')
debug_handler.setLevel(logging.DEBUG)
debug_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
debug_handler.setFormatter(debug_formatter)

# 에러 레벨 핸들러를 생성한다
error_handler = logging.FileHandler('error.log')
error_handler.setLevel(logging.ERROR)
error_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
error_handler.setFormatter(error_formatter)

# 생성한 logging 객체에 핸들러들을 등록한다
logger.addHandler(debug_handler)
logger.addHandler(error_handler)

# 로깅
logger.debug('This is a debug message')
logger.info('This is an info message')
logger.warning('This is a warning message')
logger.error('This is an error message')

 


Handler의 역할은 다양할 수 있지만, 앞선 코드와 같이 다양한 핸들러가 각기 다른 로깅 레벨에 맞춰 해당 메시지를 처리하는 경우에 활용할 수 있다.

 

 

debug_handler = logging.FileHandler('../logging/debug.log')

 

 

파일 핸들러를 만든다. 이때의 파라미터는 작성될 파일의 이름이다. 앞선 basicConfig와 동일하게 파일은 생성해주지 않는다.

 

 

debug_handler.setLevel(logging.DEBUG)
debug_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 
debug_handler.setFormatter(debug_formatter)

 

 

예상하셨겠지만, 디폴트 레벨 설정 부분과 로그 메시지의 앞쪽에 붙게 되는 템플릿이다.

 

 

이후 생성한 핸들러를 객체에 등록하면 설정이 완료된다.

 

 

logger.addHandler(debug_handler)

 

 

아래의 getLogger 메소드는 로깅 객체를 반환한다. 

 

 

logger = logging.getLogger('my_logger')

 

 

 

my_logger는 해당 logging object를 식별할 수 있는 일종의 키이다. 이후에 다시 my_logger를 getLogger에 넣어준다면 동일한 로깅 객체가 반환된다.

 

 

 

사실 FileHandler 말고 기능이 좀 더 풍부한 RotatingFileHandler와 TimeRoatatingFileHandler라는 클래스도 존재한다. 대단한 친구들은 아니고, FileHandler보다 좀 더 풍부한 기능들을 제공한다고 생각하면 된다. FileHandler를 상속받는 친구들이다.


FileHandler의 경우 다소 한계점들이 존재한다. 만약 로그 파일이 특정 사이즈에 도달하 경우 FileHandler는 기존 파일에 작성하던 걸 멈추고 파일을 닫는다. 결과적으로 로깅이 멈춘다.


반면 RotatingFileHandler, TimeRoatatingFileHandler에서는 FileHandler와 거의 동일한 역할을 수행하지만 몇 가지 추가적인 기능이 붙어있다.


예를 들어 RotatingFileHandler의 경우, maxBytes를 통해 최대 몇 바이트까지 쓰게 되면, 기존 파일을 닫고 새로운 파일을 열어 로깅을 이어나가도록 지정할 수 있다. (기본값은 0으로 닫지 않고 쭉 작성)


backupCount라는 옵션을 통해 백업의 최대 개수를 지정할 수도 있다. (기본값은 0)

'Programming Language > Python' 카테고리의 다른 글

게으른 로깅  (0) 2023.07.29
[pytest] @pytest.fixture  (0) 2023.05.02

grpc 소개

mydailylogs
|2022. 11. 15. 23:23

gRPC는 Google에서 개발한 RPC(Remote Procedure Call) 시스템입니다. 전송을 위해 TCP/IP 프로토콜과 HTTP 2.0 프로토콜을 사용하고 IDL(Interface Definition language)로 protocol buffer를 사용합니다.

gRPC에 대해 이해하기 위해선 다음에 대한 배경지식이 필요합니다.

RPC(Remote Communication Mechanism)

RPC(원격 프로시저 호출)는 한 프로그램이 네트워크의 세부 정보를 이해하지 않고도 네트워크 안의 다른 컴퓨터에 있는 프로그램에서 서비스를 요청하는 프로토콜입니다. RPC는 client-server 모델을 사용합니다. 클라이언트에서 서비스를 요청(function call)하면, 서버에서 서비스를 제공합니다.

 

HTTP 프로토콜

HTTP(Hypertext Transfer Protocol)는 웹에서 쓰이는 통신 프로토콜입니다. 프로토콜이란 상호간에 정의한 규칙을 의미합니다.

HTTP 프로토콜은 TCP/IP 프로토콜 위의 레이어(Application layer)에서 동작합니다. 각 프로토콜 별 layer 계층은 다음과 같이 간략히 표현됩니다.

HTTP 프로토콜은 stateless 프로토콜입니다. 여기서 상태가 없다는 의미는 데이터를 주고 받기 위한 각각의 데이터 요청이 서로 독립적으로 관리된다는 의미이며, 이전 데이터 요청과 다음 데이터 요청이 서로 관련이 없다는 말입니다.

HTTP는 기본적으로 서버-클라이언트 구조를 따릅니다. 이 구조에서, HTTP 프로토콜로 데이터를 주고받기 위해서는 아래와 같이 Request를 보내고 Response를 받아야 합니다.

URL(Uniform Resource Locators)은 서버에 자원을 요청하기 위해 입력하는 영문 주소입니다.

URL을 이용하면 서버에 특정 데이터를 요청할 수 있습니다. 여기서 요청하는 데이터에 특정 동작을 수행하고 싶으면 HTTP 요청 메소드를 이용합니다.

사용하는 HTTP 요청 메소드는 4개입니다.

  • Get: 존재하는 자원에 대한 요청
  • POST: 새로운 자원을 생성
  • PUT: 존재하는 자원에대한 변경
  • DELETE: 존재하는 자원에 대한 삭제

HTTP 1.1 버전

HTTP1.1은 1999년 출시 이후 지금까지 웹에서 가장 많이 사용되고있는 프로토콜입니다. HTTP1.1은 기본적으로 연결당 하나의 Request과 Response를 처리하기 때문에 동시전송 문제와 다수의 리소스를 처리하기에 속도 및 성능 이슈를 가지고 있습니다.

대표적인 HTTP 1.1의 단점은 다음과 같습니다.

HOLB(Head Of Line Blocking) - 특정 응답 지연

HTTP/1.1은 connection당 하나의 Request를 처리하기에 속도 문제가 있었습니다. 이를 개선할 기법으로 pipelining이 제안됬는데, 이 방식은 하나의 connection을 통해 다수개의 파일을 Request/Response 받을 수 있는 기법을 말하는데, 이 기법을 통해 성능 향상 할 수 있으나 문제점이 있습니다.

만약 앞선 이미지에서 볼 수 있듯 Response가 지연되면, 아래 그림과 같이 두, 세번째 이미지는 첫번째 이미지의 응답처리가 완료되기 전까지 대기하게 됩니다. 이와 같은 현상을 HTTP의 Head Of Line Blocking(HOLB) 이라 부르며 pipelining 기법의 문제점 중 하나입니다.

RTT(Round Trip Time) 증가

HTTP1.1은 하나의 connection에 하나의 request를 처리합니다. 이로인해 하나의 connection마다 tcp 연결을 하며, 신뢰성 연결을 하는 tcp connection은 시작시 3-way handshake, 종료시 4-way handshake가 반복적으로 발생, 이로인한 오버헤드가 발생합니다.

heavy header

HTTP1.1의 header에는 많은 metadata가 저장되어 있습니다. 사용자가 방문한 웹페이지는 다수의 HTTP Request가 발생하게 되는데 이 경우 매 Request마다 중복된 header값을 전송하며, 이 중 cookie가 큰 문제입니다.

HTTP 2.0

HTTP2.0은 HTTP1.1의 프로토콜을 계승해 동일한 API면서 성능 향상에 초점을 맞췄습니다.

Multiplexed Streams

한 connection으로 동시에 여러개 메시지를 주고 받을 수 있으며, Response는 순서에 상관없이 stream으로 주고 받습니다.

Stream Prioritization

리소스간 우선순위를 설정해 클라이언트가 먼저 필요한 리소스부터 보내줍니다.

Server Push

서버는 클라이언트의 요청에대해 요청하지 않은 리소스를 마음대로 보내줄 수 있습니다.

Header Compression

Header table과 Huffman Encoding 기법(HPACK 압축방식)을 이용해 압축을 했습니다.

 

IDL(Interface Definition Language)

서버와 클라이언트가 정보를 주고 받는 규칙이 프로토콜이라면, IDL은 정보를 저장하는 규칙입니다.

대표적인 IDL로는 다음의 3가지가 존재합니다.

XML

XML(eXtensible Markup Langauge)은 어떠한 데이터를 설명하기 위해 이름을 임의로 지은 태그로 데이터를 감싸며, 태그로 사용자가 직접 데이터 구조를 정의 할 수 있습니다.

XML은 HTML처럼 데이터를 보여주는 것이 목적이아닌, 데이터를 저장하고 전달할 목적으로 만들어 졌습니다.

HTTP + XML 조합으로 많이 사용 됩니다.

JSON

JSON(JavaScript Object Notation)은 javascript의 부상으로 많이 쓰이고 있는 데이터 구조입니다. XML이 가진 읽기 불편하고 복잡하고 느린 속도 문제를 해결했습니다. 특히나 key-value로 정의된 구조 자체가 굉장히 사람에게 직관적입니다.

HTTP + RESTful API + JSON 조합으로 많이 사용됩니다.

Protocol buffers(proto)

XML의 문제점을 개선하기 위해 제안된 IDL이며, XML보다 월등한 성능을 지닙니다.

Protocol buffers는 구조화(structured)된 데이터를 직렬화(serialization)하기 위한 프로토콜로 XML보다 작고 빠르고 간단합니다. XML 스키마처럼 .proto 파일에 protocol buffer 메세지 타입을 정의합니다.

Protocol buffers는 구조화된 데이터를 직렬화하는데 있어서 XML보다 많은 장점들을 가지고 있습니다. 대표적인 장점은 다음과 같습니다.

  • 간단하다
  • 파일 크기가 3에서 10배 정도 작다
  • 속도가 20에서 100배 정도 빠르다
  • XML보다 가독성이 좋고 명시적이다

현재 proto2와 proto3가 있으며, proto3의 사용을 권장합니다.

3개 방식 비교

다음은 XML, JSON, Proto 세가지 방식을 비교한 표입니다.

그래서 gRPC가 뭔데?

gRPC는 구글에서 만든 RPC 플랫폼이며 protocol buffer와 RPC를 사용합니다.

최신 버전의 IDL로 proto3를 사용합니다. Java, C ++, Python, Java Lite, Ruby, JavaScript, Objective-C 및 C#에서 사용 가능합니다.

SSL/TLS를 사용하여 서버를 인증하고 클라이언트와 서버간에 교환되는 모든 데이터를 암호화합니다. HTTP 2.0을 사용하여 성능이 뛰어나고 확장 가능한 API를 지원합니다.

gRPC에서 클라이언트 응용 프로그램을 서버에서 함수를 바로 호출 할 수 있어 분산 MSA(Micro Service Architecture)를 쉽게 구현 할 수 있습니다. 서버 측에서는 서버 인터페이스를 구현하고 gRPC 서버를 실행하여 클라이언트 호출을 처리합니다.

What are Protocol Buffers?

Protocol buffer는 structured data(구조화 데이터)를 seriailize(직렬화)하기위한 Google의 언어 중립적, 플랫폼 중립적이며 확장 가능한 메커니즘입니다.

XML보다 작지만, XML보다 빠르고 단순합니다.

IDL(Interface Definition Language)로서 data structure를 정의한 다음, .proto 파일을 protocol buffer compiler(protoc)를 이용해 compile 합니다. Complie된 소스 코드를 사용하여 다양한 데이터 스트림에서 다양한 언어로 다양한 구조의 데이터를 쉽게 읽고 쓸 수 있습니다.

프로토콜 버퍼는 현재 Java, Python, Objective-C 및 C ++에서 생성 된 코드를 지원합니다. 새로운 proto3 버전을 사용하면 proto2에 비해 더 많은 언어(Dart, Go, Ruby 및 C #)을 사용할 수 있습니다.

Proto3 Guide

Protocol buffers는 크게 proto2와 proto3가 있지만 grpc에선 최신 버전인 proto3를 사용하기에 proto3를 보겠습니다.

Defining 메세지 type

먼저 아주 간단한 예를 봅시다.

각 요청에 쿼리 문자열, 관심있는 특정 결과 페이지 및 페이지 당 여러 결과가있는 검색 요청 메시지 형식을 정의한다고 가정 해 보겠습니다. 메시지 유형을 정의하는 데 사용하는 .proto 파일은 다음과 같습니다.

//syntax가 없으면 자동으로 proto2로 인식됨
syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

SearchRequest 메시지 정의는이 유형의 메시지에 포함하려는 각 데이터에 대해 하나씩 세 개의 필드 (이름 / 값 쌍)를 지정합니다. 각 필드에는 이름과 유형이 있습니다.

Specifying Field Types

위의 예시에서 나온 Field types은 모두 scalar types입니다.(int32, string)

하지만 scalar type 뿐만 아니라 enum과 같은 다른 field type도 선언 가능합니다.

Assigning Field Numbers

각 field는 unique number를 갖습니다.

이 field number는 메세지 binary format에서 field를 식별하는데 사용됩니다. 즉 field number가 key가 되고 field type이 value가 되는 해시 구조를 지닙니다.

Field numbers의 값이 1~15 사이면 1 byte로 encoding 됩니다.

Field numbers의 값이 16~2047이면 2 bytes로 encoding 됩니다.

그렇기에 Field numbers의 값을 1~15 사이로 encoding 하는것이 효율적입니다.

Specifying Field Rules

메세지 fields는 다음 중 한 가지를 따릅니다.

  1. singular: well-formed된 메세지는 0 or 1개를 가질 수 있습니다.
  1. repeated: 이 field는 0을 포함해 여러번 반복될 수 있습니다.

Adding 메세지 Types

하나의 .proto 파일안에 다중 메세지 type이 정의될 수 있습니다. 이러한 방식은 다양한 관계를 표현하는데 도움 됩니다.

// comment(주석)은 C, C++ 스타일
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...}

What’s Generated From Your .proto?

.proto 파일을 protocol buffer compiler에 의해 complied되면, 메세지는 serializing되어 output stream으로 나가고 parsing되어 input stream으로 들어옵니다. 대표적인 3가지는 다음과 같습니다.

  • C ++의 경우 컴파일러는 파일에 설명 된 각 메세지 type에 대한 class와 함께 각 .proto에서 .h 및 .cc 파일을 생성합니다.
  • Java의 경우 compiler는 각 메세지 type에 대한 class와 메세지 class instance 작성을위한 builder class가있는 .java 파일을 생성합니다.
  • Python은 조금 다릅니다. Python compiler는 .proto에 각 메세지 type의 static discriptor가 있는 모듈을 생성 한 다음, meta class와 함께 런타임에 필요한 Python data access class를 작성하는 데 사용됩니다.

Encoding

Protocol buffer compiler가 encoding하는 방식을 알아봅시다.

Base 128 Varints

Varints는 1 or 그 이상의 bytes를 사용해서 integer를 serializing하는 방법입니다. 작은 숫자는 작은 bytes로 encoding 됩니다. 1 bytes 중에서 7 bits만 숫자 표현에 사용되고 맨 앞의 숫자는 msb(most significant bit)로 사용됩니다.

예시로 보자면, 숫자 1을 encoding 하면 다음과 같으며, msb는 set되지 않습니다.

0000 0001

숫자 300은 다음과 같습니다

1010 1100 0000 0010
-> 010 1100  000 0010 // msb 버림
-> 000 0010  010 1100 // 7bits의 두 그룹을 reverse함
-> 000 0010 ++ 010 1100 // 두 그룹 합침
-> 100101100
-> 256 + 32+ 8 + 4 = 300

메세지 Structure

메세지는 key-value 쌍입니다. 메세지의 binary version은 field number를 key로 사용합니다. 각 field의 이름과 선언 된 type은 메세지 type의 정의를 참조하여 decoding 끝에서만 확인할 수 있습니다.

사용 가능한 유형은 다음과 같습니다.

 

TypeMeaningUsed For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded 메세지s, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

자료형, 자료구조

Scalar Value Types

scalar 메세지 field는 다음 중 하나의 type을 가집니다.

 

.proto TypeNotesC++ TypeJava TypePython Type
double   double double float
float   float float float
int32 Uses variable-length encoding.Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. int32 int int
int64 Uses variable-length encoding.Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. int64 long int/long
uint32 Uses variable-length encoding. uint32 int int/long
uint64 Uses variable-length encoding. uint64 long int/long
sint32 Uses variable-length encoding.Signed int value. These more efficiently encode negative numbers than regular int32s. int32 int int
sint64 Uses variable-length encoding.Signed int value.These more efficiently encode negative numbers than regular int64s. int64 long int/long
fixed32 Always four bytes.More efficient than uint32 if values are often greater than 228. uint32 int int/long
fixed64 Always eight bytes.More efficient than uint64 if values are often greater than 256. uint64 long int/long
sfixed32 Always four bytes. int32 int int
sfixed64 Always eight bytes. int64 long int/long
bool   bool boolean bool
string A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. string String str/unicode
bytes May contain any arbitrary sequence of bytes no longer than 232. string ByteString str

Enumerations

메세지 type을 정의 할 때, enum type을 쓸 수 있습니다.

예를 들어, 각 SearchRequest에 대해 Corpus field를 추가하려고하는데, Corpus는 UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS, VIDEO field를 가집니다. 다음 예제에서는 가능한 모든 값과 Corpus라는 enum을 추가했습니다

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;

예시에서와 같이 모든 eunum type의 시작 defulat value는 0입니다.

만약 default value를 겹쳐서 사용하고 싶다면 option에 allow_alias를 사용하면 됩니다.

message Mymessage1 {
  enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }
}
message Mymessage2 {
  enum EnumNotAllowingAlias {
    UNKNOWN = 0;
    STARTED = 1;
    // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning 메세지 outside.
  }
}

enum 타입의 field들은 int32를 사용해야 합니다. 왜냐하면 enum value들은 varint encoding을 따르기 때문입니다.

Nested Types

메세지 type을 정의할 때, nested(중첩된)하게 선언도 됩니다.

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

Packages

optional하게 package specifier를 추가할 수 있습니다. 이를 통해 메세지 type 간에 충돌을 피할 수 있습니다.

package foo.bar;
message Open 
{ ... }

Defining Services

만약 gRPC와 함께 사용하고 싶다면, service 구문을 사용하면 됩니다. protocol buffer compiler가 service interface code와 stubs를 언어에 맞게 자동으로 생성해 줍니다.

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

JSON Mapping

Proto3는 JSON으로의 canonical encoding도 지원합니다. 이를 통해 보다 쉬운 시스템 간의 통신 구현이 가능합니다.

Proto와 JSON

Person이라는 객체를 만들고, 그 객체를 JSON 포맷으로 만든다고 해봅시다.(UTF-8 인코딩 가정시)

{
	"userName":"Martin",
	"favouriteNumber":1337,
	"interests":["daydreaming","hacking"]
}

데이터 크기를 보면 공백을 빼도 총 80byte를 사용했습니다. 그렇다면 Protocol Buffer를 사용했을 때는 다음 그림과 같습니다.

message Person {
    required string user_name        = 1;
    optional int64  favourite_number = 2;
    repeated string interests        = 3;
}

그림의 설명처럼 핵심은 userName과 같은 string을 field number로 대체한 것이 핵심 입니다. 따라서 proto를 이용해 데이터를 표현하게되면 같은 데이터여도 33byte만으로 표현할 수 있게 됩니다.

Proto의 장점

1. 통신이 빠름.

같은 데이터를 보낼시, 데이터의 크기가 작으니까 같은 시간에 더 많은 데이터를 보낼 수 있습니다.

2. 파싱을 할 필요가 없음

JSON 포맷으로 온 데이터를 다시 객체로 파싱해서 사용해야하지만, Protocol Buffer는 Byte Stream을 Proto file이 읽어 파싱할 필요 없습니다.

Proto의 단점

1. 인간이 읽기 불편함.

JSON 포맷의 경우, 사람이 읽기 편합니다. 반대로 protocol buffer가 쓴 데이터는 proto 파일이 없으면 아예 무슨 의미인지 모릅니다.이 문제 때문에 외부 API로 쓰이기에는 문제가 있습니다. (모든 클라이언트가 proto파일을 가지고 있어야 하기에)

그래서 내부 서비스간의 데이터 교환에서 주로 쓰입니다. (API Gateway를 이용해 REST API로 바꿔주는 방식도 존재)

2. proto 문법을 배워야 함

proto 파일 작성을 위한 문법을 배워야하며, proto파일을 한 번 정의했다 변경되면 proto파일을 쓰는 애플리케이션은 proto 파일이 다시 업데이트 되야한다는 단점이 있습니다.

목차
1. logrus란?
2. logrus의 특징
3. logrus 사용 예시

Logrus란?

logrus는 golang에서 사용할 수 있는 로깅 모듈 중의 하나로, docker와 Prometheus와 같은 많은 유명 오픈 소스 프로젝트가 이를 사용하여 로그를 기록하는 것으로 알려졌다.

 

Logrus의 특징

logrus는 다음과 같은 특징이 있다.

  • golang 표준 라이브러리의 로깅 모듈과 완벽하게 호환된다. logrus에는 6가지 로그 레벨이 존재하며, 이는 golang 표준 라이브러리의 로그 모듈 API의 상위 집합이다. 프로젝트에서 표준 라이브러리의 log 모듈을 사용하는 경우 저렴한 비용으로 logrus로 마이그레이션할 수 있다.
  • 확장 가능한 hook 매커니즘: 사용자가 hook을 통해 로컬 파일 시스템, 표준 출력, logstash, elastic search 또는 MQ와 같은 위치에 로그를 배포하거나 hook을 통해 로그 내용 및 형식을 정의할 수 있다. 
  • 선택적 로그 출력 형식: logrus에는 jsonformatter 및 textformatter의 두 가지 기본 제공 로그 형식이 있다. 이 두 형식으로도 충분하지 않은 경우 인터페이스 포맷터를 직접 구현하여 고유한 로그 형식을 정의할 수도 있다.
  • 필드 매커니즘: logrus는 긴 메세지를 통한 로깅보다 필드 매커니즘을 통해 상세하고 구조화된 로깅을 지원하고 권장한다.

 

Logrus 사용 예시

먼저, logrus를 사용하기 위해 모듈을 가져온다.

$ go get -u github.com/sirupsen/logrus

 

#1 Logrus를 사용하는 간단한 예

package main

import log "github.com/sirupsen/logrus"

func main() {
	log.WithFields(log.Fields{
		"animal": "tiger",
		"habitat": "mountain",
	}).Info("A tiger appears")
}

결과 

logrus를 사용하는 가장 간단한 예시이다. 이처럼 필드를 통해 로그의 내용을 표현하고, 로그를 7단계로 구분한다.

기존 6단계에서 trace가 추가되어 7단계로 변경되었다.

 

logrus 로그의 7단계

log.Trace("Something very low level.")                  // 1단계: Trace
log.Debug("Useful debugging information.")              // 2단계: Debug
log.Info("Something noteworthy happened!")              // 3단계: Info
log.Warn("You should probably take a look at this.")	// 4단계: Warn
log.Error("Something failed but I'm not quitting.")     // 5단계: Error
log.Fatal("Bye.")                                       // 6단계: Fatal, 로깅 이후 os.Exit(1) 호출
log.Panic("I'm bailing.")                               // 7단계: Panic, 로깅 이후 Panic() 호출

위의 코드 블록에서 다소 오해가 발생할 여지가 있는 부분이 있다. 구분을 위해, Trace를 1 단계라고 명시했지만, 낮다를 표현하고자 하였을 뿐 정해진 바는 없다. 즉, Panic이 1 단계일 수도 있다. Trace에서 Panic으로 7단계 구분이 이뤄졌다는 점만 기억하면 되겠다.

 

#2 다양한 옵션 설정도 가능하다.

대표적으로 로그 형식과 로그 출력 방식을 지정할 수 있다. 또 로그 레벨의 하한을 결정하여, 그 이하의 로그는 출력하지 않도록 할 수도 있다. 그 외에도 여기에는 등장하지 않았지만, 로그와 관련된 훅 같은 옵션도 적용할 수 있으나, 주제가 무거우니 향후 가능하다면 별도의 포스팅으로 분리하는 것으로 하자.

package main

import (
	log "github.com/sirupsen/logrus"
	"os"
)

func init() {
	// 로그 형식을 JSON 형식으로 지정
	log.SetFormatter(&log.JSONFormatter{})

	// log 출력을 표준 출력으로 설정 (기본 설정은 stderr, 표준 에러)
	log.SetOutput(os.Stdout)

	// 로그 레벨을 warn 단계 위로 설정
	log.SetLevel(log.WarnLevel)
}

func logrusOne() {
	log.WithFields(log.Fields{
		"animal": "tiger",
		"distance": 100,
	}).Info("A tiger appears")

	log.WithFields(log.Fields{
		"animal": "tiger",
		"distance": 10,
	}).Warn("A tiger coming to you")

	log.WithFields(log.Fields{
		"animal": "tiger",
		"distance": 0,
	}).Fatal("A tiger wants to play with you. Good Luck.")
}

func main() {
	logrusOne()
}

결과 

 

# 3 logrus는 log를 기록할 인스턴스를 정의하여, 각 인스턴스에 대해서 다른 옵션을 적용하는 것도 가능하다.

앞서 logrus에서 log에 대한 옵션 설정이 어떻게 이뤄지는지를 보였다. 여기에 더해 logrus의 logrus.Entry 인스턴스를 지정하여, 각각의 인스턴스마다 다른 옵션을 적용할 수도 있다.

package main

import (
	log "github.com/sirupsen/logrus"
	"os"
)

// Logrus 모듈에서는 new() 함수를 통해 logrus.Entry 인스턴스를 만들 수 있다.
// 프로젝트 내에서 원하는 수만큼 logrus.Entry인스턴스를 만들 수 있다.
var (
	logInstanceOne = log.New()
	logInstanceTwo = log.New()
)

func init() {
	// 생성한 logrus 인스턴스의 출력이 어떻게 이뤄질지를 결정한다.
	// logrus 인스턴스 출력은 io.writer라면 모두 가능하다.
	logInstanceOne.Out = os.Stdout
	logInstanceTwo.Out = os.Stderr

	// 생성한 logrus 인스턴스의 출력 형식이 어떻게 이뤄질지를 결정한다.
	// 이와 비슷하게, logrus 인스턴스 별로 "로그 레벨" 또는 "훅"을 설정할 수 있다.
	logInstanceOne.Formatter = &log.JSONFormatter{}
	logInstanceTwo.Formatter = &log.TextFormatter{}
}

func main() {
	logInstanceOne.WithFields(log.Fields{
		"animal": "tiger",
		"habitat": "mountain",
	}).Info("A tiger appears")

	logInstanceTwo.WithFields(log.Fields{
		"animal": "tiger",
		"habitat": "mountain",
	}).Info("A tiger appears")
}

 

결과

 

#4 logrus의 필드

logrus에서는 로깅을 위한 장황한 메세지를 사용하는 것을 권장하지 않는다. 대신 Fields 형식을 통한 상세하고 구조화된 로깅을 권장한다.

 

기존 표준 log 라이브러리를 사용했을 경우의 로깅

log.Fatalf("Failed to send event %s to topic %s with key %d", event, topic, key)

 

logrus 모듈에서 권장하는 필드를 활용한 로깅

log.WithFields(log.Fields{
 "event": event,
 "topic": topic,
 "key": key,
}).Fatal("Failed to send event")

WithFields 메서드를 사용하면 좀 더 상세한 정보와 함께 원하는 방식으로 로깅할 수 있겠으나, WithFields 메서드는 어디까지나 필수는 아니다. 몇몇 시나리오에서는 필드가 필요하지 않을 수 있고, 기존 log 모듈에서도 가능하듯 간단한 메시징만 필요할 수도 있다.

 

애플리케이션 내부에서는 종종 고정된 필드와 함께 로깅을 해야하는 상황이 있을 수 있다. 예를 들어 HTTP 요청을 처리할 경우, 모든 로그는 request_id user_ip가 존재한다.

이 경우, 매 번 log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})를 붙이는 것을 피하기 위해, logrus.Entry 인스턴스를 생성하고, 해당 인스턴스에 Field를 디폴트로 설정할 수 있다. 

 

requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
requestLogger.Info("something happened on that request")
requestLogger.Warn("something not great happened")

 

'Programming Language > Go' 카테고리의 다른 글

grpc 소개  (0) 2022.11.15
protocol 버퍼에 대한 소개  (0) 2022.11.15
[go 모듈 소개 - 1. gorm] Golang을 위한 ORM 라이브러리  (0) 2022.11.15

ORM(Object Relation Mapping)이란?

  • 객체와 RMDB의 데이터를 자동으로 매핑해주는 것을 말한다.

    • 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용한다. (golang은 struct로 클래스를 대신한다)

    • 객체 모델과 관계형 모델 간의 불일치가 존재한다.

    • ORM을 통해 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 불일치를 해결한다.

  • 매핑된 정보를 바탕으로 자동으로 SQL을 작성해준다.

    • Object를 통해 간접적으로 데이터베이스를 다룰 수 있다.

ORM의 장단점

장점

  • 객체 지향적인 코드이기에 비즈니스 로직에 더 집중할 수 있다.

    • ORM을 사용하면 SQL Query가 아닌 작성 중인 언어로 DB에 접근하여 데이터를 접근할 수 있다.

    • 각종 객체에 대한 코드를 별도로 작성하기 때문에 코드의 가독성이 올라간다.

    • SQL의 절차적이고 순차적인 접근이 아닌 객체 지향적인 접근으로 인해 생산성이 증가한다.

  • 재사용 및 유지보수의 편리성이 증가한다.

    • ORM은 독립적으로 작성되어 있고, 해당 객체들을 재활용할 수 있다.

    • MVC 모델을 사용할 경우 Model에서 가공된 데이터를 Controller에 의해 뷰와 합쳐지는 형태로 디자인 패턴을 견고하게 다지는데 유리하다.

    • 매핑 정보가 명확하여, ERD를 보는 것에 대한 의존도를 낮출 수 있다.

  • DBMS에 대한 종속성이 줄어든다.

    • 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하기 때문에 RDBMS의 데이터 구조와 코드의 객체지향 모델 사이의 간격을 좁힐 수 있다.

    • 대부분의 ORM 솔루션은 DB에 종속적이지 않다.

    • Object에 집중함으로써 극단적으로는 DBMS를 교체하는 거대한 작업에도 비교적 적은 리스크와 시간이 소요된다.

단점

  • 완벽한 ORM으로만 서비스를 구현하기가 어렵다.
     
    • 사용하기는 편하지만 설계는 매우 신중하게 해야한다.

    • 프로젝트의 복잡성이 커질 경우 난이도 또한 올라갈 수 있다.

    • 잘못 구현된 경우 속도 저하 및 심각할 경우 일관성이 무너지는 문제점이 생길 수 있다.

    • 일부 자주 사용되는 대형 쿼리는 속도를 위해 SP를 쓰는 등의 별도의 튜닝이 필요한 경우가 있다.

    • DBMS의 고유 기능을 사용하기 다소 어렵다. (-> 특정 DBMS의 고유 기능을 이용한다면, 이식성이 저하된다는 문제점이 있다.)


  • 프로시저가 많은 시스템이라면 ORM의 객체 지향적인 장점을 활용하기 어렵다.



gORM이란?

gORM은 golang에서 사용 가능한 ORM 라이브러리 이다.



DB 환경 세팅

1) root 계정으로 mysql 접속

$sudo su - 		# su를 통해 관리자 권한 가져옴
$mysql -u root 		# mysql에 root 사용자로 접속 (비밀번호를 설정해뒀다면, mysql -u roor -p)

 

2) 사용자 생성 및 권한 후여

# 유저 생성, CREATE USER '{username}'@'{hostname}' IDENTIFIED BY '{password}'
CREATE user 'tester'@'localhost' IDENTIFIED BY 'password123'

# 모든 권한을 해당 유저, 호스트 조합에 허용
GRANT ALL PRIVILLEGES ON *.* TO 'tester'@'localhost';

# grant 테이블을 reload하여 변경 사항을 즉시 반영하도록 함
FLUSH PRIVILLEGES


3) 생성 확인

SELECT user, host FROM user;


결과

+------------------+-----------+
| USER             | HOST      |
+------------------+-----------+
| mysql.infoschema | localhost |
| mysql.session    | localhost |
| mysql.sys        | localhost |
| root             | localhost |
| tester           | localhost |
+------------------+-----------+

 

관련 패키지 설치


1) gORM 설치

go의 프로젝트 경로에서 다음 명령어를 작성한다.

$ go get -u gorm.io/gorm


해당 명령어를 수행하면, 현재 환경 기준으로 GOPATH/pkg/mod/gorm 에 해당 패키지가 설치되고 프로젝트 경로에서는 go.mod와 go sum에서 이를 관리하게 된다.

 

-u 명령어의 경우, 패키지 및 관련된 종속성을 업데이트하는 명령어로 go get과 세트로 자주 쓰이는 옵션이다.

 

2) gORM 용 MySQL 드라이버 설치

데이터 베이스 별로 해당하는 드라이버가 필요하다.

$ go get -u github.com/go-sql-driver/mysql

 

3. 쿼리 로그 설정

  • 작성된 쿼리를 추적하기 위해, sql로 수행되는 쿼리를 파일에 로그로 담도록 지정

  • gorm을 사용할 경우, 실제 sql이 어떻게 수행되고 있는지가 궁금할 수 있다.

mysql>  SET GLOBAL general_log = 1;
Query OK, 0 rows affected (0.03 sec)

mysql> SELECT @@log_output, @@general_log, @@general_log_file;
+--------------+---------------+-----------------------------------------------------+
| @@log_output | @@general_log | @@general_log_file                                  |
+--------------+---------------+-----------------------------------------------------+
| FILE         |             1 | /usr/local/var/mysql/youngmki-MacBookPro.log 	     |
+--------------+---------------+-----------------------------------------------------+


로그를 사용하도록 설정하고, 설정 여부를 확인했다.

 

다음 명령어를 통해 실제 로그 파일이 존재하는지를 확인할 수 있다.

$cd  /usr/local/var/mysql && find "youngmki-MacBookPro.log"

 

DB 연결 예제

테스트를 위해 다음과 같이 main.go를 작성했다.

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func main() {
	// "{username}:{password}@tcp(127.0.0.1)
	dsn := "tester:password123@tcp(127.0.1:3306)/"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	if err != nil {
		panic("could not connect to the database")
	}
	
	_ = db	// 일단 컴파일이 되도록 설정
}

 

간단한 웹서버 예제

먼저 fiber라는 웹서버 라이브러리를 사용할 예정이므로, 다음 명령어를 통해 모듈을 설치한다.

$ go get -u 	"github.com/gofiber/fiber/v2"


그 다음으로는 mysql 내에 테이블을 생성하고자 한다.

 

gorm에서는 AutoMigrate라는 메서드를 통해, struct를 테이블로 옮길 수 있도록 지원한다.

 

그러기 위해서 먼저 다음을 models/user.go 에 작성했다.

package models

type User struct {
   Id       uint
   Name     string
   Email    string
   Password []byte
}


그 다음에 이를 database/connection.go 라는 파일에서 DB 연결과 함께 다뤄줬다.

package database

import (
	"github.com/fatih/color"
	"github.com/mukmookk/simple-rest/models"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func Connect() {
	// "{username}:{password}@tcp(127.0.0.1)
	dsn := "tester:password123@tcp(127.0.1:3306)/"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	if err != nil {
		panic("could not connect to the database")
	}

	DB = connection
	color.Green("Connection Opened to Database... Success!")

	connection.AutoMigrate(&models.User{})
	color.Green("Database Migrated... Success!")
}


여기서 color라는 모듈을 추가로 사용했는데, 터미널 출력에 다음처럼 색을 깔끔하게 입혀준다.

"github.com/fatih/color"


routes/routes.go 라는 파일을 생성하고, REST-API 메소드, url, handler를 정의해주었다.

package routes

import (
	"github.com/gofiber/fiber/v2"
	"github.com/mukmookk/simple-rest/models"
)

func Setup(app *fiber.App) {
	app.Post("/api/register", controllers.Register)
}

 

여기까지 작성했다면, 이젠 실제 핸들러를 구현해야 한다.

 

controller/controller.go를 생성하고 핸들러를 구현한다.

 

func Register(c *fiber.Ctx) error {
	var data map[string]string

	err := c.BodyParser(&data)
	if err != nil {
		return err
	}

	password, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 14)
	user := models.User{
		Name:     data["name"],
		Email:    data["email"],
		Password: password,
	}
	database.DB.Create(&user)
	return c.JSON(user)
}


먼저 map을 정의하여 들어온 JSON을 담아준다.


-> c.BodyParser(&data)

 

비밀번호의 경우, 그냥 넣어지지가 않는다. 때문에 한번 해쉬 변환을 거쳐준다.

-> bcrypt.GenerateFromPassword([]byte(data["password"]), 14)

 

data에 담아진 정보를 model/user.go에 정의한 struct 형식으로 옮겨주고,

 

실제 ROW를 생성한다.

-> database.DB.Create(&user)

 

package main

import (
	"github.com/gofiber/fiber/v2"
	"github.com/mukmookk/simple-rest/database"
	"github.com/mukmookk/simple-rest/routes"
)

func main() {
	app := fiber.New()

	database.Connect()
	routes.Setup(app)
	app.Listen(":3000")
}

 

서버를 실행시키고

$ go run main.go

 

POSTMAN으로 동작 여부를 다음과 같이 확인할 수 있다.

json으로 POST를 했을 때의 결과


id의 경우는 autoincrement이기에 여러번 실행하였더니 5가 되었다. 

 

앞서 설정한 로그 파일을 출력해보면 다음과 같은 쿼리가 실행되었음을 알 수 있다.

$ cat  /usr/local/var/mysql/gim-yeongmug-ui-MacBookPro.log
/usr/local/Cellar/mysql/8.0.27_1/bin/mysqld, Version: 8.0.27 (Homebrew). started with:
Tcp port: 3306  Unix socket: /tmp/mysql.sock
Time                 		   Id Command	Argument
2022-01-27T15:25:46.193725Z	   62 Query	SELECT VERSION()
2022-01-27T15:25:46.200413Z	   62 Query	SELECT DATABASE()
2022-01-27T15:25:46.236453Z	   62 Prepare	SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE ? ORDER BY SCHEMA_NAME=? DESC limit 1
2022-01-27T15:25:46.238041Z	   62 Execute	SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'testdb%' ORDER BY SCHEMA_NAME='testdb' DESC limit 1
2022-01-27T15:25:46.246686Z	   62 Close	stmt	
2022-01-27T15:25:46.262921Z	   62 Prepare	SELECT count(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ? AND table_type = ?
2022-01-27T15:25:46.262984Z	   62 Execute	SELECT count(*) FROM information_schema.tables WHERE table_schema = 'testdb' AND table_name = 'users' AND table_type = 'BASE TABLE'
2022-01-27T15:25:46.276699Z	   62 Close	stmt	
2022-01-27T15:25:46.276761Z	   62 Query	SELECT DATABASE()
2022-01-27T15:25:46.278598Z	   62 Prepare	SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE ? ORDER BY SCHEMA_NAME=? DESC limit 1
2022-01-27T15:25:46.278646Z	   62 Execute	SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'testdb%' ORDER BY SCHEMA_NAME='testdb' DESC limit 1
2022-01-27T15:25:46.279462Z	   62 Close	stmt	
2022-01-27T15:25:46.284871Z	   62 Prepare	SELECT column_name, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_scale , datetime_precision FROM information_schema.columns WHERE table_schema = ? AND table_name = ? ORDER BY ORDINAL_POSITION
2022-01-27T15:25:46.284923Z	   62 Execute	SELECT column_name, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_scale , datetime_precision FROM information_schema.columns WHERE table_schema = 'testdb' AND table_name = 'users' ORDER BY ORDINAL_POSITION
2022-01-27T15:25:46.292756Z	   62 Close	stmt	
2022-01-27T15:26:21.872960Z	   62 Query	START TRANSACTION
2022-01-27T15:26:21.893559Z	   62 Prepare	INSERT INTO `users` (`name`,`email`,`password`) VALUES (?,?,?)
2022-01-27T15:26:21.893656Z	   62 Execute	INSERT INTO `users` (`name`,`email`,`password`) VALUES ('name','author','$2a$14$DuauYNQjhtLiYn1Pa7NGyupKiG2YA.lCp3Tjh84BPjAD/jADnzy8i')
2022-01-27T15:26:21.916066Z	   62 Close	stmt	
2022-01-27T15:26:21.916193Z	   62 Query	COMMIT


이처럼 복잡한 SQL 쿼리문을 코드 단계에서 비교적 편하게 관리할 수 있다는 것이 직관적으로 느껴지는 ORM의 가장 큰 장점이다. 

'Programming Language > Go' 카테고리의 다른 글

grpc 소개  (0) 2022.11.15
protocol 버퍼에 대한 소개  (0) 2022.11.15
[go 모듈 소개 - 2. logrus] 더 자세한 로깅 모듈  (0) 2022.11.15