Microservices NGINX로 웹 프론트엔드 구축

NGINX에서는 NGINX Microservices 참조 아키텍처를 통해 Microservices 애플리케이션 개발의 최전선을 탐색하기 시작했습니다. 이 프로토타입으로 샘플 사진 저장 및 공유 사이트를 사용합니다.
해당 포스트는 웹 스케일 애플리케이션을 구축하는 개발자, 아키텍트 및 운영 엔지니어들이 일상적으로 다루는 실제 문제와 해결책을 탐구하기 위해 전문가들에게 바칩니다.

목차

1. Microservices 개요
2. MVC 프레임워크를 통한 개발 제어
 2-1. MVC 분업 체제
3. 세션 상태 유지
 3-1. 캐싱을 통한 Microservices 딜레마 해결
4. Microservices 라우팅 및 로드 밸런싱
5. 결론

1. Microservices 개요

이 포스트는 Microservices 분야에서 대부분 무시되고 있던 애플리케이션 전달 구성 요소인 웹 프론트엔드에 대해 다룹니다. 서비스 디자인에 대한 많은 기사와 책이 쓰여졌지만, Microservices 구성 요소 위에 overlay되는 리치한 사용자 경험 기반의 웹 컴포넌트를 통합하는 방법에 대한 정보는 부족합니다.
해당 포스트는 Microservices 애플리케이션에서 웹 개발의 지저분한 문제에 대한 해결책을 제공하려고 합니다.

많은 면에서 웹 프론트엔드는 Microservices 기반 애플리케이션의 가장 복잡한 구성 요소입니다.
기술적으로는 JavaScript와 PHP, HTML, CSS 등의 서버 언어를 사용하여 비즈니스 및 디스플레이 로직을 결합합니다. 더 많은 복잡성을 추가하면, 웹 앱의 사용자 경험은 일반적으로 백엔드에서 Microservices 경계를 넘나들기 때문에 웹 컴포넌트는 기본 제어 레이어가 됩니다.
이는 일종의 상태 기계를 통해 일반적으로 구현되지만 유연하고 고성능이며 우아해야 합니다.
이러한 기술적 요구 사항과 사용자 경험 요구 사항은 작고 집중적이며 일시적인 현대 Microservices 웹의 디자인 철학과는 상반됩니다. 여러 면에서 애플리케이션의 웹 프론트엔드를 서비스 기반 클라이언트 및 자체 리치 애플리케이션으로 비교하는 것이 더 나을 수 있습니다.

NGINX의 웹 프론트엔드 구축 방식은 Microservices 철학과 웹 애플리케이션 디자인의 장점을 결합하여, 서비스 기반, 상태를 유지하지 않는(stateless), 연결된 리치한 사용자 경험을 제공합니다.
Microservices 웹 컴포넌트를 구축할 때, 해결책은 제어를 위한 Model-View-Controller (MVC) 프레임워크, NGINX를 통해 라우팅을 결합하여 세션 상태를 유지하기 위한 연결된 리소스 및 서비스에 대한 액세스를 제공합니다.

2. MVC 프레임워크를 통한 개발 제어

웹 어플리케이션 디자인에서 가장 중요한 기술적 발전 중 하나는 MVC 프레임워크의 도입이었습니다. PHP의 Symfony부터 Java의 Spring, Ruby on Rails, JavaScript의 EmberJS와 같은 JavaScript 프레임워크에서도 MVC 프레임워크를 찾을 수 있습니다. MVC 프레임워크는 관심사를 유사한 영역으로 나누어 코드를 세분화합니다.
디스플레이 로직은 뷰에서 관리되며, 데이터 구조는 모델에서 관리하고, 상태 변경 및 데이터 조작은 컨트롤러를 통해 관리됩니다. MVC 프레임워크가 없으면, 제어 로직이 종종 페이지에서 디스플레이 로직과 혼합되어 나타나는데, 이는 일반적인 PHP 개발에서 흔한 패턴입니다.

2-1. MVC 분업 체제

