Enjoy My Posts

스프링 마이크로서비스 코딩 공작소 정리

Posted on By Geunwon Lim

1. 스프링, 클라우드와 만나다.

1.1 마이크로 서비스란?

마이크로서비스 개념이 발전하기 전, 대부분의 웹 기반 애플리케이션은 모놀리식 아키텍처 형태로 개발되었다. 모놀리식 아키텍처에서 애플리케이션은 배포 가능한 단일 소프트웨어 산출물로 전달된다. UI 및 비즈니스 로직, 데이터베이스 액세스 로직 모두가 하나의 애플리케이션 산출물로 패키징되고 애플리케이션 서버에 배포되는 것이다.

애플리케이션은 단일 작업 단위로도 배포할 수 있지만, 실제로는 여러 개발 팀이 한 애플리케이션에서 작업할 때가 더 많다. 일반적으로 각 개발 팀은 특정 고객에게 제공하는 애플리케이션의 개별 기능을 담당한다. 필자의 경험상 여러 팀과 관련된 애플리케이션이 있었다고 한다. 각 팀에는 요구 사항과 출시 요구 사항에 대한 고유 책임 영역이 있고, 모든 작업은 단일 코드 베이스에 동기화된다. 여기서 문제는 모놀리식 애플리케이션이 크고 복잡해지면 애플리케이션을 담당하는 각 팀의 의사 소통과 조정 비용이 증가한다는 것이다. 모놀리식 애플리케이션은 각 팀에서 변경이 있을 때마다 애플리케이션 전체를 다시 빌드하고 테스트해서 배포해야 한다.

마이크로서비스 개념은 대형 모놀리식 애플리케이션을 기술 및 조직적으로 확장하려고 할 때 생기는 많은 난제에 대한 직접적 대안이 됐다. 마이크로서비스는 느슨히 결합된 작은 분산 서비스이다. 마이크로서비스를 사용하면 대형 애플리케이션을 관리하기 쉽고, 제한된 책임을 담당하는 컴포넌트로 분해할 수 있다. 마이크로서비스는 코드 베이스를 명확히 정의한 작은 조각으로 분리해서 대형 코드 베이스에서 발생하는 전통적인 복잡성 문제를 해결한다. 마이크로서비스를 고려할 때 수용해야 할 핵심 개념은 애플리케이션 기능을 분해하고 분리해서 완전히 상호 독립적이어야 한다는 것이다.

책의 그림을 보면 각 팀이 서비스 코드와 서비스 인프라스트럭처를 완전히 소유하고 있음을 알 수 있다. 코드와 소스 관리 저장소, 인프라스트럭처가 이제 애플리케이션의 다른 부분과 완전히 독립적이기 대문에 각 팀은 독립적으로 빌드와 테스트, 배포를 할 수 있다.

마이크로서비스 아키텍처의 특징은 다음과 같다.

  1. 애플리케이션 로직을 각자 책임이 명확한 작은 컴포넌트들로 분해하고 이들을 조합해서 솔루션을 제공한다.
  2. 각 컴포넌트는 작은 책임 영역을 담ㄷ낭하고 완전히 상호 독립적으로 배포된다. 마이크로서비스는 비즈니스 영역으 ㅣ 한 부분에서만 책임을 담당한다. 그리고 여러 애플리케이션에서 재사용할 수 있어야 한다.
  3. 마이크로서비스는 몇 가지 기본 원칙에 기반을 두며, 서비스 소비자와 서비스 제공자 사이의 데이터 교환을 위해 HTTP와 JSON같은 경량 통신 프로토콜을 사용한다.
  4. 애플리케이션은 항상 기술 중립적 프로토콜(JSON이 보편적)을 사용해 통신하므로 서비스 구현 기술과는 무관하다.
  5. 작고 독립적이며 분산된 마이크로서비스를 사용해 조직은 명확히 정의된 책임 영역을 담당하는 소규모 팀을 보유할 수 있다.

1.2 스프링은 마이크로서비스와 어떤 관련이 있을까?

스프링 프레임워크는 J2EE 스택으로 애플리케이션을 구축하는 방법을 찾고 있는 엔터프라이즈 자바 애플리케이션 개발자에게 더 가벼운 대안으로 빠르게 자리 잡았다. 많은 사람이 J2EE를 강력하지만 개발팀이 상요하지 않는 기능이 많이 포함된 블로트웨어(쓸데없이 메모리를 많이 잡아먹음)로 간주했다. 게다가 J2EE 애플리케이션을 사용하면 모든 특성을 갖춘 (그래서 무거운) 자바 애플리케이션 서버로 애플리케이션을 배포해야 했다.

스프링 개발 팀은 많은 개발 팀이 애플리케이션의 프레젠테이션과 비즈니스, 데이터 액세스 로직을 함께 패키징하고 단일 산출물로 배포하는 모놀리식 애플리케이션에서 이탈하고 있다는 것을 재빨리 파악했다. 그 대신 작고 분산되어 클라우드에 쉽게 배포 가능한 서비스를 구축하려는 고도의 분산 모델로 이동하고 있었다. 이러한 변화에 부응하기 위해 스프링 개발 팀은 스프링 부트와 스프링 클라우드라는 프로젝트를 시작했다.

스프링 부트는 스프링 프레임워크를 재구성한 것이다. 스프링의 핵심 기능은 수용하지만 많은 엔터프라이즈 기능을 제거하고 대신 자바 기반의 REST 지향 마이크로서비스 프레임워크를 제공한다. 단순한 애너테이션으로 자바 개발자는 외부 애플리케이션 컨테이너 없이도 패키지하고 배포할 수 있는 REST 마이크로서비스를 신속하게 구축할 수 있다.

스프링 부트는 일반적인 REST 마이크로서비스의 작업(비즈니스 로직에 경로 설정 및 URL에서 HTTP 매개변수 파싱, JSON 을 자바 객체로 상호 매핑)을 추상화하고 개발자가 서비스 비즈니스 로직에 집중할 수 있게 한다. 스프링 부트가 해주는 일의 순서를 정리해보면 다음과 같다.

  1. 클라이언트가 스프링부트 애플리케이션에 HTTP 요청을 보낸다.
  2. 경로 매핑: 스프링 부트는 HTTP 요청을 파싱하고 HTTP 동사와 URL, URL에 정의된 매개변수를 기반으로 경로를 매핑한다. 경로는 스프링 RestController 클래스의 메서드에 매핑된다.
  3. 매개변수 분해: 스프링 부트가 경로를 인식하면 경로 내부에 정의된 매개변수를 작업을 수행핧 자바 메서드에 매핑한다.
  4. JSON -> 자바 객체 매핑: HTTP PUT이나 POST는 HTTP 본문에서 전달된 JSON을 자바 클래스에 매핑한다.
  5. 비즈니스 로직 실행: 모든 데이터가 매핑되면 스프링 부트는 비즈니스 로직을 실행한다.
  6. 자바 -> JSON 객체 매핑: 비즈니스 로직이 실행되면 스프링 부트는 자바 객체를 JSON으로 변환한다.
  7. 클라이언트는 서비스에서 JSON으로 응답을 받는다.

마이크로서비스가 클라우드 기반 애플리케이션을 구축하는 일반적인 아키텍처 패턴 중 하나로 발전했기 때문에 스프링 개발 커뮤니티는 우리에게 스프링 클라우드를 선사했다. 스프링 클라우드 프레임워크를 사용하면 사설 및 공용 클라우드에 마이크로서비스를 쉽게 운영하고 배포할 수 있다. 스프링 클라우드는 널리 사용되는 클라우드 관리용 마이크로서비스 프레임워크를 공통 프레임워크에 포함하고, 코드에서 애너테이션을 다는 것처럼 이러한 기술을 쉽게 사용하고 배포할 수 있게 했다.

1.3 애플리케이션 구축 방식을 바꾸는 이유

현대 사회는 빠르게 변화하고 있다.

  1. 복잡성이 증가했다: 고객은 조직의 모든 부분이 자신을 인식하길 기대한다. 단 하나의 데이터베이스와 통신하고 다른 애플리케이션과 통합하지 않는 단절된 애플리케이션은 더이상 표준이 아니다.
  2. 고객은 더 빠른 출시를 원한다: 고객은 더 이상 소프트웨어 패키지를 연 단위로 릴리스하거나 버전을 올리길 기대하지 않는다.
  3. 성능 및 확장성: 글로벌 애플리케이션에서는 애플리케이션이 처리해야 할 트랜잭션 양과 유입될 시점을 예측하기 어렵다. 애플리케이션은 여러 서버로 신속히 확장한 후 확장이 필요 없다면 다시 축소해야 한다.
  4. 고객은 애플리케이션을 항상 사용할 수 있길 기대한다: 고객은 한 번의 클릭만으로 경쟁사로 이탈할 수 있으므로 회사의 애플리케이션은 회복성이 높아야 한다.

이러한 기대 사항을 충족하려면 확장성과 중복성이 높은 애플리케이션을 구축하기 위해 독립적으로 빌드하고 배포할 수 있는 작은 서비스로 애플리케이션을 분해해야 한다는 역설을 수용해야 한다. 애플리케이션을 작은 서비스로 떼어내서 단일한 모놀리식 산출물에서 벗어난다면 다음 특징이 있는 시스템을 구축할 수 있다.

  1. 유연성: 새로운 기능을 신속하게 제공하도록 분리된 서비스를 구성하고 재배치할 수 있다. 다른 것과 동작하는 코드 단위가 작을수록 코드 변경에 따른 복잡성을 낮추고 코드 배포를 테스트하는 시간도 줄어든다.
  2. 회복성: 분리된 서비스란 더이상 애플리케이션 한 부분의 저하로 전체가 망가지는 진흙덩이 애플리케이션이 아니라는 의미다. 실패는 애플리케이션의 작은 부분에 국한되어 애플리케이션 전체 장애로 확대되기 전에 억제된다.
  3. 확장성: 분리된 서비스를 여러 서버에 수평적으로 쉽게 분산할 수 있어 기능 및 서비스를 적절히 확장할 수 있다. 애플리케이션의 모든 로직이 얽혀 ㅇㅆ는 모놀리식 애플리케이션은 한 부분이 병목점이 되더라도 전체를 확장해야 한다.

이러한 취지에서 마이크로서비스를 논의할 때 다음 사항을 명심하자.

작고 단순하며 분리된 서비스 = 확장 가능하고 회복적이며 유연한 애플리케이션

1.4 클라우드란 정확히 무엇인가?

클라우드 기반 컴퓨팅에는 다음 세 가지 기본 모델이 존재한다. 이 개념을 이해하기 위해 식사를 준비하는 작업 예시를 들어보자.

  1. 집에서 직접 해먹는다(집밥).
  2. 식료품점에서 냉동 음식을 사 먹는다.
  3. 배달 음식을 먹는다.
  4. 차를 타고 식당에 가서 먹는다.

선택 사항별 차이는 음식을 요리할 책임자와 요리 장소다. 온프레미스 모델(집밥)을 먹을 때는 집에 있는 오븐과 식재룔르 사용해 직접 모든 일을 해야 한다. 상점에서 구매한 음식은 IAAS 모델과 같다. 상점 요리사와 오븐으로 미리 준비해 놓은 음식을 집에서 데워 먹어야 한다. PaaS 모뎅레서도 여전히 식사에 대한 책임이 있지만 식사의 핵심 업무는 공금자에게 의존한다. 예를 들어 PaaS 모델에서 쩝시와 가구는 여러분이 제공해야 하지만 식당 주인이 오븐과 식재료, 요리사를 지원해 요리를 대신한다. SaaS 모델에서는 식당에 가서 차려진 식사를 하는 것과 같다.

각 모델에서 요점은 통제에 관한 것, 즉 누가 인프라스트 럭처를 유지 보수하고 애플리케이션 구축을 위해 어떤 기술을 사용할지 하는 점이다. IaaS 모델에서 클라우드 공급자는 기본적인 인프라스 트럭처를 제공하지만 기술 선택과 최종 솔루션 구축은 여러분 몫이다. 반면 SaaS 모델에서 여러분은 공급자가 제공하는 서비스의 수동적 소비자이며 기술 선택이나 애플리케이션을 위한 인프라스트럭처를 유지 보수할 책임이 없다.

1.5 왜 클라우드와 마이크로서비스인가?

마이크로서비스 기반 아키텍처의 핵심 개념은 각 서비스를 독립된 개별 산출물로 패키징하고 배포한다는 것이다. 서비스 인스턴스를 신속하게 시작할 수 있고 서비스 인스턴스는 서로 차이가 없어야 한다.

마이크로서비스를 작성하는 개발자는 서비스를 다음 중 어디에 배포할지 결저앻야 할 것이다.

  1. 물리적 서버
  2. 가상 머신 이미지
  3. 가상 컨테이너

클라우드에 기반을 둔 마이크로서비스의 장점은 탄력성 개념을 중심으로 한다. 클라우드 서비스 공급자를 통해 몇 분 안에 새로운 가상 머신과 컨테이너를 빠르게 가동시킬 수 있다. 서비스 용량이 감소한다면 추가 비용을 들이지 ㅇ낳고도 가상 서버를 줄일 수 있다. 클라우드 공급자를 사용해 마이크로서비스를 배포하면 애플리케이션을 위해 훨씬 더 높은 수준의 수평 확장성(서버와 서비스 인스턴스 추가)을 얻는다. 서버 탄력성은 애플리케이션 또한 회복력이 높다는 것을 의미한다. 마이크로서비스 중 하나에 문제가 발생해서 고장나더라도 새로운 서비스 인스턴스를 가동해 개발 팀이 문제를 해결할 수 있을 만큼 오랜 기간 애플리케이션을 정상으로 유지할 수 있다.

책에서는 마이크로서비스 관련 인프라스트럭처를 도커 컨테이너를 사용해 IaaS 기반 클라우드 공급자에게 배포한다.

  1. 간소화된 인프라스터럭처 관리
  2. 엄청난 수평 확장성
  3. 지리적 분산을 이용한 높은 중복성

1.6 마이크로서비스는 코드 작성 이상을 의미

견고한 마이크로서비스 애플리케이션을 실행하고 지원하는 것은 꽤 어려운데, 다음과 같은 주제가 있다.

  1. 적정 크기
  2. 위치 투명성
  3. 회복성
  4. 반복성
  5. 확장성

1.6.1 마이크로서비스 핵심 개발 패턴

마이크로서비스 핵심 개발 패턴은 마이크로서비스 구축에 대한 기본 사항(서비스 세분성, 통신 프로토콜, 인터페이스 설계, 구성 관리, 이벤트 프로세싱 등)을 다룬다.

1. 마이크로서비스 라우팅 패턴

마이크로서비스 라우팅 패턴은 마이크로서비스를 사용하려는 클라이언트 애플리케이션 서비스의 위치를 발견하고 라우팅하는 방법을 다룬다. 서비스의 물리적 IP 주소를 추상화하고 서비스 호출에 대한 단일 진입점을 만들어야 모든 서비스 호출에 대한 일관된 보안과 콘텐츠 정책을 보장할 수 있다. 서비스 디스커버리와 라우팅은 “서비스에 대한 클라이언트의 요청을 특정 서비스 인스턴스에 어떻게 전달할 수 있을까?”라는 질문에 대한 답변이다.

