728x90

적응하기 힘든 속도로 소프트웨어 아키텍처는 상당한 변화를 겪어왔고 현재 그 속도는 더욱 가파르게 진화하고 있습니다.

 

개인적인 생각으로 큰 변화로 다가왔고 앞으로 많은 영향을 끼친 아키텍처는 서버리스 였다고 생각합니다.

그렇다면 서버리스란 무엇이고 무슨 이점때문에 사용하는것일까요?


서버리스 컴퓨팅 이란 무엇인가요?

 

 

서버가 있지만 서버가 없는것처럼 개발하고 운영한다.

 

처음 서버리스에 대해 접했을 때, 이전 회사의 CTO님이 말씀해주셨고 지금와서보면 가장 적합한 설명이라 생각합니다.

 

말 그대로 특정 요구사항에 대해서 해결을 위한 개발만 하고 그 외의 다른 부분은 크게 신경 쓰지 않아도 관리가 되는 아키텍처입니다.

즉, 개발자는 서버에 대한 인프라를 관리할 필요가 없으며, 필요한 코드만 작성하고 해당 코드만 배포합니다.

 


무엇이 다른가요?

이전 회사에서 모놀리식 기반의 서버를 운영했고, 마이크로서비스를 점차 도입하다 나왔으며 현재 회사에서는 서버리스를 운영하고 있는 찍먹 개발자의 지극히 개인적인 입장에서 각 아키텍처의 장단점에 대해 써보겠습니다. 

 

모놀리식

장점

  1. 간단한 구조: 하나의 서버 운영으로 모든 기능을 구현할 수 있습니다.
  2. 개발 및 유지보수의 용이성: 하나의 서버로 개발 및 유지보수가 간단합니다.

단점

  1. 확장성의 한계: 애플리케이션의 크기가 커지면 확장이 어려워질 수 있습니다.
  2. 장애의 전파: 하나의 컴포넌트의 장애가 전체 시스템에 영향을 미칠 수 있습니다.
  3. 기술적 부채: 시간이 지남에 따라 기술적 부채가 쌓일 수 있습니다.

마이크로서비스

장점

  1. 모듈화: 각 기능을 독립적으로 개발하고 배포할 수 있습니다.
  2. 확장성: 필요한 서비스만 확장할 수 있습니다.
  3. 기술적 다양성: 각 마이크로서비스는 독립적으로 기술 스택을 선택할 수 있습니다.

단점

  1. 운영의 복잡성: 여러 개의 서비스를 관리해야 하므로 운영이 복잡해질 수 있습니다.
  2. 데이터 일관성: 여러 서비스가 동작하기 때문에 데이터 일관성을 유지하기 어려울 수 있습니다.
  3. 테스트의 어려움: 여러 서비스 간의 테스트가 복잡해질 수 있습니다.

서버리스

장점

  1. 비용 효율성: 사용한 만큼만 과금되므로 비용을 효율적으로 관리할 수 있습니다.
  2. 확장성: 자동으로 확장되므로 트래픽 증가에도 대응할 수 있습니다.
  3. 유지보수의 용이성: 서버 관리와 업그레이드 등에 대한 걱정이 없어져 개발자는 코드에 집중할 수 있습니다.
  4. 빠른 배포: 코드를 더 빠르게 개발하고 배포할 수 있습니다.
  5. 자동화된 관리: 인프라 관리가 자동화되어 개발자는 인프라에 대해 신경 쓸 필요가 없습니다.

단점

  1. 제한된 실행 환경: 서버리스 환경에서는 실행 환경이 제한되어 일부 라이브러리나 환경 변수를 사용하기 어려울 수 있습니다.
  2. 모니터링 및 디버깅의 어려움: 코드 실행과 관련된 모니터링 및 디버깅이 어려울 수 있습니다.
  3. 플랫폼 종속성: 특정 클라우드 사의 플랫폼에 종속성을 띈다.
  4. 콜드스타트: 하나의 세션이 종료 된 후 일정 시간이 지난 후, 함수 실행시 큰 지연시간이 발생한다.

서버리스, 모놀리식 아키텍처, 마이크로서비스 아키텍처는 각각 장단점이 있으며, 사용하는 상황에 따라 적절한 아키텍처를 선택해야 합니다. 서버리스는 비용 효율성과 개발 생산성을 높일 수 있지만, 실행 환경의 제약과 모니터링의 어려움을 고려해야 합니다.

 

다만, 서버리스의 경우 이전글에서 설명한 aws cdk와 같은 iac의 등장과 발전으로 인해 1,2,3번의 단점은 충분히 해결 되어 가고 있다고 생각이 듭니다. 하지만 콜드스타트의 경우, 요청이 들어왔을 때만 실행 시키는 서버리스의 컨셉상 가장 지금까지 해결되지 않고 있는 문제점 입니다.


콜드스타트의 해결방법은 없는건가요?

물론 각 클라우드사에서도 해결방법을 제시하고 몇가지 대표적인 방안이 있습니다.

aws의 람다를 기준으로 말씀드리면

1. lambda snapstart

출처: aws 블로그

함수의 실행 환경을 실행 시마다 빌드 및 함수 실행의 과정을 거치는것이 런타임 실행전 필요한 실행 환경 시점을 snapshot 형태로 caching 하여 런타임을 실행하는것을 뜻합니다.

 

다만, 이 컨셉상 java만을 지원하고 있고 당연하게도 python, node의 경우 지원하지 않고 만약 지원한다 하더라도 해당 언어의 특성상 큰 성능 개선은 되지 않을것입니다.

 

2. warmup

무한열차

aws lambda의 경우, cold start가 발생하는 시점은 람다 함수를 실행하고 이후 요청이 들어오지 않고 5분이 지난 시점입니다.

즉, cold start를 발생 시키지 않는 방식은 5분이 지나기전에 최소 1회씩 계속해서 함수를 실행 시키는 방식입니다.

 

물론, 원하는 문제는 해결되겠지만 서버리스의 사용 이유중 큰 장점인 비용 효율성 측면에서 결국 이점을 챙기지 못하는 문제가 발생합니다.

 

3. scale up

단순합니다. cpu와 ram 사용량을 늘리면 어느정도 개선이 됩니다.

당연하게도 불필요한 과금은 지속되고 기본적인 cold start에 대한 부분의 직접적인 해결책은 아니라고 생각이 듭니다.

 

그래서 해결책이 무엇이냐고요?

 

개인적인 의견으로는 현재로써는 답은 없다고 생각됩니다. 여러 방식을 제안하고는 있지만 이러한 노력에도 불구하고 큰 성능 개선 사항은 없고 cold start로 인한 문제가 지속해서 발생한다면 당장은 다른 아키텍처를 고려해보는것이 정답이라고 생각됩니다.

 

그저 서버리스의 특성상 각자 사용하는 클라우드 플랫폼에서의 개선을 기다리는게 답이라는 생각이 듭니다.


결론

서버리스는 현재 가파르게 성장하고 있으며, 앞으로 더 많은 기업들이 서버리스를 채택할 것으로 예상됩니다. 

시장 동향을 보면 현재 AWS, Azure, Google Cloud를 중심으로 한 경쟁이 치열하게 벌어지고 있으며, 이를 통해 서버리스 서비스의 품질과 다양성이 증가할 것으로 기대됩니다. 앞서 말한 큰 문제점인 cold start또한 각 기업들이 문제 파악을 하고 있고 지속해서 성능 개선에 대한 노력을 기울이고 있기 때문에 조만간 개선 될것이라 생각듭니다.

 

새롭게 마이그레이션 하는 서비스라면 한번 서버리스 아키텍처 도입을 고려해보는것 아주 좋은 선택이라고 생각됩니다.

찐 결론) 서버리스에 대한 빌드업은 끝났다!! 다음글부터 cdk 폭탄글 우수수 떨어집니다!

728x90

'기술 > Devops' 카테고리의 다른 글

"aws cdk" 누구냐, 넌?  (1) 2024.04.27
aws(2)VPC(Virtual Private Cloud)  (0) 2022.04.19
aws(1)S3(Simple Storage Services)  (0) 2022.04.19
도커(4)-이미지  (0) 2022.01.19
도커(3)-도커 컴포즈  (0) 2022.01.17
728x90

갑자기 회사에서 외계어가 나타나기 시작했습니다.

협력사: "csv로 전달드리면 될까요?"

사수님: "아무래도 데이터 양이 너무 많아서 파케이로 전달해주시는게 좋을 것 같아요. 코린이님, 저희 s3에서 csv로 저장되어 있는게 있나요? 이게 athena에서 바로 읽어 올 수 있었나요?"

코린이: "...? 확인을 좀,,,"

 

저 같은 분이 많을꺼라고 생각듭니다.

대부분 회사에서 업무를 처리할 때, 기존의 전체적인 구조는 어느정도 유지한 채 업무를 진행하게 될텐데 가끔 문제가 발생하는 부분이 세부적으로 데이터 저장형식과 같은 세부적인 부분들이라고 생각듭니다.

 

이참에 알아가보시죠!

 


무엇인가?

파일 형식중 하나입니다.

