Skip to main content

BigQuery DataFrames를 통해 Gemini로 텍스트 데이터를 보강하는 방법

· 4 min read

image

데이터 증대(Data Augmentation)는 기존 데이터에서 새로운 데이터를 생성하여 데이터 세트의 크기를 늘리기 위해 기계 학습에서 사용되는 기술입니다. 이 기술은 모델이 훈련된 데이터에 대한 과적합을 방지하여 더 효과적으로 일반화하는 데 도움이 될 수 있습니다.

데이터 회전, 뒤집기, 자르기 등을 통해 시각적 데이터에서 이 작업을 수행하는 것을 종종 생각합니다. PyTorch에는 단 몇 줄의 코드만으로 데이터 세트에 무작위 변환을 적용할 수 있는 매우 유용한 변환 패키지가 있습니다. 이로 인해 훈련 세트의 정확도가 감소할 수 있지만, 보이지 않는 데이터의 테스트 세트에 대한 정확도가 향상되는 경우가 많습니다. 이것이 바로 정말 중요합니다!

image

예: 암석 이미지의 데이터 확대 출처: TseKiChun, CC BY-SA 4.0, Wikimedia Commons 경유

이 동일한 기술을 텍스트 데이터에 적용할 수 있습니다. 이는 기존 데이터 세트를 확장하고 모델을 노이즈 및 이상값에 더욱 강력하게 만드는 것과 동일한 이점을 제공합니다. 모든 데이터 세트의 데이터 확대에는 입증된 이점이 있으며, 이는 특히 소규모 데이터 세트에 유용합니다.

널리 사용되는 기술을 사용하여 몇 가지 예를 살펴보겠습니다.

image

이러한 기술을 수동으로 적용할 수 있는 방법에는 여러 가지가 있습니다.삭제된 토큰의p=0.1인 무작위 삭제 기술을 적용한다고 가정해 보겠습니다.텍스트를 토큰화한 다음(1-p)확률로 토큰을 다시 추가할 수 있습니다. 또는 역번역의 경우 대상 언어에 대해Translation API를한 번 호출한 다음 다시 원래 언어로 번역하기 위해 두 번째 호출할 수 있습니다. 동의어의 경우임의 토큰에WordNet API를사용할 수 있습니다.

Gemini와 같은 강력한 LLM을 사용하면손끝에서 많은 트릭을 얻을 수 있습니다. 하나의 도구 세트에서 이러한 수정 작업을 비롯한 다양한 작업을 쉽게 수행할 수 있습니다. 더 이상 여러 도구를 엮을 필요가 없습니다.

스택 오버플로 질문과 답변으로 구성된 실제 데이터 세트에 이러한 기술을 적용하는 방법을 살펴보겠습니다. 모든 세부 사항은 이노트에 제공되어 있으며 여기서 주요 내용을 지적하겠습니다.

모든 종류의 문제에 BigQuery DataFrame을사용할 수 있지만BigQuery 데이터 세트의 텍스트 확대가 특히 간단해집니다. 이는 Gemini를 직접 쿼리할 수 있는 pandas 호환 DataFrame 및 scikit-learn과 유사한 ML API를 제공합니다. 모든 DataFrame 저장소가 BigQuery에 있으므로 대규모 데이터세트에 대한 일괄 작업을 처리할 수 있습니다.

이제 이러한 기술 중 하나인 동의어 교체를 시작해 보겠습니다. 먼저, 2020년부터 허용된 Stack Overflow Python 답변을 쿼리하고 이를 BigQuery DataFrame에 넣을 수 있습니다.

stack_overflow_df = bpd.read_gbq_query(
"""SELECT
CONCAT(q.title, q.body) AS input_text,
a.body AS output_text
FROM `bigquery-public-data.stackoverflow.posts_questions` q
JOIN `bigquery-public-data.stackoverflow.posts_answers` a
ON q.accepted_answer_id = a.id
WHERE q.accepted_answer_id IS NOT NULL
AND REGEXP_CONTAINS(q.tags, "python")
AND a.creation_date >= "2020-01-01"
LIMIT 550
""")

다음은 Q&A DataFrame을 살짝 엿본 것입니다.

image

이제 데이터프레임에서 여러 행을 무작위로 샘플링해 보겠습니다. n_rows를 원하는 새 샘플 수로 설정합니다.

df = stack_overflow_df.sample(n_rows)

그런 다음 Gemini 텍스트 생성기 모델을 정의할 수 있습니다.

model = GeminiTextGenerator()

다음으로 입력 텍스트와 연결된 동의어 대체 지침이 포함된 프롬프트 열과 동의어 대체가 적용된 결과 열이라는 두 개의 열을 생성해 보겠습니다.

# Create a prompt with the synonym replacement instructions and the input text
df["synonym_prompt"] = (
f"Replace {n_replacement_words} words from the input text with synonyms, "
+ "keeping the overall meaning as close to the original text as possible."
+ "Only provide the synonymized text, with no additional explanation."
+ "Preserve the original formatting.\n\nInput text: "
+ df["input_text"])

# Run batch job and assign to a new column
df["input_text_with_synonyms"] = model.predict(
df["synonym_prompt"]
).ml_generate_text_llm_result

# Compare the original and new columns
df.peek()[["input_text", "input_text_with_synonyms"]]

결과는 다음과 같습니다! 동의어 대체를 통해 텍스트의 미묘한 변화를 확인하세요.

image

이 프레임워크를 사용하면 모든 종류의 일괄 변환을 적용하여 데이터를 늘리는 것이 간단합니다. 노트북에는 역번역 및 노이즈 주입에 사용할 수 있는 더 많은 프롬프트가 표시됩니다. 또한 BigQuery DataFrames를사용하여 데이터 세트를 향상하는 것이 얼마나 쉬운지 확인했습니다.

Retrieval Augmented Generation(RAG) - LlamaIndex를 이용한 문서용 챗봇 만들기

· 6 min read

image

문서에 대화형 챗봇을 위한 Langchain과 LlamaIndex를 사용하여 RAG 기술을 구현합니다.

RAG는 프롬프트 엔지니어링 기술은 아니지만 벡터 임베딩을 사용하고 이를 LLM에 컨텍스트로 전달하여 사용 사례별 데이터/컨텍스트로 프롬프트를 향상시키는 데 사용됩니다.

RAG 구현은 지속적으로 발전해 왔으며 LLM에서 사용자 정의 데이터 소스를 사용하기 위한 새롭고 가장 선호되는 프레임워크인 LLamaIndex에 대해 이야기하여 사용자 정의 데이터를 쿼리하는 것이 좋을 것이라고 생각했습니다 .

LlamaIndex

LlamaIndex는 사용자 지정 콘텐츠/데이터를 수집하고 해당 콘텐츠를 쿼리할 수 있는 프레임워크입니다. 이는 LLM이 반드시 사용 사례별 콘텐츠/데이터/문서로 교육/세분화될 필요가 없는 대부분의 기업 사용 사례에서 매우 강력한 요구 사항입니다.

예를 들어, 저는 생산 사건/티켓/이벤트 중에 적용하는 사용자 정의 문서(예: SOP(표준 운영 절차)와 같은 표준 운영 절차)가 있는 다양한 고객과 협력합니다. 이러한 경우 테라바이트 단위의 Word를 매우 쉽게 수집할 수 있습니다. 문서 및 PDF 문서를 사용하여 엔지니어가 문서를 쿼리하는 데 사용할 수 있는 봇을 보유하고 LLM 에이전트를 사용하여 이를 자동화하여 ChatOps I의 일부로 사건 및 컨텍스트를 기반으로 적절한 콘텐츠를 검색할 수 있습니다. 곧 앱/플랫폼 개발/운영에서 LLM을 사용하는 방법에 대해 설명하는 몇 개의 블로그를 작성할 예정입니다.

LlamaIndex는 고급 RAG 애플리케이션을 생성하기 위한 라이브러리를 제공하는 프레임워크입니다.

Architecture

다음 그림은 LlamaIndex 작동 방식에 대한 높은 수준의 접근 방식을 보여줍니다.

image

이 다이어그램을 더 잘 이해해 봅시다.

  • LlamaIndex는 문서, 데이터베이스, API 등 다양한 유형의 콘텐츠를 수집할 수 있는 프레임워크를 제공합니다. 이를 통해 여러 유형의 콘텐츠가 포함된 LLM 애플리케이션을 구축할 수 있는 강력한 프레임워크가 되며 쿼리에 대한 통합된 응답을 원합니다.
  • LlamaIndex에는 2가지 주요 단계가 있습니다. 로드/인덱싱 및 쿼리.
  • 로딩 및 인덱싱 단계에서 수집된 문서는 콘텐츠 덩어리로 분류됩니다. 이러한 청크는 임베딩 모델을 사용하여 임베딩으로 변환됩니다. 이는 유사한 콘텐츠가 다차원 공간에 더 가깝게 매핑되어 콘텐츠의 벡터 표현을 생성합니다. 이 벡터는 벡터 DB에 저장되며(Pinecone 등과 같은 사용자 정의 벡터 데이터베이스도 제공 가능) LlamaIndex는 더 빠른 의미 검색을 위해 인덱스도 저장합니다.
  • 쿼리가 실행되면 쿼리는 임베딩 벡터로 변환되고 벡터 데이터베이스에서 의미론적 검색이 수행되어 쿼리의 컨텍스트 역할을 할 수 있는 모든 유사한 콘텐츠를 검색합니다. 그런 다음 응답을 위해 대규모 언어 모델로 전달됩니다.

구현

LlamaIndex를 사용하여 문서를 쿼리하는 Q&A 챗봇을 구현해 보겠습니다.

설치 환경

저는 일반적으로 각 프로젝트마다 별도의 가상 환경을 생성하여 애플리케이션을 실행하고 버전 충돌을 방지하기 위한 올바른 구성과 환경이 있는지 확인합니다.

python3 -m venv ./llamaindex-venv #setup a environment
source ./llamaindex-venv/bin/activate #activate the environment
pip install -r requirements.txt #install all the required python libraries

다음은 이 애플리케이션을 구현하는 데 사용할 모든 라이브러리가 포함된 요구사항.txt를 보여줍니다.

python-dotenv
openai
llama-index
pypdf
streamlit
nltk

모든 문서를 넣을 수 있는 폴더를 만들어야 합니다. 나는 “document”에 전화하고 있습니다. 폴더를 생성할 수 있지만 폴더/경로의 이름이 무엇인지 확인하고 이에 따라 코드를 업데이트하십시오.

파일을 폴더에 복사합니다(사용하려는 PDF 파일을 복사할 수 있지만 이 예에서는 Cannon 사용자 매뉴얼 사이트에서 다운로드한 Cannon EOS-R 매뉴얼을 사용하고 있습니다).

image

애플리케이션 코드

다음 코드에서는 필요한 모든 라이브러리를 가져옵니다.

image

  • streamlit: 이것은 애플리케이션을 streamlit구축하는 데 사용할 라이브러리입니다.streamlit
  • os: 운영 체제에 액세스하고 폴더/파일이 존재하는지 확인하려면 이 라이브러리가 필요합니다.
  • dotenv: OpenAI API 키를 파일에 저장 .env하고 이 라이브러리를 사용하여 환경 변수를 로드합니다.
  • VectorStoreIndex: VectorStoreIndex는 액세스하는 데 사용할 가장 중요한 개체입니다.
  • SimpleDirectoryReader: SimpleDirectoryReader는 문서 유형에 따라 올바른 문서 리더를 자동으로 선택합니다. load_data()콘텐츠를 추출하여 Document 개체 목록으로 변환하는 방법을 사용하겠습니다.
  • load_index_from_storage: 저장된 저장 파일에서 인덱스를 불러오는 데 사용됩니다.
  • StorageContext: 이 개체는 프레임워크 전체에서 사용되는 노드, 인덱스 및 벡터에 대한 컨테이너입니다.
  • ServiceContext: 이 객체는 llm, 프롬프트 도우미 등 모든 상황별 정보를 저장하는 유틸리티 클래스로 사용됩니다.

자세한 내용은 공식 문서를 참조하세요.

다음 코드를 살펴보겠습니다.

image

15행 : 내 OpenAI 키 값이 포함된 OPENAI_API_KEY 환경 변수가 있는 .env 파일을 로드합니다.

image

16행 : VectorDB를 저장할 폴더의 위치를 ​​설정합니다. 이 경우 로컬 디스크에 저장되는 간단한 벡터db를 구현합니다. 보다 확장 가능한 솔루션은 PineCone 등과 같은 SaaS 솔루션을 사용하는 것입니다.

17행 : 수집할 모든 문서를 저장할 "./documents" 문서의 위치를 ​​설정합니다.

18-30행 : 벡터 저장소를 초기화하고 있습니다. 문서를 이미 인덱스에 수집한 경우에는 인덱싱 및 저장을 수행할 필요가 없습니다. initialize()메소드는 인덱스가 이미 존재하는지 확인한 다음 기존 데이터베이스에서 로드하고, 그렇지 않으면 새 데이터베이스와 인덱스를 생성합니다.

Index : LlamaIndex의 주요 기능 중 하나는 수집된 콘텐츠를 인덱스로 구성하는 방식입니다. 이러한 인덱스를 사용하여 쿼리에 응답합니다. 수집된 콘텐츠는 여러 개의 청크로 나누어지며, 이를 노드라고도 합니다. 이러한 노드는 목록, 트리, 키워드 및 벡터로 색인화됩니다. 노드(청크)의 모든 콘텐츠는 벡터 임베딩으로 저장됩니다. 추론 중에 모든 LLM에는 토큰 제한이 있으므로 청킹은 매우 중요합니다. 이를 방지하기 위해 LlaamaIndex는 이러한 다양한 청크를 특정 순서로 사용하여 LLM을 쿼리하고 응답을 구체화합니다. 하나의 추론 요청으로 모든 문서 콘텐츠를 전달하는 것은 사실상 불가능하기 때문입니다.

이 방법을 사용하면 다음 파일이 생성됩니다.

image

  • default__vectors_store.json: 이 파일에는 임베딩 사전으로 저장된 모든 벡터 임베딩이 있습니다.
  • docstore.json: 이 파일은 문서 메타데이터와 문서 청크를 저장합니다.
  • index_store.json: 이 파일에는 모든 색인 메타데이터가 있습니다.
  • graph_store.json: 이 파일은 그래프를 저장하는 데 사용됩니다. 이에 대해서는 이후 블로그에서 graphdb를 수집할 때 논의할 것입니다.
  • image__vector_store.json: 이 파일은 이미지 콘텐츠의 임베딩을 저장하는 데 사용됩니다. 이에 대해서는 이후 블로그에서 이미지를 수집하는 방법에 대해 논의할 것입니다.

image

34-56행 : 채팅 종류의 인터페이스를 갖춘 간소화된 애플리케이션을 만들고 있습니다. 메시지를 st.session_state( 36, 43행 ) 에 저장하고 이는 기본 창에 채팅( 45-46행 )으로 인쇄됩니다 . st.chat_input()( Line 42–44 )에 제공된 프롬프트를 캡처 하고 Line 40에서 만든 chat_engine()( Line 52 )를 호출합니다 . LlamaIndex는 생성된 인덱스로 채팅 엔진을 생성할 수 있는 편리한 기능을 제공합니다. 이는 RAG 수행의 모든 ​​복잡성을 처리하고 응답을 얻기 위해 적절한 LLM을 호출합니다.

애플리케이션 실행

이는 간소화된 애플리케이션이므로 다음 명령을 사용하여 실행해야 합니다.

스트림라이트 실행 qa_chat.py

그러면 채팅 애플리케이션이 포함된 브라우저가 시작됩니다. 다음 스크린샷은 출력을 보여줍니다.

image

image

콘솔에서는 상위 2개 옵션에 대한 예측과 유사성 점수를 볼 수 있습니다. 이는 pprint_response(response, show_source=True)(라인 54)에 의해 콘솔에 인쇄됩니다 .

image

결론

자, 보시다시피 LlamaIndex를 사용하여 복잡한 RAG 패턴을 구현하는 것은 매우 쉽습니다.

Wagtail 사용법

· 6 min read

image

얼마 전 썼던 Wagtail vs Wordpress - 프로젝트에 적합한 CMS를 선택하는 방법에 대해 글을 작성했는데 Wagtail에 대해 직접 써보고 작성해 보고자 글을 적어봤습니다.

Wagtail과 Wordpress는 모두 뛰어난 오픈 소스 콘텐츠 관리 시스템입니다. 프로젝트에 가장 적합한 기술을 결정할 때 고려해야 할 몇 가지 사항은 다음과 같습니다. Overview 오늘날 사용할 수 있는 오

Wagtail에 대해 알게 된 배경은 한번씩 검색해 보는 Trending 에서 Python 언어로 검색했을 때 높은 순위에 있었기 떄문입니다.

image

Wagtail은 위에 설명과 같이 CMS(Content Management System)에 사용할 수 있는 Wordpress 같은 기능을 할 수 있는 오픈소스 솔루션이었습니다. wordpress를 써 본 적은 없지만 php 기반의 wordpress 보다는 python djanog 기반의 wagtail이 더 나을 것이란 생각을 했습니다.(개인적 취향입니다.)

Wordpress는 개발자가 아닌 사람들이 블로그나 간단한 소개 웹 싸이트를 만들기 쉽게 구성되어 있습니다.

하지만 커스터마이징을 위해서는 플러그인을 사용해야 하는데, 문제는 플러그인으로 인해 보안에 문제가 될 수 있다는 겁니다.

하지만 Wagtail은 Django 기반의 솔루션으로서 Wordpress와 같이 페이지를 rich text 로 작성할 수 있으면서도 그 컴포넌트들을 코드 상에 배치하고 Django framework을 이용하듯 사용할 수 있는 장점이 있습니다. 