2. 마이크로서비스 클라이언트 회복성 패턴

마이크로서비스 아키텍처는 고도로 분산되어 있어서 1개의 서비스 문제가 서비스 소비자에게 연쇄적으로 발생하지 않도록 방지해야 한다.

  1. 클라이언트 측 부하 분산
  2. 회로 차단기 패턴
  3. 폴백 패턴
  4. 벌크헤드 패턴

3. 마이크로서비스 보안 패턴

4. 마이크로서비스 로깅 및 추적 패턴

마이크로서비스의 단점은 애플리케이션과 서비스 안에서 어떤 일이 일어나고 있는지 디버깅과 추적이 훨씬 어렵다는 것이다.

  1. 로그 상관관계
  2. 로그 수집
  3. 마이크로서비스 추적

5. 마이크로서비스 빌드 및 배포 패턴

인프라스트럭처 구성을 빌드 배포 프로세스에 통합해 더이상 자바 WAR나 EAR처럼 소프트웨어 산출물을 이미 실행한 인프라스트럭처에 배포하지 않는 것이다. 그 대신 마이크로서비스와 빌드 프로세스 일부로 마이크로서비스가 실행되는 가상 서버 이미지를 빌드하고 컴파일해야 한다. 그 후 마이크로서비스를 배포할 때 서버가 실행될 머신 이미지를 배포할 수 있다.

1.7 스프링 클라우드로 마이크로서비스 구축

1.7.1 스프링 클라우드 컨피그

스프링 클라우드 컨피그는 중앙 집중식 서비스로 애플리케이션 구성 데이터 관리를 담당하고 애플리케이션 데이터(특히 환경별 구성 데이터)를 마이크로서비스와 완전히 분리한다. 따라서 마이크로서비스 인스턴스가 아무리 많더라도 항상 동일한 구성을 유지할 수 있다.

1.7.2 스프링 칼루으드 서비스 디스커버리

서비스 디스커버리를 사용하면 서비스를 사용하는 클라이언트에 서버가 배포된 물리적 위치(IP주소나 서버 이름)를 추상화할 수 있다. 서비스 소비자는 물리적 위치보다 논리적 이름을 사용해 서버의 비즈니스 로직을 호출한다.

1.7.3 넷플릭스 히스트릭스와 리본

넷플릭스 히스트릭스 라이브러리를 사용하면 회로 차단기와 벌크헤드 같은 서비스 클라이언트 회복성 패턴을 신속하게 구현할 수 있다.

1.7.4 넷플릭스 주울

서비스 라우팅 기능을 제공한다.

1.7.5 스프링 클라우드 스트림

마이크로서비스에 경량 메시지 프로세싱을 쉽게 통합할 수 있는 기술이다.

1.7.6 스프링 클라우드 슬루스

애플리케이션 안에서 사용되는 HTTP 호출과 메시지 채널에 고유 추적 식별자를 통합할 수 있다.

1.7.7 스프링 클라우드 시큐리티

서비스에 액세스할 수 있는 사람과 어떤 일을 할 수 있는지 통제할 수 있는 인증 및 인가 프레임워크다. 토큰에 기반을 두며 인증 서버가 발행한 토큰으로 서비스는 서로 통신한다.

1.7.8 프로비저닝

스프링 프레임워크는 애플리케이션 개발에 맞춰져 있고 스프링 클라우드처럼 빌드와 배포 파이프라인을 생성할 수 있는 도구가 없다. Travis CI와 도커를 사용할 것이다.

2. 스프링 부트로 마이크로서비스 구축

전통적인 폭포수 개발 방법론은 다음과 같은 단점이 발생했다.

  1. 강한 결합: 비즈니스 로직 호출은 SOAP이나 REST 같은 구현 기술에 중립적인 프로토콜 수준이 아닌 프로그래밍 언어 수준에서 이루어진다. 따라서 애플리케이션 컴포넌트를 조금만 수정해도 그 애플리케이션의 다른 부분을 깨뜨리거나 새로운 버그를 생산할 가능성이 높다.
  2. 누설: 다른 영역의 데이터에 쉽게 접근하게 되면 보이지 않는 의존성이 생겨나고 컴포넌트의 내부 데이터 구조에 대한 세부 구현이 애플리케이션 전체에 유출될 수 있다. 데이터베이스 테이블 하나를 조금만 변경해도 애플리케이션 전반에 걸쳐 엄청난 코드 수정과 회귀 테스팅이 필요할 수 있다.
  3. 모놀리식: 여러 팀에서 공유되는 단일 코드 베이스에 저장하므로 코드를 변경할 때마다 전체 애플리케이션을 재컴파일하고, 전체 테스팅 주기를 재수행하며 재배포한다. 애플리케이션 코드 베이스를 조금만 변경해도 비용이 많이 들며 오랜 시간이 소요된다.

마이크로서비스 기반 아키텍처의 접근법은 다음과 같은 특성을 가진다.

  1. 제한: 마이크로서비스는 하나의 책임 집합을 가지며 범위가 좁다.
  2. 느슨한 결합: msa 기반 애플리케이션은 작은 서비스 집합이며, HTTP와 REST처럼 비독점적 호출 프로토콜을 사용하는 구현 기술에 중립적인 인터페이스로 서로 소통한다. 서비스에 대한 인터페이스가 변하지 않는 한 마이크로서비스 소유자는 전통적인 애플리케이션 아키텍처보다 서비스를 더 자유롭게 수정할 수 있다.
  3. 추상화: 마이크로서비스는 자신의 데이터 구조와 데이터 소스를 완전히 소유한다. 마이크로서비스가 소유한 데이터는 해당 서비스만 수정할 수 있다.
  4. 독립적: 마이크로서비스는 서로 독립적으로 컴파일하고 배포할 수 있다. 이는 상호 의존성이 높은 모놀리식 애플리케이션보다 변경 사항을 훨씬 쉽게 분리하고 테스트할 수 있다는 것을 의미한다.

클라우드에 기반을 둔 개발에 이러한 msa의 특성이 중요한 이유를 알아보자. 클라우드 기반 애플리케이션은 일반적으로 다음 특징이 있다.

  1. 사용자 층이 다양하며 대규모다: 고객마다 서로 다른 제품 기능을 원하며, 이 기능을 접하기까지 애플리케이션의 긴 릴리스 주기를 기다리고 싶어 하지 않는다. 마이크로서비스는 작은 범위를 담당하고, 명확히 정의된 인터페이스를 통해 접근하므로 기능을 신속히 제공할 수 있다.
  2. 상당한 작동 시간이 요구된다: 마이크로서비스 자체의 분산적 특성 때문에 마이크로서비스 기반 애플리케이션은 애플리케이션 전체를 중단하지 않고도 고장과 문제를 더 쉽게 격리할 수 있다. 이로 인해 전반적인 애플리케이션 작동 중지 시간은 줄어들고 결함 저항력은 높아진다.
  3. 볼륨이 균일하지 않다: 기업 환경과 다르게 클라우드 기반 애플리케이션 환경에서는 용량(수요)이 갑자기 늘 수 있다. 마이크로서비스는 독립적인 배포가 가능한 작은 컴포넌트로 분리되어 있기 때문에 부하를 받는 컴포넌트를 조명하고 여러 서버에 수평 확장하기도 쉽다.

성공적인 마이크로서비스 개발 토대는 대개 다음 중요한 세 역할의 관점에서 시작한다.

  1. 아키텍트: 솔루션을 제공하기 위해 큰 그림을 바라보고 애플리케이션을 개별 마이크로서비스로 분해하는 방법과 마이크로서비스의 상호 작용 방법을 이해한다.
  2. 소프트웨어 개발자: 코드를 작성하고 마이크로서비스를 제공하기 위해 프로그래밍 언어와 해당 언어용 개발 프레임워크의 사용 방법을 자세히 이해한다.
  3. 데브옵스 엔지니어: 운영 환경 및 비운영 환경에서 서비스 배포와 관리 방법 정보를 제공한다. 모든 환경에서 일관성과 반복성을 제공해야 한다.

2.1 아키텍트 이야기: msa 설계

아키텍트는 해결해야 될 문제의 동작 모델을 제공해야 한다. 또 애플리케이션 코드가 서로 들어맞도록 개발자를 위한 발판도 제공해야 한다. 아키텍트는 다음 세 가지 일에 집중한다.

  1. 비즈니스 문제의 분해: 아키텍트는 데이터 영역이 서로 어울리지 않는다면 마이크로서비스들의 서비스 경계를 나눈다. 비즈니스 문제를 인식하고 마이크로서비스 후보로 분해하는데 다음과 같은 지침이 있다.

    (1) 비즈니스 문제를 기술하고 그 문제를 기술하는 데 사용된 명사에 주목하라

    (2) 동사에 주목하라.

    (3) 데이터 응집성을 찾아라.

  2. 서비스 세분화의 확정

    (1) 큰 마이크로서비스에서 시작해 작게 리팩토링하는 것이 더 낫다.

    (2) 서비스 간 교류하는 방식에 먼저 집중한다.

    (3) 문제 영역에 대한 이해가 깊어짐에 따라 서비스 책임도 계속 변한다.

    나쁜 마이크로서비스의 징후

    마이크로서비스가 너무 크게 나뉘어 있다면…

    1. 책임이 너무 많은 서비스
    2. 많은 테이블의 데이터를 관리하는 서비스
    3. 과다한 테스트 케이스

    마이크로서비스가 너무 잘게 나뉘어 있다면…

    1. 한 문제 영역 부분에 속한 마이크로서비스가 토끼처럼 번식한다
    2. 마이크로서비스가 지나치게 상호 의존적이다
    3. 마이크로서비스가 단순한 CRUD 집합이 된다
  3. 서비스 인터페이스의 정의: 서비스 인터페이스는 직관적이고 개발자가 1~2개의 애플리케이션 서비스를 학습하고 나면 애플리케이션의 모든 서비스에 대한 동작 규칙을 습득할 수 있어야 한다. 서비스 인터페이스 설계를 고려할 때 다음 지침을 사용할 수 있다.

    (1) REST 철학을 수용하라

    (2) URI를 사용해 의도를 전달하라

    (3) 요청과 응답에 JSON을 사용하라

    (4) HTTP 상태 코드로 결과를 전달하라

2.2 마이크로서비스를 사용하지 않아야 할 때

  1. 분산 시스템 구축의 복잡성
  2. 가상 서버/컨테이너의 스프롤: msa 기반 애플리케이션의 운영 환경에서 구축 및 관리가 필요한 서버나 커네이너가 50~100개 있을 수 있다. 클라우드에서 이들 서비스를 실행하는 데 드는 비용은 저렴하더라도 서버를 관리하고 모니터링하는 운영 작업은 엄청나게 복잡할 수 있다.
  3. 애플리케이션 유형: 마이크로서비스는 재사용성을 추구하며 높은 회복성과 확장성이 필요한 대규모 애플리케이션의 구축에 유용하다. 그렇지 않은 애플리케이션에선 유용하지 않을 수 있다.
  4. 데이터 변환과 일관성

2.3 데브옵스 이야기: 혹독한 런타임 구축

  1. 마이크로서비스는 자체완비형이며 독립적으로 배포 가능해야 한다. - 서비스 어셈블리
  2. 마이크로서비스는 구성 가능해야 한다. - 서비스 부트스트래핑
  3. 마이크로서비스 인스턴스는 클라이언트가 위치를 알지 못하도록 투명해야 한다. - 서비스 등록 및 디스커버리
  4. 마이크로서비스는 자신의 상태를 전달해야 한다. - 서비스 모니터링

2.3.1 서비스 어셈블리: 마이크로서비스의 패키징과 배포

마이크로서비스는 필요한 의존성을 모두 담아 단일 산출물로 패키징하고 설치될 수 있어야 한다. 일관된 구축, 패키징 및 배포하는 이 과정을 서비스 어셈블리라고 한다.

애플리케이션 서버의 구성을 애플리케이션과 분리하므로 배포 과정에서 문제점이 발생하는데, 많은 조직에서 애플리케이션 서버의 구성을 소스 제어 저장소에 보관하지 않고 사용자 인터페이스와 자체 제작한 스크립트를 사용해 관리하는 것이 원인이다. 내장형 런타임 엔진을 포함한 단일 산출물로 배포하면 이러한 구성 편차 문제를 상당 부분 제거한다. 전체 산출물을 소스 제어하에 관리하므로 애플리케이션 팀이 애플리케이션 빌드와 배포 방법을 더 효과적으로 추론할 수 있다.

2.3.2 서비스 부트스트래핑: 마이크로서비스의 구성 관리

서비스 부트스트래핑은 마이크로서비스가 처음 가동할 때 시작하며 애플리케이션 구성 정보를 로드한다.

2.3.3 서비스 등록과 디스커버리: 클라이언트가 마이크로서비스와 통신하는 방법

마이크로서비스는 위치 투명성을 가져야 한다. 모든 서비스에는 고유하고 비영구적인 IP 주소가 할당된다. ‘일시적’ 서비스의 단점은 끊임없이 서비스의 시작과 종료를 반복하는 상황에서 일시적 서비스를 대량으로 수동 또는 직접 관리하면서 장애가 발생할 수 있다는 것이다.

마이크로서비스 인스턴스는 제 3자 에이전트에 스스로 등록해야 하는데 이 등록 과정을 서비스 디스커버리라고 한다.

2.3.4 마이크로서비스의 상태 전달

서비스 디스커버리 에이전트는 서비스 상태를 모니터링한다. 인스턴스가 고장나면 상태 확인 로직은 그 인스턴스를 인스턴스 가용 풀에서 제거한다. 상태를 확인함으로써 마이크로서비스 인스턴스가 실행중인 서버의 상태 정보를 제공하며 모니터링을 강화할 수 있다.

3. 스프링 클라우드 컨피그 서버로 구성 관리

코드에 박힌 애플리케이션 구성 데이터는 구성을 변경할 때마다 애플리케이션을 재컴파일하거나 재배포해야 하므로 종종 문제가 된다. 이 문제를 피하기 위해 개발자는 애플리케이션 코드에서 구성 정보를 완전히 분리한다. 이것으로 컴파일 과정을 거치지 않고 구성은 쉽게 변경할 수 있게 되었지만, 애플리케이션과 함께 관리되고 배포되어야 하는 산출물이 추가되어 복잡해진다.

많은 개발자가 구성 정보를 저장하기 위해 저수준의 프로퍼티 파일(YAML, JSON, XML 등)로 전환할 것이다. 대개 이러한 프로퍼티 파일은 데이터베이스 및 미들웨어의 접속 정보와 애플리케이션 행동 양식을 정하는 메타데이터가 존재하는 서버에 둔다. 애플리케이션을 프로퍼티 파일로 분리하는 것은 쉬우며, 대부분의 개발자는 구성 파일을 소스 관리 시스템에 넣거나 애플리케이션 일부로 배포하는 일 외에는 애플리케이션 구성을 위한 어떤 운영 작업도 하지 않는다.