출처: 메가존(https://www.megazone.com/criblstream_230313/)

파일 형식은 크게 행기반 저장방식, 열(컬럼)기반 저장방식으로 나누어져 있습니다.

 

행기반 파일 형식의 경우, 데이터를 행 단위로 저장하는 방식입니다.

대부분의 사람들에게 익숙한 저장방식으로 대표적으로 CSV와 같은 텍스트 파일 형식이 행 기반 파일 형식의 대표적인 예시 입니다.

 

열기반 파일 형식의 경우, 데이터를 열 단위로 저장하는 방식입니다.

지금 알아 보는 Parquet가 가장 대표적인 열 단위 파일 형식의 예시입니다.

Parquet은 대용량 데이터 처리 시스템에서 널리 사용되며, Hadoop, Spark, Presto 등의 다양한 분산 컴퓨팅 프레임워크에서 지원됩니다. 이러한 장점들로 인해 Parquet은 데이터 과학 및 데이터 엔지니어링 분야에서 매우 인기 있는 파일 형식 중 하나로 자리매김하고 있습니다.

Parquet 내부 구조

Parquet 파일은 여러개의 행 그룹(Row Group)으로 구성되어 있습니다. 각 행 그룹은 여러 행을 포함하며, 이러한 행 그룹은 열 단위로 압축되어 저장됩니다. 이러한 구조는 Parquet 파일이 열 기반 형식이기 때문에 가능합니다. 각 행 그룹은 아래와 같은 요소로 구성되어 있습니다.

  1. Row Group Header: 각 행 그룹은 헤더를 가지고 있습니다. 헤더에는 행 그룹의 메타데이터가 포함되어 있습니다. 메타데이터에는 행 그룹에 포함된 행의 수, 각 열의 통계 정보 등이 있습니다.
  2. Column Chunks: 열 기반으로 데이터가 저장되기 때문에, 각 열은 자체적으로 압축되어 저장됩니다. 이러한 압축된 열 데이터를 Chunk라고 합니다. 각 열은 하나 이상의 Chunk로 나뉘어질 수 있습니다.
  3. Page Header: Chunk 안에는 Page가 포함되어 있습니다. Page는 실제 데이터를 포함하고 있습니다. Page Header에는 해당 Page의 메타데이터가 포함되어 있습니다. 이 메타데이터에는 Page의 유형 (Data Page, Dictionary Page 등)과 압축된 데이터의 크기 등이 있습니다.
  4. Data Pages: 데이터가 실제로 저장되는 부분입니다. 데이터 페이지에는 실제 데이터 값이 압축되어 저장됩니다.

Parquet 파일은 이러한 구조를 반복하여 여러 개의 행 그룹을 포함할 수 있습니다. 각 행 그룹은 독립적으로 저장되므로, 쿼리 시 필요한 행 그룹만 읽어와 처리할 수 있습니다. 이는 Parquet 파일이 대용량 데이터셋을 효율적으로 처리하는 데 도움이 됩니다.

 

왜 사용 하는가?

수많은 소프트웨어 도구가 구분되어 있고 파일 형식또한 구분되어 있는데는 이유가 있습니다.

 

따라서, 분야또한 데이터 분석에 대한 니즈가 없는 일반적인 기업의 경우, 열 지향 파일 형식을 몰라도 당장의 문제가 없습니다.

다만, 수많은 데이터 속 분석을 통해 가치를 찾아 내기 위한 니즈가 있는 기업이라면 열 지향 파일 형식의 필요성에 대해 알아볼 필요가 있습니다.

 

Parquet의 장점은 다음과 같습니다:

  1. 압축 효율성: Parquet은 열 지향 형식을 사용하므로 같은 값이 많은 열은 압축이 잘되어 더 작은 파일 크기를 갖게 됩니다. 이는 디스크 공간을 절약하고 데이터 전송 속도를 향상시킵니다.
  2. 쿼리 성능: Parquet 파일은 파일 내의 메타데이터를 사용하여 쿼리를 빠르게 처리할 수 있습니다. 또한, 열 지향 형식이므로 필요한 열만 읽어들이므로 전체 데이터셋을 읽어야 하는 비용이 줄어듭니다.
  3. 스키마 호환성: Parquet 파일은 스키마를 내장하고 있어, 스키마 변경이 필요한 경우에도 쉽게 처리할 수 있습니다.
  4. 분할 및 병렬 처리: Parquet 파일은 여러 개의 블록으로 나뉘어져 있어, 여러 컴퓨터에서 동시에 처리할 수 있습니다. 이는 대규모 데이터셋을 효율적으로 처리하는 데 도움이 됩니다.

반면, Parquet의 단점은 다음과 같습니다:

  1. 쓰기 속도: Parquet 파일은 압축과 메타데이터 작업으로 인해 쓰기 속도가 느릴 수 있습니다. 따라서 데이터를 빠르게 기록해야 하는 경우에는 다른 형식을 고려해야 할 수 있습니다.
  2. 최적화된 쿼리를 위한 사전 계획 필요: Parquet 파일을 효율적으로 쿼리하려면 데이터를 저장할 때 쿼리 패턴을 고려하여 저장해야 합니다. 이를 위해 데이터의 구조와 쿼리 패턴을 미리 파악해야 하는데, 이는 추가적인 작업이 필요할 수 있습니다.

 

상황에 맞게 사용 해봅시다!

너무 뻔한 내용이였나요?

고려하기 너무 어렵다면 아래의 몇가지 상황에 따라서 파일 형식을 고려해 사용합시다!

 

열기반 (Parquet)

- 대용량 데이터 분석

- 데이터 웨어하우스 운영

- 머신러닝 모델 훈련

- 데이터 레이크 구축

- 클라우드 기반 분석

 

행기반 (Json, CSV)

- 스트리밍 데이터 처리

- 간단한 데이터 공유 및 협업

- 데이터의 추가가 빈번한 로그데이터

- 작은 파일 다수 처리

 


후기

parquet 파일 형식에 대해서 그냥 대용량 처리에서는 기존에 자주사용하던 저장방식보다는 자주 사용되는구나 정도까지만 알고 넘어갔습니다. 실무를 진행하며 이런 디테일 한점을 알고 모르냐에 따라 결국 문제를 해결하는 방식이 달라지고 이방식은 결국 자원의 선택과 비용절감에 크나큰 영향을 끼친다고 생각합니다.

 

학부생 때, 파일 처리에 대해 중요성을 강조하시던 교수님을 뒤로 하고 그저 술만 마시던 저를 탓하던 하루였습니다...

728x90
728x90

"cdk 사용경험이 있으신가요?" 

최근 데이터 엔지니어 직무로 전환 이직을 하며 들었던 가장 난감했던 면접 질문이였다.

 

"그게 뭐죠?"

"그럼 Iac가 뭔지 아시나요?"

"인프라 애즈 코드,,,? 몰라요 죄송합니다."

"..."

이랬는데 합격을 했고 현재 불티나게 cdk를 만지고 있는 잡부 엔지니어 입니다.

이글을 읽는 여러분, cdk에 대해서 어느정도 뭔지 알고 들어오셨을거라 생각합니다.

이참에 한번 느껴보고 갑시다.

 


Iac가 뭔지 아시나요?

 

질문에 대해서 저처럼 답변을 했다면, 진정 그 스택의 필요성을 느끼기 힘들것이라 생각합니다.

뭐든 새로운 기술을 접했을때 진정으로 제것으로 만들기 위해서는 특정 상황에서 결핍 또는 불편함을 느껴야합니다.

 

예를들어 봅시다.



사이드 프로젝트 진행간 대부분 비슷한 환경인 ec2, rds, s3 연동간 이전과 동일한 환경을 구성하는 와중 일부 환경을 실수로 다르게 세팅하거나 반복 작업 자체에 대한 불편함을 느껴본 경험이 있으신가요?

 

이런 상황 속 해결책이 바로 Iac입니다.

 

Infra as code: 코드로 인프라를 정의한다.

 

"한번 잘작성한 코드는 십년은 간다" (feat.코린이형준) 라는 유명한말이있죠.

 

위에서 정의했던 문제인 동일한 환경이지만 새로운 프로젝트 시마다 계속해서 재구성해야하는 이러한 상황을 방지하고 뿐만아니라 기존의 복잡한 세팅도 실수 하지 않고 동일하게 세팅할 수 있도록 도와주는것이 "Iac"입니다. 

 


그럼, aws cdk 가 뭔지 아시나요?

자, 여기서 한가지 확인하고 가야할 친구가 있습니다.

 

바로, 한번 쯤은 들어봤을법한 "Terraform" 바로 이구역의 강자입니다.

resource "aws_vpc" "main" {
  cidr_block = var.base_cidr_block
}


<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
  # Block body
  <IDENTIFIER> = <EXPRESSIONS> # Argument
}

위와 같이 HCL 이라는 문법을 사용하여 인프라를 정의합니다. 이 또한 코드로 인프라를 정의할 수 있고 무엇보다 Azure, GCP등 aws 뿐만이 아니라 다양한 클라우드 서비스 또한 지원하기 때문에 멀티 클라우드 환경에서는 대체 할 수 없는 강자라고 볼수 있습니다.

 

근데, 여기서 또 한가지 불편한점이 있지 않나요?

 

 

예, 맞습니다. 우리의 시간은 한정적입니다.

 

"생각 보다 잦은 변화가 없는 인프라 구조인데 이것 때문에 새로운 언어를 배워야해요?"

"그냥 제가 잘하는 Python, Typescript, Golang, etc...로 정의할 수는 없나요?"

"우리 회사는 aws만 사용하면 되는데요...?"

 

등등 다양한 불만이 생길 수 있습니다.

 

이러한 고민을 해결하기 위해 aws 에서 직접 출시한 것이 "aws cdk" 입니다.


어떻게 동작하는가?

사실, cdk는 내부적으로 이전에 aws에서 Iac로써 출시 했던 CloudFormation 템플릿을 정의하여 사용합니다. 

외부에 프로그래밍 언어로 cloudformation 템플릿을 테스트, 구성 및 배포를 진행하는 구조입니다.

(이 챕터에서는 AWS CloudFormation에 대해 다루지 않기 때문에 모르시다면 컨셉만 확인하고 오시면 될것 같습니다.)

 

AWS CloudFormation란 무엇인가요? - AWS CloudFormation

AWS CloudFormation란 무엇인가요? AWS CloudFormation은 AWS 리소스를 모델링하고 설정하여 리소스 관리 시간을 줄이고 AWS에서 실행되는 애플리케이션에 더 많은 시간을 사용하도록 해 주는 서비스입니다.

docs.aws.amazon.com

 

간단한 실습을 통해 더욱 자세히 알아가 봅시다.

 

초기 프로젝트 구조 세팅

기본적으로 cdk에서는 typescript 뿐만 아니라 python, csharp, javascript, java, golang을 지원합니다.

이중 가장 많은 레퍼런스가 있는 typescript로 진행해보도록 하겠습니다.

cdk init --language typescript

 

S3 Stack 정의

./lib/main.ts

import * as cdk from 'aws-cdk-lib';
import { aws_s3 as s3 } from 'aws-cdk-lib';

export class S3CdkStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new s3.Bucket(this, 'MyFirstBucket', {
      versioned: true,
      bucketName: 'my-test-bucket-for-hj',
    });
  }
}

 

부트스트래핑

cdk bootstrap

cdk bootstrap 명령어는 cdk 로인프라를 프로비저닝하기 전에 필요한 초기 설정 작업을 수행하는 역할을 합니다.

 

CDK 애플리케이션을 배포하기 위해 필요한 리소스(예: S3 버킷, IAM 역할 등)를 포함하는 특정 AWS 계정과 리전에 대한 환경을 준비합니다.

 

간단히 말해, cdk bootstrap은 CDK를 사용하여 클라우드 리소스를 관리하고 배포하기 위한 기본적인 환경을 설정하는 과정입니다. 이 과정을 통해 생성된 리소스는 CDK 애플리케이션의 배포 과정에서 사용됩니다.

 

stack 배포 사전 실행

cdk synth

./bin/main.ts 하위 정의된 stack들에 대해서 사전 실행을 합니다.

실제로, 배포 이전 인프라 구성에 사용된 stack들에 대한 테스트를 진행한다고 이해하면 됩니다.

 

이과정까지 모두 문제없이 진행되었을겁니다.

자, 이제 멋드러진 배포를 진행해봅시다.

stack 배포

cdk deploy

실제로 s3 이름이 중복되지만 않는다면 별 문제 없이 s3 자원이 생성된것을 확인할 수 있습니다.

 

stack 제거

cdk destroy

배포 된 stack에 포함되어있는 aws 자원을 제거합니다.


 

후기

 

실습의 굉장히 간단하게 진행했지만 aws에서 제공하는 api를 활용해 진행하는 만큼 굉장히 신뢰도가 높고 다양한 기능을 제공하고 있습니다. 무엇보다, 유연하게 본인의 주력 언어로 코드를 작성하여 코드를 재사용하고 인프라를 명시적으로 구성할 수 있다는 큰 장점을 가지고 있는 툴이라고 생각됩니다.

 

단점 또한 존재는 합니다. 레퍼런스가 많지 않고, 공식 문서가 생각보다 불친절한 부분이 있어서 어느정도 구현에 있어서 직접 하나하나 찾아가면서 작성해야한다는 점입니다.

 

물론, 이러한 단점을 커버할 만큼 프로그래밍 언어로 인프라를 정의할 수 있다는 점에서 앞으로 점차 큰 영향력을 행사하지 않을까?하는 기대감을 가지고 있습니다.

 

결론: Iac 도입에 대해서 고민을 하고 있다면 cdk 한번 고려해보는것 나쁘지 않은 선택이라고 생각이 듭니다!

언젠가 시간이 된다면, 좀더 딥한 구조의 cdk 실습 리뷰로 찾아뵙겠습니다.

감사합니다!

 

 

