Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

시나리오

주식회사 joinc는 다양한 패션용품을 판매하는 인터넷 전자 상거래 서비스를 운영하고 있다. 이 회사의 CTO는 전자 상거래 시장이 격화되면서, 서비스 경쟁력을 확보하는게 중요한 과제가 됐다.

CTO는 우선 과제로 개인화된 상품추천 서비스를 개발하기로 했다. 상품추천의 요구사항은 아래와 같다.
  1. 사용자의 활동(Activity)를 저장하고 분석할 수 있는 시스템을 구축한다.
  2. 분석된 결과를 사용자에게 실시간 혹은 배치로 제공 할 수 있는 추천 시스템을 구축한다.
  3. 검색 시스템을 구성한다.
  4. 사용자는 (Facebook의 Feed 같은)Feed를 받아본다. 이 Feed에는 사용자가 관심있어 할 만한 상품 정보를 포함 할 것이다.
솔류션 아키텍트는 서비스 개발자와 함께 위 요구사항을 만족하는 시스템을 구축해야 한다.

서비스 아키텍처

전체 서비스를 High-Level로 아키텍처링 한다.

 High-Level 서비스 아키텍처

Feed 서비스는 크게 3개 레이어로 구성된다.
  1. OnLine : 사용자의 요청이 있을 때, 즉시 Feed 서비스를 제공한다. 온라인 사용자에게 즉시 Feed를 전송한다.
  2. NearLine : OnLine에서 사용 할 수 있는 Feed 객체를 만든다. 온라인 사용자에게 새로 추가되는 Feed를 전송한다.
  3. OffLine : 서비스 사용자의 모든 활동(activity)은 Queue를 통해서 Feed Create 서비스로 전달된다. Feed Create 서비스는 NearLine Feed를 제공하는 알고리즘을 돌리는 한편, 분석 시스템에 보낸다. 분석 시스템은 Offline에서 활동을 분석해서 추천 Feed를 생성한다. 이렇게 생성된 Feed는 다시 Online과 Nearline으로 제공된다.

Feed 객체

사용자는 Feed 객체(데이터)를 받게 된다. 사용자 접점에서 서비스 되는 데이터이기 때문에, 빠르게 랜더링 할 수 있어야 한다. 따라서 가능한 그 자체로 완결성을 가지고 있어야 한다. 뭐냐면 한번의 Feed 요청 API로 이미지, Like, Comment 등을 가져올 수 있어야 한다. Feed 하나를 만들기 위해서 Like 목록가져오기, Comment 목록 가져오기 API 등 여러 개의 API를 호출하는 것은 백앤드 효율 측면과 고객 경험 측면 모두에서 좋은 방법이 아니다.

아래와 같이 Feed 데이터를 정의 할 수 있을 것이다. go struct로 정의 했다.
type Feed struct {
    ID            int64       `json:"ID"` 
    CreatedDate   string      `json:"createdDate"` 
    User          User        `json:"user"`
    Post          Post        `json:"post"`
    Like          []User      `json:"like"`
    Comment       []Comment   `json:"comment"`
}