이러한 방식은 애플리케이션이 적은 상황에서는 적용될 수 있지만 수백 개의 마이크로서비스와 수많ㅇ느 마이크로서비스 인스턴스가 실행되는 클라우드 기반의 애플리케이션 상황에서는 전혀 통하지 않는다.

클라우드 기반 환경에 놓인 애플리케이션 팀과 운영 팀이 어떤 구성 파일을 어디에 배치할지 정하기 위해 대혼한을 겪어야 하므로 갑자기 구성 관리는 중대한 문제가 된다. 따라서 클라우드 기반의 마이크로서비스 개발에서는 다음 사항이 강조된다.

  1. 배포라는 실제 코드에서 애플리케이션의 구성을 완전하게 분리한다.
  2. 서버 및 애플리케이션을 빌드하고 배포 환경에 따라 절대 바뀌지 않는 불변 이미지를 빌드한다.
  3. 서버를 시작할 때 환경 변수나 애플리케이션의 마이크로서비스가 읽어 올 수 있는 중앙 저장소를 이용해 애플리케이션 구성 정보를 주입한다.

3.1 구성 관리

항상 사람이 수동으로 구성하거나 배포하면 구성 편차와 예상하지 못한 장애, 애플리케이션 확장 요구에 대한 지체 시간이 발생할 수 있다. 다음 네 가지 원칙이 있다.

  1. 분리: 실제 물리적인 서비스의 배포와 서비스 구성 정보를 완전히 분리하고자 한다. 애플리케이션 구성 정보를 서비스 인스턴스와 함께 배포하면 안 된다. 그 대신 시작하는 서비스에 환경 변수로 전달하거나 중앙 저장소에서 읽어 와 구성 정보를 전달해야 한다.
  2. 추상호: 서비스 인터페이스 뒷 단에 있는 구성 데이터의 접근 방식을 추상화한다. 서비스 저장소에 직접 액세스하는 코드를 작성하기(파일이나 JDBC를 사용해 데이터베이스에서 데이터를 읽기)보다 애플리케이션이 REST 기반의 JSON 서비스를 사용해 구성 데이터를 조회하게 만들어야 한다.
  3. 중앙 집중화: 클라우드 기반의 애플리케이션에는 말 그대로 수백 개의 서비스가 존재할 수 있으므로 구성 정보를 보관하는 저장소 개수를 최소로 줄이는 것이 중요하다. 애플리케이션의 구성 정보를 가능한 소수 저장소에 집중화한다.
  4. 견고성: 애플리케이션 구성 정보를 배포된 서비스와 완전히 분리하고 중앙 집중화하므로 어떤 솔루션을 사용하더라도 고가용성과 다중성을 구현할 수 있어야 한다.

핵심적으로 기억할 사항은 구성 정보를 실제 코드 외부로 분리하면 관리하고 버전 제어를 해야 할 외부 의존성이 생긴다는 것이다. 애플리케이션의 구성을 제대로 관리하지 못하면 탐지하기 어려운 버그와 예상하지 못한 장애를 만드므로 애플리케이션의 구성 데이터를 추적하고 버전을 제어하는 것은 중요하다.

3.1.1 구성 관리 아키텍처

2장에서 배웠듯 구성 관리는 마이크로서비스의 부트스트래핑 단계(어셈블리, 부트스트래핑, 디스커버리, 모니터링 중)에서 일어난다.

부트스트래핑 과정을 자세히 살펴보고, 구성 관리 서비스가 이 단계에서 어떻게 역할을 수행하는지 알아보자.

  1. 마이크로서비스 인스턴스가 시작하고 (구성 서비스에서)구성 정보를 얻는다. : 마이크로서비스 인스턴스가 시작하면 서비스 엔드포인트를 호출해 동작 중인 환경별 구성 정보를 읽어 온다. 구성 관리 서비스에 연결할 정보(접속용 자격 증명, 서비스 엔드포인트 등)는 마이크로서비스가 시작할 때 전달된다.
  2. 실제 구성 정보는 구성 서비스 저장소에 저장된다. : 구성 데이터를 보관할 수 있는 구성 저장소 구현 방식이 다양하며, 소스 관리되는 파일이나 관계형 데이터베이스, 키-값 짝 데이터 저장소 같은 구현 방식을 택할 수 있다.
  3. 개발자가 구성 정보를 변경하면 빌드 및 배포 파이프라인으로 구성 저장소에 전달된다. : 실제로 애플리케이션 배포 방식과 독립적으로 애플리케이션의 구성 데이터를 관리한다. 대개 빌드 및 배포 파이프라인으로 구성 관리를 변경하며, 변경된 구성은 버전 정보 태그를 달아 다른 환경에 배포될 수 있게 한다.
  4. 변경된 구성이 있는 애플리케이션은 갱신하도록 알림을 받는다. : 구성 관리가 변경되면 애플리케이션 구성 데이터를 사용하는 서비스는 변경 통보를 받고 보유한 애플리케이션 데이터 사본을 갱신해야 한다.

3.1.2 구현 선택

구성 관리 시스템을 구현하는 오픈 소스 프로젝트

프로젝트 이름 설명 특성
유레카 넷플릭스가 만들었고 수많은 실전 테스트를 거쳤다. 서비스 검색과 키-값 관리에 사용된다. - 분산 키-값 저장소
- 유연하지만 설정하는 데 공수가 든다.
- 동적 클라이언트 갱신 기능을 제공한다.
스프링 클라우드 컨피그 서버 다양한 백엔드와 함께 일반적인 구성 관리 솔루션을 제공하는 오픈 소스 프로젝트. 깃, 유레카 및 콘설 같은 백엔드와 통합 가능하다. - 비분산 키-값 저장소
- 스프링 및 스프링 기반이 아닌 서비스와 통합 가능하다.
- 공유 파일과 시스템, 유레카, 콘설, 깃 등 구성 데이터 저장을 위한 다양한 백엔드 사용이 가능하다.

이외에도 Etcd, 콘설, 주키퍼 등이 있다.

책에서 클라우드 컨피그 서버를 선택한 이유는

  1. 스프링 클라우드 컨피그 서버는 쉽게 설치하고 사용할 수 있다.
  2. 스프링 클라우드 컨피그는 스프링 부트와 긴밀히 통합되어 있다. 따라서 모든 애플리케이션의 구성 데이터를 사용이 간편한 애너테이션으로 읽어올 수 있다.
  3. 스프링 클라우드 컨피그 서버는 구성 데이터를 저장할 수 있는 여러 백엔드를 지원한다. 유레카나 콘설 같은 도구를 이미 사용하고 있다면 바로 스프링 클라우드 컨피그 서버에 연결할 수 있다.
  4. 깃 소스 제어 플랫폼과 직접 통합할 수 있다. 스프링 클라우드 컨피그를 깃과 통합하면 다른 솔루션으 ㅣ 추가 의존성을 제거하고 애플리케이션의 구성 데이터를 손쉽게 버전 관리할 수 있다. Etcd, 콘설, 유레카 같은 다른 도구는 자체 버전 관리 기능이 없으므로 필요하다면 직접 구축해야한다.

3.2 스프링 클라우드 컨피그 서버 구축

스프링 클라우드 컨피그 서버는 스프링 부트로 만든 REST 기반의 애플리케이션이다. 독립형 서버로 제공되지 않아 기존 스프링 부트 애플리케이션에 내장하거나 새로운 스프링 부트 프로젝트르 ㄹ 만들어 내장하는 방법으로 시작할 수 있다.

스프링 클라우드 컨피그에서 모든 것은 계층 구조로 동작한다. 애플리케이션 구성은 애플리케이션 이름을 먼저 표시하고 구성 정보가 필요한 각 환경별 프로퍼티 파일로 구분한다. 각 환경에 두 가지 구성 프로퍼티를 설정한다.

  1. 라이선싱 서비스가 직접 사용할 예제 프로퍼티
  2. 라이선싱 서비스의 데이터가 저장될 Postgres 데이터베이스를 위한 데이터베이스 구성

중대형 클라우드 애플리케이션에 파일 시스템 기반의 솔루션 사용을 권장하지 않는다. 파일 시스템 방식을 사용하는 것은 애플리케이션 구성 데이터를 액세스하려는 모든 구성 서버에 공유된 파일 마운트를 구현해야 한다는 것을 의미한다. 클라우드에 공유 파일 시스템 서버를 구축할 수 있지만 이 환경을 유지 보수해야 할 책임이 따른다.

하나의 서비스를 처음 시작할 때 명령줄로 두 가지 정보(스프링 프로파일과 스프링 클라우드 컨피그 서비스와 통신할 때 사용하는 엔드포인트)를 전달한다. 스프링 프로파일은 스프링 서비스가 추출하는 프로퍼티 환경에 매핑된다. 서비스가 처음 부팅하면 전달받은 프로파일과 엔드포인트를 사용해 스프링 클라우드 컨피그 서비스와 통신한다. 스프링 클라우드 컨피그 서비스는 URI로 전달된 특정 스프링 프로파일에 해당되는 구성 정보를 뒷 단의 구성 저장소(파일 시스템, 깃, 유레카 등)에서 조회한 후 적절한 프로퍼티 값을 라이선싱 서비스에 돌려준다. 스프레이 붙크 프레임워크는 이 프로퍼티 값을 애플리케이션에 적절히 삽입한다.

3.3 스프링 클라우드 컨피그와 스프링 부트 클라이언트의 통합

3.3.1 깃과 스프링 클라우드 컨피그 서버 사용

스프링 클라우드 컨피그 서버의 백엔드 저장소로 파일 시스템이 적합하지 않은 이유는 개발 팀이 컨피그 서버의 모든 인스턴스에 마은트될 공유 파일 시스템을 설정하고 관리해야 하기 때문이다.

깃을 사용하면 구성 관리 프토퍼티를 저장할 때 소스 관리의 모든 혜택을 누리고 빌드 및 배포 파이프라인에서 프로퍼티 구성 파일의 배포를 쉽게 통합할 수 있다.

3.3.2 스프링 클라우드 컨피그 서버에서 프로퍼티 갱신

프로퍼티가 변경될 때 스프링 클라우드 컨피그 서버가 어떻게 동적으로 애플리케이션을 갱신할까? 스프링 클라우드 컨피그 서버는 하부 저장소의 프로퍼티를 변경하면 바로 반영하여 항상 최신 버전의 프로퍼티를 제공한다. 하지만 스프링 부트 애플리케이션은 시작할 때만 프로퍼티를 읽어 오며, 스프링 클라우드 컨피그 서버에서 변경된 프로퍼티를 자동으로 읽어 오지 않는다. 스프링 부트 액추에이터는 @RefreshScope 애너테이션을 제공하므로 스프링 부트 애플리케이션이 /refresh 엔드포인트를 사용해 애플리케이션 구성 정보를 다시 읽어올 수 있다. @RefreshScope 애너테이션에 대해 두 가지 유의할 게 있다. 첫째, 이 애너테이션은 애플리케이션 구성에 있는 사용자 정의 스프링 프로퍼티만 다시 로드한다. 즉 데이터베이스 구성 정보처럼 스프링 데이터에서 정의된 구성은 @RefreshScope 애너테이션으로 다시 로드하지 않는다. 둘째, 업데이트를 수행하기 위해 http://:8080/actuator/refresh 엔드포인트를 호출한다.

Note: 마이크로서비스 구성 정보 업데이트

스프링 클라우드 컨피그 서비스를 마이크로서비스와 함께 사용할 때 프로퍼티를 동적으로 변경하기 전에 고려할 사항 중 하나는 동일한 서비스 인스턴스가 다수 실행중이고 새로운 애플리케이션 구성으로 모든 서비스를 업데이트해야 한다는 것이다.

이를 위한 첫번째 방법으로 스프링 클라우드 컨피그 서비스는 이 서비스를 이용하는 모든 클라이언트에 변경이 일어났다고 알려주는 스프링 클라우드 버스라는 푸시 기반의 메커니즘을 제공한다. 스프링 클라우드 컨피그는 RabbitMQ 같은 미들웨어를 추가해야 한다. 변경을 감지하는 유용한 방법이지만 모든 스프링 클라우드 컨피그 백엔드가 콘설 서버처럼 푸시 메커니즘을 지원하는 것은 아니다.

두 번째 방법은 스프링 클라우드 컨피그의 애플리케이션 프로퍼티를 업데이트한 후 서비스 디스커버리 엔진으로 모든 서비스 인스턴스를 조회해 /refresh 엔드포인트를 직접 호출하는 간단한 스크립트를 작성하는 것이다.

세 번째 방법은 모든 서버와 컨테이너가 새로운 프로퍼티를 업데이트하도록 재시작하는 것이다. 특히 도커 같은 컨테이너에서 서비스를 실행하고 있다면 훨씬 수월하다. 도커 컨테이너는 수초 만에 재시작해 애플리케이션 구성을 다시 읽어 올 수 있다.

3.4 중요한 구성 정보 보호

소스 코드 저장소에 중요한 자격 증명 정보를 저장하는 것은 잘못된 관행이다. 스프링 클라우드 컨피그는 중요한 프로퍼티를 쉽게 암호화할 수 있는 기능을 제공하며 대칭(공유 비밀 키 사용) 및 비대칭 암호화(공개 비공개 키 사용)를 모두 지원한다.

암호화를 적용하기 위해 다음 단계를 거쳐야 한다.

  1. 암호화에 필요한 오라클 JCE jar 파일을 내려받고 설치한다.
  2. 암호화 키를 설정한다.
  3. 프로퍼티를 암호화 및 복호화한다.
  4. 클라이언트 측에서 암호화하도록 마이크로서비스를 구성한다.

4. 서비스 디스커버리

분산 아키텍처에서는 시스템의 물리적 위치 주소를 찾아야 한다. 서비스 디스커버리는 다음 이유로 msa에서 중요하다. 첫째, 애플리케이션 팀은 서비스 디스커버리를 이용해 해당 환경에서 실행하는 서비스 인스턴스 개수를 신속하게 수평 확장하거나 축소할 수 있다. 둘째, 애플리케이션 회복성을 향상하는 데 도움이 된다.

4.1 서비스 위치 찾기

클라우드가 아닌 환경에서 서비스의 위치 확인은 대개 DNS와 네트워크 로드 밸런서로 해결되었다. 서비스 소비자에게 요청을 받으면 로드 밸런서는 사용자가 액세스하려는 경로를 기반으로 라우팅 테티블에서 물리적 주소 항목을 찾는다. 이 라우팅 테이블 항목에는 해당 서비스를 호스팅하는 서버가 1개 이상 포함된 서버 목록이 있다. 로드 밸랜서는 서버 목록에서 하나를 선택해 요청을 전달한다. 서비스 인스턴스는 여러 애플리케이션 서버에 배포된다. 애플리케이션 서버 개수는 정적이고(서비스를 호스팅하는 애플리케이션 서버 개수는 증감하지 않음), 영구적인(애플리케이션 서버를 실행하는 서버가 비정상적으로 종료하면, 종료 전과 동일한 상태와 동일한 IP 및 구성으로 복원될 것임) 경우가 많다. 고가용성을 위해 보조 로드 밸런서가 유휴 상태로 대기하고 주 로드 밸런서가 정상인지 핑으로 확인한다. 정상이 아니면 보조 로드 밸런서가 활성화되고 주 로드 밸런서의 IP 주소를 인수해 요청을 처리한다.