+) cdk for terraform이라는 cdk의 프로그래밍과 terraform의 멀티클라우드 지원이라는 장점만 모여있는 강려크한 도구가 있습니다. 확인해보시죠!

 

CDK for Terraform | Terraform | HashiCorp Developer

Cloud Development Kit for Terraform (CDKTF) lets you use familiar programming languages to define and provision infrastructure.

developer.hashicorp.com

 

728x90

'기술 > Devops' 카테고리의 다른 글

Serverless 왜 사용하나요?  (2) 2024.05.09
aws(2)VPC(Virtual Private Cloud)  (0) 2022.04.19
aws(1)S3(Simple Storage Services)  (0) 2022.04.19
도커(4)-이미지  (0) 2022.01.19
도커(3)-도커 컴포즈  (0) 2022.01.17
728x90

명시적으로 api 작성하는것과 적절한 모델링을 통해 서버 개발하는것은 기본입니다. 그렇다면, 어떤 부분에서 성능을 개선할 수 있을까? 생각해봐야합니다.

 

추후 다른 주제로도 다루겠지만 이번 챕터에서는 쿼리에 대해 다뤄보겠습니다.

 

1. select_related와 prefetch_related(feat. N+1)

이건 많이 접했을것이라 생각듭니다. 먼저 select_related와 prefetch_related에 대해 알아보기전 django orm의 고질적인 문제인 N+1문제에 대해 알아봐야합니다.

from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=100)


class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)


from .models import Book


def book_list(request):
    books = Book.objects.all()
    for book in books:
        author_name = book.author.name  # 작가 정보를 가져오기 위해 추가 쿼리 발생
    return render(request, 'book_list.html', {'books': books})

위와 같은 기본적인 작과와 도서에 대한 간단한 모델의 각 도서 목록의 작간의 이름을 가져오는 방식을 볼때의 경우,

- Book.objects.all() 도서목록을 가져오는 쿼리 1회

- book.author.name을 통해 n개의 작가 이름을 가져오는 쿼리 n회

이러한 비효율적인 쿼리가 발생하게 됩니다.

 

이러한 문제를 해결하기 위해 select_related를 활용할 수 있습니다.

select_related란, Foreinkey 혹은 OneToOneField와 같은 관계 필드에 대해 사용하며 데이터베이스에서 필요한 연관 객체를 추가로 로드하게 되어 위와같은 N+1 쿼리 문제를 해결할 수 있게 됩니다.

books = Book.objects.select_related('author').all()
for book in books:
    author_name = book.author.name  # 추가 쿼리 없이 author를 미리 로드

- Book.objects.select_related('author').all() 1회의 쿼리에서 미리 작가의 정보까지 로드

이후, 반복문의 경우 미리 로드되어 있는 author의 정보로 인해 추가적인 쿼리가 발생하지 않게 됩니다.

 

그렇다면, prefetch_related는 무엇일까요? 이또한, N+1문제를 해결할 수 있으며 select_related와는 다르게 ManyToManyField 및 reverse ForeignKey/OneToOneField 관계와 같은 역참조 필드에 사용됩니다.

class Category(models.Model):
    name = models.CharField(max_length=100)
    books = models.ManyToManyField(Book)


from .models import Category

# prefetch_related를 사용하지 않은 경우
categories = Category.objects.all()
for category in categories:
    books = category.books.all()  # N+1 쿼리 발생

# prefetch_related를 사용한 경우
categories = Category.objects.prefetch_related('books').all()
for category in categories:
    books = category.books.all()  # 추가 쿼리 없이 books를 미리 로드

- Category.objects.all() 카테고리 목록을 가져오는 쿼리 1회의 쿼리에서 미리 도서 목록의 정보까지 로드

이후, 추가 쿼리에서는 미리 로드 되어있는 books로 인해 추가적인 쿼리가 발생하지 않게됩니다.

 

물론, 이 메서드들을 때에 맞지 않게 사용할시 오히려 역효과를 나을수 있겠지만 잘 사용만 한다면 쿼리 성능을 극대화 할 수 있는 가장 효율좋은 메서드 입니다.

 

2. bulk_create, bulk_update

예시를 들어봅시다. name과 price로 이뤄진 Product 모델이 있고 각각의 데이터 100개를 저장해야합니다.

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)


from .models import Product

products_to_insert = [
        Product(name='Product 1', price=10.99),
        Product(name='Product 2', price=20.99),
        Product(name='Product 3', price=15.99),
        ...
        Product(name='Product 100', price=14.99),
    ]

for product in products_to_insert:
    product.save()

위와 같이 저장할시, 당연히 문제 없이 저장이 될것 입니다. 하지만, 100개의 쿼리가 발생하며 이말은 즉, 100개의 트랜잭션의 작업이 발생한다는 것입니다.

Product.objects.bulk_create(products_to_insert)

 bulk_create를 사용하면 저장할 Product 스키마에 맞는 데이터를 리스트에 담아 전달해주면 하나의 트랜잭션내에서 100개의 데이터를 생성해낼 수 있습니다.

물론, 성능상 이점도 있겠지만 하나의 트랜잭션 내에서 실행되므로 일관성이 유지된다는 장점 또한 존재합니다.

 

bulk_update도 어렵지 않게 bulk_create와 동일한 방식으로 사용가능합니다.

products_to_update = [
        Product(name='Product 1', price=10.99),
        Product(name='Product 2', price=20.99),
        Product(name='Product 3', price=15.99),
        ...
        Product(name='Product 100', price=14.99),
    ]

Product.objects.bulk_update(products_to_update, ['price'])

bulk_create와 마찬가지로 스키마에 맞는 데이터를 리스트에 담아 전달 후, 업데이트할 데이터를 2번째 인자로 리스트에 담아 전달해주면 bulk_update가 완료됩니다.

 

추가로 insert_or_update 기능또한 존재합니다. 기본적으로 bulk_create 메서드를 이용하여 사용하는데 사용방식은 아래와 같습니다.

from django.db import models


class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
	quantity = models.PositiveIntegerField()
    date = models.DateField()
    
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name", "date"],
                name="unique name and date",
            )
        ]


from .models import Product


product_data = [
        Product(name='Product 1', price=10.99, quantity=10, date='2021-01-03'),
        Product(name='Product 2', price=20.99, quantity=6, date='2022-01-03'),
        Product(name='Product 3', price=15.99, quantity=3, date='2023-01-03'),
        ...
        Product(name='Product 100', price=14.99, quantity=2, date='2023-05-03'),
    ]

Product.objects.bulk_create(
    product_data,
    update_conflicts=True,
    unique_fields=["name", "date"],
    update_fields=["quantity", "price"],
)

bulk_create 메서드에서 update_conflicts=True로 설정뒤 고유값으로 설정한 필드와 업데이트할 필드를 입력해줍니다.

주의사항의 경우, 기본적으로 unique하게 선언되어 있지 않던 컬럼의 경우, model단에서, constraint로 선언해주어야합니다.

 

위의경우, 기존에 name=Product 1, date:2021-01-03이였던 데이터가 있었다면 price와 quantity가 업데이트 되며, 이외의 데이터의 경우, 새롭게 생성되게 됩니다.

 

물론, bulk_create, bulk_update방식이 위와같이 가장 이상적으로 담겨진다면 문제가 없겠지만 어쩔수 없이 로직상 불가능한 경우엔 사용이 어려운 부분이 있어 상황에 맞게 사용해야함은 당연합니다.

 

개인적으로, bulk 기능을 잘이용하기위한 팁은 순서를 보장하는 자료구조와 dataclass를 사용하여 미리 데이터 셋을 정의하고 bulk 기능을 사용하는것이 유용하니 참고 해보시길 바랍니다.

 

3. cached_property

class Travel(BaseAdminModel):
    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="여행 리스트 방장")
    members = models.ManyToManyField(User, through="Member", related_name="travels", verbose_name="여행 멤버들")
    title = models.CharField(max_length=255, verbose_name="여행 제목")
    color = models.CharField(max_length=255, verbose_name="여행 색상")
    start_date = models.DateField(verbose_name="여행 시작 날짜")
    end_date = models.DateField(verbose_name="여행 끝나는 날짜")
    description = models.CharField(max_length=13, null=True, verbose_name="여행 메모")
    currency = models.CharField(max_length=15, choices=CurrencyType.CHOICES, default=CurrencyType.USD)

    def __str__(self):
        return self.title

    @property
    def total_amount(self):
        return sum(self.billings.all().values_list('total_amount', flat=True))

위의 코드의 경우, 쿼리를 할때마다 total_amount 연산을 수행하게 됩니다.

 

django에서 제공하는 decorator인 cached_property까 존재하는데 처음 호출되었을때 property 함수 결과값을 캐싱해 둔 뒤 그 이후에는 캐싱된 결과를 리턴합니다. 즉, 쿼리를 할때마다 total_amount로직을 수행하는 것이 아닌 해당 메서드를 호출할 때 캐싱된 값을 리턴하게 되어 불필요한 연산을 줄이게 됩니다.

from django.utils.functional import cached_property 


@cached_property 
def total_amount(self):
    return sum(self.billings.all().values_list('total_amount', flat=True))

캐싱된 데이터는 모델 인스턴스가 살아있는 동안만 캐싱되며, 모델인스턴스의 생명이 끝나게 되면 함께 초기화 되게 됩니다. 캐싱된 데이터이기 때문에 모델인스턴스에서 최초 1회가 아닌 연산에 변화가 생기게 되더라도 변화가 적용되지 않기 때문에 이부분을 주의하여 사용한다면, 성능 개선에 굉장한 도움이 될것입니다.

 

 

orm에서 제공하는 몇몇 메서드들만 활용하더라도, 성능 개선을 할수 있음에 많은 도움을 받을 수 있었습니다. 기본적인 orm 메서드 활용 뿐만 아니라 다른 자원을 좀 더 공격적으로 활용해 서버 개선을 할수 있으니 다음 챕터에서는 다른 자원을 활용한 서버 성능 개선에 대해 알아보도록 하겠습니다.

728x90
728x90

디자이너와 개발자들이 모여 세상에 필요한 서비스를 만들어보고자 하는 목표를 가진 it 연합동아리에서 22년도 2월부터 멤버와 운영진으로써 약 1년 6개월동안 활동을 하였습니다.

 

활동에 대해 설명할때 약간 일기처럼 작성될꺼라 반말이 사용될수 있는점 양해부탁드립니다.ㅎ

 

- 22년 django파트 멤버로서 활동

 

it 연합동아리 '프로그라피'시작

올해 2월말 부터 드디어 연합 개발 동아리를 할수 있게 되었다!! 아무런 준비가 안된상태로 지원만 하다가 매번 떨어지기 일쑤였는데 어느정도 프로젝트를 할수 있겠다!라고 생각될때 지원을 하

leeceo97.tistory.com

21년도 이스트 시큐리티에서의 인턴이 끝나고 취업전 it 연합동아리에서 활동해보고자하는 소망이 있었는데 작년 2월부터 django파트 멤버로 처음 프로그라피 활동을 시작하게 되었습니다. 글에서 언급한대로 사실 현업이 아니면 python 개발자 분들을 만나기 생각보다 어려웠기 때문에 이점에 끌려 프로그라피에 지원하게 되었고 처음 원했던 대로 현업에서 일하고 있던 당시 django파트 운영진 분과 회장님의 도움으로 실제 django에서의 안티패턴이 무엇인지 인증은 내부적으로 어떻게 이뤄지는지 그동안 내가 작성했던 코드들은 어떤 결함이 있었는지 리뷰받고 스터디를 할 수 있는기회가 생겨 너무 좋았던 기억이 있었습니다.

 