type Post struct {
    ID        int64       `json:"id"`
    Title     string      `json:"title"`
    Text      string      `json:"text"`
    Image     []Image    `json:"image"
    User      User        `json:"user"`
}

type Image struct {
    ImageURL  string    `json:"imageURL"`
    Caption   string    `json:"caption"`
}

type User struct {
    ID       int64   `json:"ID"`
    Name     string  `json:"name"`
    ImageURL string  `json:"imageURL"`
    Home     string  `json:"home"`
}

type Comment struct {
    ID       int64   `json:"ID"`
    Text     string  `json:"text"`
    User     User    `json:"user"`
}
Feed를 구성하는 모든 요소를 다 포함하고 있다. 단지 Image만 호출하면 된다.

json으로 표혐하면 아래와 같을 것이다.
{
   "FeedID" : "Feed 일련 번호",
   "Post": { 
      "ID" : "Post ID",
      "Title" : "Post Title",
      "Text" : "Post 내용",
      "ImageUrl" : ["Post Image 01", "Post Image 02"],
      "CreatedDate": "2021-05-05 11:00:00",
      "Tag": ["Tag-1", "Tag-2"]
   }
   "User": {
      "ID": "user-001",
      "Name": "yundream",
      "ImageUrl": "Profil Image",
      "Home": "User Home"
   }
   "Likes": [
     {
        "ID": "user-100",
        "Name": "user-01",
        "ImageUrl": "Profil Image",
        "Home": "User Home"
     }
   ], 
   "Comments": [
      {
          "ID": "",
          "Text": "",
          "User": "..." 
      }
   ]
}

Feed 객체 관리

위의 Feed 객체를 서비스하려면 해결 해야 할 문제들이 있다.
  1. Feed 객체는 여러 유저에게 전송해야 할 것이다. 어떻게 전송 할 수 있을까 ?
  2. Feed 객체는 그 자체로 완결된 데이터인데, 이는 static 정보 임을 의미한다. 객체의 내용이 달라지는 것은 어떻게 반영 할 것인가 ? 예를 들어 내가 읽은 Feed에 새로운 Like가 발생하거나 Comment가 달렸다면 ?
Feed 객체의 전달은 Pub/Sub 모델을 따를 것이다. 따라서 하나의 원본으로 부터, 여러 유저에게 전송될 것이다. Pub/Sub을 구현 할 수 있는 여러 툴 들이 있겠는데, Redis(ElastiCache)의 List를 이용해서 구현하기로 했다. 아래 이미지를 보자.

 Feed 객체관리

사용자(혹은 상품을 올리는 기업)의 Activity는 Feed Generator 로 전달된다. Feed Generator는 Feed를 생성하고 DynamoDB에 저장한다. 그리고 모종의 알고리즘을 이용해서 Feed를 받아볼 유저에게 퍼블리싱 한다. 퍼블리싱에는 Redis List 자료구조를 사용한다. 사용자 ID를 Key로 하고 Value로 Feed 목록이 들어간다. Capped List를 이용해서 목록의 크기를 관리 할 수 있다.

  1. 사용자가 로그인한다.
  2. Feed 화면이 뜨고 Feed Service에 Feed API를 전송한다.
  3. Feed Service는 Redis로 부터, 사용자 Feed ID를 읽어온다.
  4. Feed Service는 Feed ID를 이용해서 Feed 원본을 읽어오고 like, comment 정보를 조회해서 완전한 Feed 객체를 만들어서 리턴한다.
  5. Feed Service가 완전한 Feed 객체를 리턴하기 때문에, 앱은 한번의 API 호출로 Feed 화면을 그릴 수 있다.
세부적인 최적화를 수행 할 수는 있을 것이다. 예를 들어 Feed에 대한 Like, Comment와 같은 Activity가 있을 때, DynamoDB의 Feed 를 업데이트 할 수 있을 것이다. 이렇게 하면 Like와 Comment 데이터베이스를 조회할 필요 없이 DynamoDB의 데이터만 이용해서 Feed 객체를 만들 수 있다. 보통 읽기에 비해서 쓰기는 매우 적기 때문에 이 방식이 훨씬 효율적이다.

인프라 아키텍처

요구사항

위 서비스를 위한 인프라 아키텍처를 만들기 위해서 기술적 요구사항을 정의한다.
  1. API 형태로 외부에 제공한다. 모바일 애플리케이션, 웹 애플리케이션, 어드민에서 동일한 API를 호출한다.
  2. API 호출에 대한 인증/권한 설정을 할 수 있어야 한다.
  3. 데이터 기반의 서비스를 수행해야 한다. 실시간 데이터 서비스가 가능해야 하며, 백앤드에서는 데이터를 분석하는 작업도 할 수 있어야 한다.
  4. 데이터 분석을 위해서 사용자의 모든 행위가 안전하게 기록되야 한다.
  5. 데이터는 분석을 위해서 장기간 저장해야 한다. 다양한 비즈니스 요구사항이 있을 수 있으므로 다양한 분석 & 서비스 파이프라인을 작동시킬 수 있어야 한다.
  6. 데이터는 안전하게 관리해야 한다. 데이터의 생산, 데이터의 전송/저장, 생산/저장된 데이터에 대한 접근 권한이 통재되고 감사 기록을 남겨야 한다.
  7. 쇼핑몰은 자금의 흐름과 고객 개인정보를 다루어야 하므로 보안요구사항을 충족해야 한다.

AWS 조직(Organization) 아키텍처

AWS Organization을 사용하면, 조직과 프로젝트에 따라서 AWS 계정(Account)를 생성하고 마스터 계정을 통해서 중앙에서 관리 할 수 있다. AWS 각 계정은 고유한 계정 ID와 리소스 제한을 설정 할 수 있다. 위의 요구사항에 따라서 아래와 같은 조직을 설정했다.

 AWS Organization

  1. 개발(development) & QA 유닛
  2. 프로덕션(production) 유닛
  3. 공유(shared) 유닛
여러 개의 계정을 운영하는 것은 복잡해 보이지만 아래와 같은 이점을 누릴 수 있다.
  • 보안 경게 설정 : 보안은 결국 자원에 대한 접근과 권한에 대한 것이다. 계정은 다른 계정과 접근과 권힌이 분리가 되면서 자연스럽게 보안 경계가 설정된다. 보안의 3대 원칙은 기밀성(정보 누출의 방지), 무결성(정보 변경의 방지), 가용성(정보 파괴의 방지)에 있다. 계정을 분리해서 경계를 두는 것만으로 보안의 원칙을 준수하기가 쉬워진다. 또한 각 계정의 목표에 따라서 적당한 수준의 보안 정책을 설정 할 수 있다. 예를 들어 개발과 프로덕션은 서로 다른 보안 정책을 가져야 할 것이다. 통합 계정에서는 정책관리가 복잡해진다.
  • 확장성 : AWS는 계정당 자원 제한(Limit)이 있다. 통합된 계정은 확장성을 떨어트릴 수 있다.
  • 실패 확산 방지 : 시스템에 원치않는 문제가 생길 수 있다. 계정을 분리 함으로써, 문제가 확산되지 않도록 할 수 있다. 동일한 계정의 리소스는 혼동되거나 기술적으로 영향을 미칠 가능성이 훨씬 높다.
  • 비용 모니터링 : 단일 계정에서도 태그를 이용해서 비용을 추적 할 수 있지만 번거롭고 정확하지않다. 계정으로 분리하면 비용을 명확히 추적 할 수 있다.
계정을 많이 늘리지 않더라도 최소한 개발과 프러덕션은 분리하는게 좋다. 올바른 계정 정책은 AWS 리소스를 확장하고 제어하는데 많은 도움을 준다. 우리는 개발, 프로덕션, 공유 3개의 계정으로 시작할 것이다.

서비스 애플리케이션 아키텍처

서비스 아키텍처는 대략 아래와 같다.

 서비스 애플리케이션 아키텍처

백앤드 인프라는 EKS(Amazon Elastic Kubernetes Service)로 했다. 왜 EKS냐면, EC2 기반이든 ECS 기반이든 상관은 없겠으나 Kubernetes가 핫해서 그냥 EKS로 했다. 회사 상황에 따라서 적당한거 선택하면 된다. 서비스 실행에 별로 중요한 내용은 아니다.

API Gateway를 이용해서 클라이언트에 API를 제공 할 생각이다. API Gatway는 VPC 바깥에 있는 서비스다. VPC 내부를 알지 못하므로 Private VPCLink를 이용해서 내부에 있는 NLB와 연결했다. API Gateway 대신 ALB(Application Load Balancer)를 사용하는 방법도 있다. Application Load Balancer는 VPC에 통합되므로 API Gateway 보다 더 단순하게 인프라를 구성 할 수 있다. ALB가 API Gateway 보다 더 좋은 것 아닌가 ? 이런 물음이 들 것이다. 1-2년 전만 하더라도 API Gateway만이 가지는 기능들이 있어서 API Gateway를 선택해야 했지만 지금은 그 차이가 매우 줄었다.

  • 서비리스 자원과 연결하기 위해서는 API Gateway를 사용해야 했으나 이제 ALB도 Lambda를 호출할 수 있게 됐다.
  • API Gateway는 분산 추적 서비스인 AWS X-Ray와 통합된다. ALB는 X-Amazn-Trace-Id라는 추적 헤더를 다운 스트림으로 라우팅 되는 모든 요청에 삽입 할 수 있다. X-Ray와 비슷하게 사용 할 수 있지만 X-Amazon-Trace-Id를 이용하기 위해서는 DevOps팀에서 색인을 새야 한다.
  • API Gateway와 ALB 모두 Cognito와 통합된다.
  • API Gateway는 경로기반 라우팅을 사용한다. ALB는 경로기반 라우팅 외에, HTTP 헤더, 메서드, 쿼리 문자열, 요청자 IP 및 호스트 이름등으로 라우팅 규칙을 만들 수 있다.
EKS의 서비스들은 사용자의 활동(Activity)정보를 SQS로 전송한다. SQS를 이용해서 EKS 서비스와 분석 서비스를 서로 분리 할 수 있다. SQS의 컨슈머로 Feed 분석 & Generator 서비스가 실행된다. 여러 컨슈머가 데이터를 다양한 각도로 분석해야 한다면, MSK(Managed Kafka)나 Kinesis를 사용할 수 있다.

Feed 분석 & Generator 서비스는 SQS 메시지와 다른 데이터들을 이용해서 Feed 정보를 만들어서 DynamoDB에 저장하고, Feed를 받아볼 사용자에게 퍼블리싱한다. 퍼블리싱에는 ElastiCache를 사용 할 수 있다. 사용자에게 Feed를 퍼블리싱하기 위해서 GraphDB를 사용하기도 한다.

모든 주요 데이터들은 S3에 저장되며 ETL, DW, ML 서비스를 이용해서 분석한다. 분석한 정보들은 다시 고객에게 전달된다.

서비스는 OnLine nearLine, OffLine의 3 영역으로 구성된다.
  1. Online : API를 이용해서 실시간으로 서비스되는 영역이다. 사용자에게 즉시 서비스 해야하므로 Cache에 미리 데이터를 만들어 놓거나, 빠른 알고리즘을 이용해서 Feed를 생성한다.
  2. NearLine : 사용자의 활동을 빠르게 분석해서 OnLine에서 사용할 Feed를 빠르게 생성한다.
  3. OffLine : 정교한 알고리즘을 이용해서 Feed와 레포트를 생성한다.

데이터 파이프라인

데이터 파이프라인을 구축하고 싶다면 SQS 보다는 MSK나 Kinesis를 사용하는게 좋을 것이다. 파이프라인은 대략 아래와 같이 구성 할 수 있다.

 데이터 파이프라인

Kinesis로 구성을 만들었다. Kinesis firehose는 스트리밍 데이터를 갭쳐해서 S3, Amazon Redshift, ElastcSearch, Datadog, Splunk와 같은 서비스 공급자로 데이터를 전송한다. 위 구성에서 Kinesis는 Feed 분석 서비스(구성도에서는 생략)와 S3로 데이터를 전송한다. S3는 데이터 레이크로 작동하며, ETL을 이용해서 DW를 구성 혹은 서비스 데이터를 만들거나 SageMaker를 이용 머신러닝 모델을 생성하고 서비스를 배포한다.

참고