이런 모델은 사방이 벽으로 둘러싸인 회사 데이터센터 안에서 실행되는 애플리케이션과 정적 서버 그룹에서 실행되는 소수 서비스에서는 잘 동작한다. 하지만 클라우드 기반의 마이크로 서비스 애플리케이션에서는 그렇지 못하다. 이유는 다음과 같다.

  1. 단일 장애 지점: 로드 밸런서가 고가용성을 지원한다고 해도 여전히 전체 인프라스트럭처의 단일 장애 지점이다. 로드 밸런서를 고가용하게 만들더라도 애플리케이션 인프라스트럭처 안에서 집중화된 병목 지점이 될 가능성이 높다.
  2. 수평 확장의 제약성: 로드 밸런서 클러스터에 서비스를 모아 연결하므로 부하 분산 인프라스트럭처를 여러 서버에 수평적으로 확장할 수 있는 능력이 제한된다. 상용 로드 밸런서 다수는 중복성 모델과 라이선싱 비용이라는 두 가지 요소에 제약을 받는다. 대부분의 상용 로드 밸런서는 이중화를 위해 핫스왑 모델에 따라 부하를 처리하는데 하나의 서버만 사용하고, 보조 로드 밸런서는 주 로드 밸런서에 장애가 발생할 경우 대체 작동용으로만 사용한다. 본질적으로 하드웨어의 제약을 받는다. 그리고 상용 로드 밸런서는 가변 모델이 아닌 고정 용량에 맞춰 한정된 라이선싱 모델을 보유한다.
  3. 정적 관리: 전통적 로드 밸런서 대부분은 서비스를 신속히 등록하고 취소하도록 설계되지 않았다.
  4. 복잡성: 로드 밸런서가 서비스에 대한 프록시 역할을 하므로 서비스 소비자에게 요청할 때 물리적인 서비스에 매핑된 요청 정보가 있어야 한다. 이 변환 계층은 서비스 매핑 규칙을 수동으로 정의하고 배포해야 하므로 서비스 인프라스트럭처의 복잡성을 가중시킨다.

로드 밸런서는 대부분의 애플리케이션이 중앙 집중화된 네트워크 인프라스트럭처를 이용해 처리될 수 있는 크기와 규모기 있는 기업 환경에서 잘 동작한다. 게다가 SSL 종료를 한곳에서 처리하고 서비스의 포트 보안을 관리하는 면에서 여전히 중요한 역할을 한다. 로드 밸런서는 자기 후방의 모든 서버에 대한 인바운드 및 아웃바운드 포트 접근을 제어할 수 있다.

하지만 대용량의 트랜잭션과 중복성을 처리해야 하는 클라우드에서 중앙 집중식 네트워크 인프라스트럭처는 효율적으로 확장되지 않고 비용 효율도 낮아서 결국 제대로 동작하지 못한다.

4.2 클라우드에서 서비스 디스커버리

클라우드 기반 마이크로서비스 환경에 대한 솔루션은 다음 기능을 갖춘 서비스 디스커버리 메커니즘을 사용하는 것이다.

  1. 고가용성: 서비스 디스커버리는 서비스 검색 정보를 서비스 디스커버리 클러스터의 여러 노드가 공유하는 핫 클러스터링 환경을 지원해야 한다. 한 노드가 사용할 수 없게 되면 클러스터의 다른 노드가 인계를 받을 수 있어야 ㅠ한다.
  2. 피어 투 피어: 서비스 디스커버리 클러스터의 각 노드는 서비스 인스턴스의 상태를 공유한다.
  3. 부하 분산: 서비스 디스커버리는 요청을 동적으로 부하 분산해서 서비스 디스커버리가 관리하는 모든 서비스 인스턴스에 분배해야 한다.
  4. 회복성: 서비스 디스커버리 클라이언트는 서비스 정보를 로컬에 ‘캐시’해야 한다. 로컬 캐싱은 서비스 디스커버리 기능을 점진적이르 저하시킬 수 있는데 서비스 디스커버리 서비스가 가용하지 않을 때 애플리케이션이 로컬 캐시에 저장된 정보를 기반으로 서비스를 계속 찾을 수 있고 동작하게 한다.
  5. 장애 내성: 서비스 디스커버리는 서비스 인스턴스의 비정상을 탐지하고 가용 서비스 목록에서 인스턴스를 제거해야 한다. 그리고 서비스 장애를 감지하고 사람의 개입 없이 조치를 취해야 한다.

4.2.1 서비스 디스커버리 아키텍처

서비스 인스턴스가 추가, 제거될 때 서비스 디스커버리 에이전트를 업데이트하고 사용자 요청을 처리할 수 있는 상태가 된다. 서비스 디스커버리 인스턴스는 대개 고유하고 로드 밸런서를 앞에 두지 않는다.

클라이언트 애플리케이션은 서비스의 IP 주소를 직접 알지 못한다. 그 대신 서비스 디스커버리 에이전트에서 주소를 가져온다.

  1. 서비스 디스커버리 노드는 서비스 디스커버리 에이전트를 논리적 이름으로 서비스 위치르 ㄹ 검색할 수 있다.
  2. 서비스가 온라인 상태가 되면 자기 IP 주소를 서비스 검색 에이전트에 등록한다.
  3. 서비스 디스커버리 노드는 서비스 인스턴스의 상태 정보를 서로 공유한다.
  4. 서비스는 서비스 디스커버리 에이전트에 상태 정보를 보낸다. 서비스가 종료되면 서비스 디스커버리 계층에서 종료된 인스턴스의 IP 주소를 제거한다.

서비스 인스턴스가 시작하면 서비스 디슼커버리 인스턴스가 접근할 수 있는 자시의 물리적 위치와 경로, 포트를 등록한다. 서비스의 각 인스턴스에는 고유한 IP 주소와 포트가 있지만 동일한 서비스 ID로 등록한다. 이때 서비스 ID는 동일한 서비스 인스턴스 그룹을 고유하게 식별하는 키일 뿐이다.

서비스는 일반적으로 1개의 서비스 디스커버리 인스턴스에만 등록한다. 서비스 디스커버리 구현체 대부분은 P2P 모델을 사용해 서비스 인스턴스의 데이터를 클러스터에 있는 다른 노드에 전파한다.

서비스 디스커버리 구현에 따라 전파 메커니즘에 하드 코딩된 서비스 목록을 사용하거나 가십 같은 대중 캐스트 프로토콜 및 infection-style 프로토콜을 사용해 클러스터에서 발생된 변경을 다른 노드가 발견할 수 있다.

각 서비스 인스턴스는 자기 상태를 서비스 디스커머리 서비스에 푸시하거나 서비스 디스커버리 서비스가 인스턴스 상태를 추출한다. 정상 상태를 반환하지 못한 서비스는 가용한 서비스 인스턴스 풀에서 제거된다.

서비스가 서비스 디스커버리 서비스에 등록되면 그 서비스의 기능을 사용해야 하는 애플리케이션이나 다른 서비스에서 사용할 준비가 된 것이다.

4.2.2 스프링과 유레카를 사용한 서비스 디스커버리

  1. 서비스 부트스트래핑 시점에 라이선싱 및 조직 서비스는 자신을 유레카 서비스에 등록한다. 이 등록 과정에서 서비스 ID와 함께 각 서비스 인스턴스의 물리적 위치, 포트 번호를 유레카에 알려준다.
  2. 서비스가 다른 서비스를 호출할 때 넷플릭스 리본 라이브러리를 사용해 클라이언트 측 부하 분산 기능을 수행한다. 리본 라이브러리는 유레카 서비스에서 서비스의 위치 정보를 조회하고 로컬에 캐싱한다.
  3. 주기적으로 리본 라이브러리는 유레카 서비스를 핑해서 로컬 캐시의 서비스 위치를 새로고침한다.

4.3 스프링 유레카 서비스 구축

유레카는 등록된 서비스에서 10초 간격으로 연속 3회의 상태 정보를 받아야 하므로 등록된 개별 서비스를 보여 주는 데 30초가 걸림.

IP 주소를 선호하는 이유: 기본적으로 유레카는 호스트 이름으로 접속하는 서비스를 등록한다. DNS가 지원된 호스트 이름을 할당하는 서버 기반 환경에서 잘 동작하기 때문이다. 그러나 컨테이너 기반의 배포(도커 등)에서 컨테이너는 DNS 엔트리가 없는 임의로 생성된 호스트 이름을 부여받아 시작한다. eureka.instance.preferIpAddress를 true로 설정하지 않는다면 해당 컨테이너에 대한 DNS 엔트리가 없으므로 클라이언트 애플리케이션은 호스트 이름 위치를 정상적으로 얻지 못한다. preferIpAddress 프로퍼티를 설정하면 클라이언트 IP 주소로 전달받길 선호한다고 유레카 서비스에 알려준다. 저자는 이 속성을 항상 true로 설정한다고 한다. 클라우드 기반 마이크로서비스는 일시적이며 무상태여야 하므로 자유롭게 시작하고 종료될 수 있고, IP 주소는 이런 서비스 유형에 더 적절하기 때문이다.

유레카의 고가용성: 고가용성을 위해 클라이언트가 여러 URL 서비스를 설정하는 것만으로는 부족하다. defaultZone 프로퍼티는 클라이언트가 통신할 수 있는 유레카 서비스 목록만 제공한다. 유레카 서비스를 추가로 구축해 레지스트리 내용을 서로 복제하도록 설정해야 한다. 유레카 레지스트리 그룹은 P2P 통신 모델 기반으로 상호 통신하며 각 유레카 서비스는 클러스터의 다른 노드에 대해 알 수 있도록 구성, 설정되어야 한다.

정리

  1. 서비스 디스커버리 패턴은 서비스의 물리적 위치를 추상화하는 데 사용한다.

  2. 유레카 같은 서비스 디스커버리 엔진은 서비스 클라이언트에 영향을 주지 않고 해당 환경의 서비스 인스턴스를 원활하게 추가, 삭제할 수 있다.

  3. 클라이언트 측 부하 분산을 사용하면 서비스를 호출하는 클라이언트에서 서비스의 물리적 위치를 캐싱해 더 나은 성능과 회복성을 제공할 수 있다.

  4. 유레카는 넷플릭스 프로젝트의 스프링 클라우드와 사용하면 쉽게 구축하고 구성할 수 있다.

  5. 스프링 클라우드와 넷플릭스 유레카, 그리고 서비스를 호출하는 넷플릭스 리본으로 다음 세 가지 메커니즘을 사용할 수 있다.

    (1) 스프링 클라우드와 DiscoveryClient

    (2) 스프링 클라우드와 리본 지원 RestTemplate

    (3) 스프링 클라우드와 넷플릭스 Feign 클라이언트

5. 나쁜 상황에 대비한 스프링 클라우드와 넷플릭스 히스트릭스의 클라이언트 회복성 패턴

모든 시스템, 특히 분산 시스템은 장애를 겪는다. 보통 회복력을 갖춘 시스템을 구축하는 것에 대해 일부 인프라스트럭처나 핵심 서비스의 완전한 장애만 고려한다. 핵심 서버를 클러스터링하고, 서비스의 부하를 분산하며 인프라스트럭처를 여러 곳으로 분리하는 기술을 사용해 애플리케이션 간 계층의 중복성을 높이는 데 주력한다. 이러한 접근 방식으로 시스템 컴포넌트가 완전히 손실될 것을 고려하지만 회복력 있는 시스템을 구축하는 데 생기는 사소한 문제만 해결할 뿐이다. 서비스 하나가 충돌하면 쉽게 감지할 수 있고 애플리케이션은 그 서비스를 피해 우회할 수 있지만, 서비스가 느려질 때 성능 저하를 감지하고 우회하는 것은 어렵다. 다음과 같은 이유 때문이다.

  1. 서비스 저하는 간헐적으로 발생하고 확산될 수 있다: 서비스 저하는 사소한 부분에서 갑자기 발생할 수 있다. 순식간에 애플리케이션 컨테이너가 스레드 풀을 모두 소진해 완전히 무너지기 전까지 장애 징후는 일부 사용자가 문제점을 불평하는 정도로 나타날 것이다.
  2. 원격 서비스 호출은 대개 동기식이며 오래 걸리는 호출을 중단하지 않는다: 서비스 호출자에게는 호출이 영구 수행되는 것을 방지하는 타임아웃 개념이 없다. 애플리케이션 개발자는 서비스를 호출해 작업을 수행하고 서비스가 응답할 때까지 대기한다.
  3. 애플리케이션은 대개 부분적인 저하가 아닌 원격 자원의 완전한 장애를 처리하도록 설계된다: 서비스가 완전히 다운되지 않는다면 애플리케이션이 서비스를 계속 호출하고 빨리 실패하지 않는 일이 자주 발생한다. 애플리케이션은 제대로 동작하지 않는 서비스를 계속 호출할 것이다.

제대로 동작하는 원격서비스로 야기되는 문제가 심각하느 이유는 탐지하기 어려울 뿐만 아니라 애플리케이션 전체 생태계에 미치는 파급 효과가 크기 때문이다. 안전 장치가 없다면 제대로 동작하지 않는 서비스 하나가 여러 애플리케이션을 짧은 시간동안 다운시킬 수 있다. 클라우드와 마이크로서비스에 기반을 둔 애플리케이션이 이러한 유형의 장애에 특히 취약한 이유는 사용자 트랜잭션을 완료하는 데 필요한 여러 인프라스트럭처 위에서 세밀하게 분산된 수많은 서비스로 구성되기 때문이다.

5.1 클라이언트 회복성 패턴이란?

클라이언트의 회복성을 위한 패턴은 원격 서비스가 에러를 던지거나 제대로 동작하지 못해 원격 자원의 접근이 실패할 때, 원격 자원을 호출하는 클라이언트 충돌을 막는 데 초점이 맞춰져 있다.

  1. 클라이언트 측 부하 분산: 서비스 클라이언트는 서비스 디스커버리에서 조회한 마이크로서비스의 엔드포인트를 캐싱한다.
  2. 회로 차단기: 서비스 클라이언트가 장애 중인 서비스를 반복적으로 호출하지 못하게 한다.
  3. 폴백: 호출이 실패하면 폴백은 실행 가능한 대안이 있는지 확인한다.
  4. 벌크헤드: 불량 서비스가 클라이언트의 모든 자원을 고갈시키지 않도록 서비스 클라이언트가 수행하는 서비스 호출을 격리한다.

이러한 패턴은 원격 자원을 호출하는 클라이언트에서 구현한다. 좀 더 정확히는 원격 자원을 소비하는 클라이언트와 자원 사이에서 구현한다.

5.1.1 클라이언트 측 부하 분산