스터디 이후 팀 활동 초반 ios 개발자 한분이 프로젝트 시작전 활동을 중단하게 되면서 제가 속했던 1팀의 분위기가 다운 되기도 했지만 ios 운영진인 선우형과 android 운영진이였던 현재는 9기회장을 맡게된 준우의 투입으로 팀분위기가 안정되고 너무 잘진행되게 되어 지금도 많이 고마워하고 있는 분들입니다ㅎㅎ

 

기획단계에서 많은 시간을 소비했지만 처음으로 페이크 마케팅이라는 검증 단계를 거쳐 실제로 유저들이 원하는 서비스는 무엇인지 알아볼수 있었고 결국 it 관련 정보들을 큐레이팅해주는 서비스인 "soul"을 기획했고 개발하게 되었습니다.

 

https://www.instagram.com/so_ul.app/

위 주소는 soul의 페이크마케팅 관련 인스타 프로필입니다.

 

총 7명의 팀원이 모여 약 4개월여간 개발을 진행했고 성공적으로 프로젝트는 마무리되었지만 아쉽게도 현재는 정보를 업데이트 하고 있지는 않습니다. 아직 서비스는 계속해서 배포 되어있기때문에 궁금하신 분들이 있다면 한번씩 확인해주시면 감사하겠습니다 :) (언젠간 업데이트 하지않을까?하는 기대감이...)

 

‎소울

‎커리어 고민 이젠 소울이가 해결해줄거에요. 동아리, 오픈톡방, 교육, 링크, 피드를 확인하세요. 주요내용은 북마크를 하고, 알림을 받으세요. 소울이는 MZ세대의 멋진 커리어를 응원합니다!

apps.apple.com

 

- 23년 서버파트 운영진으로써의 활동

활동이 마무리 되고 9월, 기존의 운영진 분들의 활동 중단으로 우연찮게 django 파트 운영진으로 활동을 제의받게 되었고 저는 고민없이 운영진으로 활동에 참여 하게 되었습니다.

 

실제로, 운영진을 하게 되면서 생각보단 할것이 많았고 책임감도 따랐기에 많은 어려움이 있었지만 현재 활동이 마무리된 시점에서 생각해보면 개발적인것들 외적으로 많이 배울 수 있는 기회였습니다. 운영진 활동을 하며 해야했던것은 아래와 같습니다.

 

1. 리크루팅 과정

생각했던것보다 지원자 분들은 많았고 함께 동아리 리크루팅 홍보도 진행하며 지원자분들의 사전과제를 만들어야했습니다. django 파트 운영진은 저혼자였지만 함께 활동을 진행했던 스프링 파트 운영진 2분이 서버는 따로 관리하는게 아닌 함께 한기수동안 관리해보는거 어떨까 제안해줬고 운좋게 리크루팅 단계부터 같이 진행할 기회를 얻게 되었습니다.

 

처음엔 단순히 기본적인 변별력을 줄필요가 있을까? 생각해서 단순 RESTAPI 구현능력만 보려했던 제생각과는 달리 사전과제에서 어느정도의 변별력을 줘야한다는 의견을 받아 기본적인 RESTAPI 뿐만아니라 테스트코드, 변별력을 위한 심화 요구사항, 문서화등 많은 부분을 확인할 수 있는 사전과제 선정 및 채점방식을 만들수 있었습니다.  

 

약 2주간의 서류 및 과제 심사를 거쳐 마지막 최종 인터뷰 단계에서 느낀점은 제가 면접관으로써 처음 경험을 해보았지만 지원자분들 뿐만아니라 면접관이였던 저도 굉장히 부담되고 긴장되는 자리라는걸 알수 있었습니다. 저의 실수로 인해 동아리의 인식이 바뀔수 있는 점때문에 몇번이고 사전 연습을 했고 결국엔 정말 좋은 친구들 3명과 한기수를 진행할수 있게 되었습니다.

 

2. 인프라 자원 관리

프로그라피 aws 계정이 존재했었고 단순히 각 팀원분들께 fullaccess 계정을 발급해주지 말고 안정적인 비용관리를 위해 최소한의 필요한 권한만을 부여하고 자원을 관리하기 위해 여러 정책을 만들고 모니터링 하며 aws 자원을 관리할 수 있는 기회가 있었습니다.

 

혼자 aws 루트 계정을 사용해서 자원을 사용할때와는 정책을 설정하고 권한 및 자원을 관리하는 경험은 완전 다른 부분이라 생각보다 예산 컨트롤이 어려웠는데 함께 관리해줬던 스프링파트 형들 덕에 잘 마무리 할 수 있었습니다.

줌각코 지분 1위 서버 파트 찰캌

 

3. 세션 기획

많은 운영진이 있었지만 대부분 격주로 진행되는 세션들때문에 매번 세션 진행계획을 정했어야했는데 기존의 마케팅 세션이 마무리되고 실제 개발 단계의 마일스톤을 맡아 기획하게 되었습니다. 실제로, 세션 기획을 진행하게 되면서 멤버분들이 오프라인 세션에 참여했을때 의미있는 세션을 만들고자 노력해서 진행 했는데 이게 잘전달이 되었을까는 한번 피드백을 받아봐야할것 같습니다. 지금와서 생각해보면 좀더 좋은 세션을 만들게 더 노력했을수 있지 않았을까하는 아쉬움은 남지만 그래도 정말 좋은 경험이였습니다.

 

4. 프로젝트 개발

원래 이부분은 계획에 없었지만 초기 4분의 django파트 팀원들을 모집했지만 OT당일, 한분이 활동을 안하게 되면서 자연스럽게 프로젝트 개발에 투입하게 되었고 이부분 때문에 이번한기수 좀 바쁘게 진행되지 않았나 싶습니다. 지금와서 생각해보면 팀에 소속되어 프로젝트를 개발하게 됨으로써 좀더 6팀 친구들과 친해질 수 있었고 또 하나의 서비스를 만들게 되어 나름 많이 뿌듯한것 같습니다.

 

초기 기획 단계에서 많은 의견 충돌 및 제대로 결정하지 못했지만 결국 잘해결해냈고 데모데이도 성공적으로 마무리 지어서 함께 한기수동안 서비스를 만들어간 우리 6팀 친구들 애정한다!!

 

저니리스트 - P도 J가 되는 저니리스트 - Google Play 앱

한 가계부, 저급한 정산을 대전주는 계산도우미, 친구들과 함께 짜는 여행 이벤트.

play.google.com

 

크게는 위와 같고 나름 많이 힘들었지만 항상 열정적이고 열심히 살아가는 운영진 분들뿐만아니라 프로그라피 활동인원 모두에게 자극받아서 많이 배울 수 있고 한발자국 더 성장하게된 기회가 되지 않았나? 생각이 듭니다.

 

격주 주말마다 그리고 평일마다 회의 일정이 있던 나름 많은 시간을 함께했던 프로그라피 활동을 이제는 안하게 되었는데 시간적으로 여유야 생기겠지만 뭔가 나름 시원섭섭한 느낌이 들것 같네요.

 

it 직무 관련 많은 인사이트도 얻고 기술적으로 성장할 수 있는 기본적인 성장 기회도 있겠지만 무엇보다 프로그라피를 하며 얻을 수 있는 가장 큰것은 같은 관심사를 가지고 얘기나눌수 있는 동료이자 친구들을 많이 얻었다는것 입니다.

프로그라피 8기 데모데이

혹시라도, 프로그라피 지원에 있어 뭔가 망설이는 분들이 있다면 고민말고 꼭한번씩 경험해보면 좋을것 같습니다!!

728x90
728x90

elasticsearch에 대해서 처음 접하게 됐을때 다들 기본적으로 검색을 할때 사용되는 도구다라고만 할수 있는데 준 실시간성으로 로그시스템을 구축할때 elasticsearch와 함께 logstash, kibana 그리고 요즘엔 filebeat까지 합쳐 elk 스택이라 불리며 자주사용되는 스택이 있습니다.

 

1. 기본 개념

각각의 구성 요소들이 담당하는 큰 역할에 대해 알아볼 필요가 있습니다.

- Elasticsearch

  • 로그데이터를 저장하고 검색하는 역할을 담당
  • 실시간 분석과 검색을 지원하며, 데이터베이스 쿼리와는 다르게 RESTFul API를 통해 데이터에 액세스 할 수 있음

- Logstash

  • 로그 데이터 수집 및 필터링, 변환 ,파싱하는 역할을 담당
  • 실질적으로 로그에 출력되길 원하는 내용만을 필터링하는 역할
  • pipeline을 통해 여러개의 로그데이터 별로 설정을 진행할 수 있음

input, filter ouput으로 이루어져 있으며 input, output, filtering에 많은 플러그인들을 제공하고 있어 손쉽게 사용 가능합니다.

아래의 주소에서 사용가능한 plugin과 사용방법이 자세히 나와 있으니 참고 해주시기 바랍니다.

 

Input plugins | Logstash Reference [8.9] | Elastic

 

www.elastic.co

 

- Kibana

  • ElasticSearch 데이터를 시각화하는 도구로, 로그데이터를 분석하고 대시보드화 하는 역할을 담당
  • 뿐만 아니라 elasticsearch의 모든 설정을 손쉽게 관리 할 수 있도록 GUI 역할또한 담당
  • Kibana는 다양한 시각화 도구를 제공하며, 기본적으로 로깅용 뿐만 아니라 시간대별, 지리적, 통계 등 다양한 방식으로 데이터를 시각화 할 수 있음.

kibana 또한 Elasticsearch 뿐만아니라 여러 plugin 형태로 제공된 저장소를 손쉽게 연결 및 관리 하여 시각화 할 수 있는 아주 강력한 도구입니다. 

 

- Filebeat

  • 데이터 추출 및 전송을 담당
  • 경량화된 로그 수집 도구 역할
  • resource(CPU와 RAM)를 상당히 적게 소모함
  • 간단한 filter기능도 제공하지만 이는 logstash로 대체해 사용

사실, Filebeat가 왜 필요한가? 라는점에 초점을 맞춰 봐야합니다.

기존에 logstash가 담당하는 로그 수집 역할만을 filebeat가 담당하고 있습니다. 왜 logstash에서 기능을 제거했을까? 이유는 바로 jvm 위에서 동작하는 logstash의 리소스 부담을 줄이기 위해서 입니다.

 

filebeat와 logstash의 베이스는 언어자체에서도 큰 차이가 있습니다. filebeat=Go, logstash=java 베이스 코드로 짜여져 있으며 filebeat의 컨셉은 경량화된 로그 수집기 입니다. 기본적으로 로그 수집 자체의 역할만을 담당하여 서버의 CPU와 RAM을 상당히 적게 소모하여 logstash나 아에 elasticsearch로 전송할 수 있습니다.

 

그렇다면 어떻게 서버 구성을 하는게 이점이 될수 있을까? 생각해봅시다.

 

2. 인프라 구성

Filebeat는 서버의 자원을 최소화하여 로그 수집할 수 있는 장점이 있습니다. 하지만, 아래와 같이 구성할경우, 어떨까요?

3대의 ec2 인스턴스가 각각 존재하고 하나의 서버안에 elk + filebeat 스택을 구성했다고 가정해 봅시다.

물론 elk 인스턴스의 물리적 리소스가 굉장히 큰 서버라면 문제없이 동작할것입니다. 하지만, filebeat가 나온 히스토리를 생각해봅시다.