MVC의 분업 체제는 웹 애플리케이션을 Microservices 처럼 프론트엔드 구성 요소로 변환하는 프로세스를 안내하는 데 매우 유용합니다. 다행히도 가장 큰 변경 사항은 컨트롤러 레이어로 제한됩니다.
뷰는 중요한 방식으로 변경할 필요가 없습니다. Microservices 의 상태 없음 및 단기간 유지됨 특성은 데이터가 표시되는 기본 방식을 변경하지 않습니다.
비슷하게, MVC 시스템에서 모델은 Microservices 의 데이터 구조에 쉽게 매핑되며, 모델과 상호 작용하는 기본 접근 방식은 해당 모델을 관리하는 Microservices 를 통해 수행됩니다.
이러한 상호 작용 모드는 모델 개발을 담당하는 Microservices 팀이 데이터 구조와 조작 방법을 구현하기 때문에 웹 프론트엔드 팀보다 더 쉬워집니다.

컨트롤러가 가장 큰 변경이 필요한 부분입니다.
컨트롤러는 일반적으로 사용자의 작업, 데이터 모델 및 뷰 간 상호 작용을 관리합니다.
사용자가 링크를 클릭하거나 양식을 제출하면 컨트롤러가 요청을 가로채고 관련 구성 요소를 구성하며 모델 내의 메서드를 초기화하여 데이터를 변경하고 데이터를 수집하여 뷰에 전달합니다.
컨트롤러는 유한 상태 기계(FSM)를 구현하고 작업 및 논리적 상태의 상호 작용을 설명하는 상태 전이 테이블을 관리합니다.
여러 서비스 간에 복잡한 상호 작용이 있는 경우, 컨트롤러와 상호 작용하는 관리 서비스를 구축하는 것이 일반적입니다.
이렇게 하면 테스트가 더 쉽고 직접적으로 이루어집니다.

NGINX Microservices 참조 아키텍처에서는 MVC 시스템으로 PHP 프레임워크인 Symfony를 사용했습니다. Symfony는 MVC를 구현하기 위한 많은 강력한 기능을 제공하며 우리가 MVC 시스템에서 찾고 있던 분명한 관심사 분리를 준수합니다.
우리는 백엔드 Microservices와 직접 연결되는 서비스를 사용하여 모델을 구현하고 뷰를 TWIG 템플릿으로 구현했습니다. 컨트롤러는 사용자 작업, 서비스(façades를 사용하여) 및 뷰 간의 인터페이스를 처리합니다.
웹 프론트엔드에서 MVC 프레임워크를 사용하지 않고 애플리케이션을 구현했다면, 코드와 Microservices 간의 상호 작용이 지저분해지며 웹 프론트엔드를 Microservices 위에 덧붙일 명확한 영역이 없을 것입니다.

3. 세션 상태 유지

웹 애플리케이션은 서비스 경계를 넘나드는 일련의 작업에 대한 일관된 인터페이스를 제공할 때 복잡해질 수 있습니다.
일반적인 전자상거래 쇼핑 카트 구현을 생각해보면, 사용자는 사이트를 탐색하면서 구매할 제품을 선택합니다.
쇼핑이 끝나고 결제하려면 사용자는 카트 아이콘을 클릭하여 구매 프로세스를 시작합니다. 앱은 구매할 항목 목록과 수량 등과 함께 제시됩니다.
사용자는 이후 구매 프로세스를 진행하면서 배송 정보, 청구 정보를 입력하고 주문 내역을 검토한 후 결제를 승인합니다.
각 양식은 일반적으로 유효성이 검사되며 다음 화면에서도 정보를 사용할 수 있습니다.
(검토할 수량, 배송 정보를 청구로 전달 등). 사용자는 주문을 제출할 때까지 화면 간에 이동하며 변경할 수 있습니다.

Oracle ATG Web Commerce와 같은 모놀리식 애플리케이션에서는 폼 데이터가 세션 내에서 유지되어 애플리케이션 객체에서 쉽게 액세스할 수 있습니다.
이 관련성을 유지하기 위해 사용자는 세션 쿠키를 통해 애플리케이션 인스턴스에 연결됩니다.
ATG는 클러스터 환경에서 세션을 유지하기 위한 복잡한 방법을 갖추고 있어 시스템 오류가 발생할 경우 복구력을 제공합니다.