클라이언트 측 부하 분산은 클라이언트가 넷플릭스 유레카 같은 서비스 디스커버리 에이전트를 이용해 서비스의 모든 인스턴스를 검색한 후 해당 서비스 인스턴스의 실제 위치를 캐싱하는 것이다.

서비스 소비자가 서비스 인스턴스를 호출해야 할 때마다 클라이언트 측 로드 밸런서는 서비스 위치 풀에서 관리하는 서비스 위치를 하나씩 전달한다.

클라이언트 측 로드 밸런서는 서비스 클라이언트와 서비스 소비자 사이에 위치하므로 서비스 인스턴스가 에러를 전달하거나 불량 동작하는지 감지한다. 클라이언트 측 로드 밸런서가 문제를 감지할 수 있다면 가용 서비스 위치 풀에서 문제가 된 서비스 인스턴스를 제거해 서비스 호출이 그 인스턴스로 전달되는 것을 막는다.

5.1.2 회로 차단기

전기 시스템에서 회로 차단기는 전기선에 유입된 과전류를 감지한다. 회로 차단기가 문제를 감지하면 모든 전기 시스템과 연결된 접속을 차단하고 하부 컴포넌트가 과전류에 손상되지 않도록 보호하는 역할을 한다.

소프트웨어 회로 차단기는 원격 서비스 호출을 모니터링한다. 호출이 오래 걸린다면 회로 차단기가 중재해 호출을 중단한다. 회로 차단기는 원격 자원에 대한 모든 호출을 모니터링하고, 호출이 필요한 만큼 실패하면 회로 차단기가 활성화되어 빨리 실패하게 만들며, 고장난 원격 자원은 더이상 호출되지 않도록 차단한다.

5.1.3 폴백 처리

폴백 패턴을 사용하면 원격 서비스에 대한 호출이 실패할 때 예외를 발생시키지 않고 서비스 소비자가 대체 코드 경로를 실행해 다른 방법으로 작업을 수행할 수 있다. 일반적으로 이 패턴은 다른 데이터 소스에서 데이터를 찾거나 향후 처리를 위해 사용자 요청을 큐에 입력하는 작업과 연관된다. 사용자 호출에 문제가 있다고 예외를 표시하지 않지만 나중에 해당 요청을 수행할 수 있다고 전달받을 수 있다.

5.1.4 벌크헤드

벌크헤드 패턴은 선박을 건조하는 개념에서 유래한다. 벌크헤드 설계를 적용하면 배는 격벽이라는 완전히 격리된 수밀 구획으로 나뉜다. 선체에 구멍이 뚫린 경우에도 배가 수밀 구획(격벽)으로 분리되어 있으므로 침수 구역을 제한하고 배 전체의 침수와 침몰을 방지할 수 있다.

서비스에 벌크헤드 패턴을 적용하면 원격 자원에 대한 호출을 자원별 스레드 풀로 분리하므로 특정 원격 자원의 호출이 느려져 전체 애플리케이션이 다운될 수 있는 위험을 줄일 수 있다. 스레드 풀은 서비스를 위한 벌크헤드(격벽) 역할을 한다. 각 원격 자원은 분리되어 스레드 풀에 할당된다. 한 서비스가 느리게 반응한다면 해당 서비스 호출을 위한 스레드 풀은 포화되어 요청을 처리하지 못하겠지만 다른 스레드 풀에 할당된 다른 서비스 호출은 포화되지 않는다.

5.2 클라이언트 회복성이 중요한 이유

애플리케이션은 상호 연결된 의존성 그래프로, 의존성 사이의 원격 호출을 관리하지 않는다면 제대로 동작하지 않는 원격 자원 하나가 그래프의 모든 서비스를 다운시킬 수 있다.

회로 차단기 패턴이 분산 자원을 호출하는 모든 곳에 구현되었다면 이 시나리오를 피할 수 있다. 클라이언트 서비스는 서버 서비스를 호출할 때 직접 호출하지 않고 회로 차단기에 호출을 위임한다. 회로 차단기는 그 호출을 스레드(대개 스레드 풀에서 관리하는)로 감싼다. 호출을 스레드로 감싸면 클라이언트는 호출 완료를 직접 기다리지 않는 대신 회로 차단기가 스레드를 모니터링하고 스레드가 너무 오래 수행되면 호출을 중단한다. 클라이언트 서비스가 호출할 때 에러를 받지만 서버 서비스 호출이 완료될 때까지 자원이 점유되지 않는다. 호출이 타임아웃 되었다면 회로 차단기는 발생한 실패 횟수를 추적하기 시작한다. 특정 시간동안 특정 서비스 에러가 기대 이상 발생하면 회로 차단기는 회로를 차단하기 때문에 서버 서비스를 호출하지 않아도 서버 서비스에 대한 모든 호출이 실패한다.

  1. 클라이언트 서비스는 현재 회로 차단기가 타임아웃하기 전에 문제가 있다는 것을 즉시 안다(질문: 타임아웃 돼야 문제 아는 거 아닌가?).
  2. 클라이언트 서비스는 완전히 실패하거나 대체 코드를 (폴백) 사용하는 조치를 취하는 것 중에서 선택할 수 있다.
  3. 회로 차단기가 차단된 동안 클라이언트 서비스가 서버 서비스를 호출하지 못하므로 서버 서비스에 복구할 수 있는 여유가 생긴다. 이 기회를 이용해 서버 서비스는 숨돌릴 틈을 갖고 서비스 저하로 발생되는 연쇄 장애를 막을 수 있다.
  4. 회로 차단기는 저하된 서비스를 간헐적으로 호출하고, 호출이 연속적으로 충분히 성공하면 회로 차단기를 재설정한다.

회로 차단 패턴이 제공하는 핵심 기능은 다음과 같다.

  1. 빠른 실패: 원격 서비스가 저하를 겪으면 애플리케이션은 빨리 실패함으로써 애플리케이션 전체를 다운시킬 수 있는 자원 고갈 이슈를 방지한다.
  2. 원만한 실패: 타임아웃과 빠른 실패 방법을 사용하는 회로 차단기 패턴으로 애플리케이션 개발자는 원만하게 실패하거나 사용 의도로 수행되는 대체 메커니즘을 찾을 수 있다.
  3. 원활한 회복: 회로 차단기는 중개자로서 요청 자원이 온라인 상태인지 주기적으로 확인하고, 사람의 개입 없이 자원 접근을 다시 허용할 수 있다.

5.3 히스트릭스 시작 및 구현

회로차단기, 폴백, 벌크헤드 패턴을 구현하려면 스레드와 스레드 관리에 대한 조예가 깊어야 한다. 스레드 코드를 견고하게 작성하는 것은 매우 어렵고, 회로 차단기와 폴백, 벌크헤드 패턴을 고품질로 개발하려면 엄청나게 많은 양의 작업이 필요하다. 스프링 클라우드와 넷플릭스 히스트릭스로 쉽게 구현할 수 있다.

히스트릭스 구현 방법을 두 가지 범주로 살펴본다. 첫 번째는 서비스 각각 자기 데이터베이스에 대한 호출을 히스트릭스 회로 차단기에 연결하고, 두 번째는 두 서비스 사이의 호출을 히스트릭스에 연결하는 것이다.

동기 호출 방식에서 서비스는 (자기) 데이터를 검색할 때, 다음 처리를 계속하기 전에 SQL 문이 완료되거나 회로 차단기가 타임아웃될 때까지 대기한다.

히스트릭스와 스프링 클라우드는 @HystrixCommand 애너테이션을 사용해 히스트릭스 회로 차단기가 관리하는 자바 클래스 메서드라고 표시한다. 스프링 프레임워크가 @HystrixCommand를 만나면 메서드를 감싸는 프록시를 동적으로 생성하고 원격 호출을 처리하기 위해 확보한 스레드가 있는 스레드 풀로 해당 메서드에 대한 모든 호출을 관리한다.

@HystrixCommand 애너테이션을 사용하면 메서드가 호출될 때마다 히스트릭스 회로 차단기와 해당 호출이 연결된다. 회로 차단기는 메서드 호출이 1,000밀리초보다 오래 걸릴 때마다 호출을 중단한다.

@HystrixCommand는 구현하기 쉽지만 애너테이션의 설정 없이 기본 @HystrixCommand를 사용하는 것은 주의해야 한다. 기본적으로 프로퍼티 없이 @HystrixCommand 애너테이션을 지정하면 애너테이션은 모두 원격 서비스 호출에 동일한 스레드 풀을 사용하므로 애플리케이션에서 문제를 일으킬 수 있다. 벌크 헤드 패턴을 구현할 때 원격 서비스 호출을 자체 스레드 풀로 분리하는 방법과 스레드 풀을 독립적으로 동작시키는 구성 방법을 공부하자.

분산 환경에서 개발 팀이 자기들의 서비스에서 평균 5~6초가 소요되므로 원격 서비스 호출에 1초 타임아웃은 너무 낮다고 생각될 수 있다. 그런데 이것은 대개 서비스가 호출될 때 해결하지 못한 성능 문제가 있음을 나타낸다. 느리게 실행되는 서비스 호출 문제에 대해 절대로 해결할 수 없는 경우가 안이라면, 히스트릭스 호출의 기본 타임아웃 시간을 늘리고 싶은 유혹을 이겨내야 한다. 일부 서비스 호출이 다른 서비스 호출보다 느려지는 상황이 발생하면 서비스 호출을 별도 스레드 풀로 분리하는 것을 꼭 고려한다.

5.4 폴백 프로세싱

회로 차단기 패턴의 장점은 원격 자원의 소비자와 리소스 사이에 중간자를 두어 개발자에게 서비스 실패를 가로채고 다른 대안을 선택할 기회를 준다는 것이다. 이것은 히스트릭스에서 폴백 전략이라고 알려져 있고, 구현도 쉽게 할 수 있다.

@HystrixCommand가 보호하는 메서드에 전달되는 모든 매개변수를 폴백이 받으므로 폴백 메서드는 이전 메서드와 서식이 완전히 동일해야 한다.

폴백 전략은 마이크로서비스가 데이터를 검색한 호출이 실패하는 상황에 적합하다. 필자는 고객 정보와 고객 정보 요약본을 다른 저장소에 저장하고, 고객 정보 데이터베이스가 성능 문제나 에러를 겪으면 고객 정보 요약본 저장소를 검색했다고 한다. 폴백 전략 사용 여부를 결정할 때 중요한 것은 고객 데이터 수명의 허용 정도와 문제가 있는 애플리케이션을 감추는 것이 얼마나 중요한지에 달려있다. 필자의 경험에서는 비즈니스 팀이 고객이 에러를 보거나 애플리케이션이 완전히 다운되는 것보다 이전 고객 데이터를 제공하는 것이 더 낫다고 판단했다고 한다.

폴백 전략의 구현 여부를 결정할 때 다음을 염두해야 한다.

  1. 폴백은 자원이 타임아웃되거나 실패할 때 행동 방침을 제공하는 메커니즘이다.
  2. 폴백 기능으로 수행하는 행동을 알고 있어야 한다. 폴백 서비스에서 다른 분산 서비스를 호출한다면 @HystrixCommand 애너테이션으로 폴백을 감싸야 할 수 있다. 1차 폴백 행동 방침을 겪게 한 동일한 장애가 2차 폴백 옵션에도 영향을 줄 수 있다는 것을 기억하자. 방어적으로 코딩해야 한다.

5.5 벌크헤드 패턴

벌크헤드 패턴을 적용하지 않는다면 기본적인 호출 행위는 전체 자바 컨테이너에 대한 요청을 처리하는 스레드에서 이뤄진다. 대규모 상황에서 한 서비스에서 발생한 성능 문제로 자바 컨테이너의 모든 스레드가 최대치에 도달할 작업 처리를 대기하고, 새로운 요청들은 적체된다. 결국 자바 컨테이너는 비정상 종료한다. 벌크헤드 패턴은 원격 자원 호출을 자신의 스레드 풀에 격리하므로 오작동 서비스를 억제하고 컨테이너의 비정상 종료를 방지한다.

히스트릭스는 스레드 풀을 사용해 원격 서비스에 대한 모든 요청을 위임한다. 기본적으로 모든 히스트릭스 명령은 요청을 처리하기 위해 동일한 스레드 풀을 공유한다. 이 스레드 풀에는 원격 서비스 호출을 처리할 10개의 스레드가 있고, 원격 서비스 호출은 REST 서비스 호출, 데이터베이스 호출 등 무엇이든 될 수 있다.

여러 유형의 자원을 공유하는 기본적인 히스트릭스 스레드 풀 모델은 애플리케이션 안에서 액세스하는 원격 자원이 적고 비교적 균등하게 각 서비스를 호출할 때 효과적으로 동작한다. 문제는 다른 서비스보다 훨씬 호출량이 많고 호출을 완료하는 데 오래 걸리는 서비스가 히스트릭스의 기본 스레드 풀에 있는 모든 스레드를 차지하므로 결국 모든 스레드가 고갈된다는 것이다.

다행히 히스트릭스는 서로 다른 원격 자원 호출 간에 벌크헤드를 생성하기에 용이한 메커니즘을 제공한다. 각 원격 자원 호출은 자기 스레드 풀을 이용한다. 성능이 나쁜 서비스가 동일 스레드 풀 안에 있는 서비스 호출에만 영향을 미치므로 호출에서 발생할 수 있는 피해가 제한된다.

5.6 히스트릭스가 해주는 일과 결정 과정

히스트릭스는 오래 수행되는 호출을 타임아웃하는 것보다 더 많은 일을 한다. 호출 실패 횟수를 모니터링해 호출이 필요 이상으로 실패할 때 원격 자원에 도달하기 전에 호출을 실패시켜 서비스로 들어오는 이후 호출을 자동으로 차단한다.

이렇게 하는 이유는 두 가지다. 첫째, 원격 자원에 성능 문제가 있는 경우 빨리 실패하게 되면 호출 애플리케이션이 호출 타임아웃을 기다리는 것을 막아, 호출 애플리케이션이나 서비스의 자원 고갈 문제와 비정상 종료될 위험을 크게 낮춘다. 둘째, 서비스 클라이언트의 빠른 실패로 호출을 막으면 힘겨워하는 서비스가 부하를 견디고 부하 때문에 완전히 비정상 종료되지 않는다. 빨리 실패하게 되면 시스템은 회복할 수 있는 성능 저하 시간을 얻는다.

원격 자원 호출이 실패할 때 히스트릭스의 결정 과정

  1. 문제 발생
  2. 최소 여청 수가 실패했는가?
  3. 에러 임계치에 도달했는가? (도달했으면 회로 차단기 차단)
  4. 원격 서비스 호출에 여전히 문제가 발생하는가?