기존의 logstash의 작업중 로그 수집의 리소스를 분리해 물리적인 리소스를 적게 소모하는 경량화된 로그수집기입니다. 과연 filebeat가 필요할까요? 라는 생각이 듭니다. 

 

만약 위와같은 구조로 구성한다면, filebeat를 제외한 logstash에게 로그 수집의 역할까지 담당하게 하는것이 좋은 패턴이 될것입니다.

그렇다면 어떻게 구성하는게 좋을까요?

위와 같은 패턴으로 ec2 인스턴스 각각에 filebeat를 설치하여 로그를 수집한뒤 logstash가 있는 5044포트로 로그를 전송하는 역할만 담당하게 구성하면됩니다. 서버의 자원에 따라 elasticsearch와 kibana를 분리하는것도 방법이 될수 있으나 filebeat를 어디에 배치하느냐가 elk와 filebeat의 공존의 이유를 가장 잘 설명한 방법이 될수 있습니다.

 

logstash를 여러대 띄어서 관리하는 방법도  있지 않을까?하는 생각도 들수 있지만 logstash의 경우, pipeline형태로 여러대의 logstash 자원을 관리할 수 있어 이기능을 적극적으로 활용하여 관리해주시기 바랍니다.

 

3. elk 설치 튜토리얼

mac을 기준으로 elk 스택 설치 방법에 대해 설명드리겠습니다.

일단 elasticsearch, kibana, logstash, filebeat를 brew 명령어를 통해 설치해주시기 바랍니다.

brew install filebeat-full
brew install logstash-full
brew install elasticsearch-full
brew install kibana-full

설치가 완료 되었다면, 기본적으로 filebeat의 output은 localhost:5044, logstash의 output은 localhost:9200, kibana의 input은 localhost:9200으로 구성되어 있습니다.

 

- filebeat

/usr/local/etc/filebeat 위치의 filebeat.yml 

filebeat.inputs:
 - type: log
  id: test-filestream

  enabled: true
  paths:
    - /usr/local/var/log/sample.log
    
  # exclude_lines: ['^DBG']
  # include_lines: ['^ERR', '^WARN']
  
  # multiline.pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]]
  # multiline.negate: true
  # multiline.match: after
  
output.logstash:
  hosts: ["localhost:5044"]

filebeat의 설정은 위와같이 기본적으로 로그를 수집할 input과 output으로 나뉘어져있습니다.

 

Filebeat Reference [8.9] | Elastic

 

www.elastic.co

input과 output에 본인이 원하는 플러그인 형태로모두 명시해서 사용하시면 되며 위의 자습서에서 확인 후 요구사항에 따라 변경해주시길 바랍니다.

 

위 설정의 경우, /usr/local/var/log/sample.log 이경로의 로그 파일을 input대상으로 확인하며 output으로 localhost:5044의 로그스태시로 보낸다는 간단한 설정입니다.

 

최근 트러블 슈팅간 syslog에서 error로그를 수집하기 위해 설정을 진행했는데 실질적으로 필요한 traceback로그는 수집이 안되고 500에러만 출력되는 문제가 생겨 이걸 하나의 라인으로 포함시켜 보내기 위해 multiline의 개념을 도입해 사용했습니다.

 

multiline은 기본적으로 정규표현식으로 패턴을 정의할 수 있으며, 특정 정규표현식의 패턴과 일치하지 않을경우 일치하지 않는 모든 로그를 한줄의 로그로 인식해 보낸다는 개념입니다. 여기엔 negate와, match로 설정을 진행해주셔야하며 설정 내용은 아래와 같습니다.

 

negate: 패턴이 무효화되는지 여부 정의, 기본값은 false

match: 일치하는 라인을 하나로 볼지 일치하지 않는 라인을 하나로 볼지 결정

negate match 결과
false after 패턴과 일치하는 연속줄을 일치하지 않는 이전줄에 포함
false before 패턴과 일치하는 연속 줄은 일치하지 않는 다음줄 앞에 포함
true after 패턴과 일치하지 않는 연속줄을 일치하는 이전 줄에 추가
true before 패턴과 일치하지 않는 연속 줄은 일치하는 다음줄 앞에 추가

syslog의 특성상 시스템의 모든로그가 출력되기 때문에 수집될 대상의 로그의 경우수가 너무나 많기 때문에 exclude_lines와 include_lines를 적절히 설정하여 수집되는 로그의 기준을 확실히 정하는것이 작업의 리소스를 줄일 수 있는 방식이 될것 입니다.

 

- logstash

여러 logstash 설정 방식이 존재하겠지만 pipeline형식으로 관리하는 방식으로 리뷰하겠습니다.

/usr/local/etc/logstash 위치에 conf.d라는 디렉터리를 생성 후 sample.cfg 파일을 생성해줍니다.

input {
  beats {
    port => 5044
  }
}

filter {
  if "target log" in [message] {
    grok {
      match => { "message" => "%{TIMESTAMP_ISO8601:log_datetime} %{LOGLEVEL:level} %{DATA:log_message}: %{GREEDYDATA:json_data} %{SPACE}\[PID:%{NUMBER:pid}:%{DATA:thread}\]" }
    }

    date {
         match => [ "log_datetime", "yyyy-MM-dd HH:mm:ss,SSS" ]
         timezone => "Asia/Seoul"
    }
  }

  else {
    drop { }
  }
}

output {
  elasticsearch {
    hosts => ["http://localhost:9200"]
    index => "test-%{+YYYY.MM.dd}"
  }
}

input

Logstash Beats 입력 플러그인을 통해 로그 데이터를 수신한다는 것을 정의합니다. 설정에서는 5044 포트를 통해 Beats로부터 들어오는 데이터를 수신하도록 설정되어 있습니다.

 

filter

데이터 필터링 및 구문 분석을 수행하는 부분입니다. message 필드에 "target log"가 포함되어 있는 경우에만 실행됩니다.

  • grok 필터는 로그 메시지를 정규 표현식을 사용하여 구문 분석하여 필드로 추출합니다. 예를 들어, %{TIMESTAMP_ISO8601:log_datetime}은 ISO 8601 형식의 타임스탬프를 log_datetime 필드로 추출합니다.
  • date 필터는 log_datetime 필드의 값을 지정된 패턴에 맞춰서 파싱하여 날짜 및 시간 필드를 생성합니다. 지정된 타임존인 "Asia/Seoul"로 변환됩니다.

만약 "target log" message 필드에 없는 경우에는 drop 필터를 사용하여 이벤트를 삭제하므로 처리되지 않습니다.

 

output

필터링 구문 분석이 완료된 데이터를 Elasticsearch 색인하는 부분입니다. elasticsearch 출력 플러그인을 사용하여 데이터를 Elasticsearch 클러스터로 전송합니다. hosts에는 Elasticsearch 클러스터의 호스트 주소를 설정하며, index 색인 이름을 설정합니다. 여기서는 "test-" 다음에 날짜를 추가하여 일별로 색인을 생성하도록 설정되어 있습니다.

 

기본설정을 정의했다면 /usr/local/etc/logstash/pipeline.yml 파일에 pipeline을 정의해줍니다.

- pipeline.id: beats-pipeline
   pipeline.workers: 1
   pipeline.batch.size: 1
   path.config: "/usr/local/etc/logstash/conf.d/sample.cfg"

logstash의 동작을 위한 관련한 모든 기본 설정은 마무리가 되었습니다.

 

elasticsearch와 kibana의 경우, 기본적으로 logstash의 5044를 인풋으로 그리고 9200포트를 기본 elasticsearch로 바라보고 있기 때문에 단순히 실행만 해주신다면 문제없이 기본적인 로깅 시스템 세팅이 마무리됩니다.

brew services start filebeat-full
brew services start logstash-full
brew services start elasticsearch-full
brew services start kibana-full

이번 챕터에서 다뤄본 내용은 elk+beat 스택의 동작을 위한 기본 설정만 다뤄봤기 때문에 실제로 어떤 데이터를 수집할것인지에 따라 beat와 logstash의 설정은 달라질수 있으며 실제로 데이터 수집이후 elasticsearch의 lifecycle이나 index pattern등 정의할것들에 대한 작업들은 많이 남아 있습니다. 기본적으로 비즈니스 요구사항을 먼저 파악한 후 이러한 설정들을 어떻게 설정할지는 공식문서를 통해 더욱 구체화 해주시길 바랍니다.

 

이후에 다뤄볼 내용은 indextemplate과 cluster 관리에 대해 더욱 자세히 알아보도록 하겠습니다.

감사합니다.

728x90
728x90

최근 시스템을 파악하며 느낀것이 Elasticsearch의 강력함을 느끼고 있습니다. 사내 시스템에서는 검색, 데이터 저장으로 사용 하고 있는데 왜 elasticsearch를 사용하는것일까? 이게 무엇인지 알아봐야할 필요성을 느끼게 되었습니다.

 

(1) elasticsearch란 무엇인가?

apache Lucene에 구축되어 배포된 검색 및 분석 엔진입니다. 데이터 저장소가 아니라 Mysql같은 데이터베이스를 대체할 수 는 없지만 방대한 양의 데이터를 신속하고 거의 실시간으로 저장, 검색, 분석 할 수 있습니다. 

 

(2) elasticsearchdml 기본 구성

elasticsearch는 기본적으로 클러스터와 노드에 대해 알아야합니다.

클러스터란, 여러대의 컴퓨터 혹은 구성 요소들을 논리적으로 결합하여 하나의 컴퓨터, 혹은 하나의 구성 요소처럼 사용할 수 있게 해주는 기술입니다. elasticsearch 클러스터 역시 여러개의 프로세스들을 논리적으로 결합하여 하나의 elasticsearch 프로세스처럼 사용할 수 있게 해줍니다. 이때, 클러스터를 구성하는 하나하나의 elasticsearch 프로세스를 노드라고 부릅니다. 즉, 여러개의 elasticsearch 노드들을 하나의 elasticsearch처럼 동작하게 하는 것이 elasticsearch 클러스터라고 할 수 있습니다.

 

그렇다면 왜 단일 노드로 클러스터를 구성하지 않을까요? 여러개의 노드로 클러스터를 구성하는 이유가 뭘까? 하는 궁금증이 있습니다.

특정 노드에 장애가 발생했을때, 이후의 요청을 처리할 수 없는 요청 불가 상태로 elasticsearch 서비스 전체에 영향을 끼치게 됩니다.

 

하지만 여러개의 노드로 클러스터를 구성할경우, 하나의 노드에 장애가 발생해도 다른 노드에 요청할 수 있기 때문에 안정적으로 클러스터를 유지할 수 있고 이를 통해서 높은 수준의 안정성을 보장하며 서비스를 운영할 수 있게 됩니다.

 

그렇다면 노드의 구조에 대해서도 간단히 알아봅시다.

노드의 경우, 클러스터를 구성하는 논리적인 elasticsearch프로세스 하나를 의미합니다. 노드도 클러스터와 마찬가지로 각각의 고유한 노드 이름과 UUID가 있고, 역할에 따라 여러가지 노드로 구분할 수 있습니다.

노드 역할 설명
마스터 클러스터 구성에서 중심이 되는 노드, 클러스터의 상태 등 메타데이터를 관리
데이터 사용자의 문서를 실제로 저장하는 노드
인제스트 사용자의 문서가 저장되기 전 문서 내용을 사전 처리하는 노드
코디네이트 사용자의 요청을 데이터 노드로 전달하고, 다시 데이터 노드로부터 결과를 취합하는 노드