Wordpress 보다 개발자 친화적이고 구성 방법도 어렵지 않습니다. 심지어 공식 문서도 잘 되어 있으니 한번 읽어 보는 것이 좋습니다. 그리고 NASA에서도 사용한다고 합니다.

Welcome to Wagtail’s documentation — Wagtail Documentation 6.0.2 documentation

시작하는 방법은 간단합니다.

저는 Mac OS 기준으로 작성합니다. Windows OS도 거의 비슷한데 경로 설정이나 약간의 환경적인 부분이 다를 수 있으니 유의하세요.

우선 가상 환경을 생성하고 가상 환경을 활성화 합니다.(프로젝트 별로 환경을 별도로 두는 것이 글로벌 환경에 영향을 덜 주고 깔끔합니다.)

# Create a virtual environment
python -m venv venv
# Activate the virtual environment
source venv/bin/activate

가상 환경이 활성화 되었다면 이제 라이브러리를 설치해 줍니다.

# Upgrade pip
pip install --upgrade pip
# Install Wagtail
pip install wagtail

이제 wagtail 라이브러리를 사용할 수 있습니다.

wagtail을 이용해 프로젝트를 생성해 보세요.

# Create a new project
wagtail start myapp
# Go into the project
cd myapp
# Install requirements
pip install -r requirements.txt

myapp 폴더에 들어가면 아래와 같이 폴더 구조가 보입니다.(db.sqlite3은 다음 단계에서 생성됩니다.)

image

생성된 폴더에서 필요한 라이브러리들도 모두 설치했으니 myapp/myapp/settings/base.py 에 있는 데이터베이스 설정을 원하는대로 변경해 줍니다. (변경을 하지 않아도 default로 있던 sqlite3를 그대로 사용해도 됩니다.)

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}

이제 기본 wagtail 솔루션 실행을 위한 데이터베이스 마이그레이션 및 서버 실행을 진행합니다.

python manage.py migrate
# Create superuser
python manage.py createsuperuser
# Run server
python manage.py runserver
# Open Website in browser
http://localhost:8000
# Open Admin Website in browser
http://localhost:8000/admin

wagtail 프로젝트를 생성할 때 이미 기본 마이그레이션을 생성해 뒀기 때문에 python manage.py makemigrations를 하지 않고 바로 python manage.py migrate를 진행한 겁니다. 아무튼 이렇게 해서 어드민 계정도 생성했고 서버도 실행하면 8000포트로 솔루션 초기 화면과 관리자 화면을 확인할 수 있습니다.

image

wagtail 본 화면은 React 초기 화면과 같이 아무런 의미가 없습니다. admin으로 들어가서 superuser 생성 때 만든 계정을 적어서 로그인 해줍니다. 어드민에 로그인하고 나면 아래와 같이 pages, images, documents, reports, settings, help 메뉴가 보입니다. (search 는 검색하는 거라서 생략)

image

페이지를 누르면 edit 아이콘이 보인느데 그걸 눌러줍니다.

image

잠시 코드로 돌아와서 myapp/home/model.py 를 아래와 같이 수정합니다.

from django.db import models

from wagtail.models import Page
from wagtail.fields import RichTextField

# import MultiFieldPanel:
from wagtail.admin.panels import FieldPanel, MultiFieldPanel


class HomePage(Page):
# add the Hero section of HomePage:
image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
help_text="Homepage image",
)
hero_text = models.CharField(
blank=True,
max_length=255, help_text="Write an introduction for the site"
)
hero_cta = models.CharField(
blank=True,
verbose_name="Hero CTA",
max_length=255,
help_text="Text to display on Call to Action",
)
hero_cta_link = models.ForeignKey(
"wagtailcore.Page",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name="Hero CTA link",
help_text="Choose a page to link to for the Call to Action",
)

body = RichTextField(blank=True)

# modify your content_panels:
content_panels = Page.content_panels + [
MultiFieldPanel(
[
FieldPanel("image"),
FieldPanel("hero_text"),
FieldPanel("hero_cta"),
FieldPanel("hero_cta_link"),
],
heading="Hero section",
),
FieldPanel('body'),
]

또한 myapp/home/templates/home/home_page.html을 아래와 같이 수정합니다.

위에 model에서 추가된 필드들을 html에서 불러오도록 맵핑해주는 작업입니다. 원래는 보여주는 것이 page.title 밖에 없었는데 page.image, page.hero\_text, ... 추가 된 것을 볼 수 있습니다. 또한 가장 중요한 명시인 load wagtailcore\_tags는 꼭 있어야 합니다. 이것이 있어야 내용을 보여준다고 합니다.

{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}

{% block body_class %}template-homepage{% endblock %}

{% block content %}
<div>
<h1>{{ page.title }}</h1>
{% image page.image fill-480x320 %}
<p>{{ page.hero_text }}</p>
{% if page.hero_cta_link %}
<a href="{% pageurl page.hero_cta_link %}">
{% firstof page.hero_cta page.hero_cta_link.title %}
</a>
{% endif %}
</div>

{{ page.body|richtext }}
{% endblock content %}

위에서 model을 수정했으니 마이그레이션을 진행해줍니다.

python manage.py makemigrations
python manage.py migrate
# Create superuser
python manage.py createsuperuser
# Run server
python manage.py runserver

다시 실행한 화면에서 admin에 들어온 뒤 다시 page edit 화면으로 온 뒤 아래와 같이 수정해 줍니다.

이미지 추가부터 텍스트 입력 모든 것이 가능합니다. 아까 수정한 html을 이용해 다른 수정들도 또 가능합니다.

또한 body 는 입력이 RichTextField이기 때문에 텍스트 편집기 작성하듯 편하게 작성할 수 있습니다.

게다가 아래 오른쪽을 보면 프리뷰까지 보여줍니다.(모바일, 웹, 테블릿 등 환경 별로도 보여줍니다.)

image

오른쪽 상단에 라이브 버튼을 누르면 아직 프리뷰 화면에 작성한 내용이 보이지 않고 있는데,

image

이건 Pages 버튼을 눌러 페이지 목록으로 간 뒤 해당 페이지를 체크하면 아래 publish 버튼이 보이는데 이걸 클릭하면 라이브에도 반영됩니다!

image

wagtail을 이용하면 아래와 같이 문서, 이미지를 쉽게 등록하고 이것을 페이지에 보여주는 것도 자연스럽게 이용해 보여줄 수 있습니다.

image

image

또한 히스토리 관리도 해주고, 싸이트 설정, 유저 및 그룹 관리, 리다이렉트 등 엔터프라이즈 급의 관리를 아무 코딩 없이 바로 솔루션을 통해 사용 가능합니다. 

image

image

구성에 대한 이해도 직관적으로 알 수 있습니다. html에 database에서 관리되는 필드들을 저장하고 가져와 보여주는 방식입니다.

html에 등록할 때 Django 유저에 친숙하게 {{}} 를 통해 값을 찝어 넣어주면 됩니다.

아직 많이 사용해 보지 않아 익숙하진 않지만 조금 더 사용하다 보면 CMS 기능을 만들 때는 이걸로 계속 만들지 않을까 생각합니다.

배포 방법은 공식 문서를 참조하면 좋습니다. Dockerfile도 있기 때문에 Dockerize 할 수 있는 방법도 제공합니다.

Wagtail이 정말 좋은 솔루션이라는 생각이 들었고 더 많이 사용하게 되서 오픈 소스가 더 발전해 나가고 모두에게 좋은 기회가 되길 기원합니다!

GCP에서 Serverless 구성하기(API Gateway + Cloud Run)

· 15 min read

image

컨테이너화된 애플리케이션이 제공되는 클라우드 서비스 공급자(CSP)에서 현대적인 3계층 서버리스 애플리케이션을 생성하는 것은 많은 클라우드 엔지니어의 일반적인 작업입니다. 문제는 개발 노력을 가속화하고 비용과 복잡성을 줄이는 동시에 다양한 애플리케이션 계층에 대한 CSP의 광범위한 서비스를 효율적으로 활용할 수 있다는 것입니다. 이 문서에서는 배포 시간을 단축하는 동시에 비용과 복잡성을 줄이는 현대적인 3계층 서버리스 애플리케이션을 Google Cloud에 배포하기 위한 단계별 가이드와 이 구현에 사용되는 서비스에 대한 자세한 설명을 제공합니다.

프런트엔드(프레젠테이션 계층), 백엔드(애플리케이션 계층), Firestore 데이터베이스(데이터 계층)로 구성된 3계층 마이크로서비스 아키텍처를 사용하는 "Amazing Employees"라는 예제 애플리케이션을 사용하겠습니다. Angular를 사용하여 개발된 프런트엔드는 사용자와 상호 작용하고, Python Flask로 구축된 백엔드는 비즈니스 로직을 관리합니다. “Amazing Employees”는 단일 프런트엔드 및 백엔드 컨테이너로 구성됩니다. 그러나 마이크로서비스 아키텍처는 확장 가능합니다. 더 많은 컨테이너를 추가하면 더 많은 기능을 활용할 수 있습니다. 애플리케이션의 인프라는 Terraform을 통해 정의되며 Cloud Build는 애플리케이션의 빌드 및 배포를 관리합니다.

image

그림 1: Google Cloud 3계층 서버리스 아키텍처.

왜 서버리스인가?

서버리스 기술은 인프라 관리의 복잡한 작업 대부분을 CSP에 오프로드하여 개발자가 애플리케이션 코드 생성 및 배포에 집중할 수 있도록 합니다. 서버리스 제품은 다음을 포함하여 많은 이점을 제공합니다.

  • 거의 무한한 자동 스케일링
  • 고가용성
  • 단순화된 인프라 관리
  • 잠재적인 비용 절감(사용한 만큼만 비용 지불)
  • 더 빠른 개발 주기

서버리스 기술은 마이크로서비스 아키텍처와 같이 확장성이 뛰어난 여러 아키텍처 유형을 지원하는 데 도움이 됩니다. 마이크로서비스는 애플리케이션을 독립적으로 배포할 수 있는 더 작은 단위로 나눕니다. 개별 마이크로서비스를 동적으로 확장하고 오류를 격리하는 기능을 통해 개발자는 응답성이 뛰어나고 비용 효율적인 솔루션을 만들 수 있습니다. 서버리스 기술은 마이크로서비스 애플리케이션을 생성하는 데 필요하지 않지만 관리 및 확장 측면을 크게 단순화하므로 리소스 활용도를 최적화하고 개발 노력을 간소화하려는 조직에 이상적인 선택입니다.

서버리스 애플리케이션의 또 다른 일반적인 아키텍처는 이벤트 중심 아키텍처입니다. 이 아키텍처에서는 애플리케이션이 사용자 작업, 데이터 변경, 외부 시스템 상호 작용 등 다양한 소스에 의해 트리거되는 이벤트에 동적으로 응답합니다. 이를 통해 개발자는 들어오는 이벤트에 따라 자동으로 확장되는 시스템을 설계하여 최대 부하 시 최적의 성능을 보장하고 조용한 기간 동안 비용 절감을 보장할 수 있습니다.

응용 서비스

'Amazing Employees' 애플리케이션의 프런트엔드와 백엔드를 위해 Google Cloud의 완전 관리형 서버리스 컨테이너 환경인 Cloud Run을 활용합니다. Cloud Run은 Google Cloud가 개척한 서버리스 컨테이너 기술인 Knative를 기반으로 합니다. Cloud Run은 인프라 계층을 추상화하고 기본 Kubernetes 클러스터를 관리할 필요가 없기 때문에 Google Kubernetes Engine(GKE)과 같은 다른 컨테이너 조정 서비스보다 사용하기 쉬운 경우가 많습니다. Cloud Run은 수신 트래픽을 기준으로 컨테이너를 자동으로 확장합니다. Google Cloud에서 서버리스 프런트엔드 또는 백엔드를 구축하는 데 널리 사용되는 또 다른 선택인 Cloud Functions의 최신 버전은 백그라운드에서 Cloud Run을 사용합니다.

물론 단순함을 통해 얻는 것은 통제력을 잃게 됩니다. 이는 클라우드의 모든 관리형 서비스에 해당됩니다. 사용 사례에서 컨테이너를 완전히 제어해야 하는 경우(세부적인 수준에서 네트워킹, 확장, 리소스 할당 구성 등) Cloud Run 대신 GKE를 사용해야 할 수도 있습니다. 가능하다면 Cloud Run과 같은 완전 관리형 서비스를 사용하는 것이 좋습니다. 간단히 말해 삶이 훨씬 편해지기 때문입니다.

API 게이트웨이는 프런트엔드와 백엔드 Cloud Run 인스턴스 간의 트래픽을 보호하는 데 사용됩니다. 일반적으로 API 게이트웨이를 사용하는 것은 복잡성을 줄이면서 보안, 성능 및 유연성을 향상시키기 때문에 마이크로서비스 아키텍처를 구축할 때 모범 사례입니다. Google Cloud의 API 게이트웨이에는 API, 게이트웨이, 구성이라는 세 가지 주요 구성요소가 있습니다. 구성은 OpenAPI 사양을 사용하며 infra 디렉터리에서 볼 수 있습니다. 두 가지 변수가 사용되므로 수동으로 변경하지 않고도 구성을 배포할 수 있습니다. 첫 번째 변수는 Variable.tf 에 제공되는 ${project\_id} 이고 , 두 번째 변수는 ${url} 입니다 . ${url} 변수 값은 백엔드 Cloud Run 인스턴스의 URL을 확인하고 이를 구성에 추가하는 Terraform 데이터 블록에서 제공됩니다. 두 경우 모두 변수 대체는 Terraform을 통해 수행되며 infra/api-gateway.tf 에서 볼 수 있습니다 .

애플리케이션 데이터베이스의 경우 서버리스 아키텍처에 원활하게 통합되는 Firestore가 자연스러운 선택이었습니다. Google Cloud Firebase 서비스의 NoSQL 데이터베이스인 Firestore는 주로 자동 확장, 강력한 쿼리 지원, 고급 보안 규칙 및 신뢰할 수 있는 애플리케이션 인증을 제공하는 기능 때문에 선택되었습니다. 우리 애플리케이션의 맥락에서 우리는 사용자 로그인에 Firebase 인증을 사용하고 직원 데이터 관리를 위해 기본 CRUD 데이터베이스 작업을 사용합니다.

인프라 및 배포

코드형 인프라의 경우 HashiCorp가 개발한 오픈 소스 도구인 Terraform을 사용합니다. Terraform을 사용하면 Google Cloud를 포함한 다양한 클라우드 제공업체 전반에 걸쳐 선언적 방식으로 인프라 리소스를 정의, 관리, 프로비저닝할 수 있습니다.

선택적으로 Cloud Storage를 사용하여 Terraform 상태를 저장할 수 있습니다. Cloud Storage는 파일, 미디어 등 구조화되지 않은 데이터를 저장할 수 있고 여러 스토리지 클래스에 걸쳐 방대한 양의 데이터를 관리할 수 있는 확장성을 제공하는 Google Cloud 내의 객체 스토리지 서비스입니다. Cloud Storage는 복제를 통해 데이터 내구성을 제공하고 짧은 지연 시간으로 접근성을 제공합니다. 액세스 제어 및 암호화와 같은 보안 기능; 버전 관리, 수명 주기 관리, 다른 Google Cloud 서비스와의 통합 등이 있습니다.

애플리케이션의 프런트엔드와 백엔드를 구축하기 위해 Google Cloud에서 빌드를 실행할 수 있는 Google Cloud의 서버리스 CI(지속적 통합) 및 CD(지속적 배포) 플랫폼인 Cloud Build를 사용합니다.

컨테이너 이미지를 저장하기 위해 우리는 Java, Node, Python용 패키지와 같은 소프트웨어 아티팩트에 대한 안전하고 확장 가능한 저장소를 제공하는 Google Cloud의 관리형 서비스인 Artifact Registry를 활용하지만, 이 경우에는 Docker 이미지를 저장하는 데 이를 사용하겠습니다.

다음은 Cloud Build 작업의 흐름을 보여주는 단계 및 개략적인 다이어그램에 대한 설명입니다.

  1. 사용자가 Google Cloud 명령어를 사용하여 로컬 저장소에서 Cloud Build를 트리거합니다.
  2. Docker 이미지는 현재 작업 디렉터리에서 빌드됩니다.
  3. Docker 이미지가 Artifact Registry로 푸시됩니다.
  4. Cloud Run 인스턴스는 Docker 이미지에서 배포됩니다.

image

그림 2: Cloud Build 실행 흐름.

비용에 대한 참고 사항