히스트릭스 명령이 서비스 에러를 포착하면 서비스 호출이 실패 빈도 검사용 10초(설정 가능) 타이머를 시작한다. 히스트릭스가 처음 하는 일은 매 10초 동안 호출 횟수를 확인하는 것이다. 그 시간대에 발생한 호출 횟수가 최소 호출 횟수 이하라면 히스트릭스는 호출이 다소 실패하더라도 아무런 조치를 취하지 않는다. 10초 시간대 동안 원격 자원 호출이 최소 호출 횟수를 넘으면 히스트릭스는 전체 실패 비율을 조사하기 시작한다. 전체 실패 비율이 임계치(기본 50%)를 초과하면 히스트릭스는 회로를 차단하고 거의 모든 호출이 실패한다. 비율을 초과하면 히스트릭스는 회로를 차단하고 원격 자원에 대한 추가 호출을 막는다. 원격 호출 비율에 도달하지 않고 10초 시간대가 지나면 히스트릭스는 회로 차단기 통계를 초기화한다. 히스트릭스가 원격 호출에 대해 회로 차단기를 차단하면 새로운 활동 시간대를 시작한다. 5초(설정 가능) 마다 히스트릭스는 고전하는 서비스에 호출을 허용한다. 호출이 실패하면 히스트릭스는 차단된 상태를 유지하고 5초 후에 재시도한다.

히스트릭스의 구성 재검토

히스트릭스 환경을 구성할 때는 히스트릭스의 세 가지 구성 레벨을 기억해야 한다.

  1. 애플리케이션 기본값
  2. 클래스 기본값
  3. 클래스 안에서 정의된 스레드 풀 레벨

5.7 스레드 컨텍스트와 히스트릭스

THREAD와 SEMAPHORE라는 두 격리 전략을 수행할 수 있고, THREAD가 기본 전략이다. 호출을 보호하는 데 사용된 모든 히스트릭스 명령은 호출을 시도한 부모 스레드와 컨텍스트르 ㄹ 공유하지 않는 격리된 스레드 풀에서 수행된다. 이는 히스트릭스가 자기 통제 하에서 원래 호출을 시도한 부모 스레드와 연관된 어떤 활동도 방해하지 않고 스레드 실행을 중단할 수 있다는 것을 의미한다.

기본적으로 히스트릭스는 부모 스레드의 컨텍스트를 히스트릭스 명령이 관리하는 스레드에 전파하지 않는다. 예를 들어 부모 스레드에서 ThreadLocal로 설정된 값은 기본적으로 부모 스레드가 호출하는 메서드에서 사용할 수 없고 @HystrixCommand 객체로 보호된다.

6. 스프링 클라우드와 주울로 서비스 라우팅

여러 서비스 호출 사이에서 발생하는 보안과 로깅, 사용자 추적 등 주요 행위를 확인해야할 때 각 개발 팀은 자체 솔루션을 구축하지 않고 이러한 속성이 모든 서비스에 일관되게 적용되게 하고 싶을 것이다. 공통 라이브러리나 프레임워크를 사용해 서비스마다 이런 기능을 직접 구축할 수 있지만, 다음 세 가지를 염두에 두어야 한다.

첫째, 구축 중인 각 서비스에 이러한 기능을 일관되게 구현하기 어렵다. 개발자는 제품 기능을 전달하는 데 매달리고 정신없이 바쁜 일상 때문에 서비스 로깅과 추적 기능 구현을 잊기 쉽다.

둘째, 이러한 기능을 적절하게 구현하기 어렵다. 마이크로서비스에 대한 보안 기능들을 구현 중인 각 서비스에 설정하고 구성하기 어려울 수 있다. 보안과 같은 횡단 관심사를 각 개발 팀이 담당하면 제대로 구현되지 않거나 누락할 가능성이 높다.

셋째, 서비스 간 복잡한 의존성을 만든다. 전체 서비스가 공유하는 공통 프레임워크에 더 많은 기능을 추가할수록 서비스의 재컴파일과 재배포 없이 공통 코드의 동작 변경이나 추가 하는 일이 어려워진다. 서비스가 6개 정도 있다면 대수롭지 않겠지만 30개 이상이라면 큰일이 될 것이다. 공유 라이브러리의 핵심 기능 업그레이드가 갑자기 수개월이 걸리는 마이그레이션 과정이 될 수 있다.

이 문제를 해결하려면 특정 서비스에서 이런 횡단 관심사들을 추상화하고 독립적인 위치에서 애플리케이션의 모든 마이크로서비스 호출에 대한 필터와 라우터 역할을 해야 한다. 이런 횡단 관심사를 서비스 게이트웨이라고 한다. 서비스 클라이언트가 서비스를 직접 호출하지 않고 단일한 정책 시행 지점 역할을 하는 서비스 게이트웨이로 모든 호출을 경유시켜 최종 목적지로 라우팅한다.

6.1 서비스 게이트웨이란?

서비스 게이트웨이는 서비스 클라이언트와 호출될 서비스 사이에서 중개 역할을 한다. 서비스 클라이언트는 서비스 게이트웨이가 관리하는 하나의 URL을 통해 통신한다. 서비스 게이트웨이는 서비스 클라이언트 호출에서 보낸 경로를 추려내고 서비스 클라이언트가 호출하려는 서비스를 판별한다. 서비스 게이트웨이는 사용자를 대상 마이크로서비스와 해당 인스턴스로 안내한다. 서비스 게이트웨이는 애플리케이션 안의 마이크로서비스 호출로 유입되는 모든 트래픽에 대해 게이트키퍼 역할을 한다.

서비스 게이트웨이는 클라이언트가 각 서비스에 보내는 모든 호출 사이에 위치하므로 게이트웨이는 서비스 호출에 대한 중앙 집중식 정책 시행 지점 역할도 한다. 중앙 집중식 PEP를 사용한다는 것은 각 개발 팀이 이러한 관심사를 구현하지 않고도 서비스의 횡단 관심사를 단일 지점에서 구현할 수 있다는 것을 의미한다. 서비스 게이트웨이에서 구현할 수 있는 횡단 관심사 예는 다음과 같다.

  1. 정적 라우팅: 서비스 게이트웨이는 단일 서비스 URL과 API 경로로 모든 서비스를 호출하게 한다. 개발자는 모든 서비스에 대해 하나의 서비스 엔드포인트만 알면 되므로 개발이 간단해진다.
  2. 동적 라우팅: 서비스 게이트유ㅞ이는 유입되는 서비스 요청을 조사하고 요청 데이터를 기반으로 서비스 호출자 대상에 따라 지능형 라우팅을 수행할 수 있다. 예를 들어 베타 프로그램에 참여하는 고객의 서비스 호출은 모두 다른 코드 버전이 수행되는 ㅌㄱ정 서비스 클러스터로 라우팅될 수 있다.
  3. 인증과 인가: 모든 서비스 호출은 서비스 게이트웨이로 라우팅되므로 서비스 게이트웨이는 서비스 호출자가 자신을 인증하고 서비스를 호출할 권한 여부를 확인할 수 있는 최적의 장소다.
  4. 측정 지표 수집과 로깅: 서비스 게이트웨이를 사용하면 서비스 호출이 서비스 게이트웨이를 통과할 때 측정 지표와 로그 정보를 수집할 수 있다. 규격화된 로깅을 보장하기 위해 사용자 요청에서 주요 정보가 누락되지 않았는지 확인하는 데도 사용된다. 이는 각 서비스에서 측저ㅗㅇ 지표를 수집할 필요가 없다는 것이 아니라 서비스 게이트웨이를 사용하면 서비스가 호출된 횟수와 응답 시간처럼 많ㅇ느 기본 측정 지표를 한곳에서 수집할 수 있다는 의미다.

서비스 게이트웨이가 단일 장애 지점이나 잠재적 병목 지점은 아닐까?

집중화된 로드 밸런서가 단일 장애 지점과 서비스의 병목점이 될 가능성이 있다고 얘기했다. 서비스 게이트웨이를 올바르게 구현하지 않는다면 동일한 위험 부담이 있다. 다음을 염두하자.

로드 밸런서는 각 서비스 그룹 앞에 있을 때 여전히 유용하다. 이때 여러 서비스 게이트웨이 인스턴스 앞에 로드 밸런서를 두는 것은 적절한 설계이고, 서비스 게이트웨이를 확장할 수 있다. 모든 서비스 인스턴스 앞에 로드 밸런서를 두는 것은 병목점이 될 수 있어 좋은 생각은 아니다.

작성하는 서비스 게이트웨이 코드를 무상태로 유지하자. 서비스 게이트웨이의 정보를 메모리에 저장하지 말자. 주의하지 않으면 게이트웨이의 확장성을 제한하고 모든 서비스 게이트웨이 인스턴스에 데이터가 복제되도록 해야 한다.

작성하는 서비스 게이트웨이 코드를 가볍게 유지하자. 서비스 게이트웨이는 서비스 호출에 대한 병목점이다. 여러 데이터베이스 호출이 포함된 복잡한 코드는 서비스 게이트웨이에서 추적하기 힘든 성능 문제의 원인이 된다.

6.2 스프링 클라우드와 넷플릭스 주울 소개 및 경로 구성

주울은 본래 리버스 프록시다. 주울이 하위 클라이언트와 통신하려면 유입되는 호출을 어떻게 하위 경로로 매핑할지 알아야 한다. 다음 케머니즘을 제공한다.

  1. 서비스 디스커버리를 이용한 자동 경로 매핑
  2. 서비스 디스커버리를 이용한 수동 경로 매핑
  3. 정적 URL을 이용한 수동 경로 매핑

주울은 application.yml에 경로를 정의해서 모든 경로를 매핑한다. 또 특별한 구성 없이도 서비스 ID를 기반으로 요청을 라우팅한다. 경로를 지정하지 않으면 호출되는 서비스의 유레카 서비스 ID를 사용해 하위 서비스 인스턴스에 매핑한다.

유레카와 함께 주울을 사용하면 호출할 수 있는 단일 엔드포인트를 제공할 뿐만 아니라 주울 수정 없이도 인스턴스를 추가하고 제거할 수 있는 장점이 있다. 예를 들어 유레카에 새로운 서비스를 추가하면 주울은 자동으로 이 서비스 인스턴스에 라우팅하는데, 서비스 엔드포인트의 실제 물리적 위치를 유레카와 소통하고 있어 가능하다.

6.2.1 정적 URL을 이용한 수동 경로 매핑

유레카로 관리하지 않는 서비스를 라우팅하는 데 주울을 사용할 수 있다. 고정 URL에 직접 라우팅하도록 설정함으로써 말이다.

경로를 정적으로 매핑하고 리본에서 유레카를 비활성화할 때 문제는 주울 서비스 게이트웨이로 실행되는 모든 서비스에 대해 리본을 지원할 수 없다는 것이다. 즉 주울이 서비스 검색 결과를 캐시하는데 리본을 사용할 수 없으므로 유레카 서버가 더 많은 부하를 받게 된다는 것을 의미한다. 리본은 호출할 때마다 유레카를 호출하는 것이 아니다. 그 대신 서비스 인스턴스 위치를 로컬에 캐시한 후 유레카에 변경 사항을 정기적으로 확인한다. 리본을 사용하지 않는다면 주울은 서비스 위치를 확인할 필요가 있을 때마다 유레카를 호출할 것이다.

JVM 기반이 아닌 애플리케이션은 이러한 경로를 처리할 별도의 주울 서버를 설정할 수 있다. 하지만 스프링 클라우드의 사이드카 인스턴스를 설정하면 더 낫다. 스프링 클라우드 사이드카를 사용하면 JVM이 아닌 서비스를 유레카 인스턴스에 등록한 후 주울로 프록시할 수 있다.

6.2.2 경로 구성을 동적으로 로딩

경로를 동적으로 재로딩하면 주울 서버를 재활용하지 않고도 경로 매핑을 변경할 수 있어 유용하다. 기존 경로를 신속하게 수정하고 새로운 경로를 추가하면 환경 내 각 주울 서버를 재활용해야 한다. 3장에서 스프링 클라우드 컨피그 서비스를 사용해 마이크로서비스 구성 데이터를 외부화하는 방법을 설명했는데, 같은 방법으로 주울 경로를 외부화할 수 있다.

6.2.3 주울과 서비스 타임아웃

주울은 넷플릭스의 히스트릭스와 리본 라이브러리를 사용해 오래 수행되는 서비스 호출이 서비스 게이트웨이의 성능에 영향을 미치지 않도록 한다. 기본적으로 주울은 요청을 처리하는 데 1초 이상 걸리는 모든 호출을 종료하고 HTTP 500 에러를 반환한다. 주울 서버 구성에서 히스트릭스 타임아웃 프로퍼티를 설정해 이런 동작을 구성할 수 있다.

6.3 주울의 진정한 힘! 필터

주울 게이트웨이로 유입되는 모든 요청을 프록시해서 서비스 호출을 단순화할 수 있지만, 주울의 진정한 능력은 게이트웨이를 통과하는 모든 서비스 호출에 대해 사용자 정의 로직을 작성할 때 드러난다. 이러한 사용자 정의 로직 대부분은 모든 서비스에 대한 보안과 로깅 및 추적처럼 일관된 애플리케이션 정책을 시행하는 데 사용된다.

이러한 애플리케이션 정책을 구현하기 위해 애플리케이션의 각 서비스를 수정하지 않고 모든 서비스에 적용하기 원하기 때문에 이 정책들을 횡단 관심사로 간주한다. 마찬가지로 주울 필터는 J2EE 서블릿 필터나 스프링 애스펙트와 유사한 방식으로 사용되어 다양한 동작을 가로채고, 처음 작성자 모르게 호출의 행동 양식을 꾸미거나 변경할 수 있다. 서블릿 핉너나 스프링 애스펙트는 특정 서비스에 국한되지만 주울과 주울 필터를 사용하면 주울로 라우팅되는 모든 서비스에 대해 횡단 관심사를 구현할 수 있다.

주울은 다음 세 가지 필터 타입을 지원한다.

  1. 사전 필터: 주울에서 목표 대상에 대한 실제 요청이 발생하기 전에 호출된다. 일반적으로 사전 필터는 서비스의 일관된 메시지 형식(예를 들어 주요 HTTP 헤더의 포함 여부)을 확인하는 작업을 수행하거나 서비스를 이용하는 사용자가 인증 및 인가 되었는지 확인하는 게이트키퍼 역할을 한다.
  2. 사후 필터: 대상 서비스를 호출하고 응답을 클라이언트로 전송한 후 호출된다. 일반적으로 사후 필터는 대상 서비스의 응답을 로깅하거나 에러 처리, 민감한 정보에 대한 응답을 감시하는 목적으로 구현된다.
  3. 경로 필터: 대상 서비스가 호출되기 전에 호출을 가로채는 데 사용된다. 일반적으로 경로 필터는 일정 수준의 동적 라우팅 필요 여부를 결정하는 데 사용된다. 예를 들어 동일 서비스의 다른 두 버전을 라우팅할 수 있는 경로 단위 필터를 사용해 작은 호출 비율만 새 버전의 서비스로 라우팅할 수 있다. 이렇게 하면 모든 사용자가 새로운 서비스를 이용하지 않고도 소수 사용자에게 새로운 기능을 노출할 수 있다.