위와 같이 노드가 할수 있는 역할은 총 4가지 이며, 각각하나의 역할만 할 수 있는게 아니라 여러개의 역할을 할 수 있습니다. 

꼭 유념해야하는 개념은 클러스터 내에서 메타데이터를 관리하는 마스터 노드는 하나라는것입니다. 실질적인 마스터 노드와 마스터노드에서 문제가 발생했을때, 대체할 마스터 후보 노드가 있다고 생각하면 쉬운데, 마스터 후보 노드들은 마스터 노드로부터 지속적으로 클러스터 운영에 필요한 데이터를 전달받기 때문에 항상 같은 메타 데이터를 유지하고 있습니다. 그래서 마스터 노드에 장애가 발생해서 새로운 마스터 노드가 선출되어도 중단없이 서비스를 지속할 수 있습니다.

 

- 샤드와 세그먼트

샤드는 인덱스에 색인되는 문서들이 저장되는 논리적인 공간을 의미하며, 세그먼트는 샤드의 데이터들을 가지고 있는 물리적인 파일을 의미합니다. 인덱스와 샤드, 세그먼트의 관계는 아래와 같은 그림이 됩니다.

하나의 인덱스는 다수의 샤드로 구성되어 있고 하나의 샤드는 다수의 세그먼트로 구성됩니다. 샤드는 1개 이상의 세그먼트로 구성되는데 샤드마다 세그먼트의 개수는 서로 다를 수 있습니다.

 

위와 같은 elasticsearch의 기본적인 구성에 의해 서비스를 안정적이게 운영할 수 있게됩니다.

 

 

(3) Elasticsearch와 관계형 DB 비교

- 구조

ElasticSearch 관계형 데이터 베이스
인덱스(Index) 데이터베이스 (Database)
샤드(Shard) 파티션 (Partition)
타입(Type) 테이블 (Table)
문서(Document) 행 (Row)
필드(Field) 열 (Column)
매핑(Mapping) 스키마 (Schema)
QueryDSL SQL

- CRUD

HTTP Method 기능 데이터 베이스 문법
GET 데이터 조회 SELECT
POST 인덱스 업데이트 UPDATE
PUT 데이터 생성 INSERT
DELETE 데이터 삭제 DELETE
HEAD 인덱스 정보 확인 -

 

(4) elasticsearch API를 통해 조회 간단히 실습해보기

- 검색

기본적으로 RDB에서는 User라는 테이블의 데이터를 검색할때는 아래와 같이 검색합니다.

SELECT *
FROM User;

elasticsearch의 API는 아래와 같이 검색합니다.

GET /User/_search HTTP/1.1
Host: <elsticsearch host>
Content-Type: application/json

- 조건 검색

user의 gender가 man이고 age가 10인 유저를 검색할때는 RDB에서 아래와 같이 검색합니다.

조건이 1개 일때
SELECT *
FROM User
WHERE gender='man';

조건이 2개 이상일때
SELECT
FROM User
WHERE gender='man'
and age = 10;

elasticsearch의 API는 아래와 같이 검색합니다.

GET /Users/_search HTTP/1.1
Host: <elasticsearch host>
Content-Type: application/json

조건이 1개 일때,
{"query": {
	"match": {
    	"gender": {
        	"query": "man"
          }
        }
      }
    }

조건이 2개 이상일때,
{"query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "man"
          }
        },
        {
          "match": {
            "age": "10"
          }
        }
      ]
    }
  }}

 

- 범위 검색

User테이블에서 age가 10초과인 유저를 검색할때 RDB에서는 아래와 같습니다.

SELECT *
FROM User
WHERE age > 10;

elasticsearch의 API는 아래와 같습니다.

GET /Users/_search HTTP/1.1
Host: <elasticsearch host>
Content-Type: application/json

{"query": {
    "bool": {
      "must": {"match_all": {} },
      "filter": {
        "range": {
          "gender": {
            "age": {
              "gt": 10
            }
          }
        }
      }
    }
  }}

위와 같이 RESTAPI를통해 조회를 간단히 진행할 수 있습니다.

 

(5) 왜 빠른걸까?

  1. 분산 아키텍처: Elasticsearch는 데이터를 여러 노드에 분산하여 저장하고 처리합니다. 이렇게 함으로써 데이터의 부하가 분산되므로 병렬 처리를 통해 빠른 검색 및 질의 성능을 제공할 수 있습니다.
  2. 역색인(Indexing): Elasticsearch는 역색인(index)을 사용하여 문서 내용의 토큰화 및 색인화를 수행합니다. 이로 인해 효율적인 텍스트 검색과 특정 조건에 따른 필터링이 가능해집니다.
  3. 분산 검색 및 질의 처리: Elasticsearch는 검색 요청을 여러 노드에 분산하여 처리합니다. 이는 검색 작업을 여러 개의 작은 작업으로 나누어 처리하므로 전체적으로 빠른 검색 응답 시간을 보장합니다.
  4. 메모리 기반 캐싱: Elasticsearch는 자주 사용되는 데이터와 검색 결과를 메모리 캐시에 저장하여 다음 검색에서 더 빠른 응답 시간을 제공합니다. 이를 통해 반복적인 검색 작업의 성능을 향상시킬 수 있습니다.
  5. 복제 및 샤딩(Sharding): Elasticsearch는 데이터를 여러 파티션으로 나누어 저장하는 샤딩 기술을 사용합니다. 이를 통해 데이터베이스의 확장성과 성능이 향상되며, 복제(replication)를 통해 데이터 손실을 방지하고 고가용성을 보장합니다.
  6. 다양한 검색 기능: Elasticsearch는 다양한 검색 기능과 질의 기능을 제공합니다. 풀 텍스트 검색, 필터링, 집계, 자동 완성, 유사한 문서 검색 등 다양한 기능을 효율적으로 처리할 수 있습니다.
  7. 분석 기능: Elasticsearch는 데이터를 실시간으로 분석하고 시각화할 수 있는 다양한 도구와 플러그인을 제공합니다. 이를 통해 데이터를 실시간으로 모니터링하고 인사이트를 얻을 수 있습니다.

요약하면, Elasticsearch 빠른 성능은 분산 아키텍처, 역색인 기술, 분산 검색 질의 처리, 메모리 기반 캐싱, 복제 샤딩, 다양한 검색 분석 기능 다양한 기술적 요소와 최적화가 결합되어 있기 때문에 빠르고 쉽게 이용할 수 있게 됩니다.

 

간단하게 이번 챕터에서는 elasticsearch의 기본 사용방법과 요소에 대해 알아봤는데 다음챕터로는 elasticsearch와 함께 주되게 사용되는 elk스택 + filebeat에 대해 알아보도록 하겠습니다.

 

- 출처

 

Elastic 가이드 북 - Elastic 가이드북

7. 인덱스 설정과 매핑 - Settings & Mappings

esbook.kimjmin.net

 

 

기초부터 다지는 ElasticSearch 운영 노하우 | 박상헌 - 교보문고

기초부터 다지는 ElasticSearch 운영 노하우 | [이 책에서 다루는 내용] ▶ ElasticSearch 기본 개념 ▶ 클러스터 구축 방법과 운영 방법 ▶ 클러스터 성능 모니터링과 최적화 ▶ 분석 엔진으로 활용하는

product.kyobobook.co.kr

 

728x90
728x90

데이터 엔지니어링이란 무엇이고 각 단계별 어떤기술이 사용되는지  그리고 데이터 엔지니어링에서 사용되는 용어에 대해 먼저 알아보려 합니다.

출처:&nbsp;https://tech-story.devsisters.com/stories/2020-10-data-driven-story-1-data-platform-cell/

위의 이미지를 보다시피 데이터 엔지니어링의 목적은 여러 데이터 소스들에서 유의미한 데이터들을 수집, 가공 및 분석하여 데이터 소비자들에게 알맞게 전달하는 것이 목적입니다.

 

단계별로 아래의 과정을 거쳐 소비자들에게 전달됩니다.

 

1. 데이터 소스

데이터 엔지니어링의 첫 번째 단계는 데이터 소스를 식별하고 확보하는 것입니다. 데이터는 다양한 소스에서 나올 수 있습니다. 내부 시스템 로그, 외부 API, 데이터베이스, 외부 웹사이트 등이 데이터 소스의 예시입니다. 데이터를 수집할 때 어떤 유형의 데이터를 어떻게 수집할 것인지 결정하는 것이 중요합니다.

ex) 비/관계형 데이터베이스, excel, log, etc..

 

2. 로그 수집, 전처리

데이터를 소스로부터 수집하면 다음 단계는 데이터를 로그로 기록하고 필요한 형식으로 전처리하는 것입니다. 로그 수집은 데이터의 추적 가능성과 안정성을 보장하는 데 도움이 됩니다. 데이터 전처리는 데이터를 클린하게 만들어서 분석이나 저장 단계에서 문제가 발생하지 않도록 하는 과정입니다. 이 단계에서는 데이터 누락, 이상치, 중복 등을 처리하고 필요한 형식으로 변환합니다.

ex) airflow, spark, kafka, elk, aws kinesis etc..

 

3. 데이터 저장

전처리된 데이터는 영구적으로 저장되어야 합니다. 데이터베이스 시스템을 활용하여 구조화된 데이터를 저장하거나, 분산 스토리지 시스템을 사용하여 대용량 데이터를 저장할 수 있습니다. 데이터 저장소를 선택할 때는 데이터의 양과 형태, 접근 속도, 보안 요구 등을 고려해야 합니다. 일반적으로 데이터 엔지니어는 데이터 웨어하우스, NoSQL 데이터베이스, 분산 파일 시스템 등을 고려하게 됩니다.

ex) bigquery, s3, 비/관계형 데이터베이스, etc..

 

4. 데이터 시각화 (선택적)

데이터 시각화는 데이터를 이해하고 전달하기 위한 중요한 단계입니다. 데이터 시각화를 통해 데이터의 경향성, 패턴, 이상치 등을 빠르게 파악할 있습니다. 시각화 도구를 사용하여 그래프, 차트, 대시보드 등을 생성하여 데이터의 시각적 표현을 만들어냅니다. 데이터 시각화는 비즈니스 의사 결정이나 데이터 분석 결과를 공유할 매우 유용합니다. 실질적으로 이부분은 대부분 개발자분들 보다도 비개발직군분들이 다른 업무에서 의사결정에 활용하려는 목적으로 많이 사용됩니다.

ex) superset, tableau, kibana, slack, etc..

 

필자의 경우, 회사업무를 진행하며 경험해본 스택은 airflow, elk 스택, aws s3, kinesis, superset, bigquery, pandas 정도가 있습니다.

다음 챕터 부터는 elasticsearch 및 elk 스택, airflow, superset의 순으로 리뷰를 할예정이며 이후부터는 사용해보지 않은 apache spark, kafka에 대해 알아볼 예정입니다.

미리 각 기술이 사용되는 이유 및 요약을 하자면 아래와 같습니다.

 

1. airflow

위 사진이 airflow에 대해 가장 잘 설명된거라고 생각하는데 airflow는 쉽게 말해 데이터 엔지니어링 잡들을 통합적으로 관리해주는 스케줄링 도구라고 생각합니다. 기본적은 python기반의 코드로 작성되며 papermill이라는 라이브러리를 통해 jupyter파일을 주입할 수 있어 개인적으로 굉장히 큰 장점 중 하나라고 생각합니다.

 

 

2. elk스택 + filebeat