반면, Microservices 접근 방식에서는 페이지 요청을 통한 메모리 내 세션 데이터와 세션 상태 개념을 배제하므로, 위에서 설명한 쇼핑 카트 상황을 Microservices 웹 앱은 어떻게 다루는 것일까요?

3-1. 캐싱을 통한 Microservices 딜레마 해결

위 내용은 Microservices 환경에서 웹 앱의 본질적인 딜레마입니다.
이 시나리오에서 웹 앱은 서비스 경계를 넘나드는 경우가 많습니다. 예를 들어, 배송 폼은 배송 서비스에 연결하고, 청구 폼은 청구 서비스에 연결하는 식입니다.
그렇다면 웹 앱이 일시적이고 상태를 유지하지 않아야 한다면, 제출된 데이터와 프로세스의 상태를 어떻게 추적해야 할까요?

이 문제를 해결하는 다양한 방법이 있지만, 우리가 선호하는 형식은 세션 상태를 유지하기 위해 캐싱 지향적인 첨부 리소스를 사용하는 것입니다(더 자세한 내용은 12factor.net을 참조하세요).
Redis와 같은 첨부 리소스를 사용하여 세션 상태를 유지하면, 모놀리식 앱에서 사용되는 논리적 흐름과 접근 방식을 Microservices 웹 앱에 적용할 수 있지만, 데이터는 웹 프론트엔드 인스턴스의 메모리에 저장되는 것이 아니라 Redis나 Memcached와 같은 고속 및 원자적으로 트랜잭션 처리 가능한 캐싱 시스템에 저장됩니다.

캐싱 시스템이 있으면 사용자가 어떤 웹 프론트엔드 인스턴스로 라우팅되더라도 데이터는 메모리 내 세션 시스템을 사용하는 것과 마찬가지로 즉시 사용 가능합니다.
이는 사용자가 구매하기 전에 사이트를 나가기로 선택한 경우에도 세션 지속성을 제공하는 추가적인 이점을 가지고 있습니다.
캐시의 데이터는 일반적으로 일, 주, 또는 월 단위로 액세스할 수 있지만, 메모리 내 세션 데이터는 일반적으로 약 20분 후에 지워집니다.
캐싱 시스템 대신 메모리 내 객체를 사용하는 것에 비해 약간의 성능 저하가 있을 수 있지만, 마이크로서비스 접근 방식의 본질적인 확장성으로 인해 애플리케이션은 부하에 따라 더 쉽게 확장할 수 있으며, 모놀리식 애플리케이션과 일반적으로 관련된 성능 병목 현상은 문제가 되지 않게 됩니다.

NGINX Microservices 참조 아키텍처는 Redis 캐시를 구현하여 필요한 경우 세션 상태를 요청 간에 저장할 수 있도록 합니다.

4. Microservices 라우팅 및 로드 밸런싱

세션 상태 유지는 시스템에 복잡성을 추가하지만, 현대의 웹 애플리케이션은 서버 로직에서 기능적인 사용자 상호작용만 구현하지 않습니다.
다양한 사용자 경험 이유로 대부분의 웹 애플리케이션은 브라우저에서 JavaScript로 시스템의 핵심 기능도 구현합니다.
예를 들어, NGINX Microservices 참조 아키텍처는 클라이언트의 JavaScript에서 많은 사진 업로드 및 표시 로직을 구현합니다. 그러나 JavaScript에는 크로스 사이트 스크립팅(XSS)이라는 보안 기능으로 인해 Microservices에 직접 액세스하기가 어렵습니다.
XSS는 자바스크립트 애플리케이션이 로드된 서버 이외의 서버에 액세스하는 것을 방지합니다.
이를 Origin이라고 합니다. NGINX Plus를 사용하여 우리는 Origin을 통해 마이크로서비스로 라우팅하여 이 제한을 극복할 수 있습니다.