서비스 클라이언트의 요청을 처리하는 과정에서 사전 및 사후, 경로 필터의 상호작용

  1. 서비스 클라이언트가 주울을 사용해 서비스를 호출
  2. 사전 필터는 요청이 주울에 유입될 때 실행됨
  3. 경로 필터는 사용자를 원하는 곳으로 라우팅하도록 주울의 기본 라우팅 로직을 재정의 할 수 있음
  4. 경로 필터는 주울 외부의 서비스로 동적 라우팅할 수 있음
  5. 주울은 대상 경로를 결정하고 요청을 목표 대상으로 보냄
  6. 대상 서비스가 호출된 후 되돌아온 응답은 주울 사후 필터로 다시 들어감

좀 더 자세히 과정을 살펴보면…

  1. 요청이 주울 게이트웨이에 유입되면 정해진 사전 필터가 호출된다. 사전 필터는 HTTP 요청이 실제 서비스에 도달하기 전에 요청을 검사하고 수정한다. 사전 필터는 사용자를 다른 엔드포인트나 서비스로 향하게 할 수 없다.
  2. 주울은 유입된 요청에 대해 사전 필터를 실행한 후 정해진 경로 필터를 실행한다. 경로 필터는 서비스가 향하는 목적지를 변경할 수 있다.
  3. 경로 필터는 주울 서버가 전송하도록 구성된 경로가 아닌 다른 경로로 서비스 호출을 리다이렉션하는 것도 가능하다. 하지만 주울의 경로 필터는 HTTP 리다이렉션 대신 유입된 HTTP 요청을 종료한 후 원래 호출자를 대신해 그 경로로 호출한다. 이것은 경로 필터가 동적 경로 호출을 완전히 소유해야 하므로 HTTP 리다이렉션할 수 없다는 것을 의미한다.
  4. 경로 필터가 호출자를 새로운 경로로 동적 리다이렉션하지 않는다면 주울 서버는 원래 대상 서비스의 경로로 보낸다.
  5. 대상 서비스가 호출되었다면 주울의 사후 필터가 호출된다. 사후 필터는 호출된 서비스의 응답을 검사하고 수정할 수 있다.

6.4 상관관계 ID를 생성하는 주울의 사전 필터 작성

먼저 TrackingFilter라는 사전 필터를 만들어 게이트웨이로 들어오는 모든 요청을 검사하고, 요청 안에 tmx-correlation-id라는 HTTP 헤더가 있는지 판별한다. tmx-correlation-id 헤더에는 여러 마이크로서비스에 전달되는 사용자 요청을 추적하는 데 사용할 수 있는 고유한 GUID(Globally Unique ID)가 포함된다. HTTP 헤더에 tmx-correlation-id가 없다면 TrackingFilter가 상관관계 ID를 생성하고 설정한다. 상관관계 ID가 이미 포함되어 있다면 주울은 상관관계 ID에 대해 아무 일도 하지 않는다. 상관관계 ID가 있다는 것은 해당 호출이 사용자 요청을 수행하는 일련의 서비스 호출 일부임을 의미한다. 이때 TrackingFilter 클래스는 아무 작업도 수행하지 않는다.

주울에서 필터를 구현하려면 ZuulFilter 클래스를 확장하고 filterType(), filterOrder(), shouldFilter(), run() 등 4개의 메서드를 재정의해야 한다. 앞의 3개 메서드는 구축하려는 필터 타입과 해당 타입의 다른 필터와 비교해 실행해야 하는 순서, 활성화 여부를 주울에서 설정한다. 나머지 run() 메서드에서 필터의 비즈니스 로직을 구현한다.

6.5 서비스 호출에서 상관관계 ID 사용

이제 주울을 관통하는 모든 마이크로서비스 호출에 상관관계 ID 추가가 보장되었으므로 다음 작업이 가능하다.

  1. 호출되는 마이크로서비스에서 상관관계 ID를 손쉽게 접근한다.
  2. 마이크로서비스가 호출하는 하위 서비스 호출에도 상관관계 ID를 전파한다.

다음 과정을 보고 여러 부분을 어떻게 구성하는지 알아보자.

  1. 주울 게이트로 A서비스가 호출되면 TrackingFilter는 주울로 유입되는 모든 호출의 HTTP 헤더에 상관관계 ID를 삽입한다.
  2. UserContextFilter 클래스는 사용자 정의 HTTP ServletFilter이며 상관관계 ID를 UserContext 클래스에 매핑한다. UserContext 클래스는 나중에 호출할 때 사용할 수 있도록 스레드 로컬 저장소에 저장된 값이다.
  3. A 서비스 비즈니스 로직은 B 서비스에 대한 호출을 실행해야 한다.
  4. RestTemplate은 B 서비스를 호출하는 데 사용되고, 사용자 정의된 Spring Interceptor 클래스로 상관관계 ID를 아웃바운드 호출의 HTTP 헤더에 삽입한다.

이걸 다시 살펴보면…

  1. A 서비스가 주울 경로로 호출된다.
  2. UserContextFilter는 HTTP 헤더에서 상관관계 ID를 추출해 UserContext 객체에 저장한다.
  3. 서비스의 비즈니스 로직은 UserContext에서 검색된 모든 값에 액세스할 수 있다.
  4. UserContextInterceptor는 모든 아웃바운드 REST 호출에 UserContext의 상관관계 ID가 포함되어 있는지 확인한다.

코드 중복 vs 공유 라이브러리

사용자 정의 프레임워크가 서비스 간 인위적인 의존성을 가져오기 때문에 서비스 전반에 사용하면 안 된다는 주장과(그 프레임워크의 비즈니스 로직이나 버그를 수정하면 모든 서비스를 광범위하게 리팩토링해야 할 수 있다), 공통 라이브러리를 만들어 서비스 간 공유해야 하는 특정 상황이 존재한다는 주장이 대립한다. 이에 대해 다음 절충안이 있다.

공통 라이브러리는 인프라스트럭처 유형의 작업을 처리할 때는 사용해도 좋다. 하지만 비즈니스 경향의 클래스를 공유하기 시작하면 서비스 사이의 경계를 허물기 때문에 화를 자초하게 된다.

책에서는 쉽게 하기 위해 중복을 허용했지만, utils 패키지 클래스는 모두 서비스 사이에 공유된다.

유입되는 HTTP 요청을 가로채는 UserContextFilter

UserContextFilter는 서비스에 유입되는 모든 HTTP 요청을 가로채서 HTTP 요청에서 상관관계 ID(일부 다른 값도 포함)를 UserContext 클래스에 매핑하는 HTTP 서블릿 필터다.

UserContextFilter는 필요한 HTTP 헤더 값을 자바 클래스인 UserContext에 매핑하는 데 사용한다.

서비스가 쉽게 액세스할 수 있는 HTTP 헤더를 만드는 UserContext

UserContext 클래스는 마이크로서비스가 처리하는 개별 서비스 클라이언트 요청에 대한 HTTP 헤더 값을 저장하는 데 사용된다. 이 클래스는 java.lang.ThreadLocal 값을 검색하고 저장하는 getter/setter 메서드로 구성된다. 예제에서 현재 UserContext 클래스는 HTTP 요청에서 추출한 값을 보관하는 POJO일 뿐이다. UserContextHolder 클래스를 사용해 ThreadLocal 변수에 UserContext를 저장한다. ThreadLocal 변수는 사용자 요청을 처리하는 해당 스레드에서 호출되는 모든 메서드에서 액세스 가능한 변수다.

상관관계 ID의 전파를 보장하는 사용자 정의 RestTemplate과 UserContextInterceptor

UserContextInterceptor는 RestTemplate 인스턴스에서 실행되는 모든 HTTP 기반 서비스 발신 요청에 상관관계 ID를 삽입한다. 이 작업은 서비스 호출 간 연결을 형성하는 데 수행된다.

로그 수집과 인증 등

상관관계 ID가 각 서비스에 전달되므로 호출과 연관된 모든 서비스를 관통하는 트랜잭션을 추적할 수 있다. 이를 위해 각 서비스 로그를 중앙 집중식 로그 지점으로 보내 모든 서비스의 로그 항목을 단일 지점으로 캡처해야 한다. 로그 수집 서비스에서 캡처된 로그 항목에는 각각 상관관계 ID가 있다.

6.6 상관관계 ID를 전달받는 사후 필터 작성

주울은 대상 서비스 호출에 대한 응답을 검사한 후 수정하거나 추가 정보를 삽입할 수 있다. 주울 사후 필터는 사전 필터로 데이터를 캡처하는 것과 연계할 때 측정 지표를 수집하고 사용자 트랜잭션과 연관된 모든 로깅을 완료할 최적의 장소다. 마이크로서비스 사이에 전달된 상관관계 ID를 사용자에게 다시 전달해 이런 이점을 얻을 수 있다.

우리는 주울의 사후 필터를 사용해 서비스 호출자에게 다시 전달될 HTTP 응답 헤더에 상관관계 ID를 삽입해서 이 작업을 수행할 것이다. 이러면 메시지 본문에 손대지 않고 상관관계 ID를 호출자에게 되돌려 줄 수 있다.

6.7 동적 경로 필터 작성

사용자 정의 경로 필터가 없다면 매핑 정의를 기반으로 모든 라우팅을 수행한다. 하지만 주울 경로 필터를 작성하면 서비스 클라이언트의 호출을 라우팅하는 방식에 지능을 더할 수 있다.

SpecialRoutesFilter는 주울이 호출할 서비스에 대한 유레카 서비스 ID를 가져와 다른 마이크로서비스인 SpecialRoutes를 호출한다. SpecialRoutes 서비스는 내부 데이터베이스에서 대상 서비스 이름의 존재 여부를 확인한다. 대상 서비스 이름이 존재하면 가중치와 대체 위치의 목표 대상을 반환한다.

SpecialRoutesFilter는 전달받은 가중치에 따라 대체 서비스로 라우팅할지 여부를 결정하는 데 사용할 난수를 생성한다. 다음은 SpecialRoutesFilter를 사용할 때 발생하는 호출 순서를 보여 준다.

  1. 서비스 클라이언트는 주울로 서비스를 호출한다.
  2. SpecialRoutesFilter는 서비스 ID를 검색한다.
  3. SpecialRoutes 서비스는 새로운 대체 서비스의 엔드포인트 존재 여부와 신, 구 서비스로 보낼 호출 비율을 확인한다.
  4. SpecialRoutesFilter는 난수를 생성하고 라우팅 결정을 위해 가중치를 확인한다.
  5. 새로운 대체 서비스의 엔드포인트로 요청을 라우팅하더라도 주울은 미리 정의된 사후 필터로 응답을 다시 라우팅한다.

즉 서비스 클라이언트가ㅣ 앞 단에 주울이 있는 어떤 서비스를 호출하면 SpecialRoutesFilter는 다음 작업을 수행한다.

  1. SpecialRoutesFilter는 호출 중인 서비스의 ID를 검색한다.
  2. SpecialRoutesFilter는 SpecialRoutes 서비스를 호출한다. SpecailRoutes 서비스는 대상 엔드포인트에 대해 정의된 대체 엔드포인트가 있는지 확인한다. 대체 엔드포인트 레코드가 있다면 레코드로 신, 구 서비스에 보낼 호출 비율(가중치)을 주울에 전달한다.
  3. 그다음 SpecialRoutesFilter는 난수를 생성하고 SpecialRoutes 서비스가 반환한 가중치와 비교한다. 난수가 대체 엔드포인트 가중치보다 작다면 SpecialRoutesFilter는 신규 버전의 서비스로 요청을 보낸다.
  4. SpecialRoutesFilter가 새로운 서비스로 요청을 보내면 주울은 미리 정의된 원래 파이프라인을 유지하고 정해진 사후 필터로 대체 서비스 엔드포인트에서 받은 응답을 되돌려 보낸다.

7. 마이크로서비스의 보안

안전한 애플리케이션은 다음과 같은 여러 보호 계층을 포함한다.

  1. 사용자를 적절히 통제해 사용자 본인 여부르 ㄹ 확인하고 수행하려는 작업에 대한 권한 여부를 검증할 수 있다.
  2. 서비스가 실행되는 인프라스트럭처를 꾸준히 패치하고 최신 상태로 유지해 취약점의 위험을 최소화한다.
  3. 서비스는 명확히 정의된 포트로만 접근하고 소수의 인가된 서버만 접근할 수 있도록 네트워크 접근을 통제한다.

여기에서는 첫 항목인 마이크로서비스를 호출하는 사용자가 본인인지 인증하는 방법과 특정 마이크로서비스에서 요청한 작업을 수행할 수 있는 권한을 부여받았는지 확인하는 방법을 다룬다.

OAuth2는 토큰 기반으로 사용자가 제 3자 서비스에서 자신을 인증할 수 있다. 사용자는 인증에 성공하면 요청을 보낼 때마다 제시할 토큰을 전달받는다. 그런 다음 인증 서비스에서 토큰의 유효성을 확인할 수 있다. OAuth2의 주요 목표는 사용자 요청을 수행하기 위해 여러 서비스를 호출할 때 이 요청을 처리할 서비스에 일일이 자격 증명을 제시하지 않고도 사용자를 인증하는 것이다. OAuth2의 진정한 힘은 애플리케이션 개발자가 쉽게 외부 클라우드 공급자와 통합할 수 있고, 사용자의 자격 증명을 외부업체의 서비스에 계속 전달하지 않고도 그 서비스에서 사용자 인증과 인가를 수행할 수 있다는 것이다.

7.1 OAuth2 소개

OAuth2는 다음 4개의 컴포넌트로 구성된다.

  1. 보호 자원: 보호하려는 자원(여기서는 마이크로서비스)이며 적절한 권한을 부여받은 인증된 사용자만 액세스할 수 있다.
  2. 자원 소유자: 서비스를 호출할 수 있는 애플리케이션 및 서비스에 접근할 수 있는 사용자, 그리고 서비스에서 수행할 수 있는 작업을 정의한다. 자원 소유자가 등록한 애플리케이션은 식별 가능한 애플리케이션 이름과 시크릿 키를 받는다. 애플리케이션 이름과 시크릿 키는 OAuth2 토큰을 인증할 때 전달되는 자격 증명의 일부다.
  3. 애플리케이션: 사용자를 대신해 서비스를 호출할 애플리케이션이다. 즉 사용자는 서비스를 직접 호출하지 않는 대신 애플리케이션에 의존해 작업을 수행한다.
  4. OAuth2 인증 서버: 애플리케이션과 소비되는 서비스 사이의 중개자다. OAuth2 서버를 사용하면 애플리케이션이 사용자 대신 호출하는 모든 서비스에 사용자의 자격 증명을 전달하지 않고도 사용자 자신을 인증할 수 있다.

사용자는 자격 증명만 제시하면 된다. 인증에 성공하면 서비스 간 전달되는 인증 토큰을 발급받는다. OAuth2는 토큰 기반의 보안 프레임워크이기 때문에 사용자는 자원에 접근하려는 애플리케이션을 통해 자격 증명을 제시하고 OAuth2 서버에서 인증한다. 사용자의 자격 증명이 유효하면 OAuth2 서버는 사용자 애플리케이션이 이용하는 서비스가 보호 자원에 접근하려고 시도할 때마다 제시할 토큰을 제공한다.