굉장히 유명한 오픈소스 검색엔진인 elasticsearch와더불어 함께 사용되는 logstash, kibana 그리고 최근들어 많이 함께 사용되는 filebeat를 통칭하는 것이 elk스택입니다. 거의 실시간으로 로그 및 데이터를 수집 및 시각화가 가능하여 많이 활용 됩니다. 각각의 역할에 대해 알아보면 아래와 같습니다.

- filebeat: 기존 로그 수집, 필터 및 전달까지 담당하던 logstash의 기능중 데이터 추출 및 전송의 역할을 담당하는 도구입니다. logstash도 충분한데 등장하게 된이유는 기본적으로 go로 작성된 filebeat의 경우 cpu와 ram과 같은 resource를 상당히 적게 소모하기 때문이며, 보통 경량화된 로그 수집 도구라고 알려져있습니다.

- logstash: 앞서 말한 바와 같이 로그 수집, 필터 및 전달을 담당하며 기본적으로 로그를 수집하는 input과 수집된 로그를 필터링하는 filter, 필터링된 데이터를 전달하는 output으로 구성되어 있으며 input과 output에 여러 플러그인을 제공하여 비교적 쉽게 사용 가능합니다.

- elasticsearch: 데이터 저장소 및 검색의 역할을 담당하며, 데이터베이스 쿼리와는 다르게 RESTFUL API를 통해 데이터에 액세스 할수 있습니다. 역색인을 통해 검색이 굉장히 빠릅니다.

- kibana: elasticsearch의 index 관리 및 데이터 시각화를 담당하고 있습니다.

 

3. superset

모든 과정을 거쳐 수집된 데이터들을 차트와 대시보드 단위로 쿼리해 소비자들에게 시각화하여 제공해주는 역할을 합니다. 개발자들도 볼수 있겠지만 대부분 비개발직군 분들이 유의미한 데이터를 통해 추후 업무에 활용할때 많이 사용됩니다.

 

4. kafka

Kafka는 대규모 실시간 데이터 스트리밍 아키텍처를 구성하는 데 사용되며, 웹 사이트의 로그 처리, 실시간 분석, 이벤트 소싱 등에 활용됩니다. ELK 스택과 함께 사용하여 로그 데이터를 Kafka로 수집하고, 이후 Logstash를 사용하여 데이터를 전처리하여 Elasticsearch로 전송하는 등 다양한 데이터 파이프라인을 구축할 수 있습니다.

 

5. spark

대규모 데이터 처리와 분석을 위한 오픈 소스 클러스터 컴퓨팅 프레임워크입니다. Hadoop MapReduce 모델을 보완하고, 빠르고 효율적인 데이터 처리를 제공하는 것이 주요 목표입니다.

 

chatGPT에게 데이터 엔지니어링에서 사용되는 토픽에 대해 리스트를 뽑아봤는데 아래와 같습니다.

  1. ETL (Extract, Transform, Load):
    • Extract: 다양한 데이터 원본에서 데이터를 추출합니다.
    • Transform: 추출한 데이터를 필요한 형식으로 변환하고 정제합니다.
    • Load: 변환한 데이터를 타겟 데이터베이스나 데이터 웨어하우스에 로드합니다.
  2. Data Pipeline (데이터 파이프라인): 데이터의 이동과 변환 과정을 일련의 단계로 정의한 시스템 또는 프로세스입니다.
  3. Batch Processing (배치 처리): 대량의 데이터를 일괄적으로 처리하는 방식을 말합니다.
  4. Real-time Processing (실시간 처리): 데이터를 실시간으로 처리하고 분석하는 방식을 말합니다.
  5. Data Warehouse (데이터 웨어하우스): 다양한 데이터 원본에서 추출한 데이터를 통합하고 저장하는 중앙 저장소입니다.
  6. Data Lake (데이터 레이크): 다양한 유형과 형식의 데이터를 대규모로 저장하고 분석하기 위한 저장소입니다.
  7. Data Ingestion (데이터 인제스처): 외부 데이터 원본에서 데이터를 추출하고 처리 파이프라인으로 전송하는 과정을 말합니다.
  8. Data Transformation (데이터 변환): 데이터를 필요한 형식으로 변환하고 정제하는 작업을 말합니다.
  9. Data Modeling (데이터 모델링): 데이터의 구조와 관계를 정의하여 데이터를 효과적으로 저장하고 쿼리할 수 있도록 설계하는 작업을 말합니다.
  10. Schema (스키마): 데이터의 구조와 형식을 정의하는 설계도입니다.
  11. Data Governance (데이터 거버넌스): 데이터의 품질, 보안, 규정 준수 등을 관리하는 프로세스와 정책을 의미합니다.
  12. Data Quality (데이터 품질): 데이터의 정확성, 일관성, 완전성 등을 나타내는 개념입니다.
  13. Data Integration (데이터 통합): 여러 데이터 소스에서 데이터를 추출하고 통합하는 작업을 의미합니다.
  14. Data Partitioning (데이터 파티셔닝): 대량의 데이터를 논리적 또는 물리적으로 분할하여 관리하는 방식을 말합니다.
  15. Data Replication (데이터 복제): 데이터를 여러 위치에 복사하여 고가용성과 장애 복구를 보장하는 작업을 의미합니다.
  16. Data Catalog (데이터 카탈로그): 조직 내의 데이터 자산을 관리하고 검색할 수 있는 메타데이터 저장소를 말합니다.

 

회사에서 데이터 엔지니어링 업무를 주로 맡으며, 깊게 각각의 기술에 대해 알아볼 기회가 없었는데 이번기회를 통해 자세히 알아볼 예정입니다. 

위에서 잠시 요약했던 기술에서 kafka는 aws kinesis firehose, spark는 pandas와 비교되지 않을까?하는 생각이 듭니다. 사실, 회사에서 kinesis firehose, pandas만으로도 업무 진행에 문제가 없어 필요성에 대해서는 확실히 파악하진 못했는데 추후 실습을 통해 리뷰하도록 하겠습니다.

 

감사합니다 :)

728x90
728x90

docker를 사용해서 프로젝트를 배포하는 방법에 대해 리뷰해보겠습니다.

크게 django, nginx, postgresql 컨테이너를 생성하여 튜토리얼을 진행해보겠습니다.

 

1. django 컨테이너

서버 컨테이너의 경우, docker hub의 오피셜한것이 아닌 직접 빌드를 해야하기 때문에 Dockerfile 작성이 선행되어야합니다.

dockerfile에서는 프로젝트 copy, 패키지 설치 및 collectstatic만 진행합니다. 

# /Dockerfile
FROM python:3.11.4

RUN apt-get -y update
RUN apt-get -y install vim

RUN pip install --upgrade pip

COPY . /server

WORKDIR /server

RUN pip install -r requirements.txt
RUN echo yes | poetry run python manage.py collectstatic

EXPOSE 8000

기존에 만약에 static 디렉터리가 존재할경우, django에서는 overwrite할지의 여부를 묻기 때문에 echo yes를 붙여주게 되었습니다.

--> 보통, aws s3를 사용하기 때문에 대부분 존재할 경우가 있을것입니다.

 

docker-compose.yml

version: '3'

services:
  server:
    build:
      context: .
    command: >
      bash -c "python3 manage.py makemigrations --settings=config.settings.deploy
      && gunicorn --bind 0.0.0.0:8000 config.wsgi.local:application"
    ports:
      - "8000:8000"
    volumes:
      - ./server:/server

migration 과정을 dockerfile이 아닌 docker-compose에서 빌드를 하는 이유는 현재의 경우, postgresql 컨테이너가 뜨기전 미리 docker image 생성 -> postgresql up -> server up의 과정을 거치게 됩니다. 때문에 순서상의 이유로 db가 올라오지 않은 상태로 migration을 진행하게 되어 제대로 빌드가 되질 않습니다.

 

2. postgresql 컨테이너

 

postgres - Official Image | Docker Hub

Note: the description for this image is longer than the Hub length limit of 25000, so has been trimmed. The full description can be found at https://github.com/docker-library/docs/tree/master/postgres/README.md. See also docker/hub-feedback#238 and docker/

hub.docker.com

postgresql의 경우, dockerhub에 있는 이미지를 가져와 컨테이너로 활용하기 때문에 별도의 dockerfile은 필요없이 compose에 업데이트 후 django의 세팅만 수정해주신 후 의존성만 명시해주면 쉽게 해결가능합니다.

 

- docker-compose 파일 수정

version: '3'

services:
  postgres:
    image: postgres
    hostname: postgres
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: "postgres"
      POSTGRES_USER: "postgres"
      POSTGRES_PASSWORD: "postgres"

  server:
    build:
      context: .
    command: >
      bash -c "python3 manage.py makemigrations --settings=config.settings.deploy
      && gunicorn --bind 0.0.0.0:8000 config.wsgi.local:application"
    ports:
      - "8000:8000"
    volumes:
      - ./server:/server
    depends_on:
      - postgres

environment 값의 경우, 만약 실제로 사용하신다면 시스템 환경변수를 이용해 설정해주시기 바랍니다.

 

- django settings.py 변경

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "postgres",
        "HOST": "postgres",
        "USER": "postgres",
        "PASSWORD": "postgres",
        "PORT": 5432,
    }
}

docker-compose파일에 명시해준 내용과 매칭을 하게 된다면 POSTGRES_DB=NAME, POSTGRES_USER=USER, POSTGRES_PASSWORD=PASSWORD, hostname=HOST가 됩니다. 기본적으로 django에서 postgresql을 사용하기 위해선 psycopg2 설치가 선행되어야합니다.

docker에서는 기본적으로 service명=host 입니다. 위의 docker-compose에서 명시적으로 hostname을 명시하기는 했지만 만약 안했어도 기본적으로 postgres로 설정 되게 됩니다.

 

3. nginx 컨테이너

- docker-compose 수정

version: '3'

services:
  postgres:
    image: postgres
    hostname: postgres
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: "postgres"
      POSTGRES_USER: "postgres"
      POSTGRES_PASSWORD: "postgres"

  server:
    build:
      context: .
    command: >
      bash -c "python3 manage.py makemigrations --settings=config.settings.deploy
      && gunicorn --bind 0.0.0.0:8000 config.wsgi.local:application"
    ports:
      - "8000:8000"
    volumes:
      - ./server:/server
    depends_on:
      - postgres
 
  nginx:
    image: nginx
    depends_on:
      - server
    volumes:
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "80:80"

- ./nginx/conf.d/default.conf

server {
        listen 80;
        server_name 본인 서버 호스트;
        charset utf-8;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        location / {
                proxy_pass http://server:80/;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location /static/ {
                autoindex on;
                alias /static/;
        }
}

nginx의 기본 설정을 바라보는 경로인 /etc/nginx/conf.d/default.conf와 로컬 경로를 docker volume을 활용하여 공유 하기 때문에 미리 위와같은 default.conf 파일이 작업된상태로 존재해야합니다.

nginx의 경우, 서버가 정상적으로 실행된 이후 마지막에 웹서버가 떠있어야하므로 의존성을 server로 설정해주었고 위에서 말했듯이 docker에서는 서비스명 = 호스트가 되므로 proxy_pass를 http://server:80으로 설정해주었습니다.

 

이후엔 이전 과정의 설정이 문제없었다면 아래 명령어를 통해 배포를 진행해주시면 됩니다.

docker compose up -d --build

 

사실 db의 경우, 실제 배포시엔 따로 데이터베이스 호스트가 이미 존재해있을것이기 때문에 요구사항에 맞춰 참고용으로 봐주시면 감사드리겠습니다 :) 

 