Microservices를 구현하는 일반적인 방법 중 하나는 각 서비스에 DNS 항목을 제공하는 것입니다.
예를 들어, 우리는 Microservices Reference Architecture에서 업로더 Microservices를 uploader.example.com으로, 웹 앱 페이지를 pages.example.com으로 지정할 수 있습니다.
이렇게 하면 Service Discovery가 비교적 간단해지며, Endpoint를 찾기 위해 DNS 조회만 필요합니다.
그러나 XSS로 인해 자바스크립트 애플리케이션은 원래 출처 이외의 호스트에 액세스할 수 없습니다.
아래 예에서 자바스크립트 앱은 pages.example.com에만 연결할 수 있으며, uploader.example.com에는 연결할 수 없습니다.

NGINX Microservices Reference Architecture에서 웹 앱을 구현하기 위해 PHP Symfony 프레임워크를 사용합니다.
최고의 성능을 달성하기 위해 NGINX Plus가 FastCGI Process Manager (FPM) PHP 엔진을 실행하는 Docker 컨테이너에서 시스템을 구축했습니다.
NGINX Plus와 FPM을 결합하면 HTTP/HTTPS 구성 요소를 구성하는 데 매우 유연성을 제공하고 강력한 소프트웨어 기반 로드 밸런싱 기능을 제공합니다.
로드 밸런싱 기능은 JavaScript가 상호작용해야 하는 Microservices에 액세스하도록 제공할 때 특히 중요합니다.

NGINX Plus를 웹 서버 및 로드 밸런서로 구성함으로써, location 지시문 및 업스트림 서버 정의를 사용하여 필요한 Microservices에 대한 경로를 쉽게 추가할 수 있습니다.
이 경우, JavaScript 애플리케이션은 uploader.example.com 대신 pages.example.com/uploader에 액세스합니다.
이렇게 하면 NGINX Plus가 uploader.example.com의 인스턴스 수에 관계없이 서비스의 헬스 체크와 최소 연결 수 로드 밸런싱과 같은 강력한 로드 밸런싱 기능을 제공할 수 있습니다.
이러한 방식으로 JavaScript 애플리케이션은 Microservices와 완전히 상호작용할 수 있도록 XSS 제한을 극복할 수 있습니다.

http {
    resolver ns.example.com valid=30s;
    # use local DNS and override TTL to whatever value makes sense

    upstream uploader {
        least_time header;
        server uploader.example.com;
        zone backend 64k;
    }

    server {
        listen 443;
        server_name www.example.com;
        root /www/public_html;
        status_zone pages;

        ## Default location
        location / {
            # try to serve file directly, fall back to app.php
            try_files $uri /app.php$is_args$args;
        }

        location /uploader/image {
            proxy_pass http://uploader;
            proxy_set_header Host uploader.example.com;
        }
    }
}

5. 결론

Microservices 애플리케이션에서 웹 애플리케이션 구성 요소를 구현하는 것은 표준 Microservices 구성 요소 아키텍처에 깔끔하게 들어맞지 않기 때문에 도전적입니다.
보통 서비스 경계를 넘나드는 서버 로직과 브라우저 기반 디스플레이 로직이 필요합니다.
이러한 독특한 특징들은 마이크로서비스 환경에서 올바르게 작동하려면 복잡한 솔루션이 필요합니다. 가장 쉬운 방법은 다음과 같습니다.

  • MVC 프레임워크를 사용하여 웹 앱을 구현하여 논리적인 제어를 데이터 모델과 디스플레이 뷰와 명확하게 분리합니다.
  • 세션 상태를 유지하기 위해 고속 캐싱 첨부 리소스를 사용합니다.
  • NGINX Plus를 사용하여 Microservices에 대한 라우팅 및 로드 밸런싱을 수행하여 브라우저 기반 JavaScript 로직이 상호작용해야하는 Microservices에 대한 액세스 권한을 부여합니다.

이 접근 방식은 Microservices의 모범 사례를 유지하면서 월드클래스 웹 프론트 엔드에 필요한 풍부한 웹 기능을 제공합니다. 이 방법을 사용하여 구현된 웹 프론트 엔드는 Microservices 접근 방식의 확장성과 개발 이점을 누릴 수 있습니다.

Microservices 및 NGINX Plus를 사용하여 웹 프론트엔드를 구축하는 방법에 대한 자세한 정보와 Microservices의 많은 측면에 대한 깊이있는 정보를 원하신다면 아래 뉴스레터를 구독하여 알림을 받아보세요.