보호 자원은 OAuth2 서버에 접속해 토큰 유효성을 확인하고 사용자가 지정한 역할을 조회할 수 있다. 역할은 연관된 사용자를 함께 그룹으로 묶고 사용자 그룹이 액세스할 수 있는 자원을 정의한다.

웹 서비스 보안을 제대로 이해하려면, 서비스를 호출할 대상 (기업 망의 내부 사용자, 외부 사용자), 서비스를 호출하는 방법(기업 망 내부 웹 기반 클라이언트나 모바일 장치, 기업 망 외부의 웹 애플리케이션), 코드에서 수행할 작업을 이해해야 한다. OAuth2를 사용하면 그랜트라는 인증 체계를 이용해 다양한 시나리오에서 REST 기반 서비스를 보호할 수 있다. OAuth2 명세에는 다음 네 가지 그랜트 타입이 있다.

  1. 패스워드
  2. 클라이언트 자격 증명
  3. 인가 코드
  4. 암시적

여기에서는

  1. 단순한 OAuth2 그랜트 타입을 선택해 마이크로서비스가 OAuth2를 사용할 수 있는 방법을 논의한다.
  2. 자바스크립트 웹 토큰을 사용해 더 견고한 OAuth2 솔루션을 제공하고, OAuth2 토큰 정보를 인코딩하는 표준을 수립한다.
  3. 마이크로서비스를 구축할 때 고려해야 할 추가 보안 사항을 살펴본다.

OAuth2 명세, 모든 그랜트 타입 구현에 관심이 있다면 OAuth2 in action을 권장한다.

인증과 인가

인증은 자격 증명을 제공해 자신이 누구인지 증명하는 행위다. 인가는 사요자가 수행하려는 작업의 허용 여부를 결정한다. 예를 들어 사용자 철수는 자신의 ID와 패스워드로 신원을 증명할 수는 있지만 급여 데이터 같은 민감한 데이터는 조회할 권한이 없을지도 모른다. 사용자는 권한을 받기 전에 인증을 받아야 한다.

7.2 OAuth2 액세스 토큰 전파

중요한 것은 ‘한 서비스에서 다른 서비스로 OAuth2 토큰을 어떻게 전달할 것인가?’이다.

인증된 사용자의 OAuth2 토큰 흐름을 살펴보자.

  1. 사용자는 이미 OAuth2 서버에서 인증받고 웹 애플리케이션을 호출한다. 사용자의 OAuth2 액세스 토큰은 사용자 세션에 저장된다. 웹 애플리케이션은 일부 라이선싱 데이터를 조회하기 위해 A 서비스의 REST 엔드포인트를 호출해야 할 것이다. A 서비스 엔드포인트를 호출할 때 웹 애플리케이션은 HTTP ‘Authorization’ 헤더에 OAuth2 액세스 토큰을 추가한다. A 서비스는 게이트웨이 뒤에서만 접근할 수 있다.
  2. 서비스 게이트웨이는 A 서비스를 검색한 후 A 서비스 중 한 서버에 호출을 전달한다. 서비스 게이트웨이는 유입되는 호출의 HTTP ‘Authorization’ 헤더를 복사하고 새로운 엔드포인트에 이 헤더를 전달해야 한다.
  3. A 서비스는 호출을 받는다 A 서비스는 보호 자원이므로 OAuth2 서비스에서 토큰의 유효성을 확인하고 사용자 역할에 적합한 권한이 있는지 확인한다. 이 작업에서 A 서비스는 B 서비스를 호출한다. 호출 과정에서 A 서비스는 사용자의 OAuth2 액세스 토큰을 B 서비스에 전달해야 한다.
  4. B 서비스는 호출을 받으면 HTTP ‘Authorization’ 헤더에서 토큰을 가져와 OAuth2 서비스에 토큰의 유효성을 검증한다.

ㅇl러한 흐름을 구현하려면 세 가지 작업을 해야 한다.

7.3 JWT

JWTY는 다음 특징이 있다.

  1. 작다: Base64로 인코딩되어 URL이나 HTTP 헤더, HTTP POST 매개변수로 쉽게 전달될 수 있다.
  2. 암호로 서명되어 있다: JWT 토큰은 토큰을 발행하는 인증 서버에서 서명된다. 즉 토큰이 조작되지 않았다는 것을 보장할 수 있다.
  3. 자체 완비형이다: JWT 토큰은 암호로 서명되므로 서비스를 수신하는 마이크로서비스는 토큰의 내용물이 유효하다는 것을 보장받을 수 있다. 수신 마이크로서비스가 토큰의 서명 유효성을 검증하고 토큰 내용물(토큰의 만료 시간과 사용자 정보 같은)을 확인할 수 있으므로 토큰 내용물을 검증하기 위해 인증 서비스를 다시 호출할 필요가 없다.
  4. 확장 가능하다: 인증 서비스가 토큰을 생성할 때 토큰이 봉인되기 전에 토큰에 추가 정보를 넣을 수 있다.

7.4 보안 정리

OAuth2는 마이크로서비스 보안이라는 퍼즐의 한 조각일 뿐이다. 실제 운영을 위해 마이크로서비스를 빌드할 때는 다음 지침에 따라 마이크로서비스를 구축해야 한다.

  1. 모든 서비스 통신에 HTTS/SSL을 사용한다.
  2. 모든 서비스 호출은 API 게이트웨이를 통과해야 한다.
  3. 공개 API와 비공개 API 영역을 정한다.
  4. 불필요한 네트워크 포트를 차단해 마이크로서비스의 공격 범위를 제한한다.

8. 스프링 클라우드 스트림을 사용한 이벤트 기반 아키텍처

세계와 상호 작용하는 것이 동기식이거나 선형적이지 않고 제한적인 요청-응답 모델도 아니라 바로 끊임없이 메시지를 주고받는 메시지 기반이라는 것이다. 우리는 종종 진행중인 본래 작업을 중단하고 이러한 메시지에 반응한다.

이벤트 기반 아키텍처의 접근 방식을 사용하면 특정 라이브러리나 서비스에 밀접하게 결합하지 않고 변화에 대응할 수 있는 높은 수준으로 분리된 시스템을 구축할 수 있다. 이벤트 기반 아키텍처가 마이크로서비스와 합쳐지면 애플리케이션이 발송하는 이벤트 스트림을 서비스가 수신하는 것만으로도 새로운 기능을 애플리케이션에 신속하게 추가할 수 있다.

A, B 서비스를 운영 환경에 배포한 후 B 서비스에 있는 정보를 조회하는 A 서비스 호출이 오래걸린다고 가정해보자. 데이터베이스 액세스 비용을 들이지 않고 B 데이터의 읽기를 캐싱할 수 있다면 응답 시간을 크게 향상시킬 수 있을 것이다.

  1. 캐싱된 데이터는 라이선싱 서비스의 모든 인스턴스에 일관성이 있어야 한다: 어떤 인스턴스에 접근하더라도 동일한 조직 데이터 읽기가 보장되어야 하므로 데이터를 A 서비스 안에 로컬 캐싱해서는 안 된다는 것을 의미한다.
  2. A 서비스를 호스팅하는 컨테이너 메모리에 B 데이터를 캐싱해서는 안 된다: 서비스를 호스팅하는 런타임 컨테이너는 종종 크기 제약이 있으며 대양한 액세스 패턴으로 데이터를 액세스한다. 로컬 캐시는 클러스터 내 다른 모든 서비스와 동기화를 보장해야 하므로 복잡성도 증가한다.
  3. 업데이트나 삭제 연산으로 B 레코드가 변경될 때 A 서비스는 B 서비스의 상태 변화를 인식해야 한다.

이런 요구사항을 구현하는 데 첫 번째 방법은 동기식 요청-응답 모델을 사용해 구현하는 것이다. 두 번째 방법은 B 서비스가 자신이 변경되었음을 알리는 비동기 이벤트를 발송하는 것이다.

동기식으로 구현할 때 다음과 같은 문제가 발생할 수 있다.

  1. A 서비스와 B 서비스가 강하게 결합된다.
  2. 결합은 두 서비스 사이에 깨지기 쉬운 성질이 생긴다. 예를 들어 캐시 변경을 무효화하는 A 서비스의 엔드포인트가 변경되면 B 서비스도 따라서 변경되어야 한다.
  3. B 데이터의 변경 사실을 인식하도록 다른 서비스를 호출하는 B 서비스 코드를 수정하지 않고 B 데이터의 소비자를 추가할 수 없기 때문에 이 방식은 유연하지 않다.

상태 전달에 메시지 큐를 사용하면 다음과 같은 이점이 있다.

  1. 느슨한 결합

    HTTP 응답은 강한 의존성을 만든다. 메시징 방식에서는 상태 변화를 전달하는 과정에서 두 서비스가 서로 알지 못하므로 결합되지 않는다.

  2. 내구성

    큐가 존재하므로 서비스 소비자가 다운될 때도 메시지 전달을 보장할 수 있고, B 서비스는 A 서비스가 가용하지 않더라도 메시지를 계속 발행할 수 있다.

  3. 확장성

    메시지가 큐에 저장되므로 메시지 발신자는 메시지 소비자의 응답을 기다릴 필요가 없다. 받는 쪽 입장에서도 소비자 서비스를 더 많이 가동시켜 큐의 메시지를 신속히 처리하는 것은 큰 일이 아니다. 이 확장성이 수평 확장의 한 예다. 큐에서 메시지를 읽어 오는 것과 관련한 전통적인 확장 메커니즘은 메시지 소비자가 한 번에 처리할 수 있는 스레드 개수를 늘리는 것이다. 하지만 이 방식은 결국 메시지 소비자가 사용할 수 있는 CPU 개수에 제한을 받는다. 마이크로서비스 모델은 메시지 소비자의 호스팅 머신을 늘려 확장하기 때문에 이러한 제약이 없다.

  4. 유연성

    메시지 발신자는 누가 메시지를 소비할지 모른다. 즉 원래 발신 서비스에 영향을 주지 않고 새로운 메시지 소비자를 쉽게 추가할 수 있다. 기존 서비스를 건드리지 않고 새로운 기능을 애플리케이션에 추가할 수 있는 것이다.

8.1 메시지 아키텍처의 단점

  1. 메시지 처리의 의미론

    메시지 기반 아키텍처에서는 메시지를 발행하고 소비하는 방법을 아는 것뿐 아니라, 애플리케이션이 메시지의 소비 순서를 기반으로 어떻게 동작할지와 메시지가 순서대로 처리되지 않을 때 어떤 일이 발생할지 이해해야 한다. 이것은 메시징을 사용해 데이터의 상태 전이를 엄격하게 다뤄야 할 때 예외가 발생하거나 순서대로 처리되지 않는 시나리오를 애플리케이션의 설계 단계에서부터 고려해야 한다는 것을 의미한다. ‘메시지가 실패하면 에러 처릴르 재시도할 것인가? 아니면 그냥 실패하게 둘 것인가? 특정 고객 메시지 중 하나가 실패할 경우 해당 고객과 관련된 향후 메시지를 어떻게 처리할 것인가?’ 등은 깊이 생각할 주제다.

  2. 메시지 가시성

    마이크로서비스에서 메시지 사용은 종종 동기식 서비스 호출과 서비스 내 비동기 처리가 합쳐진 것을 의미한다.

  3. 메시지 코레오그래피

    선형적으로 처리되지 않기에 비즈니스 로직을 추론하는 것이 어려워진다. 메시징 기반 애플리케이션의 디버깅은 사용자 트랜잭션의 순서가 바뀌고 다른 시점에 실행될 수 있는 다양한 서비스의 로그를 꼼꼼히 살펴보는 일이다.

8.2 스프링 클라우드 스트림

서비스 클라이언트는 서비스를 호출하고 서비스는 자기 데이터 상태를 변경한다. 이 작업은 서비스의 비즈니스 로직에서 수행된다.

소스는 메시지를 발행하는 서비스의 스프링 코드다.

메시지가 채널로 발행된다.

바인더는 스프링 클라우드 스트림의 프레임워크 코드로 특정 하부 메시징 시스템과 통신한다.

메시지 처리 순서는 서비스가 메시지를 받을 때 변경된다.

싱크는 서비스별 코드로 채널을 수신하고 수신된 메시지를 처리한다.

소스

소스는 발행될 메시지를 표현하는 POJO를 전달받는 스프링 애너테이션 인터페이스다. 소스는 메시지를 받아 직렬화하고 메시지를 채널로 발행한다.

채널

채널은 메시지 생산자와 소비자가 메시지를 발행하거나 소비한 후 메시지를 보관할 큐를 추상화한 것이다. 채널 이름은 항상 대상 큐의 이름과 관련이 있지만 코드에서는 큐 이름을 직접 사용하지 않고 채널 이름을 사용한다. 따라서 채널이 읽거나 쓰는 큐를 전환하려면 애플리케이션 코드가 아닌 구성 정보를 변경한다.

바인더

바인더는 스프링 클라우드 스트림 프레임워크의 일부인 스프링 코드로 특정 메시지 플랫폼과 통싲한다. 바인더를 사용하면 메시지를 발행하고 소비하기 위해 플랫폼마다 별도의 라이브러리와 API를 제공하지 않고도 메시징을 사용할 수 있다.

싱크

싱크는 들어오는 메시지를 위해 채널을 수신 대기하고, 메시지를 다시 POJO로 역직렬화한다.

9. 스프링 클라우드 슬루스와 집킨을 이용한 분산 추적

마이크로서비스는 유연한 만큼 복잡해진다. 기본적으로 분산되어 있어 문제가 발생한 곳에서 디버깅하려는 것은 끔찍한 일이다. 서비스가 분산되었다면 여러 서비스와 물리 머신, 다양한 데이터 저장소 사이에서 하나 이상의 트랜잭션을 추적하고 정확한 상황을 종합하려고 노력해야 한다는 것을 의미한다.

9.1 스프링 클라우드 슬루스와 상관관계 ID

상관관계 ID는 임의로 생성되는 고유한 숫자 또는 문자열이며 트랜잭션을 시작할 때 주입된다. 트랜잭션이 여러 서비스를 거치기 때문에 상관관계 ID는 서비스 간 호출로 전파될 수 있다. 6장에서는 주울 필터를 사용해 유입되는 모든 HTTP 요청을 검사하고 상관관계 ID가 없으면 삽입했다.

상관관계 ID가 있다면 모든 서비스에서 스프링 HTTP 필터를 사용자 정의해 들어오는 변수를 사용자 정의 가능한 UserContext 객체에 매핑한다. UserContext 객체를 사용하면 상관관계 ID를 로그 문에 추가햇는지 확인하며, 수동으로 모든 로그 문에 추가하거나 약간의 작업으로 상관관계 ID를 직접 스프링 MDC, 즉 매핑 진단 컨텍스트에 추가할 수 있다. 또 서비스에서 나가는 호출의 HTTP 헤더에 상관관계 ID를 추가해 서비스의 모든 HTTP 호출이 상관관계 ID를 추가해 서비스의 모든 HTTP 호출이 상관관계 ID를 전파할 수 있게 하는 스프링 인터셉터도 작성했다.