도커를 활용하면서 느끼는점이 물론 배포시에도 큰 이점이 있지만 초기 세팅할때 정말 편하게 활용 가능하다는점이 있다고 생각합니다.

최근, 회사에 신규 입사자가 들어오시면서 도커로 빌드가 안되어있던 프로젝트와 되어 있던 프로젝트 세팅을 하며 확실히 그차이를 느낄수 있었습니다. 초기 환경 설정에 있어서 귀찮고 힘들수도 있지만 이것이 나중을 생각했을때 불러올 스노우볼을 생각해보면 정말 좋은 오픈소스 프로젝트라고 생각합니다!!

728x90
728x90

이렇게 글을 쓰게 된 이유는 사실 django에서 소셜로그인용 라이브러리를 사용하면 쉽게 구현이 가능합니다.

but, 아무래도 매번 이렇게 사용하다보니 내부 동작이 어떻게 이뤄지고 있는지 확인이 안되었기 때문에 이번에 middleware부터 authentication, jwt 발급까지 최대한 라이브러리 사용없이 글을 작성해보려합니다.

 

카카오 소셜로그인의 경우, 위와 같이 크게
1. 인가 코드 발급
2. 토큰(kakao)
3. 사용자 로그인 처리(server의 주된 로직)

과정을 거쳐 진행됩니다.

 

이번에 이 글에서 다룰 방식은 기본적으로 1,2 번의 경우, 프론트에서 모두 발급한 이후 토큰(kakao)을 서버에 전달하여 인증 처리하는것을 다룰 것입니다. 간단하게 2번단계의 결과물인 토큰(kakao)을 발급받는 방식은 아래의 방식을 따라 가면 됩니다.

https://kauth.kakao.com/oauth/authorize?client_id={kakao 서비스 restapi key}&redirect_uri=http://127.0.0.1:8000&response_type=code
위 url을 직접 접속하여 kakao id, pass를 입력 후 로그인합니다.

http://127.0.0.1:8000/?code={code}
당연하게도, 페이지는 404를 띄어 주겠지만 상단 주소창에 있는 code 값을 복사하여 줍니다.
이것이 바로 인가코드 입니다. 이 인가코드를 통해 kakao 인증용 토큰을 발급 받을 수 있습니다.

headers = {"Content-type": "application/x-www-form-urlencoded;charset=utf-8"}
data = {
    "grant_type": "authorization_code",
    "client_id": {kakao 서비스 restapi key},
    "redirect_uri":"http://127.0.0.1:8000",
    "code": {code},
}

response = requests.post("https://kauth.kakao.com" + f"/oauth/token", data=data, headers=headers)

print(response.json())

 

이후 response의 access_token값을 서버로 보내준다가 전제입니다.

이제 이 access_token을 통해 서버에서의 전반적인 처리 과정은

1. 클라이언트로 부터 kakao access_token을 전달 받아 이 토큰을 통해 kakao의 내서비스에 회원가입 or 로그인 시킨다.

    - 인증된 사용자라면 social_id값을 저장 한다.

    - 내 서비스 인증을 위한 jwt 토큰을 생성 및 발급해준다.

2. auth api외의 다른 api에서 접근시, 헤더의 jwt 토큰을 확인할 수 있도록 authentication 및 middleware를 설정해준다.

입니다.

 

1. 클라이언트로 부터 kakao access_token을 전달 받아 이 토큰을 통해 kakao의 내서비스에 회원가입 or 로그인 시킨다.

토큰을 받는 형식은 아래와 같습니다.

POST /auth/kakao
{"access_token": "~~"}

 

- models.py

from django.contrib.auth.base_user import AbstractBaseUser

class User(AbstractBaseUser):
    nickname = models.CharField(max_length=255)
    social_id = models.CharField(max_length=255)

- views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from datetime import datetime
from datetime import timedelta
from django.conf import settings
import jwt


def generate_access_jwt(user_id):
    iat = datetime.now()
    expired_date = iat + timedelta(weeks=2)

    payload = {
        "user_id": user_id,
        "expired": expired_date.strftime("%Y-%m-%d %H:%M:%S"),
        "iat": iat.timestamp(),
    }

    return jwt.encode(payload, settings.SECRET_KEY, 'HS256')


@api_view(['POST'])
def kakao(self, request):
    data = request.data.copy()
    access_token = data.get('access_token')

    headers = {"Authorization": f"Bearer ${access_token}",
               "Content-type": "application/x-www-form-urlencoded;charset=utf-8"}
    response = requests.get("https://kapi.kakao.com/v2/user/me", headers=headers)
	
    if response.status_code == 200:
        kakao_data = response.json()
        kakao_id = kakao_data['id']
        nickname = kakao_data['kakao_account']['profile']['nickname']
        kakao_info = {"id": kakao_id, "nickname": nickname}
        
        try:
            user = User.objects.get(social_id=kakao_info["id"])
            status_code = status.HTTP_201_CREATED
        except User.DoesNotExist:
            user = User.objects.create(
            	nickname=kakao_info["nickname"],
            	social_id=kakao_info["id"],
        	)
            status_code = status.HTTP_200_OK
    	return Response({'message': '인증에 성공하였습니다.', 'access_token': generate_access_jwt(user.id)}, status=status_code)
        
    else:
        return Response({'message': '유효하지 않은 access_token입니다.'}, status=status.HTTP_400_BAD_REQUEST)

위 코드의 경우, 

1. https://kapi.kakao.com/v2/user/me api를 통해 전달받은 access_token을 body에 담아 request를 보냅니다.

2. 토큰이 유효했다면 아래와 같이 반환이 됩니다.(설정을 nickname만 받도록 해서 아래와 같이 반환되지만 추가적으로 정보 요청을 할경우, 유저의 정보가 더 추가되어 반환 됩니다.)

{
'id': 2819922431, 
'connected_at': '2023-06-04T15:43:11Z', 
'properties': {'nickname': '이형준'}, 
'kakao_account': {
'profile_nickname_needs_agreement': False, 
'profile': {'nickname': '이형준'}
}
}

3. 반환값의 id의 경우, 유일한 값이므로 이값이 만약 서비스의 social_id값과 일치하는 데이터가 존재한다면 "로그인" 아니라면, "회원가입"이 되게 됩니다.

4. 모든 인증 절차가 완료 되었다면 서비스내에서 jwt 토큰을 발급하여 반환합니다.

5. 액세스 토큰을 통한 kakao 인증 및 jwt 토큰 발급이 모두 마무리 됩니다.

이 단계를 통해 기본적으로 로그인, 회원가입이 완료 되었다면, 이젠 다른 api에서 정상적으로 유저를 식별 할 수 있는 방식을 준비해야합니다.

 

2.auth api외의 다른 api에서 접근시, 헤더의 jwt 토큰을 확인할 수 있도록 authentication 및 middleware를 설정해준다.

기본적으로 이 단계를 진행하기 전에 앞서 middleware와 authentication이 어떤것인지 간단히 알아봐야합니다.

미들웨어란 http 혹은 요청에 대한 응답에 있어 공통된 전후 처리 작업을 담당하는 역할을 합니다.

authentication의 경우, 쉽게 말해 등록한 인증 방법에 맞춰 유저를 식별해내는 방법이며, 더 쉽게 말해 기본적으로 django에서는 request.user를 통해 유저를 식별해낼수 있는 방법입니다.

자세한 내용은 아래의 내용을 참고해주시면 됩니다.

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

- views.py 추가

def decode_jwt(token):
    try:
        return jwt.decode(token, settings.SECRET_KEY, 'HS256')
    except:
        return None


def check_jwt_expired_date(now_date, expired_date):
    now_date = datetime.strptime(now_date, "%Y-%m-%d %H:%M:%S")
    expired_date = datetime.strptime(expired_date, "%Y-%m-%d %H:%M:%S")

    return True if expired_date <= now_date else False

 

- middleware.py

from datetime import datetime

from jwt import ExpiredSignatureError
from rest_framework.exceptions import PermissionDenied

from .views import decode_jwt, check_jwt_expired_date


class JsonWebTokenMiddleWare(object):

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        try:
            if (
                request.path != "/auth/kakao"
                and "admin" not in request.path
                and "swagger" not in request.path
            ):
                access_token = request.headers.get("Authorization", None)
                if not access_token:
                    raise PermissionDenied()

                auth_type, token = access_token.split(' ')
                if auth_type == "Bearer":
                    payload = decode_jwt(token)
                    if not payload:
                        raise PermissionDenied()

                    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    token_expired = payload.get('expired')

                    if check_jwt_expired_date(now, token_expired):
                        raise ExpiredSignatureError()

                    user_id = payload.get("user_id", None)
                    if not user_id:
                        raise PermissionDenied()

                else:
                    raise PermissionDenied()

            response = self.get_response(request)
            return response

        except (PermissionDenied, User.DoesNotExist):
            return JsonResponse(
                {"error": "Authorization Error"}, status=status.HTTP_401_UNAUTHORIZED
            )

        except ExpiredSignatureError:
            return JsonResponse(
                {"error": "Expired token. Please log in again."},
                status=status.HTTP_403_FORBIDDEN,
            )

1. 기본적으로 인증을 진행해야하는 /auth/kakao와 swagger, admin 페이지의 경우, token이 없어도 접속이 가능해야함을 전제로 하여 middleware에서 바로 통과하도록 설정하였습니다.

2. header의 Autorization에 담겨있는 token을 가져와 유효성을 증명하고 문제없다면 통과 아니라면 middleware단에서 오류 반환 합니다.

 

- authentication.py

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

from .views import decode_jwt
from .models import User


class JsonWebTokenAuthentication(BaseAuthentication):

    def authenticate(self, request):
        access_token = request.headers.get("Authorization", None)
        if not access_token:
            return None

        auth_type, token = access_token.split(' ')
        payload = decode_jwt(token)

        user_id = payload.get("user_id", None)
        try:
            user = User.objects.get(id=user_id)
            return user, None
        except User.DoesNotExist:
            raise AuthenticationFailed({"message": "INVALID_TOKEN"})

1. 기본적으로 이미 middleware를 통해 token이 유효하다는것이 증명 되었으므로 authentication에서는 다른 유효성 검증 절차를 진행하지 않습니다.

2. access_token에 해당하는 user값을 반환해줍니다.

 

위의 과정을 거쳐 인증이 진행되며 작성한 코드의 경우, settings.py에서 등록해줘야합니다.

- settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        '<app_name>.<file_name>.<class_name>',
    ]
}

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    '<app_name>.<file_name>.<class_name>',
]

 

모든 인증 관련 로직 준비가 완료되었습니다.

추가로 생각해봐야할점은

1. 클라이언트와 서버의 인증에 있어 역할의 분리를 클라이언트 = access_token까지 발급으로 나눴는데 과연이게 정답일지? 아무래도 키값을 클라이언트에서 가지고 있다보니 탈취의 위험도가 있는데 이것이 과연 괜찮은 방법일지는 고려를 해봐야합니다.

2. middleware단에서 로직을 처리할때 물론 개발에 있어 편의성을 굉장히 늘어나지만, 일부 로직에서는 의미 없는 리소스가 소모 된다는 단점 또한 존재합니다. 이것이 middleware에서 처리하기 적합한 과정일지는 요구사항에 맞춰 결정해야합니다.

 

이글의 경우, 글 작성의 편의성을 위해 대부분의 코드가 분리 되어있지않아 있으며 최소한의 예외처리가 진행되었지만 전체적인 흐름 파악의 의미에서 참고를 한후 개발을 진행하면 좋을것 같습니다.

감사합니다 :)

728x90

+ Recent posts