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

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