이 문서는 Google Cloud의 서버리스 제품을 살펴보고 싶어하는 클라우드 엔지니어를 위해 작성되었습니다. 따라서 비상업적 사용을 위한 일반적인 애플리케이션 배포에는 Google Cloud 무료 프로그램이 적용되어야 합니다. 자세히 알아보려면 Google Cloud 무료 등급 제품 페이지 (https://cloud.google.com/free) 를 방문하세요 . 다음은 이 튜토리얼에 사용할 서비스 및 관련 비용(2023년 9월 기준)입니다.

  • Cloud Run 무료 등급은 월간 요청 200만 개, 메모리 360,000GB-초, 컴퓨팅 시간 180,000vCPU-초, 네트워크 송신 1GB(북미만 해당)를 제공합니다.
  • Firestore 무료 등급은 프로젝트당 1GB의 스토리지와 프로젝트당 일일 읽기 50,000회, 쓰기 20,000회, 삭제 20,000회를 제공합니다.
  • API 게이트웨이는 무료 계층 서비스에 포함되지 않지만 청구 계정당 매월 최대 200만 개의 API 호출을 무료로 제공합니다.
  • Cloud Build 무료 등급은 하루 120분의 빌드 시간을 제공합니다.
  • Artifact Registry 무료 등급은 매월 0.5GB의 저장용량을 제공합니다.

여기서는 서버리스 3계층 웹 애플리케이션에 대한 비용 추정의 한 가지 예를 예시 번호와 함께 살펴보겠습니다. 실제 비용은 사용량에 따라 달라집니다.

비용 추정을 위해 다음 가정을 사용합니다.

  • Cloud Run은 서비스로 사용되며 실행 시에만 CPU가 할당됩니다.
  • Cloud Run 인스턴스에는 vCPU 1개와 메모리 512MB가 있습니다.
  • 월간 사용자는 10,000명입니다.
  • 각 사용자는 매월 평균 400건의 요청을 받습니다.
  • 각 요청을 완료하는 데 1,000밀리초가 걸립니다.
  • 요청의 60%는 데이터베이스 읽기입니다.
  • 요청의 40%는 데이터베이스 쓰기입니다.
  • Firestore의 데이터 용량은 15GB 미만입니다.
  • 모든 리소스는 동일한 지역에 배포됩니다.

API 게이트웨이:

월간 요청 200만~10억 건 = 3.00 USD

Firestore:

15GB 스토리지 = 월 $2.52

Document reads:

50,000

문서 100,000개당 $0.036

Document writes:

20,000

문서 100,000개당 $0.108

사용자 100명 * 월별 요청 40,000개 = 월별 요청 400만 개 ⇒ 월별 읽기 240만 개 및 쓰기 160만 개

읽기 비용: 읽기 240만 개 / 일일 요청 30 ➡ 80,000개 — 무료 요청 50,000개 = 일일 유료 요청 30,000개 = 읽기 요청의 경우 하루 $0.036 * 30 = 월 $1.08

쓰기 비용: 쓰기 160만 개 / 일일 요청 30 ➡ 54,000개 — 무료 요청 20,000개 = 일일 유료 요청 34,000개 = 쓰기 요청의 경우 일일 0.216 USD * 30 = 월 6.48 USD

Egress:

동일한 지역 내 = $0.00

Cloud Run:

CPU 할당 시간: 200,000 vCPU-초 = $0.48

메모리 할당 시간: 100,000GiB-초 = $0.00

요청 수: 4,000,000 = $0.80

Cloud Run 총액: $1.28 * 인스턴스 2개 = 월 $2.56

Monthly costs:

API 게이트웨이: $3.00

소방서: $10.08

송신: $0.00

클라우드 런: $2.56

총액 = 월 $15.64

단계별 가이드

Google Cloud 프로젝트 설정

Google Cloud에서 리소스를 관리하고 구성하려면 프로젝트가 필요합니다. 이 튜토리얼에서는 새 프로젝트를 생성하는 것이 좋습니다. 이 가이드에서는 관리자(소유자) 액세스 권한이 있는 Google Cloud 프로젝트가 있고 nano 또는 vim과 같은 명령줄 편집기에 대한 지식이 있거나 Cloud Shell 편집기에 대한 액세스 권한이 있는 Google Cloud Shell에 액세스할 수 있다고 가정합니다. Cloud Shell 편집기는 Cloud Shell에 바로 내장된 텍스트 편집기이며 명령줄 편집기에 익숙하지 않은 경우 유용한 옵션입니다.

Google Cloud 콘솔에서 프로젝트에 액세스하려면 Google Cloud 콘솔 로 이동하여 로그인(또는 계정 생성)하고 프로젝트를 선택하세요. 아직 Google Cloud 프로젝트가 없다면 메뉴 > IAM 및 관리자 > 프로젝트 만들기 로 이동하여 콘솔의 메시지를 따릅니다.

초기 설정

단순화를 위해 단일 저장소를 사용하여 모든 애플리케이션, 인프라 및 파이프라인 코드를 보관합니다. 초기 설정을 위해 프로젝트의 Google Cloud 콘솔에서 Cloud Shell 세션을 엽니다. Cloud Shell은 개발 및 운영 작업을 위한 인터넷 기반 플랫폼을 나타내며 웹 브라우저를 통해 편리하게 접근할 수 있습니다. Google Cloud는 이 서비스를 무료로 제공합니다. 이 플랫폼 내에서 다양한 도구를 갖춘 온라인 터미널을 사용하여 자산을 감독할 수 있습니다. 사전 설치된 도구에 대해 자세히 알아보려면 https://cloud.google.com/shell/docs/how-cloud-shell-works#tools 를 참조하세요 .

https://console.cloud.google.com/?cloudshell=true를 방문하여 새 Cloud Shell 세션을 열 수 있습니다 .

아직 업데이트하지 않은 경우 Google Cloud Shell 콘솔을 업데이트하여 프로젝트를 사용하세요.

gcloud config set project [YOUR GOOGLE CLOUD PROJECT]

gcloud config list

1단계: GitHub 저장소 복제

Cloud Shell 세션에서 참조 애플리케이션을 git clone합니다. 이 저장소를 복제하여 시작할 수 있습니다. 그러나 리포지토리를 포크하고 포크된 리포지토리를 복제하는 것이 좋습니다. 이 단계별 가이드의 일부로 Google 프로젝트에 3계층 서버리스 애플리케이션을 배포하기 위해 코드를 변경하는 방법에 대한 지침을 제공합니다.

git clone [original https://github.com/maksoodmohiuddin/google-cloud-serverless-app-pattern.git or forked repo] cd google-cloud-serverless-app-pattern

여기에서 '편집기 열기'를 선택하여 Cloud Shell 편집기를 시작할 수 있습니다.

2단계: Google 프로젝트 ID 및 지역 업데이트

다음으로, 사용 중인 Google Cloud 프로젝트와 이 애플리케이션을 배포하려는 대상 Google Cloud 지역(예: 'us-west2')으로 project_id 및 위치를 업데이트하겠습니다 . 우리의 예에서는 nano를 편집기로 보여줍니다. 그러나 원하는 다른 사용 가능한 편집기를 사용하도록 선택할 수 있습니다.

cd infra
nano variables.tf

variable "project_id" {
description = "Project ID of the GCP project where resources will be deployed"
type = string
default = "PLEASE UPDATE WITH YOUR GOOGLE PROJECT ID"
}

variable "location" {
description = "Location (region) where resources will be deployed"
type = string
default = "PLEASE UPDATE YOUR GOOGLE CLOUD REGION"
}

Cloud Build, Cloud Run, Firebase, API Gateway, Artifact Registry를 지원하는 지역을 선택하세요. 개인 계정의 경우 Cloud Build는 특정 지역으로 제한됩니다. 따라서 개인 계정을 사용하는 경우 아래 지역 중 하나를 사용하는 것이 좋습니다.

  • 미국 서부 2
  • 아시아-동부1
  • 호주-남동부1

Google Cloud 지역 및 이 앱 서비스가 지원되는 지역에 대해 자세히 알아보려면 다음을 방문하세요.

초기 인프라 배포

이 섹션에서는 Terraform을 사용하여 초기 인프라를 배포합니다. Terraform에 알려지지 않은 종속성 매핑으로 인해 특정 리소스를 대상으로 하고 애플리케이션을 순차적으로 설정해야 합니다. 기본적으로 Terraform은 terraform.tfstate

라는 파일에 상태를 로컬로 저장합니다 . 이 튜토리얼에서는 로컬 상태를 사용할 수 있습니다. 그러나 원하는 경우 Google Cloud Storage를 사용하여 상태를 원격 상태로 저장할 수 있습니다. 개발자가 여러 명인 프로젝트에서는 원격 상태가 항상 선호됩니다. Terraform에 특히 능숙하다면 Terraform 로컬 상태로 버킷을 만든 다음 해당 상태를 버킷으로 마이그레이션하는 것이 더 쉬울 수 있습니다. 자세한 내용은 Google Cloud 문서를 참조하세요.

1단계: Terraform 초기화

terraform init

2단계: Terraform 계획 검증

terraform plan

Cloud Shell을 승인하라는 메시지가 표시되면 '수락'을 선택하여 계속하세요. 이는 Cloud Shell이 ​​Google Cloud 명령어에 사용자 인증 정보를 사용하려면 권한이 필요하고 '승인'을 클릭하면 이 호출과 향후 호출에 대한 권한이 부여되기 때문입니다.

출력은 다음과 같아야 합니다.

계획: 추가 18개, 변경 0개, 파괴 0개

3단계: 초기 리소스에 Terraform 적용

terraform apply

메시지가 표시되면 "yes"를 입력하여 리소스를 배포합니다. 계획과 일치해야 합니다.

백엔드, API 게이트웨이, 프런트엔드 배포

1단계: Google 프로젝트를 사용하도록 백엔드 Python Flask 애플리케이션 코드 업데이트

백엔드 폴더로 이동하여 firestore.py 파일을 엽니다.

cd ../app/backend/ nano -l firestore.py

사용 중인 Google Cloud 프로젝트로 10행 클라이언트를 업데이트합니다.

client = firestore.Client(project="PLEASE_UPDATE_PROJECT_ID")

2단계: Cloud Build를 트리거하여 백엔드 배포

먼저 Cloud Build 파일을 검토하고 Google Cloud 명령줄을 사용하여 Cloud Build 파일을 기반으로 백엔드 Cloud Run을 배포할 수 있습니다.

cat backend-cloudbuild.yaml gcloud builds submit --region=[YOUR GOOGLE CLOUD REGION] --config backend-cloudbuild.yaml e.g. gcloud builds submit --region=us-west2 --config backend-cloudbuild.yaml

Cloud Build 실행을 완료하는 데 몇 분 정도 걸릴 수 있습니다. 이것은 예상됩니다. 이 Cloud Build 파일은 백엔드 서비스용 Docker 이미지를 빌드하여 Artifact Registry에 푸시한 후 Artifact Registry의 이미지를 Cloud Run 인스턴스에 배포합니다.

3단계: Swagger 사양 검토

OpenAPI Swagger 사양과 함께 API 게이트웨이를 사용하여 백엔드 Cloud Run을 API 게이트웨이에 연결하고 API 게이트웨이 구성을 설정합니다. infra 폴더 아래의 api-gateway--espv2-definition.yml.tmpl 파일을 검토합니다 .

먼저 cd를 사용하여 infra 디렉터리로 돌아가서 활성화 _api_gateway 플래그를 true로 활성화합니다.

cd ../../infra cat api-gateway--espv2-definition.yml.tmpl

다음을 방문하면 API Gateway 인증으로서 API Gateway, Swagger 및 Firebase용 OpenAPI 사양에 대해 자세히 알아볼 수 있습니다.

https://cloud.google.com/api-gateway/docs/openapi-overview

https://swagger.io

https://cloud.google.com/api-gateway/docs/authenticating-users-firebase

4단계: API 게이트웨이 배포

인프라 디렉터리에 있는 동안 활성화 _api_gateway 플래그를 true로 활성화합니다.

nano variables.tf

variable "enable_api_gateway" { description = "Feature flag to enable/disable API Gateway. Leverage this to deploy infra sequentially." type = bool default = true }

Terraform 계획을 실행한 후 적용합니다.

terraform plan
terraform apply

메시지가 표시되면 "yes"를 입력하여 리소스를 배포합니다. 세 가지 리소스를 배포해야 합니다. API 게이트웨이는 일반적으로 다른 리소스보다 배포하는 데 시간이 더 오래 걸립니다. 이것은 정상입니다.

5단계: Firebase 콘솔을 통해 Firestore 데이터베이스 구성

https://console.firebase.google.com/ 으로 이동하여 프로젝트 생성/추가 > 드롭다운에서 Google Cloud 프로젝트 선택 > 약관 동의 > 계속 > 계획 확인 > Google Analytics 활성화 여부 선택 > 계속을 선택합니다 .

Firebase 프로젝트의 방문 페이지에서 앱을 Firebase에 웹 앱으로 추가합니다.

또는 프로젝트 개요 에서 톱니바퀴 아이콘을 클릭하고 프로젝트 설정 으로 이동한 후 앱을 Firebase에 웹 앱으로 등록하세요.

선택한 이름으로 앱을 등록합니다(예: Google 프로젝트 이름을 사용할 수 있음).

앱이 등록되면 SDK 설정 및 구성 섹션(구성 섹션으로 전환 가능)에서 const firebaseConfig 값을 복사 하고 해당 값을 app/frontend/src/environments/environments.ts 에 붙여넣습니다 .

경고: Enviroment.ts 값 은 민감한 정보이므로 git 저장소에 체크인하지 마세요 .

cd ../app/frontend/src/environments/ nano -l environment.ts

export const environment = { firebase: { apiKey: "PLEASE UPDATE", authDomain: "PLEASE UPDATE", projectId: "PLEASE UPDATE", storageBucket: "PLEASE UPDATE", messagingSenderId: "PLEASE UPDATE", appId: "PLEASE UPDATE", measurementId: "PLEASE UPDATE" // if measurement is enabled }, production: false };

Firebase 콘솔에서 모든 제품 > 인증 > 시작하기 > Google > 활성화 > 지원 이메일 추가 > 저장 단계에 따라 Google을 Firebase용 인증 공급자로 활성화합니다.

6단계: 프런트엔드 API 게이트웨이 구성 설정

먼저 다음 명령을 실행하여 api-gateway 구성을 가져옵니다.

gcloud api-gateway gateways describe employee-gateway --location LOCATION --project PROJECT_ID --format 'value(defaultHostname)'

출력은 다음과 같습니다: Employee-gateway-#.#.gateway.dev

그런 다음 app/frontend/src/employee/services/firestore.service.ts 에서 세 개의 URL(13~15행)을 업데이트합니다 . 경로(예: 직원)를 그대로 유지합니다.

cd ../employee/services

nano -l firestore.service.ts

addEmployeeUrl = 'https://employee-gateway-#.#.gateway.dev/employee'; employeesUrl = 'https://employee-gateway-#.#.gateway.dev/employees'; deleteEmployeeUrl = 'https://employee-gateway-#.ue.gateway.dev/employee';

7단계: Cloud Build를 트리거하여 프런트엔드 배포

프런트엔드 폴더로 이동하여 Cloud Build 파일을 검토하고 Google Cloud CLI를 사용하여 백엔드 클라우드 인스턴스를 배포합니다.

cd ../../.. cat frontend-cloudbuild.yaml gcloud builds submit --region=[YOUR GOOGLE CLOUD REGION]] --config frontend-cloudbuild.yaml e.g. gcloud builds submit --region=us-west2 --config frontend-cloudbuild.yaml

8단계: 프런트엔드 Cloud Run URL로 Firebase 승인 도메인 업데이트

Firebase 앱에서 OAuth 작업을 수행하려면 프런트엔드 Cloud Run URL을 승인해야 합니다.

Google Cloud를 사용하여 Cloud Run URL을 가져올 수 있습니다.

gcloud run services describe SERVICE --region REGION --format 'value(status.url)'

e.g. gcloud run services describe amazing-employees-frontend-service --region us-west2 --format 'value(status.url)'

출력은 다음과 같아야 합니다. https://amazing-employees-frontend-service-###.a.run.app

해당 URL을 복사하고 Firebase 콘솔의 OAuth 리디렉션 도메인 목록에 추가하세요.

https://console.firebase.google.com/ 으로 이동하여 프로젝트를 선택합니다.

인증 > 설정 탭 > 승인된 도메인 > 도메인 추가를 클릭 하고 프런트엔드 Cloud Run URL을 붙여넣습니다.

마지막 단계: 엔드투엔드 앱 검증

프런트엔드 Cloud Run URL(https://amazing-employees-frontend-service-###.a.run.app)로 이동하고 Google 계정을 사용하여 앱에 로그인합니다. Google Cloud 서버리스 애플리케이션 패턴은 다음을 사용합니다. Cloud Run, API 게이트웨이, Firebase가 실행 중입니다.

정리하다

API 게이트웨이 구성 종속성으로 인해 여기서는 Terraform 삭제를 실행할 수 없습니다.

이 튜토리얼에서 사용한 리소스 비용이 GCP 계정에 청구되지 않도록 하려면 프로젝트를 삭제하면 됩니다.

프로젝트를 삭제하면 다음과 같은 결과가 발생합니다.

  • 기존 프로젝트를 사용한 경우 해당 프로젝트에서 수행한 다른 작업도 모두 삭제됩니다.
  • 삭제된 프로젝트의 프로젝트 ID는 재사용할 수 없습니다. 나중에 사용하려는 맞춤 프로젝트 ID를 만든 경우 대신 프로젝트 내의 리소스를 삭제하세요. 이렇게 하면 appspot.com URL과 같이 프로젝트 ID를 사용하는 URL을 계속 사용할 수 있습니다.

프로젝트를 삭제하려면 다음을 수행하세요.

  • Cloud 콘솔에서 프로젝트 페이지(https://console.cloud.google.com/iam-admin/projects)로 이동합니다.
  • 프로젝트 목록에서 삭제하려는 프로젝트를 선택하고 '삭제'를 클릭하세요.
  • 대화 상자에서 프로젝트 ID를 입력하고 "종료"를 클릭하여 프로젝트를 삭제하세요.

요약

이 문서에서는 Google Cloud에 서버리스 3계층 애플리케이션을 배포하는 방법을 살펴보았습니다. 우리는 데이터베이스 요구사항에 Firestore를 활용하면서 Angular 컨테이너화된 프런트엔드와 Python Flask 컨테이너화된 백엔드 모두에 Cloud Run을 사용하는 방법을 살펴보았습니다. 효율적인 인프라 설정 및 배포를 위해 Terraform과 Cloud Build가 강조되었습니다. 실습을 원하는 분들을 위해 배포의 각 단계를 복제하고 이해할 수 있도록 자세한 단계별 가이드를 제공했습니다.

이러한 주요 구성요소와 기술을 이해함으로써 독자는 Google Cloud에서 서버리스 프로젝트를 처리할 수 있는 더 나은 준비를 갖추게 될 것입니다.

Multi-tenancy with Nestjs

· 10 min read

Nest.js를 처음 사용하는 경우 탄력적인 웹 애플리케이션을 구축하기 위한 탁월한 프레임워크의 공식문서를 살펴보는 것이 좋습니다 .

다중 테넌트에 대해 이미 잘 알고 있고 Nest.js를 사용한 구현에 대해 코드로 바로 알고 싶다면 GitHub 저장소 에서 코드를 직접 검토해 보세요.

준비

먼저 Nest.js CLI 프로그램을 아직 설치하지 않았다면 설치하세요.

Nestjs cli 설치: https://docs.nestjs.com/cli/overview

nest new multitenant 프로젝트를 발판으로 실행합니다 . 저는 패키지 관리자로 pnpm을 선택했습니다. 또는 pnpm을 사용하지 않으려는 경우 npm 또는 Yarn을 선택할 수 있습니다.

멀티 테넌시(Multi-tenancy)가 무엇인지 알아봅시다.

멀티 테넌시는 SaaS(Software as a Service) 애플리케이션에 사용되는 널리 사용되는 아키텍처 접근 방식으로, 일반적으로 테넌트라고 하는 고객 간의 리소스 공유를 촉진합니다.

위키피디아 정의: https://en.wikipedia.org/wiki/Multitenancy

Multitenancy에서 가장 중요한 개념은 Host vs Tenant 입니다.

호스트는 SaaS 애플리케이션 시스템의 관리를 소유하고 감독할 책임이 있습니다.

테넌트는 서비스를 활용하는 SaaS 애플리케이션의 유료 고객을 의미합니다.

코드를 봅시다.

IService먼저, 나중에 애플리케이션 서비스에서 사용할 일반 인터페이스를 만들어야 합니다 .

폴더 app.interface.ts안에 파일을 만듭니다 .src

export interface IService<T, C, U> {
get: (uuid: string, tenantId?: string) => T;
create: (data: C, tenantId?: string) => void;
update: (uuid: string, data: U, tenantId?: string) => void;
delete: (uuid: string, tenantId?: string) => void;
getAll: (tenantId?: string) => T[];
}

애플리케이션에서 T는 TodoModel 로 예시된 모델 엔터티를 나타냅니다 . C 와 U는 각각 CreateDto 와 UpdateDto를 나타냅니다 . 또한, 곧 명백해질 이유 때문에 중요한 의미를 지닌 선택적 테넌트 ID 속성이 있습니다 .

다음으로, 기능에 따라 애플리케이션의 폴더를 구성하기 위해 src 디렉터리 내에 테넌트 와 todo라는 두 개의 폴더를 생성해 보겠습니다. 테넌트 기능  우리의 주요 초점으로 작용하고, todo는 테넌트(고객)가 접근할 수 있는 서비스 역할을 하며 호스트도 이를 활용할 수 있습니다.

기본 설정이 완료되면 테넌트 기능을 완료해 보겠습니다. 테넌트 폴더 에는 모델, 컨트롤러, 서비스, 미들웨어 등을 포함한 해당 논리가 포함됩니다.

image

테넌트 논리 설정

테넌트 기능에 대한 DTO, 모델 및 서비스를 만드는 것부터 시작해 보겠습니다. 모델 폴더 내에서 TenantModel.ts 라는 파일을 시작합니다 .

export class TenantModel {
id: string;
name: string;
subdomain?: string; // https://store.mysassapp.com
constructor(id: string, name: string, subdomain?: string) {
this.id = id;
this.name = name;
this.subdomain = subdomain || 'https://mysassapp.com';
}
}

테넌트 모델은 id 및 name 과 같은 필수 속성으로 구성되며 둘 다 필수입니다. 또한 도메인 또는 하위 도메인을 포함하여 고객이 도메인을 맞춤설정할 수 있는 옵션을 제공할 수 있습니다.

참고: 게시물에서 도메인/하위 도메인 부분을 다루지는 않겠습니다.

이제 dtos와 서비스를 만들어 보겠습니다.

src/tenant/dtos/CreateTenantDto.ts

export class CreateTenantDto {
name: string;
subdomain?: string;
constructor(name: string, subdomain?: string) {
this.name = name;
this.subdomain = subdomain;
}
}

src/tenant/dtos/UpdateTenantDto.ts`

export class UpdateTenantDto {
id: string;
name: string;
subdomain?: string;
constructor(id: string, name: string, subdomain?: string) {
this.id = id;
this.name = name;
this.subdomain = subdomain;
}
}

src/tenant/services/TenantService.ts

import { randomUUID } from 'crypto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { IService } from '../../app.interface';
import { TenantModel } from '../models/TenantModel';
import { CreateTenantDto } from '../dtos/CreateTenantDto';
import { UpdateTenantDto } from '../dtos/UpdateTenantDto';

@Injectable()
export class TenantService
implements IService<TenantModel, CreateTenantDto, UpdateTenantDto>
{
private readonly tenants: TenantModel[] = []; // Temp local database..

create(data: CreateTenantDto): void {
const uuid = randomUUID();
this.tenants.push(new TenantModel(uuid, data.name, data.subdomain));
}

delete(uuid: string): void {
const index = this.tenants.findIndex((tenant) => tenant.id === uuid);
if (index === -1) throw new NotFoundException('Tenant not found');
this.tenants.splice(index, 1);
}

get(uuid: string): TenantModel {
const todo = this.tenants.find((tenant) => tenant.id === uuid);
if (!todo) throw new NotFoundException('Tenant not found');
return todo;
}

update(uuid: string, data: UpdateTenantDto): void {
const tenant = this.tenants.find((tenant) => tenant.id === uuid);
if (!tenant) throw new NotFoundException('Tenant not found');
tenant.name = data.name;
tenant.subdomain = data.subdomain;
}

getAll(): TenantModel[] {
return this.tenants;
}
}

테넌트 서비스에서는 현재 테넌트 정보를 저장하기 위해 임시 배열을 활용하고 있습니다. 프로덕션 환경에서는 데이터 지속성을 위해 실제 데이터베이스를 통합하는 것이 필수적입니다. 또한 개별 테넌트에 대해 서로 다른 데이터베이스를 구성할 수 있는 옵션도 있습니다. (이 기능을 구현하는 방법을 배우고 싶다면 댓글로 문의해 주세요.)

앞으로 테넌트 컨트롤러를 만들어 보겠습니다.

src/tenant/controllers/tenant.controller.ts

import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Post,
Put,
Req,
} from '@nestjs/common';
import { CreateTenantDto } from '../dtos/CreateTenantDto';
import { UpdateTenantDto } from '../dtos/UpdateTenantDto';
import { TenantService } from '../services/TenantService';

@Controller()
export class TenantController {
constructor(private readonly tenantService: TenantService) {}

@Get('/tenants')
getAll() {
return this.tenantService.getAll();
}

@Post('/tenants')
createTodo(@Req() req: Request, @Body() data: CreateTenantDto) {
this.tenantService.create(data);
return HttpStatus.CREATED;
}

@Get('/tenants/:uuid')
getTenant(@Req() req: Request, @Param('uuid') uuid: string) {
return this.tenantService.get(uuid);
}

@Put('/tenants/:uuid')
updateTenant(
@Req() req: Request,
@Param('uuid') uuid: string,
@Body() data: UpdateTenantDto,
) {
this.tenantService.update(uuid, data);
return HttpStatus.NO_CONTENT;
}

@Delete('/tenants/:uuid')
deleteTodo(@Req() req: Request, @Param('uuid') uuid: string) {
this.tenantService.delete(uuid);
return HttpStatus.ACCEPTED;
}
}

좋습니다. 테넌트 논리 구현을 완료했습니다. 다음으로app.module.ts에 컨트롤러와 서비스를 등록합니다. 그런 다음 Postman과 같은 선호하는 REST 클라이언트를 실행하고 엔드포인트를 테스트하세요.

image

image

좋습니다. 테넌트 API가 예상대로 작동합니다. 할 일 논리를 만들어 보겠습니다.

Todo 로직 설정

먼저 todo의 dto, 모델 및 서비스를 계속 생성해 보겠습니다. 모델 폴더 내에 할 일 모델을 생성합니다.TodoModel.ts

export class TodoModel {
uuid: string;
title: string;
done: boolean;
tenantId?: string;

constructor(uuid: string, title: string, done: boolean) {
this.uuid = uuid;
this.title = title;
this.done = done;
}

setTenantId(tenantId: string) {
this.tenantId = tenantId;
}

우리의 할일 모델에서 tenantId 속성은 선택 사항입니다. 이는 각 할일 항목의 소유권 관리를 용이하게 하기 위해 설계되었습니다. 예를 들어, 호스트는 호스트의 고객(테넌트)에게 보이지 않는 상태로 유지되는 할 일 항목을 가질 수 있습니다.

참고: 실제 프로덕션 코드에서는 다대일 관계를 설정하는 todo 모델 엔터티를 소유해야 합니다.

나머지 todo 로직을 계속 진행해 보겠습니다.

src/todo/dtos/CreateTodoDto.ts

export class CreateTodoDto {
title: string;
done: boolean;
constructor(title: string, done: boolean) {
this.title = title;
this.done = done;
}
}

src/todo/dtos/UpdateTodoDto.ts

export class UpdateTodoDto {
id: string;
title: string;
done: boolean;
constructor(id: string, title: string, done: boolean) {
this.title = title;
this.done = done;
}
}

src/todo/services/TodoService.ts

import { randomUUID } from 'crypto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { IService } from '../../app.interface';
import { TodoModel } from '../models/TodoModel';
import { CreateTodoDto } from '../dtos/CreateTodoDto';
import { UpdateTodoDto } from '../dtos/UpdateTodoDto';

@Injectable()
export class TodoService
implements IService<TodoModel, CreateTodoDto, UpdateTodoDto>
{
private readonly todos: TodoModel[] = []; // temp local databse to store all our todo items

create(data: CreateTodoDto, tenantId?: string): void {
const uuid = randomUUID();
const newTodo = new TodoModel(uuid, data.title, data.done);
if (tenantId) newTodo.setTenantId(tenantId);
this.todos.push(newTodo);
}

delete(uuid: string, tenantId?: string) {
const index = this.todos.findIndex((todo) => todo.uuid === uuid);
if (index === -1) throw new NotFoundException('Todo not found');
if (tenantId && this.todos[index].tenantId !== tenantId)
throw new NotFoundException('Todo not found');
this.todos.splice(index, 1);
}

get(uuid: string, tenantId?: string): TodoModel {
const todo = this.todos.find((todo) => todo.uuid === uuid);
if (!todo) throw new NotFoundException('Todo not found');
if (tenantId && todo.tenantId !== tenantId)
throw new NotFoundException('Todo not found');
return todo;
}

update(uuid: string, data: UpdateTodoDto, tenantId?: string): TodoModel {
const todo = this.todos.find((todo) => todo.uuid === uuid);
if (!todo) throw new NotFoundException('Todo not found');
if (tenantId && todo.tenantId !== tenantId)
throw new NotFoundException('Todo not found');
todo.title = data.title;
todo.done = data.done;
return todo;
}

getAll(tenantId?: string): TodoModel[] {
if (tenantId)
return this.todos.filter((todo) => todo.tenantId === tenantId);
return this.todos.filter((todo) => !todo.tenantId);
}
}

src/todo/controllers/todo.controller.ts`

import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Post,
Put,
Req,
} from '@nestjs/common';
import { CreateTodoDto } from '../dtos/CreateTodoDto';
import { UpdateTodoDto } from '../dtos/UpdateTodoDto';
import { TodoService } from '../services/TodoService';

@Controller()
export class TodoController {
constructor(private readonly todoService: TodoService) {}

@Get('/todos')
getTodos(@Req() req: Request) {
return this.todoService.getAll(req['tenantId']);
}

@Post('/todos')
createTodo(@Req() req: Request, @Body() data: CreateTodoDto) {
this.todoService.create(data, req['tenantId']);
return HttpStatus.CREATED;
}

@Get('/todos/:uuid')
getTodo(@Req() req: Request, @Param('uuid') uuid: string) {
return this.todoService.get(uuid, req['tenantId']);
}

@Put('/todos/:uuid')
updateTodo(
@Req() req: Request,
@Param('uuid') uuid: string,
@Body() data: UpdateTodoDto,
) {
this.todoService.update(uuid, data, req['tenantId']);
return HttpStatus.NO_CONTENT;
}

@Delete('/todos/:uuid')
deleteTodo(@Req() req: Request, @Param('uuid') uuid: string) {
this.todoService.delete(uuid, req['tenantId']);
return HttpStatus.ACCEPTED;
}
}

app.module.ts 에 todo 컨트롤러와 todo 서비스를 등록하여 진행해 보겠습니다 . 그런 다음 todo API를 테스트할 수 있습니다. 할 일 항목을 생성할 수 있어야 하지만 아직 테넌트 ID를 구성하지 않았다는 점에 유의하는 것이 중요합니다.

할 일 서비스에 대한 테넌트 ID를 구성해 보겠습니다.

미들웨어

Nestjs는 expressjs 위에 구축되었으므로 미들웨어를 사용할 수 있습니다. 들어오는 HTTP 요청과 나가는 응답을 가로챌 수 있는 기능입니다.

미들웨어를 사용하여 테넌트 ID가 포함된 HTTP 수신 요청 헤더(예: x-tenant-id: f4d6f363-e4cf-4bda-af19-f0dc2feada81) 를 읽습니다.

테넌트 ID가 로컬 DB에 존재하는지 확인한 다음 요청 객체에 테넌트 ID를 속성으로 추가합니다. 그래서 우리는 그것을 todo 컨트롤러에서 사용할 수 있습니다.

그렇지 않으면 테넌트 ID가 null이 됩니다. 이는 할 일 항목이 호스트에 속함을 의미합니다.

마지막 단계는 todoService가 될 서비스에 미들웨어를 구현하는 것입니다. 엔드포인트를 호출하여 할일 서비스에 액세스할 수 있습니다 /todos.

미들웨어를 만들어 보겠습니다.

` 안에 폴더를 만들고 ` src/tenant/middlewares파일을 만듭니다 .TenantMiddleware.ts

import {
HttpException,
HttpStatus,
Injectable,
NestMiddleware,
Logger,
} from '@nestjs/common';
import { NextFunction } from 'express';
import { TenantService } from '../services/TenantService';

@Injectable()
export class TenantMiddleware implements NestMiddleware {

private readonly logger = new Logger(TenantMiddleware.name);
constructor(private readonly tenantService: TenantService) {}

async use(req: Request, res: Response, next: NextFunction) {
const { headers } = req;

const tenantId = headers['X-TENANT-ID'] || headers['x-tenant-id'];

if (!tenantId) {
this.logger.warn('`X-TENANT-ID` not provided');
req['tenantId'] = null;
return next();
}
const tenant = this.tenantService.get(tenantId);
req['tenantId'] = tenant.id;
next();
}
}

app.module.ts 에 미들웨어를 등록 하고 NestModule 인터페이스를 구현하여 구성해 보겠습니다.

@Module({
imports: [],
controllers: [AppController, TodoController, TenantController],
providers: [TodoService, TenantService, TenantMiddleware],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TenantMiddleware).forRoutes('/todos');
}
}

curl을 사용하여 2개의 테넌트를 생성해 보겠습니다.

curl -X POST --location "http://localhost:3000/api/tenants" \ -H "Content-Type: application/json" \ -d '{ "name": "Tenant 1" }'
curl -X POST --location "http://localhost:3000/api/tenants" \ -H "Content-Type: application/json" \ -d '{ "name": "Tenant 2" }'

모든 임차인을 가져옵니다

curl -X GET --location "http://localhost:3000/api/tenants" \ -H "Accept: application/json"
[
{
"id": "f4d6f363-e4cf-4bda-af19-f0dc2feada81",
"name": "Tenant 1",
"subdomain": "https://mysassapp.com"
},
{
"id": "df19344b-1063-4699-809a-e596138b2194",
"name": "Tenant 2",
"subdomain": "https://mysassapp.com"
},
]

이제 호스트, 테넌트 1, 테넌트 2에 대한 할 일 항목을 만들어 보겠습니다.

curl -X POST --location "http://localhost:3000/api/todos" \ -H "Content-Type: application/json" \ -d '{ "title": "Belongs to host", "done": false }'
curl -X POST --location "http://localhost:3000/api/todos" \ -H "Content-Type: application/json" \ -H "X-Tenant-ID: f4d6f363-e4cf-4bda-af19-f0dc2feada81" \ -d '{ "title": "Belongs to tenant 1", "done": false }'
curl -X POST --location "http://localhost:3000/api/todos" \ -H "Content-Type: application/json" \ -H "X-Tenant-ID: df19344b-1063-4699-809a-e596138b2194" \ -d '{ "title": "Belongs to tenant 2", "done": false }'

이제 요청 헤더에tenantId를 전달하여 할일 항목을 가져옵니다.

curl -X GET --location "http://localhost:3000/api/todos" \ -H "Accept: application/json" \ -H "X-Tenant-ID: f4d6f363-e4cf-4bda-af19-f0dc2feada81"
[
{
"uuid": "0438dcc5-3061-4bf6-824d-7857885501ca",
"title": "Belongs to tenant 1",
"done": false,
"tenantId": "f4d6f363-e4cf-4bda-af19-f0dc2feada81"
}
]

요청 헤더에 테넌트 ID를 전달하지 않으면 호스트에 속한 할 일 항목을 가져옵니다.

curl -X GET --location "http://localhost:3000/api/todos" \ -H "Accept: application/json"
[
{
"uuid": "12922bd3-2d2b-4dff-9130-32adbaee0432",
"title": "Belongs to host",
"done": false
}
]

결론

우리는 Nest.js를 사용하여 다중 테넌트의 기능적 예를 성공적으로 만들었습니다. 그럼에도 불구하고 실제 시나리오를 위한 다중 테넌트 애플리케이션을 개발할 때는 다음과 같은 몇 가지 요소를 고려하는 것이 중요합니다.

1. Redis와 같은 캐싱 메커니즘으로 보완된 MongoDB 또는 PostgreSQL과 같은 강력한 다중 클러스터 데이터베이스를 활용합니다.
2. 각 테넌트에 맞게 여러 데이터베이스 연결을 구성합니다.

Wagtail vs Wordpress - 프로젝트에 적합한 CMS를 선택하는 방법

· 7 min read

image

Wagtail과 Wordpress는 모두 뛰어난 오픈 소스 콘텐츠 관리 시스템입니다. 프로젝트에 가장 적합한 기술을 결정할 때 고려해야 할 몇 가지 사항은 다음과 같습니다.

Overview

오늘날 사용할 수 있는 오픈 소스 및 독점 CMS가 너무 많기 때문에 새 웹 사이트에 사용할 콘텐츠 관리 시스템(CMS)을 결정하는 것은 어려운 결정일 수 있습니다 . Wagtail과 Wordpress를 보고 있다면 오픈 소스 옵션을 사용하고 싶을 가능성이 높습니다. 특정 프로젝트에 어떤 오픈 소스 기술이 잘 작동할지는 항상 명확하지 않습니다. 그럼 Wagtail과 Wordpress의 장점을 비교하고, 각각 어떤 유형의 프로젝트가 잘 작동하는지 비교해 보겠습니다.

Wagtail 소개

Wagtail은 Python으로 작성되었으며 오픈 소스 Django 웹 프레임워크를 통합한 CMS입니다 . Django는 거의 20년 동안 존재해 왔으며 Spotify 및 Instagram과 같은 회사에서 사용됩니다. Wagtail은 2014년에 만들어졌으며 이후 NASA, Google, National Health Service와 같은 조직에서 채택되었습니다. Wagtail의 장점은 다음과 같습니다.

  • 사용자 정의: 데이터 구조부터 프런트엔드까지 Wagtail은 프로젝트의 특정 요구 사항을 충족하도록 사용자 정의하기가 매우 쉽습니다.
  • 유연성: Wagtail은 확장성이 매우 뛰어나며 매우 쉽게 확장하여 프로젝트에 필요한 사용자 정의 기능을 통합할 수 있습니다.
  • 엔터프라이즈 기능: Wagtail에는 권한, 워크플로, 보고서, 기록 추적 및 대규모 팀에 필요한 기타 기능이 포함되어 있습니다.
  • 보안: Wagtail은 다양한 일반적인 공격 방법 에 대한 보호를 포함하는 Django의 보안 기능을 통합합니다 .

Wordpress 소개

Wordpress는 일반적으로 MySQL 데이터베이스와 쌍을 이루는 PHP로 작성된 CMS입니다. Wordpress는 거의 20년이 되었으며 상위 1,000만개 웹사이트 중 42.8%가 사용할 정도로 인기가 높습니다. Wordpress의 장점은 다음과 같습니다.

  • 신속한 배포: 유명한 5분 설치 프로세스 덕분에 Wordpress를 사용하면 웹 사이트를 신속하게 배포하는 것이 매우 쉽습니다.
  • 로우 코드 솔루션: Wordpress는 코딩 없이 설정 및 설치할 수 있으므로 개인 프로젝트나 개발자가 없는 소규모 팀에 적합한 솔루션입니다.
  • 잘 알려진 CMS: 지구상에서 가장 인기 있는 CMS로서 프로젝트를 지원할 수 있는 사람들이 부족하지 않습니다.
  • 쉬운 업그레이드: Wordpress 프로젝트는 이전 버전과의 호환성에 중점을 두고 업그레이드가 상대적으로 수월하도록 합니다.
 WagtailWordpress
CostWagtail은 오픈 소스이며, 라이센스 비용이 없습니다. 웹싸이트 제작을 위한 개발자나 대행사 비용이 포함될 가능성이 있습니다.Wordpress는 오픈 소스이며 라이센스 비용이 없습니다. 시작하고 실행하는데 매우 저렴합니다.
LanguagePythonPHP
Framework효율성과 보안을 위해 설계된 최신 웹 프레임워크인 Django를 통합합니다.웹 프레임워크를 통합하지 않습니다.
CustomizationWagtail은 사용자 정의가 가능하고 블로그, 앱, 온라인 서비스 등 무엇이든 맞춤화하기 좋게 구성되어 있습니다.Wordpress는 블로그용으로 설계되어 블로그가 아닌 다른 프로젝트에 사용하려면 사용자 정의를 수행해야 합니다.
ExtendabilityWagtail은 확장이 매우 쉽고 패키지 또는 Python 코드를 사용해 기능을 추가할 수 있습니다.Wordpress는 플러그인을 사용해 쉽게 확장할 수 있습니다. 
ScalabilityWagtail은 성능에 심각한 영향을 주지 않고 대부분의 웹 싸이트에서 대량의 콘텐츠를 관리할 수 있습니다.Wordpress는 중소 규모 웹싸이트에 가장 적합합니다. 플러그인 수가 많을수록 성능에 영향을 줄 수 있습니다.
Required SkillWagtail은 웹 싸이트를 설정하고 유지할 개발자가 필요합니다.Wordpress는 개발자가 필요하지 않습니다.
Enterprise-level features대규모 조직에 일반적으로 필요한 워크플로, 권한, 주석, 보고서, 기록 추적 및 접근 기능 등이 포함됩니다.Wordpress는 고급 엔터프라이즈 기능을 추가하려면 플러그인이나 사용자 정의 개발이 필요합니다.
SecurityDjango의 고급 보안 기능과 일반적 공격에 대한 보호 기능이 포함됩니다.Wordpress는 그 자체로 우수한 보안을 제공하지만 플러그인으로 인해 보안 위험과 성능 문제가 발생할 수 있습니다.
Support무료 커뮤니티 지원과 세계적 네트워크가 있습니다.무료 커뮤니티 지원과 대규모 네트워크가 있습니다.

시작하는 것이 얼마나 쉬운가요?

Wordpress와 Wagtail은 모두 개발자와 콘텐츠 제작자가 비교적 배우기 쉽습니다. 둘 다 강력한 문서, 지원을 제공하는 번성하는 커뮤니티 및 직관적인 사용자 인터페이스를 갖추고 있습니다.

현재 둘 사이의 주요 차이점은 Wagtail 웹사이트를 시작하려면 기술적 능력이 필요하다는 것입니다. Wagtail은 직원 중에 개발자가 있거나 프리랜서 개발자 또는 대행사를 고용하려는 팀 및 조직에 이상적입니다. 개발자와 협력하더라도 작은 Wagtail 웹사이트를 구축하는 데 일반적으로 최소 일주일이 걸립니다. 반면에 Wordpress는 개발자 없이도 시작할 수 있으며 단 하루 만에 시작할 수 있습니다.

따라서 웹사이트가 정말 빨리 필요하거나 개발자와 협력할 여력이 없다면 Wordpress가 프로젝트에 가장 적합한 옵션일 수 있습니다. 예산이 적당하고 장기적으로 귀하 또는 귀하의 조직에 도움이 될 웹사이트에 투자하는 데 더 관심이 있다면 Wagtail이 더 나은 선택일 것입니다.

Wagtail과 Wordpress는 얼마나 사용자 정의가 가능합니까?

Wagtail은 사용자 정의가 가능합니다. Django 스타일 모델을 사용하여 Wagtail에서 프로젝트 시작 초기부터 데이터 구조와 기능을 사용자 정의할 수 있으므로 프로젝트에 특별히 필요한 기능에 맞게 만들어지지 않은 기능을 조정하는 것에 대해 걱정할 필요가 없습니다. 또한 Wagtail은 특정 프런트엔드 프레임워크에 의존하거나 필요하지 않으므로 원하는 프런트엔드 기술과 자유롭게 결합할 수 있습니다.

Wordpress는 블로그에 최적화되어 있습니다. 코드를 조정하고 다른 작업을 수행하도록 사용자 정의할 수 있지만 추가한 새 기능을 지원하기 위한 것이 아닌 인프라를 해결해야 하는 경우가 많습니다. Wordpress는 로우 코드 솔루션으로 설계되었기 때문에 대부분의 사람들이 Wordpress 테마를 통해 적응할 수 있는 프런트엔드도 내장되어 있습니다. 프런트엔드를 맞춤설정하려면 맞춤 테마를 만들어야 하는 경우가 많습니다.

프로젝트가 대부분 콘텐츠 중심이고 완전히 맞춤화된 시각적 디자인이 필요하지 않다면 Wordpress가 프로젝트에 적합할 수 있습니다. 데이터 구성 방식에 대한 더 많은 제어가 필요하거나, 사용자 정의된 사이트 구조를 원하거나, 프로젝트의 시각적 모양에 대해 더 많은 제어를 원할 경우 Wagtail이 더 나은 선택이 될 것입니다.

Wagtail과 Wordpress는 얼마나 안전합니까?

Wagtail은 Django 웹 프레임워크에 포함된 고급 보안 기능을 통합합니다. 이러한 기능은 교차 사이트 스크립팅, 교차 사이트 요청 위조, SQL 삽입 등을 비롯한 다양한 일반적인 공격으로부터 보호합니다. Wagtail과 Wagtail을 확장하는 패키지가 데이터베이스와 상호 작용하는 방식으로 인해 악의적인 행위자가 웹 사이트를 표적으로 삼고 데이터를 훔치는 것이 훨씬 더 어려워집니다.

Wordpress는 그 자체로 매우 안전합니다. 그러나 플러그인이 최신이 아니거나, 잘 작성되지 않았거나, 보안을 염두에 두고 생성되지 않은 경우에는 취약점이 발생할 수 있습니다 . Wordpress의 인기로 인해 Wordpress는 해커에게 일반적이고 비교적 쉬운 표적이 됩니다.

사용자로부터 대량의 데이터를 수집하거나 금융 정보나 건강 정보와 같은 민감한 데이터를 수집할 계획이 없다면 Wordpress가 적합할 수 있습니다. 결국, 난초에 대한 기사를 게시할 계획이라면 귀하의 웹사이트가 해커의 특히 귀중한 표적이 될 가능성은 거의 없습니다. 사용자 정보를 수집하거나 민감한 데이터로 작업하려는 경우 사람들이 해당 데이터를 훔치기 어렵게 만드는 Wagtail과 같은 CMS를 선택하는 것이 좋습니다.

Wagtail과 Wordpress는 얼마나 확장 가능합니까?

Wordpress와 Wagtail은 모두 매우 쉽게 확장할 수 있습니다. Wordpress는 한 번의 클릭으로 설치하고 웹 사이트에 추가할 수 있는 플러그인을 사용합니다. Wordpress에는 수천 개의 무료 및 유료 플러그인이 있습니다. Wagtail은 개발자가 웹사이트 백엔드에 설치해야 하는 패키지를 사용합니다. 현재 Wagtail에 특별히 사용할 수 있는 패키지는 더 적지만 , 많은 Django 패키지 도 Wagtail과 잘 작동합니다.

Wagtail과 Wordpress의 주요 차이점 중 하나는 Wagtail 패키지가 웹 사이트가 커지고 더 많은 트래픽을 처리함에 따라 계속 잘 작동하는 경향이 있다는 것입니다. 반면에 Wordpress 플러그인은 종종 서로 충돌할 수 있으며 성능 문제의 일반적인 원인인 것으로 나타났습니다.

프로젝트가 상대적으로 간단하고 플러그인으로 제공되는 복잡한 기능이 많이 필요하지 않다면 Wordpress가 프로젝트에 적합한 선택입니다. 프로젝트가 더 복잡하고 많은 추가 기능이 필요하거나 웹 사이트 트래픽이 많은 경우 Wagtail을 고려해야 합니다.

내 프로젝트에 어느 것이 더 나은 투자입니까?

이 질문에 대한 답은 여러분이 만들고 있는 프로젝트의 유형과 타임라인에 따라 달라집니다. 각 CMS가 실제로 빛나는 시나리오는 다음과 같습니다.

와그테일은 다음에 좋습니다:

  • 엄청난 양의 콘텐츠를 생산하고 관리하는 조직
  • 시간이 지남에 따라 꾸준히 성장하고 많은 수의 사용자를 지원해야 하는 장기 프로젝트
  • 사용자 및 출판 워크플로를 구성하기 위해 기본 제공 도구가 필요한 중대형 팀
  • 많은 사용자 정의 또는 독특한 디자인이 필요한 프로젝트

WordPress는 다음에 적합합니다.

  • 솔로 프로젝트 또는 소규모 팀
  • 매우 빠르게 출시되어야 하는 개념 증명 프로젝트
  • 개발자가 없는 팀
  • 상대적으로 트래픽이 적은 개인 블로그나 웹사이트

Go의 이벤트 중심 아키텍처

· 13 min read

RabbitMQ, 도메인 중심 설계 및 클린 아키텍처 사용

EDA(이벤트 중심 아키텍처)는 애플리케이션의 느슨한 결합, 확장성, 독립성 및 복원성을 활성화하기 위해 이벤트의 생성, 감지 및 소비를 강조하는 소프트웨어 디자인 패턴입니다.

image

이벤트란 무엇인가요?

주황색 포스트로 표시되는 이벤트는 과거 에 발생한 중요한 사실이며 일반적으로 UserCreated, OrderPaid, InvoiceCanceled 등과 같은 명령을 실행한 결과입니다.

명령이란 무엇입니까?

파란색 포스트로 표시되는 명령은 시스템의 작업 또는 의도이며 항상 명령형 모드입니다(예: CreateUser, PlaceOrder, PayOrder, GetProducts 등).

배우란 무엇인가?

노란색 포스트로 표시되는 행위자는 명령을 실행하는 사람입니다(예: 사용자, 고객, 판매자, 구매자, 시스템 등).

어디서부터 시작하나요?

이벤트 모델링 !!!

문제:

image

내가 제안한 솔루션:

image

직접 체험해 보세요

폴더 구조(여기서는 Clean Arch 및 DDD 레이어 개념을 사용하고 있습니다)

image

이벤트 만들기

내부/도메인/이벤트 폴더 에 이벤트 order_created_event.go를 생성합니다.

package event

type OrderCreatedEvent struct {
Id string
Items []OrderItem
TotalPrice float64
Status string
}

type OrderItem struct {
ProductName string
Quantity int
TotalPrice float64
}

내부/도메인/이벤트 폴더 에 이벤트 order_paid_event.go 생성

package event

import (
"time"
)

type OrderPaidEvent struct {
OrderId string
PaidValue float64
PaymentDate time.Time
}

엔터티 만들기

먼저 uuid 패키지를 사용하여 ID를 생성하고 셸에서 실행합니다.

go mod init eda-example
go get github.com/google/uuid

내부/도메인/엔티티 폴더 에 엔터티 order_item_entity.go를 생성합니다.

package entity

type OrderItemEntity struct {
productName string
productPrice float64
quantity int
}

func NewOrderItemEntity(productName string, productPrice float64, quantity int) *OrderItemEntity {
return &OrderItemEntity{
productName: productName,
productPrice: productPrice,
quantity: quantity,
}
}

// getters
func (o *OrderItemEntity) GetProductName() string {
return o.productName
}

func (o *OrderItemEntity) GetProductPrice() float64 {
return o.productPrice
}

func (o *OrderItemEntity) GetQuantity() int {
return o.quantity
}

func (o *OrderItemEntity) GetTotalPrice() float64 {
return o.productPrice * float64(o.quantity)
}

내부/도메인/엔티티 폴더 에 엔터티 order_entity.go를 생성합니다.

package entity

import (
"errors"
"github.com/google/uuid"
)

const (
OrderStatusPending = "pending"
OrderStatusPaid = "paid"
)

type OrderEntity struct {
id string
status string
items []*OrderItemEntity
totalPrice float64
paidValue float64
}

func NewOrderEntity() (*OrderEntity, error) {
return &OrderEntity{
id: uuid.New().String(),
status: OrderStatusPending,
}, nil
}

// to populate with an existing order
func RestoreOrderEntity(id, status string) (*OrderEntity, error) {
return &OrderEntity{
id: id,
status: status,
}, nil
}

func (o *OrderEntity) AddItem(item *OrderItemEntity) {
o.items = append(o.items, item)
o.totalPrice += item.GetTotalPrice()
}

func (o *OrderEntity) Pay(value float64) error {
if value < o.totalPrice {
return errors.New("value is less than the total price")
}
o.paidValue = value
o.status = OrderStatusPaid
return nil
}

// getters
func (o *OrderEntity) GetItems() []*OrderItemEntity {
return o.items
}

func (o *OrderEntity) GetTotalPrice() float64 {
return o.totalPrice
}

func (o *OrderEntity) GetID() string {
return o.id
}

func (o *OrderEntity) GetStatus() string {
return o.status
}

게시자 인터페이스 만들기

내부/도메인/대기열 폴더 에 인터페이스 publisher**.go를 생성합니다.**

package queue

import "context"

type Publisher interface {
Publish(ctx context.Context, body interface{}) error
}

DTO 생성

내부/응용 프로그램/dto 폴더 에 dto create_order_dto.go를 생성합니다.

package dto

type CreateOrderDTO struct {
Items []Item `json:"items"`
}

type Item struct {
ProductId string `json:"product_id"`
Qtd int `json:"qtd"`
}

명령(또는 사용 사례) 만들기

내부/애플리케이션/유스케이스 폴더 에 유스케이스 create_order_usecase.go 생성

package usecase

import (
"context"
"fmt"
"eda-example/internal/application/dto"
"eda-example/internal/domain/entity"
"eda-example/internal/domain/event"
"eda-example/internal/domain/queue"
)

type CreateOrderUseCase struct {
publisher queue.Publisher
}

func NewCreateOrderUseCase(publisher queue.Publisher) *CreateOrderUseCase {
return &CreateOrderUseCase{
publisher: publisher,
}
}

func (u *CreateOrderUseCase) Execute(ctx context.Context, input dto.CreateOrderDTO) error {
fmt.Println("--- CreateOrderUseCase ---")

// create order
order, err := entity.NewOrderEntity()
if err != nil {
return err
}

for _, item := range input.Items {
// TODO: find product in the repository database here
fakeProductName := "Product " + item.ProductId
fakeProductPrice := 10.50

// create fake order item
i := entity.NewOrderItemEntity(fakeProductName, fakeProductPrice, item.Qtd)

// add items to order
order.AddItem(i)
}

// TODO: save the order in the repository database here

var eventItems []event.OrderItem
for _, item := range order.GetItems() {
eventItems = append(eventItems, event.OrderItem{
ProductName: item.GetProductName(),
TotalPrice: item.GetTotalPrice(),
Quantity: item.GetQuantity(),
})
}

// publish event OrderCreatedEvent passing the order data
err = u.publisher.Publish(ctx, event.OrderCreatedEvent{
Id: order.GetID(),
TotalPrice: order.GetTotalPrice(),
Status: order.GetStatus(),
Items: eventItems,
})
if err != nil {
return err
}
return nil
}

컨트롤러 생성

내부/응용 프로그램/컨트롤러 폴더 에 컨트롤러 order_controller.go를 생성합니다.

package controller

import (
"eda-example/internal/application/dto"
"eda-example/internal/application/usecase"
"encoding/json"
"net/http"
)

type OrderController struct {
createOrderUserCase *usecase.CreateOrderUseCase
}

func NewOrderController(createOrderUserCase *usecase.CreateOrderUseCase) *OrderController {
return &OrderController{
createOrderUserCase: createOrderUserCase,
}
}

func (u *OrderController) CreateOrder(w http.ResponseWriter, r *http.Request) {
var requestData dto.CreateOrderDTO
json.NewDecoder(r.Body).Decode(&requestData)
err := u.createOrderUserCase.Execute(r.Context(), requestData)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.WriteHeader(http.StatusCreated)
}

MemoryQueueAdapter 만들기

내부/ 인프라 /queue 폴더 에 memory_queue_adapter.go 생성

package queue

import (
"context"
"encoding/json"
"log"
"reflect"
)

type MemoryQueueAdapter struct {
}

func NewMemoryQueueAdapter() *MemoryQueueAdapter {
return &MemoryQueueAdapter{}
}

func (eb *MemoryQueueAdapter) Publish(ctx context.Context, eventPayload interface{}) error {
eventType := reflect.TypeOf(eventPayload)
payloadJson, _ := json.Marshal(eventPayload)
log.Printf("** [Publish] %s: %v ---", eventType, payloadJson)
return nil
}

애플리케이션 진입점/시작 생성

cmd/api 폴더 에 main.go 생성

package main

import (
"eda-example/internal/application/controller"
"eda-example/internal/application/usecase"
"eda-example/internal/infra/queue"
"fmt"
"net/http"
)

func main() {
// initialize dependencies and implementations
queue := queue.NewMemoryQueueAdapter()
createOrderUseCase := usecase.NewCreateOrderUseCase(queue)
orderController := controller.NewOrderController(createOrderUseCase)

// register routes
http.HandleFunc("POST /create-order", orderController.CreateOrder)

// start server
fmt.Println("Server is running on port 8080")
http.ListenAndServe(":8080", nil)
}

애플리케이션 실행

image

/create-order 엔드포인트 테스트

우편 배달부에서 요청 보내기:

image

게시된 로깅 이벤트를 확인하세요.

image

지금까지의 요약

주문을 생성하기 위해 엔드포인트를 생성합니다. 주문을 생성한 직후에는 주문이 생성되었음을 알리는 이벤트가 게시됩니다.

이제 이 이벤트를 수신 하고 여기에서 다른 명령(ProcessOrderPayment, StockMovement 및 SendOrderEmail)을 실행 해야 합니다.

리스너 명령 만들기

내부/애플리케이션/유즈케이스 폴더 에 process_order_pay_usecase.go 생성

package usecase

import (
"context"
"fmt"
"time"
"eda-example/internal/domain/entity"
"eda-example/internal/domain/event"
"eda-example/internal/domain/queue"
)

type ProcessOrderPaymentUseCase struct {
publisher queue.Publisher
}

func NewProcessOrderPaymentUseCase(publisher queue.Publisher) *ProcessOrderPaymentUseCase {
return &ProcessOrderPaymentUseCase{
publisher: publisher,
}
}

func (h *ProcessOrderPaymentUseCase) Execute(ctx context.Context, payload *event.OrderCreatedEvent) error {
fmt.Println("--- ProcessOrderPaymentUseCase ---")

// TODO: find order by id in the repository database here
order, err := entity.RestoreOrderEntity(payload.Id, payload.Status)
if err != nil {
return err
}

for _, i := range payload.Items {
item := entity.NewOrderItemEntity(i.ProductName, i.TotalPrice/float64(i.Quantity), i.Quantity)
order.AddItem(item)
}

paymentValue := payload.TotalPrice
err = order.Pay(paymentValue)
if err != nil {
return err
}

fmt.Printf("Order Paid. Value: %f \n", payload.TotalPrice)
err = h.publisher.Publish(ctx, event.OrderPaidEvent{OrderId: payload.Id, PaidValue: paymentValue, PaymentDate: time.Now()})
if err != nil {
return err
}
return nil
}

내부/애플리케이션/유스케이스 폴더 에 stock_movement_usecase.go 생성

package usecase

import (
"context"
"eda-example/internal/domain/event"
"fmt"
)

type StockMovementUseCase struct {
}

func NewStockMovementUseCase() *StockMovementUseCase {
return &StockMovementUseCase{}
}

func (h *StockMovementUseCase) Execute(ctx context.Context, payload *event.OrderCreatedEvent) error {
fmt.Println("--- StockMovementUseCase ---")
for _, item := range payload.Items {
fmt.Printf("Removing %d items of product %s from stock\n", item.Quantity, item.ProductName)
}
return nil
}

내부/애플리케이션/유즈케이스 폴더 에 send_orderemail_usecase.go 생성

package usecase

import (
"context"
"eda-example/internal/domain/event"
"fmt"
)

type SendOrderEmailUseCase struct {
}

func NewSendOrderEmailUseCase() *SendOrderEmailUseCase {
return &SendOrderEmailUseCase{}
}

func (h *SendOrderEmailUseCase) Execute(ctx context.Context, payload *event.OrderCreatedEvent) error {
fmt.Println("--- SendOrderEmailUseCase ---")
fmt.Printf("--- MAIL Order Created: R$ %f \n", payload.TotalPrice)
return nil
}

OrderController에서 리스너 핸들러 생성

내부/응용 프로그램/컨트롤러 폴더 의 order_controller.go 에 ProcessOrderPayment , StockMovement 및 SendOrderEmail 메서드를 만듭니다 .

package controller

import (
"encoding/json"
"net/http"
"eda-example/internal/application/dto"
"eda-example/internal/application/usecase"
"eda-example/internal/domain/event"
)

type OrderController struct {
createOrderUserCase *usecase.CreateOrderUseCase
processOrderPaymentUseCase *usecase.ProcessOrderPaymentUseCase
stockMovementUseCase *usecase.StockMovementUseCase
sendOrderEmailUseCase *usecase.SendOrderEmailUseCase
}

func NewOrderController(createOrderUserCase *usecase.CreateOrderUseCase,
processOrderPaymentUseCase *usecase.ProcessOrderPaymentUseCase,
stockMovementUseCase *usecase.StockMovementUseCase,
sendOrderEmailUseCase *usecase.SendOrderEmailUseCase) *OrderController {
return &OrderController{
createOrderUserCase: createOrderUserCase,
processOrderPaymentUseCase: processOrderPaymentUseCase,
stockMovementUseCase: stockMovementUseCase,
sendOrderEmailUseCase: sendOrderEmailUseCase,
}
}

func (u *OrderController) CreateOrder(w http.ResponseWriter, r *http.Request) {
var requestData dto.CreateOrderDTO
json.NewDecoder(r.Body).Decode(&requestData)
err := u.createOrderUserCase.Execute(r.Context(), requestData)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.WriteHeader(http.StatusCreated)
}

func (u *OrderController) ProcessOrderPayment(w http.ResponseWriter, r *http.Request) {
var body event.OrderCreatedEvent
json.NewDecoder(r.Body).Decode(&body)
err := u.processOrderPaymentUseCase.Execute(r.Context(), &body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.WriteHeader(http.StatusCreated)
}

func (u *OrderController) StockMovement(w http.ResponseWriter, r *http.Request) {
var body event.OrderCreatedEvent
json.NewDecoder(r.Body).Decode(&body)
err := u.stockMovementUseCase.Execute(r.Context(), &body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.WriteHeader(http.StatusCreated)
}

func (u *OrderController) SendOrderEmail(w http.ResponseWriter, r *http.Request) {
var body event.OrderCreatedEvent
json.NewDecoder(r.Body).Decode(&body)
err := u.sendOrderEmailUseCase.Execute(r.Context(), &body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.WriteHeader(http.StatusCreated)
}

리스너 구조체 생성

내부/ 인프라 /queue 폴더 에 listener.go 생성

package queue

import (
"net/http"
"reflect"
)

type Listener struct {
eventType reflect.Type
callback func(w http.ResponseWriter, r *http.Request)
}

QueueResponseWriter 구조체 생성

내부/ 인프라 /queue 폴더 에 response_writer.go 생성

package queue

import (
"fmt"
"net/http"
)

type QueueResponseWriter struct {
body []byte
statusCode int
header http.Header
}

func NewQueueResponseWriter() *QueueResponseWriter {
return &QueueResponseWriter{
header: http.Header{},
}
}

func (w *QueueResponseWriter) Header() http.Header {
return w.header
}

func (w *QueueResponseWriter) Write(b []byte) (int, error) {
w.body = b
return 0, nil
}

func (w *QueueResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
}

var okFn = func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}

func main() {
r := &http.Request{
Method: http.MethodPost,
}
w := NewQueueResponseWriter()
okFn(w, r)
fmt.Println(w.statusCode)
}

대기열 인터페이스 생성

내부/도메인/queue 폴더 에 queue.go 생성

package queue

import (
"context"
"net/http"
"reflect"
)

type Queue interface {
ListenerRegister(eventType reflect.Type, handler func(w http.ResponseWriter, r *http.Request))
Connect(ctx context.Context) error
Disconnect(ctx context.Context) error
Publish(ctx context.Context, body interface{}) error
StartConsuming(ctx context.Context, queueName string) error
}

MemoryQueueAdapter에서 메서드 구현

내부/ 인프라 /queue 폴더 의 memory_queue_adapter.go 에 메소드 구현

package queue

import (
"bytes"
"context"
"encoding/json"
"log"
"net/http"
"reflect"
)

type MemoryQueueAdapter struct {
listeners map[string][]Listener
}

func NewMemoryQueueAdapter() *MemoryQueueAdapter {
return &MemoryQueueAdapter{
listeners: make(map[string][]Listener),
}
}

func (eb *MemoryQueueAdapter) ListenerRegister(eventType reflect.Type, handler func(w http.ResponseWriter, r *http.Request)) {
eb.listeners[eventType.Name()] = append(eb.listeners[eventType.Name()], Listener{eventType, handler})
}

func (eb *MemoryQueueAdapter) Publish(ctx context.Context, eventPayload interface{}) error {
eventType := reflect.TypeOf(eventPayload)
payloadJson, _ := json.Marshal(eventPayload)

log.Printf("--- Publish %s ---", eventType)

for _, listener := range eb.listeners[eventType.Name()] {
w := NewQueueResponseWriter()
body := bytes.NewBuffer(payloadJson)
r, err := http.NewRequestWithContext(ctx, http.MethodPost, eventType.Name(), body)
if err != nil {
return err
}

listener.callback(w, r)
if err != nil {
return err
}
}

return nil
}

func (eb *MemoryQueueAdapter) Connect(ctx context.Context) error {
log.Println("--- MemoryQueueAdapter connected ---")
return nil
}

func (eb *MemoryQueueAdapter) Disconnect(ctx context.Context) error {
log.Println("--- MemoryQueueAdapter disconnected ---")
return nil
}

func (eb *MemoryQueueAdapter) StartConsuming(ctx context.Context, queueName string) error {
log.Printf("--- MemoryQueueAdapter StartConsuming queue %s ---", queueName)
return nil
}

리스너 핸들러를 사용하여 이벤트 바인딩

cmd/api 폴더 의 main.go 파일 에 리스너를 등록합니다.

package main

import (
"context"
"eda-example/internal/application/controller"
"eda-example/internal/application/usecase"
"eda-example/internal/domain/event"
"eda-example/internal/infra/queue"
"fmt"
"log"
"net/http"
"reflect"
)

func main() {
ctx := context.Background()

// initialize queue
queue := queue.NewMemoryQueueAdapter()

// use cases
createOrderUseCase := usecase.NewCreateOrderUseCase(queue)
processPaymentUseCase := usecase.NewProcessOrderPaymentUseCase(queue)
stockMovementUseCase := usecase.NewStockMovementUseCase()
sendOrderEmailUseCase := usecase.NewSendOrderEmailUseCase()

// controllers
orderController := controller.NewOrderController(createOrderUseCase, processPaymentUseCase, stockMovementUseCase, sendOrderEmailUseCase)

// register routes
http.HandleFunc("POST /create-order", orderController.CreateOrder)

// mapping listeners
var list map[reflect.Type][]func(w http.ResponseWriter, r *http.Request) = map[reflect.Type][]func(w http.ResponseWriter, r *http.Request){
reflect.TypeOf(event.OrderCreatedEvent{}): {
orderController.ProcessOrderPayment,
orderController.StockMovement,
orderController.SendOrderEmail,
},
}

// register listeners
for eventType, handlers := range list {
for _, handler := range handlers {
queue.ListenerRegister(eventType, handler)
}
}

// connect queue
err := queue.Connect(ctx)
if err != nil {
log.Fatalf("Error connect queue %s", err)
}
defer queue.Disconnect(ctx)

// start consuming queues
OrderCreatedEvent := reflect.TypeOf(event.OrderCreatedEvent{}).Name()

go func(ctx context.Context, queueName string) {
err = queue.StartConsuming(ctx, queueName)
if err != nil {
log.Fatalf("Error running consumer %s: %s", queueName, err)
}
}(ctx, OrderCreatedEvent)

// start server
fmt.Println("Server is running on port 8080")
http.ListenAndServe(":8080", nil)
}

이벤트 소비

애플리케이션을 다시 시작하세요.

image

엔드포인트 POST /create-order를 다시 호출하고 콘솔 로그를 확인하세요.

image

추가(RabbitMQ로 구현)

Rabbitmq lib를 설치합니다:

go get github.com/rabbitmq/amqp091-go

내부/ 인프라 /queue 폴더 에 Rabbitmq_adapter.go를 생성합니다 .

package queue

import (
"bytes"
"context"
"encoding/json"
"errors"
"log"
"net/http"
"reflect"
"time"

amqp "github.com/rabbitmq/amqp091-go"
)

type RabbitMQAdapter struct {
uri string
conn *amqp.Connection
listeners map[string][]Listener
}

type QueueMessage struct {
Body []byte
}

func NewRabbitMQAdapter(uri string) *RabbitMQAdapter {
return &RabbitMQAdapter{
uri: uri,
listeners: make(map[string][]Listener),
}
}

func (r *RabbitMQAdapter) Connect(ctx context.Context) error {
conn, err := amqp.Dial(r.uri)
if err != nil {
return err
}
r.conn = conn
return nil
}

func (r *RabbitMQAdapter) Disconnect(ctx context.Context) error {
return r.conn.Close()
}

func (r *RabbitMQAdapter) Publish(ctx context.Context, eventPayload interface{}) error {
eventName := reflect.TypeOf(eventPayload).Name()

ch, err := r.conn.Channel()
if err != nil {
return err
}
defer ch.Close()

q, err := ch.QueueDeclare(
eventName, // queue name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
return err
}

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

eventJson, err := json.Marshal(eventPayload)
if err != nil {
return errors.New("error converting struct to json")
}

err = ch.PublishWithContext(ctx,
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(eventJson),
})
if err != nil {
return err
}
log.Printf(" [x] Sent to queue %s: %s\n", eventName, eventJson)
return nil
}

func (r *RabbitMQAdapter) StartConsuming(ctx context.Context, queueName string) error {
ch, err := r.conn.Channel()
if err != nil {
return err
}
defer ch.Close()

q, err := ch.QueueDeclare(
queueName, // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
return err
}

msgs, err := ch.ConsumeWithContext(
ctx,
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
if err != nil {
return err
}

go func() {
for d := range msgs {
log.Printf("Received a message on queue %s: %s", queueName, d.Body)
hasError := false
for _, listener := range r.listeners[queueName] {
w := NewQueueResponseWriter()
body := bytes.NewBuffer(d.Body)
r, err := http.NewRequestWithContext(ctx, http.MethodPost, queueName, body)
if err != nil {
log.Printf("Error processing message: %s", err)
hasError = true
break
}

listener.callback(w, r)
if w.statusCode >= 400 {
log.Printf("Error processing message: %s", string(w.body))
hasError = true
break
}
}

if !hasError {
d.Ack(false)
}
}
}()

var forever chan struct{}
log.Printf(" [*] Waiting for messages on queue %s. To exit press CTRL+C", queueName)
<-forever
return nil
}

func (r *RabbitMQAdapter) ListenerRegister(eventType reflect.Type, handler func(w http.ResponseWriter, r *http.Request)) {
r.listeners[eventType.Name()] = append(r.listeners[eventType.Name()], Listener{eventType, handler})
}

Docker를 사용하여 RabbitMQ 서버 가동

루트 폴더에 docker-compose.yml 파일 생성

services:
rabbitmq:
image: "rabbitmq:3.8-management-alpine"
hostname: rabbitmq
ports:
- "15672:15672"
- "5672:5672"
volumes:
- "rabbitmq_data:/var/lib/rabbitmq/mnesia"
environment:
- RABBITMQ_DEFAULT_USER=guest
- RABBITMQ_DEFAULT_PASS=guest

volumes:
rabbitmq_data:

RabbitMQ 서버 실행

docker compose up -d

image

RabbitMQAdapter를 사용하도록 main.go 파일을 변경하세요 .

package main

import (
"context"
"eda-example/internal/application/controller"
"eda-example/internal/application/usecase"
"eda-example/internal/domain/event"
"eda-example/internal/infra/queue"
"fmt"
"log"
"net/http"
"reflect"
)

func main() {
ctx := context.Background()

// initialize queue
queue := queue.NewRabbitMQAdapter("amqp://guest:guest@localhost:5672/")

// use cases
createOrderUseCase := usecase.NewCreateOrderUseCase(queue)
processPaymentUseCase := usecase.NewProcessOrderPaymentUseCase(queue)
stockMovementUseCase := usecase.NewStockMovementUseCase()
sendOrderEmailUseCase := usecase.NewSendOrderEmailUseCase()

// controllers
orderController := controller.NewOrderController(createOrderUseCase, processPaymentUseCase, stockMovementUseCase, sendOrderEmailUseCase)

// register routes
http.HandleFunc("POST /create-order", orderController.CreateOrder)

// mapping listeners
var list map[reflect.Type][]func(w http.ResponseWriter, r *http.Request) = map[reflect.Type][]func(w http.ResponseWriter, r *http.Request){
reflect.TypeOf(event.OrderCreatedEvent{}): {
orderController.ProcessOrderPayment,
orderController.StockMovement,
orderController.SendOrderEmail,
},
}

// register listeners
for eventType, handlers := range list {
for _, handler := range handlers {
queue.ListenerRegister(eventType, handler)
}
}

// connect queue
err := queue.Connect(ctx)
if err != nil {
log.Fatalf("Error connect queue %s", err)
}
defer queue.Disconnect(ctx)

// start consuming queues
OrderCreatedEvent := reflect.TypeOf(event.OrderCreatedEvent{}).Name()

go func(ctx context.Context, queueName string) {
err = queue.StartConsuming(ctx, queueName)
if err != nil {
log.Fatalf("Error running consumer %s: %s", queueName, err)
}
}(ctx, OrderCreatedEvent)

// start server
fmt.Println("Server is running on port 8080")
http.ListenAndServe(":8080", nil)
}

서버를 실행하고 주문 생성 엔드포인트를 호출합니다.

image

RabbitMQ 대시보드에서 생성 및 사용된 대기열을 확인하세요.

image

전체 코드: https://github.com/hanhyeonkyu/event-driven-golang

DevOps - Bash Linux

· 6 min read

1. 쉘 스크립팅에 사용되는 다양한 유형의 변수는 무엇입니까?

쉘 스크립팅에는 다음과 같은 두 가지 유형의 변수가 사용됩니다.

1. 시스템 생성 변수 - 이름에서 알 수 있듯이 운영 체제에서 생성된 변수입니다. 이러한 변수는 Set 명령을 호출하여 볼 수 있습니다.

2. 사용자 정의 변수 - 사용자가 생성한 변수로, echo 명령을 호출하여 볼 수 있습니다.

2. 이름이 example인 파일이 경로에 존재하는지 여부를 어떻게 확인합니까?

및 명령 [은 test본질적으로 스위치입니다. trueor 응답을 내보내 false지만 둘 다 성공으로 간주합니다. 명령을 &&및 같은 논리 연산자와 쌍을 이루어 이를 사용할 수 있습니다 ||. &&응답이 다음과 같을 때 연산자가 실행 됩니다 true.

$ touch example
$ test -e example && echo "foo"
foo
$ test -e notafile && echo "foo"
$

|| 응답이 다음과 같을 때 연산자가 실행 됩니다 false.

$ touch example
$ test -e example || echo "foo"
$ test -e notafile || echo "foo"
foo
$

원하는 경우 대신 대괄호를 사용할 수 있습니다 test. 모든 경우에 결과는 동일합니다.

$ touch example
$ [ -e example &#93; && echo "foo"
foo
$ [ -e notafile &#93; && echo "foo"
$

 

3. 파일이 생성될 때 파일의 기본 권한은 무엇입니까?

Linux 또는 UNIX에서 생성된 모든 파일에는 기본 권한이 있으며 파일 권한을 보려면 새로 생성된 파일에 대해 unmask 또는 user 마스크 명령이 사용됩니다. 기호값을 설정하고 표현하는데 사용되는 4자리 8진수입니다. 새로 생성된 파일의 기본 권한은 rw-rw-r 이며 664 입니다 .

4. CRONTAB이란 무엇입니까?

CRON 테이블을 표현하는데 사용됩니다. CRON 일정을 사용하여 작업을 실행합니다. 정기적인 스케줄러 작업을 실행하는 데 사용되는 명령 목록입니다. 스케줄러의 이름은 CRONTAB입니다.

5. 쉘 스크립트와 쉘 프로그램에서 발생한 문제를 어떻게 디버깅합니까?

  1. 첫 번째 방법은 쉘 스크립트 프로그램에 디버그 명령을 넣어 프로그램의 오류나 버그를 출력하거나 표시하는 것입니다.
  2. 두 번째 방법은 set –x를 사용하여 스크립트를 디버깅하는 것입니다.

6. 쉘 스크립트 명령을 사용하여 디스크 사용량을 어떻게 알 수 있습니까?

쉘 스크립트 명령을 사용하여 디스크 사용량을 확인하는 세 가지 방법이 있습니다.

  • dfspace 명령: — 이는 여유 디스크 사용량을 메가바이트 단위로 확인하는 데 사용됩니다.
  • df 명령: — 여유 디스크 공간을 확인하는 데 사용됩니다.
  • du 명령: — 디렉터리별 디스크 사용량을 확인하는 데 사용됩니다.
    예: du -sh /data01/*

7. 쉐뱅라인의 목적을 알려주세요.

shebang 라인은 엔진의 위치를 ​​결정하는 데 사용되는 각 스크립트의 상단에 있으며 스크립트를 실행하는 데 사용됩니다.

8. Linux 프로세스의 4단계에 대해 설명하세요.

4단계에는 다음이 포함됩니다.

  • 대기 중 - Linux 프로세스가 리소스를 기다리고 있습니다.
  • 실행 중 - Linux 프로세스가 현재 실행 중입니다.
  • 중지됨 - 성공적인 실행 후 Linux 프로세스가 중지되었습니다.
  • 좀비 - Linux 프로세스가 프로세스 테이블에서 계속 활성화되어 있지만 중지되었습니다.

9. 메타문자란 무엇입니까?

메타문자는 프로그램이나 쉘의 데이터 필드에 포함된 고유한 문자입니다. 다른 캐릭터에 대한 정보를 제공합니다. 예를 들어, 문자 's'로 시작하는 모든 파일을 나열하려면 'ls s*' 명령을 사용하십시오.

10. 경로에서 모든 .txt 파일의 이름을 .log로 바꿉니다.

ls | cut -d. -f1 | xargs -i mv {}.txt {}.log

11. 쉘 스크립트에서 명령줄 인수를 읽는 방법은 무엇입니까?

Bash 쉘에는 쉘 스크립트를 통해 전달하는 인수를 가리키도록 예약된 특수 변수가 있습니다. Bash는 이러한 변수를 숫자로 저장합니다($1, $2, $3, … $n).

위치 매개변수인 특수 문자도 있지만 해당 기능은 명령줄 인수와 밀접하게 연결되어 있습니다.

특수 문자 $#는 총 인수 수를 저장합니다. 또한 모든 인수를 나타내는 데 사용되는 와일드카드 문자로 $@ 및 $* 가 있습니다 . $$를 사용하여 현재 쉘 스크립트의 프로세스 ID를 찾는 반면, $? 스크립트의 종료 코드를 인쇄하는 데 사용할 수 있습니다.

#!/bin/sh
echo "Script Name: $0"
echo "First Parameter of the script is $1"
echo "The second Parameter is $2"
echo "The complete list of arguments is $@"
echo "Total Number of Parameters: $#"
echo "The process ID is $$"
echo "Exit code for the script: $?"
./PositionalParameters.sh learning command line arguments

image

12. Linux에서 표준 스트림의 이름을 지정합니다.

Linux의 표준 스트림은 표준 입력, 표준 출력 및 표준 오류입니다.

13. $@와 $*를 구별하세요

$*는 전체 위치 인수 집합을 단일 문자열로 간주하는 반면, $@는 각 인용 인수를 별도의 인수로 처리합니다.

14. 조건문을 사용하여 파일 시스템에 파일이 존재하는지 확인하는 방법

if [ -f /var/log/messages ]
then
echo "File exists."
fi

15. echo 명령을 사용하여 문자열 변수의 일부를 가져옵니다.

echo ${variable:x:y} 여기서 X는 시작 위치이고 y는 문자열 길이입니다.

variable="Her name is Jen, and she is a developer."
echo ${variable:12:6}

#Output:
#Jen, a
#As J is at index 12 start from 0, and followed by 6 characters

16. [[ $string == “efg*” ]] 와 [[ $string == efg* ]] 사이의 차이점은 무엇입니까?

[[ $string == efg* ]] > 문자열이 efg로 시작하는지 확인합니다.

[[ $string == “efg*” ]] > 문자열이 efg인지 확인합니다.

17. SSH 인증

tail -n 10 /var/log/secure
tail -n 10 /var/log/auth.log

home 또는 ~/.ssh 디렉토리에 그룹 쓰기 권한이 있는 경우 SSH는 이를 좋아하지 않습니다. 홈 디렉토리는 본인만 쓸 수 있어야 하며 ~/.ssh 는 700, Authorized_keys 는 600 이어야 합니다.

chmod 700 /home/user
chmod 700 /home/user/.ssh
chmod 600 /home/user/.ssh/authorized_keys

[

SSH 인증 거부됨: 잘못된 소유권 또는 디렉터리 모드

수정 방법 알아보기: 공개 설정을 시도할 때 "SSH 인증 거부됨: 잘못된 소유권 또는 디렉토리 모드" 오류…

chemicloud.com

](https://chemicloud.com/kb/article/ssh-authentication-refused-bad-ownership-or-modes-for-directory/?source=post_page-----7e22928a1486--------------------------------)

18. 특수 쉘 변수는 무엇입니까**$?**

Bash의 특수 쉘 변수는 $?마지막 명령이나 가장 최근에 실행된 프로세스의 종료 상태를 가져오는 데 사용됩니다. 변수 를 사용하여 $?명령이 성공적으로 실행되었는지 여부를 확인하고 결과에 따라 적절한 조치를 취할 수 있습니다. 0이 아닌(1–255) 종료 상태는 실패를 나타냅니다.

#!/bin/bash

# Try to copy a file that does not exist
cp foo.txt bar.txt

# Check the exit status of the cp command
if [ $? -eq 0 ]; then
echo "Copy successful"
else
echo "Copy failed"
fi

# Output:
# cp: foo.txt: No such file or directory
# Copy failed

[## Bash에서 종료 상태 코드를 반환하는 방법

Bash에서 종료 코드(반환 코드 또는 상태 코드라고도 함)는 실행된 명령에 의해 반환되는 숫자 값입니다.

byby.dev](https://byby.dev/bash-exit-codes?source=post_page-----7e22928a1486--------------------------------

19. 무엇을 **grep -oP '\d+'**합니까?

grep -oP '\d+'텍스트의 숫자만 검색하여 출력하는 데 사용됩니다. \d+하나 이상의 숫자와 일치하는 정규식 패턴입니다. 플래그 -o는 일치하는 패턴만 인쇄되도록 보장하고 -P플래그는 Perl 호환 정규식을 활성화합니다.

echo "finnone-integration [Version=RevNo.201963]" | grep -oP '\d+'
# Output: 201963

20. 목록 방식으로만 파일 이름을 나열하는 방법.

ls -1다른 속성 없이 파일 이름만 표시됩니다.

ls -1a다른 속성 없이 모든 파일 이름만 표시됩니다.

나중에 사용할 수 있도록 출력을 변수에 저장합니다.filelist=`ls -1 /somedir/`

21. zip 파일에 디렉토리가 포함되어 있는지 확인하는 방법

unzip -l yourfile.zip | grep -q "/$"

image

설명:

  • 이 unzip -l명령은 zip 파일의 내용을 나열합니다.
  • 이 grep -q "/$"명령은 디렉터리를 나타내는 슬래시로 끝나는 줄을 검색합니다.
  • 이 -q옵션은 출력을 억제하는 데 사용되며 반환 코드에만 관심이 있습니다.

zip 파일에 디렉터리가 포함되어 있으면 명령은 반환 코드 0으로 종료됩니다. zip 파일에 디렉터리가 없으면 명령은 0이 아닌 반환 코드로 종료됩니다.

예:

if unzip -l yourfile.zip | grep -q "/$"; then
echo "The zip file contains a directory."
else
echo "The zip file does not contain a directory."
fi

22. 쉘 변수 주위에 중괄호가 필요한 경우는 언제입니까?

변수에서 읽으 려면 (즉, 변수를 확장하려면 ) 다음을 사용해야 합니다.$

$var # use the variable
${var} # same as above
${var}bar # expand var, and append "bar" too
$varbar # same as ${varbar}, i.e expand a variable called varbar, if it exists.

중괄호는 다음과 같은 경우에도 무조건 필요합니다.

  • 다음과 같이 배열 요소를 확장합니다.${array\[42\]}
  • ${filename%.\*}(확장 제거) 와 같이 매개변수 확장 작업을 사용하여
  • 위치 매개변수를 9개 이상으로 확장:"$8 $9 $10 $11"

소프트웨어 엔지니어에서 소프트웨어 설계자로 — 성공을 위한 로드맵

· 9 min read

image

소프트웨어 아키텍처를 마스터하는 길은 끝이 없는 여정입니다. 

해당 주제에 관해 샅샅이 뒤지다 보면 방대한 양의 자료에 압도당하게 됩니다.

더욱이, 소프트웨어 아키텍처에 관한 콘텐츠의 대부분은 여러분과 같은 사람들이 자신만의 방식으로 이 여정을 이해하려고 노력하면서 게시한 것이기 때문에 여러분이 발견한 것 중 대부분은 품질이 다양합니다.

소프트웨어 아키텍처 주제에 관한 수많은 비디오, 블로그, 튜토리얼, 서적, 강좌 및 기타 유형의 콘텐츠가 있지만 모두 동일하게 만들어지는 것은 아닙니다. 일부는 훌륭하고 다른 일부는 어느 정도 장점이 있는 반면, 수많은 다른 리소스는 오해의 소지가 있고 완전히 잘못되었습니다.

그래서 제가 아래에서 한 일은 소프트웨어 아키텍처 주제에 대해 엄선된 고품질 리소스 목록을 모아 숙련된 소프트웨어 설계자뿐만 아니라 야심 찬 사람들도 쉽게 사용할 수 있도록 하는 것입니다. 이것이 소프트웨어 아키텍처 숙달을 향한 올바른 길을 설정하는 데 도움이 되기를 바랍니다.

무료 및 유료 리소스 모두 소프트웨어 아키텍처의 다양한 측면과 업계에서 소프트웨어 아키텍처의 역할에 대해 설명합니다.

이 컬렉션의 목표는 소프트웨어 엔지니어가 소프트웨어 아키텍트가 되거나 현재 소프트웨어 아키텍트가 자신의 역할을 더 잘 수행할 수 있도록 일종의 로드맵 역할을 하는 것입니다. 경험이 풍부한 소프트웨어 설계자라도 이러한 리소스를 사용하여 격차를 줄이고 명확성을 찾거나 사물에 대한 새로운 시각을 얻을 가능성이 높습니다.

image

제가 이 로드맵을 위에서 아래로 구성한 방식은 각 단계가 이전 단계를 기반으로 구축되도록 논리적 순서로 되어 있습니다. 물론 이것은 궁극적으로 관련 없는 리소스의 모음이므로 내가 정리한 순서가 의미가 있기를 바랍니다.

아래의 각 항목에 대해 무료인지 유료인지 식별하고 있습니다. 또한 해당 항목이 다루는 내용과 그것이 소프트웨어 아키텍트(또는 더 훌륭하고 성공하며 번창하는 소프트웨어 아키텍트)가 되는 여정에 정확히 얼마나 도움이 되는지에 대한 분석도 볼 수 있습니다.

더 이상 고민하지 마세요 — 소프트웨어 설계자 리소스 — 성공을 위한 로드맵! 🚀

👉 모든 개발자가 소프트웨어 아키텍처에 대해 알아야 할 5가지 • Simon Brown • GOTO 2020

이것이 유용한 이유: 소프트웨어 개발자가 소프트웨어 아키텍처에 대해 알아야 할 주요 항목에 대한 명확하고 간단한 설명입니다.

📺 유형 : 비디오

💰무료 : 예

👉 소프트웨어 아키텍처는 어떤 모습이어야 할까요?

이것이 유용한 이유: 적절한 소프트웨어 아키텍처의 기본 사항에 대한 더 많은 통찰력.

📺 유형 : 비디오

💰무료 : 예

👉 “Good Enough” 아키텍처 • Stefan Tilkov • GOTO 2019

이것이 유용한 이유: 소프트웨어 아키텍처가 목적을 달성하는 방법을 다룹니다. "완벽한 아키텍처"를 추구하면 그것을 무너뜨릴 수 있습니다.

📺 유형 : 비디오

💰무료 : 예

👉 최소 실행 가능 아키텍처 • Randy Shoup • YOW! 2022년

이것이 유용한 이유: 최소 실행 가능 아키텍처(또는 MVA)의 개념은 최소 실행 가능 제품(MVP)만큼 자주 논의되지 않습니다. 그러나 소프트웨어 설계자로서 우리가 하는 모든 일, 특히 비즈니스와 기술의 연계에 있어 이것이 얼마나 중요한지 이해하는 것이 중요합니다. 이 개념은 위의 “Good Enough Architecture” 개념과 관련이 있습니다.

📺 유형 : 비디오

💰무료 : 예

👉 소프트웨어 아키텍처 팁 더 빨리 알았으면 좋았을 텐데요

이것이 유용한 이유: 이전 개념을 바탕으로 소프트웨어 아키텍처 및 일반 응용 프로그램에 대한 몇 가지 빠른 팁을 제공합니다.

📺 유형 : 비디오

💰무료 : 예

👉 훌륭한 소프트웨어 설계자가 되는 방법 • Eberhard Wolff • GOTO 2019

이것이 유용한 이유: 이것은 오래된 비디오입니다. 그러나 여기서 말하는 개념은 "충분히 좋은 아키텍처"인 MVA의 개념과 일반적으로 소프트웨어 아키텍처 측면에서 소프트웨어 설계자가 집중해야 하는 개념과 밀접하게 연관되어 있습니다.

📺 유형 : 비디오

💰무료 : 예

👉 AWS re:Invent 2022 — 건축가 엘리베이터: 회의실과 IT 연결(ENT218)

이것이 유용한 이유: Gregor Hohpe의 저서 "The Software Architect Elevator"와 조직에서 소프트웨어 설계자의 주요 기능을 요약한 버전입니다.

📺 유형 : 비디오

💰무료 : 예

👉 소프트웨어 아키텍처의 기초(O'Reilly Press)

이것이 유용한 이유: 이는 소프트웨어 아키텍처가 무엇인지, 무엇에 중점을 두고 있는지에 대한 많은 중요한 부분을 식별하는 훌륭한 요약입니다. 이는 야심 찬 소프트웨어 설계자와 숙련된 소프트웨어 설계자 모두에게 지식과 이해의 많은 격차를 메울 것입니다.

📘 종류 : 도서

💰무료 : 아니요

소프트웨어 아키텍처에 대한 심층 분석 - 확대

image

👉 리액티브 선언문

이것이 유용한 이유 : Reactive 선언문은 Reactive 시스템을 구축하는 방법과 이유에 대해 설명합니다. 이는 소프트웨어 아키텍처 내의 틈새 시장이지만, 그 중 많은 부분이 확장 가능하고 탄력적이며 현대적인 아키텍처 구축의 기초이므로 해당 개념을 이해하는 것이 유용합니다.

🌐 유형 : 사이트

💰 무료 : 예

👉 소프트웨어 아키텍처 가이드(Martin Fowler 작성)

이것이 유용한 이유: Martin Fowler는 소프트웨어 개발 및 아키텍처 분야에서 소개가 필요 없는 또 다른 이름입니다. 그의 사이트에는 소프트웨어 아키텍처의 다양한 측면과 설계자의 역할에 대한 확실한 이해를 구축할 수 있을 만큼 자세하고 전체적인 풍부한 정보가 있습니다.

🌐 유형: 사이트

💰 무료: 예

👉 소프트웨어 아키텍처: 어려운 부분

이것이 유용한 이유: 업계 최고의 전문가가 쓴 또 다른 훌륭한 책입니다. 앞서 언급한 "소프트웨어 아키텍처의 기초"와 마찬가지로 소프트웨어 아키텍처의 기초와 고급 개념에 대해 설명합니다. 그러나 여기서 초점은 소프트웨어 설계자가 매일 처리하는 과제, 즉 어려운 부분에 있습니다.

📘 유형: 책

💰 무료: 아니요

👉 마크 리차드의 채널

이것이 유용한 이유: Mark는 소프트웨어 아키텍처 세계에서 잘 알려진 인물입니다. O'Reilly의 인기 작가이자 독립 컨설턴트이자 해당 주제에 대한 놀라운 비디오 모음집을 발행한 사람입니다. 그의 방대한 비디오는 소프트웨어 아키텍처 분야의 다양한 주제를 다루고 있습니다. Mark의 채널은 풍부한 정보를 제공합니다.

📺 유형 : 유튜브 채널

💰무료 : 예

전문성 구축 - 더욱 확대

image

👉 실제 소프트웨어 아키텍처, 4판

이 책이 유용한 이유: 이 책은 이미 언급된 다른 책들과 일부 겹치는 부분이 있습니다. 그러나 이는 다소 다른 관점을 제공하며 이를 통해 소프트웨어 엔지니어링과 아키텍처 간의 격차를 더 많이 해소합니다.

📘 종류 : 도서

💰무료 : 아니요

👉 데이터 집약적인 애플리케이션 설계

이 책이 유용한 이유: 이 책은 고전입니다. 데이터 처리에 중점을 두고 대규모 시스템을 구축하는 데 중점을 둡니다. 대부분의 최신 엔터프라이즈 시스템은 대규모 데이터 처리 및 관리에 의존합니다. 이 책은 그러한 시스템을 구축하는 데 필요한 과제, 방법 및 전략을 설명합니다.

📘 종류 : 도서

💰무료 : 아니요

👉 혁신적인 아키텍처 구축, 2판

이것이 유용한 이유: 소프트웨어 아키텍처는 대체로 시간이 지남에 따라 발전할 수 있는 시스템을 구축하는 것입니다. 완벽한 아키텍처는 존재하지 않으며, 이를 추구하면 실제 문제를 해결하고 비즈니스에 서비스를 제공하며 가치를 제공하는 시스템을 구축하는 능력을 잃게 됩니다. 이 책은 업계에서 가장 잘 알려진 사람들의 전체적인 개요입니다. 시간이 지남에 따라 진화하고 시간의 시험을 견디는 건축물 구축이라는 주제를 훌륭하게 마무리합니다.

📘 종류 : 도서

💰무료 : 아니요

👉 마이크로서비스.io

이것이 유용한 이유: Microservices.io는 소프트웨어 아키텍처 회로의 또 다른 잘 알려진 인물인 Chris Richardson의 프로젝트입니다. Chris는 마이크로서비스 아키텍처, 소프트웨어 제공 및 아키텍처 패턴에 관해 글을 쓰고 강연합니다. 그의 작업은 아키텍처 스타일로서의 마이크로서비스에 초점을 맞추고 있지만 그가 말하는 패턴과 방법론은 다양한 방식으로 보편적으로 적용 가능합니다. 안정적이고 확장 가능하며 현대적인 시스템을 구축하려면 이러한 개념을 숙지하고 이해하는 것이 중요합니다.

🌐 유형 : 사이트

💰무료 : 예

클라우드 공급업체 Well-Architected 아키텍처 프레임워크

image

이것이 유용한 이유: 세 가지 주요 클라우드 공급업체는 각각 아키텍처 및 시스템 설계 모범 사례에 대한 강력한 지침 및 표준 세트를 보유하고 있습니다. 이들은 "잘 설계된" 프레임워크로 훌륭하게 패키지되어 있습니다. 각 공급업체에는 개념을 설명하는 고유한 접근 방식과 방법이 있습니다. 그러나 이들 모두의 기본에는 거의 동일한 여러 기둥이 있습니다.

이 세 공급업체는 분명히 자사의 클라우드 서비스를 강조하지만 그들이 말하는 개념은 보편적으로 적용 가능합니다. 세 가지 프레임워크는 모두 중요하며 완료하는 데 시간이 걸립니다. 다음은 이러한 문제를 해결하는 방법에 대한 몇 가지 팁입니다. 그럼에도 불구하고 해당 프레임워크 중 하나를 검토하고 이해한다는 것은 다른 프레임워크의 90%도 이해했다는 의미입니다. 세 가지를 모두 살펴보고 더 직관적으로 느껴지는 것을 선택하는 것이 좋습니다. 그렇지 않다면 가장 직관적이고 따라하기 쉬운 GCP부터 시작하겠습니다.

🌐 유형: 사이트

💰무료 : 예

👉 Google Cloud 아키텍처 프레임워크

GCP 아키텍처 프레임워크는 사용자 친화적이고 탐색하기 쉽습니다. 목차를 살펴보는 것은 잘 정리된 안내서를 살펴보는 것과 같습니다. 그들이 아키텍처 원칙을 논의하는 방식은 대부분 불가지론적이고 실제 GCP 서비스와 독립적입니다.

👉 AWS Well-Architected 프레임워크

Amazon Web Services에는 클라우드에서 애플리케이션을 설계하고 설계하는 것과 유사한 가이드도 있습니다. GCP에서 제공하는 것과 유사하며 상호 참조하고 공백을 메우는 데 사용할 수 있습니다.

👉 Azure Well Architected 프레임워크

Azure Well-Architected 프레임워크는 탐색하기 다소 덜 직관적일 수 있지만 최신 클라우드 기반, 확장 가능하고 탄력적인 애플리케이션을 설계하는 데 대한 풍부한 정보도 포함합니다. 또한 업계 모범 사례에 중점을 두고 특정 Azure 기술을 학습할 수 있는 무료 과정과 "경로"를 제공합니다.

10,000피트 뷰 - 축소

image

👉 소프트웨어 아키텍트 엘리베이터

이것이 유용한 이유: Software Architect Elevator는 훌륭한 읽기 자료이자 모든 소프트웨어 설계자에게 꼭 필요한 책입니다. 이 책에서 Gregor Hohpe는 소프트웨어 설계자가 업계에 제공하는 가장 중요한 가치, 이와 관련된 장점, 함정 및 미묘한 차이에 대해 이야기합니다. 이 책은 소프트웨어 아키텍트 역할에 대한 이해를 돕기 위한 좋은 책입니다.

📘 유형: 책

💰무료 : 아니요

👉 기술 레이더 — 오늘날의 기술 환경에 대한 독보적인 가이드

이것이 유용한 이유: Thoughtworks는 소프트웨어 아키텍처 및 일반적인 기술 상태에 대한 가장 중요한 콘텐츠 중 일부를 생산하므로 따라갈 가치가 있는 회사입니다. Technology Radar는 업계 전반의 추세, 문제 및 기술 예측에 대해 설명하는 종합 가이드로 매년 발표됩니다.

🌐 유형: 사이트 및 디지털 가이드

💰무료 : 예

지속적인 학습과 소프트웨어 설계자로서의 레벨업을 위한 리소스

image

👉 https://www.deararchitects.xyz/

“Dear Architects” 뉴스레터는 소프트웨어 아키텍처 분야에서 주목할 만한 또 다른 유명인 Luca Mezarilla의 프로젝트입니다. 이 뉴스레터는 소프트웨어 아키텍처 세계의 모든 분야에서 훌륭한 통찰력, 사용 사례 및 정보를 제공합니다.

👉 https://dzone.com/

소프트웨어 엔지니어링, 아키텍처 및 기술에 대한 인기 있는 리소스입니다. 여기에서 추세 보고서, 참조 카드 및 풍부한 정보를 찾을 수 있습니다.

👉 https://www.infoq.com/

위의 Dzone과 마찬가지로 이는 소프트웨어 아키텍처, 엔지니어링 및 기술 콘텐츠 측면에서 업계의 거대 기업입니다.

👉 https://www.gartner.com/en/insights

Gartner의 통찰력 및 동향 보고서와 기사는 사실상의 표준이며 대부분의 CTO 및 기술 리더 읽기 목록의 필수 항목입니다. Gartner는 대규모 설문 조사를 진행하고 사고 리더십, 기술 상태 및 업계 동향에 대한 깊은 통찰력을 제공하는 기관입니다. Gartner 보고서를 따르는 이유는 반드시 심층적인 기술 지식을 갖추기 위한 것이 아니라 산업 기술 환경 전반에 대한 지식과 인식을 구축하기 위한 것입니다.

👉 GOTO 컨퍼런스

GOTO 컨퍼런스는 소프트웨어 엔지니어링 및 아키텍처 분야에서 최고의 컨퍼런스 중 하나입니다. 채널을 팔로우하고 최신 콘텐츠를 유지하는 것은 가치 있는 일입니다.

Google Cloud Storage(GCS)를 통한 Angular CDN 구현

· 3 min read

image

Angular 애플리케이션을 배포하기 위해 GCP 저장소 버킷을 빠르게 만드는 방법을 보여 드리겠습니다.

내 Angular 프로젝트를 저장하기 위해 스토리지 버킷을 생성해야 합니다.

그런 다음 Application Load Balancer는 트래픽을 URL 또는 IP에서 스토리지 버킷의 콘텐츠로 리디렉션합니다.

세부 사항을 살펴 보겠습니다.

프런트엔드 준비

우선 Storage Bucket에 업로드할 파일을 준비해야 합니다.

해당 파일은 프로젝트 파일이 아니지만 프로젝트의 컴파일된 버전을 얻으려면 Angular 프로젝트를 빌드해야 합니다.

컴파일을 통해 전체 프로젝트가 포함된 5~7개의 파일 세트가 생성됩니다.

이는 프로젝트의 모든 JavaScript, HTML 및 CSS 콘텐츠를 단일 파일로 압축한 것입니다.

해당 파일을 얻으려면 package.json 파일을 살펴보거나 다음 명령을 실행하면 됩니다.

1ng build

결과 파일은 dist 폴더 에 있습니다 . 그 안에는 프로젝트 이름이 적힌 또 다른 폴더가 표시됩니다. 그리고 이 폴더 안에는 생성된 모든 파일이 있습니다.

GCP 저장소 버킷 구성

이제 컴파일된 파일을 업로드하기 위해 스토리지 버킷을 생성하고 구성해야 합니다. 로드 밸런서를 통해 URL을 통해 액세스할 수 있도록 이 버킷을 구성해야 합니다.

모든 정보는 이 링크 에서 찾을 수 있습니다 .

먼저 모든 콘텐츠를 저장할 버킷을 만들어 보겠습니다. 기본 옵션과 프라이빗 버킷을 선택합니다.

image

GCP 저장소 버킷 만들기

Angular 프로젝트의 파일을 저장할 버킷이 있으면 이제 URL 요청을 index.html 파일로 리디렉션하는 로드 밸런서를 생성할 차례입니다.

네트워크 설정으로 이동하여 Application Load Balancer인 Load Balancer를 생성합니다.

image

Application Load Balancer 생성

Application Load Balancer를 구성할 때 프런트엔드와 백엔드를 설정해야 합니다.

프런트엔드의 경우 기본값을 그대로 둡니다. 프런트엔드 구성은 Load Balancer, 포트, 프로토콜 등에 액세스하는 방법을 정의합니다.

그리고 백엔드의 경우 Load Balancer 뒤에 무엇이 있는지 정의합니다. 로드 밸런서 뒤에 스토리지 버킷을 두고 싶습니다. 이제 백엔드 로드 밸런서를 버킷으로 구성해 보겠습니다.

image

Application Load Balancer 백엔드 버킷 구성

마지막으로 프런트엔드 및 백엔드 구성을 표시하는 요약 창이 있습니다. 생성 버튼을 클릭하면 로드 밸런서가 생성됩니다.

image

로드 밸런서 요약

Load Balancer가 생성되면 요청할 IP를 얻기 위한 세부 정보를 확인할 수 있습니다.

image

로드 밸런서 세부정보 보기

나중에 IP를 복사합니다.

콘텐츠 업로드

GCP의 모든 아키텍처가 생성되었습니다. 이제 Angular 프로젝트를 내 스토리지 버킷에 업로드할 차례입니다.

내 프로젝트를 업로드하려면 터미널에서 다음 GCP 명령을 실행해야 합니다.

1gcloud storage cp -r dist/frontend gs://my-bucket

이렇게 하면 dist/frontend 폴더 의 콘텐츠 (내 프로젝트 이름이 frontend 인 경우)가 my-bucket 이라는 스토리지 버킷에 업로드됩니다 .

귀하의 콘텐츠에 접근하세요

이제 브라우저에서 로드 밸런서의 IP 주소에 액세스할 수 있습니다. 그러면 브라우저에 내 스토리지 버킷의 콘텐츠가 표시됩니다.

더 나아가서 HTTPS 프로토콜을 사용하여 Angular 프로젝트로 이동하도록 로드 밸런서에서 SSL 인증서를 구성할 수 있습니다.

또한 접근성이 높고 빠른 액세스가 가능하도록 CDN(Content Delivery Network)을 구성하여 전 세계 여러 지역에 프런트엔드 파일을 분산시킬 수 있습니다.

마지막으로 IP 주소 대신 DNS 레코드를 생성하여 로드 밸런서에 도메인 이름을 연결할 수 있습니다. 이렇게 하면 IP 주소 대신 맞춤 URL을 갖게 됩니다.