소개
이전 장은 끝났지만 아직 마무리하지 못한 일이 있다. 세 번째 사용자 스토리를 완성하고, 사용자가 게임 진행 상황을 볼 수 있게 하는 작업이다. 이 작업을 아직 끝내지 않은 이유는 더 나은 마이크로서비스 아키텍처를 구축하기 위해서이다. 이를 위해서는 시스템 UI를 새로운 서비스로 뽑아내서 독립적인 부분으로 만들고, 기존의 곱셈, 게임화 서비스와 상호작용을 하도록 만들어야 한다.
UI를 따로 두고 마이크로서비스와 통신한다면 환경은 더욱 복잡해진다. 하지만 이게 마이크로서비스이다! 장점도 있지만 그만큼 복잡해진다.
이번 장에서는 두 개의 서비스를 호스트명과 포트로 호출하는 UI부터 시작한다. 그러면 서비스끼리 강하게 결합되어 인프라를 확장해도 시스템은 확장할 수 없으며 유지 관리가 매우 어려운 시스템이 된다. 물론 이것은 잘못된 접근법이므로 서비스 디스커버리 개념을 도입해서 서비스의 결합도를 낮출 것이다. 그런 다음 어떻게 서비스를 확장해서 고가용성을 확보할지 분석한다. 마지막으로 현재 아키텍처를 평가한 후, 서버 측 로드밸런싱을 할 수 있는 라우팅 서비스(게이트웨이)를 살펴본다.
이러한 목표를 달성하기 위해 다룰 도구는 스프링 클라우드(Spring Cloud)이다. 스프링 클라우드의 유레카(Eureka), 리본(Ribbon), 주울(Zuul), 하이스트릭스(Hystrix)와 스프링 클라우드의 일부인 사이드카(Sidecar)와 페인(Feign)이 어떻게 동작하는지도 살펴보자.
UI를 추출하고 게임화 서비스와 연결하기
곱셈 서비스에서 정적 콘텐츠를 추출한 후, 새로운 웹 서버 컴포넌트(9090 포트)에 넣을 것이다. 자바스크립트 코드는 사용자 인터페이스가 없는 곱셈 및 게임화 마이크로서비스에서 제공하는 REST API와 연결한다. 이런 접근법으로 UI와 마이크로서비스를 독립적으로 변경할 수 있다. 또한 곱셈 마이크로서비스와 별도의 전략으로 UI 서버를 확장 또는 축소할 수 있다.
정적 콘텐츠 옮기기
모든 정적 콘텐츠(HTML, CSS, 자바스크립트)를 옮기는 것은 서비스를 분리하기 위해서이다. 스프링 부트까지는 필요가 없고, 그저 성능 좋고 안정적이며 가능하다면 경량인 웹 서버가 필요하다. 톰캣, Nginx, 제티 등 다양한 웹 서버가 있는데, 그중 자바 위에서 동작하며 리눅스, 윈도우에서 모두 쉽게 설치하고 실행할 수 있는 제티를 사용한다.
먼저 제티를 다운로드하고 설치해야 한다. 제티의 유용한 기능인 제티 베이스(Jetty Base)를 다른 폴더에 생성해 웹 서버와 웹 앱(그리고 웹 앱의 설정)을 따로 분리한다. 이런 접근법은 사용자 서버 구성 계층을 버전 관리할 수 있고, 서버 바이너리 파일과 분리해서 향후 자동화된 배포를 구축하기 쉽기 때문에 좋은 방법이다.
정적 콘텐츠를 저장할 새로운 JETTY_BASE(mallang-ui 폴더)를 만든다.
최종 파일 구조는 다음과 같다.
- mallang-ui
- start.d
- deploy.ini
- http.ini
- webapps
- ui
- index.html
- multiplication-client.js
- styles.css
- ui
- start.d
FROM jetty:9.4-jre11
COPY ./start.d/deploy.ini /var/lib/jetty/start.d/deploy.ini
COPY ./start.d/http.ini /var/lib/jetty/start.d/http.ini
COPY ./webapps /var/lib/jetty/webapps
EXPOSE 9090
ENV JETTY_BASE /var/lib/jetty
CMD ["java", "-jar", "/usr/local/jetty/start.jar"]
docker build -t mallang-jetty .
docker run -d -p 9090:9090 --name mallang-jetty mallang-jetty
docker exec -it mallang-jetty /bin/bash
UI와 게임화 서비스 연결
gamification-client.js
데이터를 조회하고 테이블을 수정하기 위해 게임화 마이크로서비스(8081 포트로 실행되는 서비스)로 GET 요청을 하는 두서너 개의 함수를 만든다. 또한 리더보드를 새로고침하는 버튼(refresh-leaderboard)을 만든다. 그리고 버튼에 클릭 리스너를 붙인다.
gamification-client.js는 http://localhost:8081/...과 같은 URL을 사용하고, multiplication-client.js는 http://localhost:8080/...을 사용한다. URL을 하드코딩하고 호스트 주소와 포트로 특정 서비스를 지정하는 것은 시간에 따라 바뀔 수 있다. 마이크로서비스를 사용하면 호스트, 포트, URL 컨텍스트(예를 들면 /results) 등을 바꿔야 하기 때문에 이런 접근 법을 사용하면 안 된다. 게다가 특정 포트를 사용하면 시스템을 투명하게 확장할 수 없다. 곱셈 서비스의 추가 인스턴스가 필요할 경우 이를 감지하고 로드 밸런싱할 코드를 웹 클라이언트에 구현해야 한다.
다행히 이런 위험한 방식에는 해결책이 있다! 다음 절에서 서비스 디스커버리와 게이트웨이 서비스를 사용하는 더 나은 대안을 살펴보자. 하지만 그 전에 우리가 만든 마이크로서비스에 새로운 아키텍처를 적용하고 웹 클라이언트를 새롭게 바꿔보도록 하자.
function updateLeaderBoard() {
$.ajax({
url: "http://localhost:8081/leaders"
}).then(function (data) {
$('#leaderboard-body').empty();
data.forEach(function (row) {
$('#leaderboard-body').append('<tr><td>' + row.userId + '</td>' +
'<td>' + row.totalScore + '</td>');
});
});
}
function updateStats(userId) {
$.ajax({
url: "http://localhost:8081/stats?userId=" + userId,
success: function (data) {
$('#stats-div').show();
$('#stats-user-id').empty().append(userId);
$('#stats-score').empty().append(data.score);
$('#stats-badges').empty().append(data.badges.join());
},
error: function (data) {
$('#stats-div').show();
$('#stats-user-id').empty().append(userId);
$('#stats-score').empty().append(0);
$('#stats-badges').empty();
}
});
}
$(document).ready(function () {
updateLeaderBoard();
$("#refresh-leaderboard").click(function (event) {
updateLeaderBoard();
});
});
기존 서비스 수정
UI 서비스를 새로운 프로젝트로 분리하는 것이 곱셈 서비스와 게임화 서비스에 별 영향을 미치지 않는 것처럼 보여도 사실은 그렇지 않다. 이제 정적 콘텐츠를 백엔드 서비스와 다른 곳(localhost:9090)에서 제공한다. 그리고 포트 번호도 동일하지 않다(8080과 8081). 스프링 시큐리티(Spring Security)는 기본적으로 Same-Origin 정책을 사용하기 때문에, 백엔드를 수정하지 않으면 몇 가지 문제가 있다. 여기서는 포트 번호에 대한 문제지만 호스트명을 사용할 경우에도 문제가 생길 수 있다.
이 문제를 해결하려면 다른 오리진에서 오는 요청을 허용하기 위해 백엔드 서비스에서 CORS(Cross-Origin Resource Sharing)를 활성화해야 한다. 그래서 두 서비스에 스프링 설정을 추가한다.
CORS :: Spring Framework
Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section describes how to do so.
docs.spring.io
package me.progfrog.mallang.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}
단순하게 하기 위해 모든 오리진에 대해 CORS를 활성화하고 모든 요청을 매핑한다(/** 패턴 사용). 시스템이 성숙해지고 인프라가 구축되면 몇 가지 설정 값으로 특정 도메인만 허용할 수 있다.
(거의) 노력 없이 새롭고 더 나은 UI 만들기
부트스트랩으로 디자인을 수정한다. 페이지 레이아웃을 두 가지 영역으로 나눈다.
- 왼쪽은 곱셈 답안 폼 영역이다. 그리고 사용자 통계 정보인 총점과 배지(게임화 서비스에서 가져온 내용)를 보여준다.
- 오른쪽은 상위 유저의 리더보드(게임화 서비스에서 가져온 내용) 영역이다. 그리고 그 아래 방금 플레이한 사용자의 최근 답안(곱셈 서비스에서 제공하는 내용)을 표로 보여준다.
그 밖에도 multiplication-client.js를 수정해 새로운 기능을 추가한다. 주요 변경사항은 다음과 같다.
- 페이지가 로드되면 최근 결과(updateResults(())뿐 아니라 게임화 서비스의 데이터(점수와 배지, 리더보드)를 조회한다.
- 서버에서 정보를 조회하기 위해 지연시간(300ms)을 추가한다. 이벤트를 전파할 시간을 주고 정보를 갱신한다. UI를 단순하게 유지하려고 간단한 해결책을 사용하지만, 웹소켓(WebSocket) 같은 기술로 데이터가 준비될 때 서버가 클라이언트에 알리는 방법이 더 좋다.
- updateResults 함수는 updateStats가 서버에서 데이터를 조회할 때 사용할 사용자 ID를 반환한다.
function updateMultiplication() {
$.ajax({
url: "http://localhost:8080/multiplications/random"
}).then(function (data) {
// 폼 비우기
$("#attempt-form").find("input[name='result-attempt']").val("");
$("#attempt-form").find("input[name='user-alias']").val("");
// 무작위 문제를 API로 가져와서 추가하기
$('.multiplication-a').empty().append(data.factorA);
$('.multiplication-b').empty().append(data.factorB);
});
}
function updateResults(alias) {
var userId = -1;
$.ajax({
async: false,
url: "http://localhost:8080/results?alias=" + alias,
success: function (data) {
$('#results-div').show();
$('#results-body').empty();
data.forEach(function (row) {
$('#results-body').append('<tr><td>' + row.id + '</td>' +
'<td>' + row.multiplication.factorA + ' x ' + row.multiplication.factorB + '</td>' +
'<td>' + row.resultAttempt + '</td>' +
'<td>' + (row.correct === true ? 'YES' : 'NO') + '</td></tr>');
});
userId = data[0].user.id;
}
});
return userId;
}
$(document).ready(function () {
updateMultiplication();
$("#attempt-form").submit(function (event) {
// 폼 기본 제출 막기
event.preventDefault();
// 페이지에서 값 가져오기
var a = $('.multiplication-a').text();
var b = $('.multiplication-b').text();
var $form = $(this),
attempt = $form.find("input[name='result-attempt']").val(),
userAlias = $form.find("input[name='user-alias']").val();
// API 에 맞게 데이터를 조합하기
var data = {user: {alias: userAlias}, multiplication: {factorA: a, factorB: b}, resultAttempt: attempt};
// POST 로 데이터 보내기
$.ajax({
url: 'http://localhost:8080/results',
type: 'POST',
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json",
async: false,
success: function (result) {
if (result.correct) {
$('.result-message').empty()
.append("<p class='bg-success text-center'>정답입니다! 두뇌가 한층 더 말랑해지셨군요!</p>");
} else {
$('.result-message').empty()
.append("<p class='bg-danger text-center'>아이쿠, 아쉬운 오답ㅜㅜ 그래도 포기하지 마세요!</p>");
}
}
});
updateMultiplication();
setTimeout(function () {
var userId = updateResults(userAlias);
updateStats(userId);
updateLeaderBoard();
}, 300);
});
});
수정 후 웹 클라이언트를 확인할 수 있다. 부트스트랩 덕분에 훨씬 더 보기가 좋아졌다! 이제 시스템을 실행하기 전에 몇 가지 추가 단계가 필요하다.
- RabbitMQ 브로커를 실행한다.
- 곱셈 마이크로서비스(UI를 제외한)를 IDE에서 실행한다.
- 게임화 마이크로서비스도 동일하게 실행한다.
- UI 루트 폴더에서 제티 서버를 실행한다.
- 그런 다음 http://localhost:9090/ui/undex.html로 접속한다.
현재 아키텍처
이제 따로 서비스로 분리한 UI 서버와 브라우저(실제 요청을 보내는 클라이언트)를 포함해 시스템을 논리적인 관점에서 다시 살펴보자.
우리의 아키텍처는 진정한 마이크로서비스 아키텍처로 한 단계씩 성장하고 있다. 시스템을 독립적으로 수정할 수 있고, 유연하게 확장할 수 있다는 장점이 있다. 하지만 아직 갈 길이 멀다ㅠㅠ...
앞서 게임화 서비스의 UI 리더보드와 백엔드 서비스를 연결할 때 소개한 것처럼 현재 설계에는 두 가지 큰 문제가 있다.
- 여전히 UI 페이지가 백엔드 구조를 알고 있다. 물론 페이지는 게임화 마이크로서비스와 곱셈 마이크로서비스가 있다는 걸 알고 있어야 한다. 하지만 마이크로서비스 일부를 분리하거나 결합할 경우 UI에 영향을 미치므로 새로운 백엔드 구조에 맞게 수정해야 한다.
- UI 페이지에서 곱셈과 게임화 마이크로서비스의 위치를 하드코딩했다. 게임화 서비스에서 곱셈 서비스를 연결할 때도 마찬가지이다. 직접 링크를 수정하지 않으면 시스템을 확장할 수 없다.
이제 우리 아키텍처는 중요한 전환점을 맞이했다. 이러한 문제를 해결하기 위해 서비스 디스커버리, 로드 밸런싱, API 게이트웨이(또는 라우팅) 같은 몇 가지 패턴이 필요하다. 마이크로서비스의 세계에는 유레카, 컨설(Consul), 리본, 주울 등 이러한 패턴을 구현한 도구나 프레임워크가 있다.
이렇게 많은 도구 속에서 길을 찾기가 쉽지 않다. 이 도구가 언제 필요할까? 마이크로서비스를 잘 구현하려면 전부 필요할까? 이 질문의 답은 다음 절에서 찾아보고, 먼저 서비스 디스커버리와 로드 밸런싱을 살펴보자. 동작 방식을 이해하고 API 게이트웨이 패턴을 다룬 후 전체적으로 어떻게 동작하는지 알아보자. 그다음 코드에 적용하고 이점을 확인한다.
실운영 준비: 배포와 테스트
시스템이 점점 더 복잡해지고 있다. 현재 아키텍처를 보면 마이크로서비스 아키텍처를 성공적으로 구현하기 위해서는 자동화된 배포와 테스트가 얼마나 중요한지 알 수 있다.
- 애플리케이션 전체를 시작하려면 앞서해본 것처럼 많은 부분을 수동으로 시작해야 한다. 첫 배포 전략으로 각 서비스를 한 번에 실행하는 스크립트를 만드는 것도 좋을 것이다. 마이크로서비스 배포는 일체형 배포와 달리 쉽지 않다.
- 시스템에 데이터를 제공하고 테스트하려면 여러 사용자가 직접 문제를 풀고 검증해야 한다. 이를 수작업으로 하려면 해야 할 일이 꽤 많고 모든 유스 케이스를 다루지도 않는다. 그래서 다음 장에서 다룰 엔드투엔드 테스트가 필요하다.
서비스 디스커버리와 로드 밸런싱
서비스 디스커버리
현재 아키텍처를 다시 살펴보자. 게임화 마이크로서비스는 데이터를 조회하기 위해 곱셈 마이크로서비스의 REST API를 호출한다. 게임화 서비스는 http://localhost:8080을 가리키는 프로퍼티로 곱셈 서비스의 위치를 알고 있고, 곱셈 인수를 조회하기 위해 요청한다.
앞서 말했듯이 이건 잘못된 설계이다. 왜 게임화 서비스는 곱셈 서비스의 물리적인 위치(IP 주소와 포트)를 알아야 할까? 모든 곳에 배포할 수 있는 수십 개의 서비스가 있는 환경에서 이런 접근 방식을 유지할 수 없다. 이 시스템은 전혀 확장되지 않았다. 곱셈 마이크로서비스의 두 번째 인스턴스가 생성된다면 어떨까? 게임화 서비스는 어떤 인스턴스를 호출해야 할까?
UI가 마이크로서비스 아키텍처에 대해 알면 안 된다는 걸 설명할 때 언급한 것과 비슷한 문제이다. 차이점은 이 경우에는 두 마이크로서비스 사이의 통신이라는 점이다.
우리가 찾는 해결책은 서비스 디스커버리이다. 서비스 디스커버리의 구성 요소는 다음과 같다.
- 서비스 레지스트리(Service Registry): 모든 서비스 인스턴스와 인스턴스의 이름을 추적
- 레지스터 에이전트(Register Agent): 모든 서비스가 서로를 찾을 수 있도록 설정을 제공하는 에이전트
- 서비스 디스커버리 클라이언트(Service Discovery Client): 서비스 레지스트리에서 별칭으로 서비스를 조회
컨설, 유레카 등 스프링이 지원하는 서비스 디스커버리 도구는 다양하다. 이 책에서는 유명한 넷플릭스 OSS 스택의 일부인 유레카를 사용한다. 스프링은 스프링 클라우드 프로젝트에서 넷플릭스 도구를 사용할 수 있도록 래퍼를 제공한다. 바로 스프링 클라우드 넷플릭스(Spring Cloud Netflix)이다.
컨설과 같은 다른 구현체를 사용할 수도 있다. 중요한 것은 개념을 이해하는 것이다. 하드코딩 링크가 없는 상황에서 서비스 인스턴스가 서로를 찾을 수 있는 메커니즘이 필요하다. 나중에 자세히 공부하겠지만, 각 서비스의 참조 뒤에는 하나 이상의 인스턴스가 있을 수 있기 때문에 로드 밸런싱과 서비스 디스커버리는 밀접한 연관이 있다.
개념과 구성 요소를 살펴보았으니, 이제 논리적인 관점에서 서비스 디스커버리가 아키텍처에 어떻게 적용되는지 살펴보자. 다음 그림은 최종 버전은 아니지만 먼저 전체 시스템이 동작하는 것을 이해하고, 나중에 서비스 디스커버리와 API 게이트웨이의 차이점과 시너지 효과를 이해해 보자.
이 그림은 서비스 디스커버리의 세 가지 구성 요소가 함께 동작하는 것을 나타낸다. 먼저 새로운 구성 요소인 서비스 레지스트리가 있다. 이를 새로운 마이크로서비스로 배포할 것이다! 곱셈과 게임화 마이크로서비스는 시작될 때 각자의 레지스트리 에이전트로 서비스 레지스트리에 연락해 스스로를 등록한다. 그때 각 인스턴스는 레지스트리 안에서 별칭(기본적으로 마이크로서비스의 이름)이 붙는다. 그러면 http:///[호스트]:[포트] 대신 http://multiplication/ 또는 http://gamification/ 같은 주소로 찾을 수 있다. 하지만 해당 주소가 동작하려면 마이크로서비스가 각자의 레지스트리 클라이언트를 사용해야 한다. 이 클라이언트는 서비스 레지스트리에 있는 매핑을 보고 별칭을 특정 URL로 변환한다. 이 경우 게임화 서비스의 레지스트리 클라이언트만 http://multiplication/를 http://localhost:8080으로 변환한다.
이 패턴은 동적 DNS를 떠올리게 한다! 서비스에 별칭을 할당해서 서비스가 배포된 특정 위치(또는 IP)와 상관없이 위치를 옮길 수 있다.
로드 밸런싱
아직 아키텍처에 빈틈이 있다. 같은 서비스의 여러 인스턴스가 있다면 유레카는 어떻게 동작할까? 넷플릭스는 리본이라는 유레카와 통합된 클라이언트 사이드 로드 밸런싱을 구현했다.
곱셈 서비스 인스턴스가 두 개가 되면 애플리케이션 이름이 같으므로 둘 모두 같은 별칭으로 유레카에 등록된다. 새로운 인스턴스가 http://localhost:8082에 위치한다고 하자. 게임화 마이크로서비스가 클라이언트로써 http://multiplication/에 접속하려고 하면 유레카는 두 URL을 모두 반환하고 소비자가 어떤 인스턴스를 호출할지 결정한다(로드 밸런서인 리본과 유레카의 레지스트리 클라이언트를 함께 사용한다). 기본적으로 리본은 간단한 라운드 로빈(Round-Robin) 전략을 활용하지만, 나중에 이를 어떻게 바꾸는지 알아보자. 다음 그림을 살펴보자!
클라이언트 사이드 로드 밸런싱은 까다로운 개념으로 처음 접할 땐 자연스러워 보이지 않는다. 왜 호출하는 쪽에서 로드 밸런싱이나 다른 서비스의 인스턴스 수를 걱정해야 할까? 맞는 말이다. 걱정할 필요가 없다! 유레카와 리본이 투명하게 기능을 제공하기 때문에 코드에서 해당 기능을 처리할 필요가 없다. 하지만 기억할 것은, 리본은 로드 밸런싱을 숨기지만 여전히 클라이언트 안에 있다는 점이다.
현재 논리적 관점의 문제
마지막 그림에서 UI는 곱셈 서비스 중 하나에 직접 접속한다(8080 포트). 이건 나쁜 아이디어지만 지금으로서는 최선이다. 문제는 서비스 디스커버리와 UI를 통합할 수 없다는 것이다. 그러면 어떻게 해결할까? 다음 절에서 해결책으로 라우팅과 API 게이트 패턴을 알아본다.
폴리글랏 시스템, 유레카, 리본
이 시점에서 폴리그랏 환경을 좋아하는 분들에게 아주 중요한 질문이 있다. 마이크로서비스 중 하나가 스프링을 사용하지 않는다면 어떻게 해야 할까? 어떻게 유레카와 리본을 추가할까? 답은 평소와 같이 마이크로서비스 생태계에 하나의 컴포넌트를 더 추가하는 것이다. 바로 스프링 클라우드 사이드카(Spring Cloud Sidecar)이다. 사이드카는 넷플릭스 프라나(Prana)에서 영감을 받은 프로젝트로, 이름에서 알 수 있듯이 자바가 아닌 애플리케이션 인스턴스의 부록(오토바이 사이드카)처럼 실행된다.
우리 시스템에서 사이드카를 사용하지는 않는다. 다음 그림은 자바가 아닌 언어로 게임화 마이크로서비스를 작성한 가상의 상황을 보여준다.
사이드카 마이크로서비스(5678 포트)는 프록시처럼 동작하고, 애플리케이션이 엔드포인트를 노출해 해당 서비스가 발견되고 상태를 체크하게 한다. 유레카로 자바가 아닌 애플리케이션의 인스턴스를 등록(및 업데이트)하고, 레지스트리 클라이언트로 다른 마이크로서비스의 가용한 인스턴스를 얻는다. 애플리케이션은 사이드카의 API에 접근해 다른 서비스의 인스턴스를 찾는다. 사이드카에서 가용한 (특정 인스턴스가 아닌) 인스턴스의 목록을 가져올 것이기 때문에 로드 밸런싱은 애플리케이션에서 처리한다.
고가용성을 확보하기 위해서는 사이드카 마이크로서비스는 중복된 레이어를 추가함으로써 확장되고 모니터링되어야 한다. 이런 방법은 마이크로서비스가 많지 않을 때는 괜찮지만 시스템이 큰 경우에는 악몽이 될 수 있다. 아키텍처를 설계할 때 이런 단점을 상쇄하고 좋은 결정을 내리는 것은 팀원, 기술 리더, 아키텍트의 몫이다. 이 경우에 두 가지 대안은 시스템 전체를 일관된 언어로 작성하거나(대부분의 마이크로서비스를 자바와 스프링 부트로 작성하거나) 또는 자바가 아닌 마이크로서비스에 로드 밸런싱 및 고가용성을 위한 다른 전략을 사용하는 것이다.
API 게이트웨이와 라우팅
API 게이트웨이 패턴
앞서 본 것처럼 서비스 디스커버리와 로드 밸런싱으로 분산 시스템을 만들 수 있다. 이로써 인프라와 긴밀하게 결합하지 않고 확장할 수 있다. 하지만 여전히 해결해야 할 문제가 있다.
- 웹 클라이언트가 브라우저에서 실행 중이다. 따라서 서비스 디스커버리 클라이언트를 실행하거나 로드 밸런싱 처리를 할 수 없다. 로드 밸런싱을 유지하면서 백엔드의 마이크로서비스와 연결하려면 추가 컴포넌트가 필요하다.
- 일반적으로 인증, API 버저닝, 또는 요청 필터링과 같은 작업은 이 분산 시나리오에 아직 맞지 않는다. API(곱셈과 게임화 서비스의 REST 엔드포인트)를 중앙에서 집중 제어할 지점이 필요하다.
- REST API는 시스템의 아키텍처를 따라 소비자가 시스템에 의존하게 만든다. 이 문제는 보기보다 어려운 문제이다. 예제와 함께 살펴보자.
이러한 문제를 해결하기 위해 API 게이트웨이를 구현한다. 주울은 스프링 클라우드 넷플릭싀 일부이기도 하고 이 프레임워크에 포함된 나머지 도구와도 쉽게 통합된다. 서비스 레지스트리와 비슷하게 주울도 다른 서비스와 연결되는 추가 마이크로서비스로 작성한다.
API 게이트웨이가 어떻게 동작하는지 차근차근 살펴보자. 그러면 API 게이트웨이가 어떻게 문제를 해결하는지 이해할 수 있다.
먼저 마이크로서비스마다 전용 UI가 있다고 가정해서 로드 밸런싱 문제는 잠시 미뤄두고, 세 번째 문제에 집중하자. 소비자가 마이크로서비스의 설계를 알고 있다는 문제이다. 이게 왜 좋은 아이디어가 아닌지 더 쉽게 이해하기 위해 가상의 시나리오를 보자.
통계 기능을 새로운 마이크로서비스로 추출하고 /stats/ 엔드포인트로 옮기려고 한다. 소비자(이 경우에는 웹 클라이언트)가 마이크로서비스 구조를 알고 있기 때문에 새로운 URL(http://statsmanager/stats 같은)을 가리키도록 수정해야 한다.
만약 REST API를 외부에 공개한다면 이 짜증나는 부작용은 더 심해질 것이다. 외부 애플리케이션에 적용하기 위해 우리가 만든 모든 마이크로서비스를 리팩터링해야 하기 때문이다. 따라서 이런 방법 대신 내부 구조를 드러내지 않는 REST API를 만들자. 그러면 나중에 수정 사항이 있더라도 다른 부분에 영향을 주지 않고도 유연하게 수정할 수 있다. 그래서 http://gamification/leaders와 http://multiplication/results 대신 http://application/leaders와 http://application/results를 사용한다(이러면 아키텍처를 알 수 없다).
API 게이트웨이 패턴이 이 문제의 해결책이다. 우리가 선택한 주울은 URL 패턴을 설정하면 소비자가 내부 구조를 완전히 모른 상태에서 요청을 적절한 서비스로 라우팅 한다. 이 솔루션을 적용하면 시스템 내에서 유연하게 기능을 옮길 수 있다.
API 게이트웨이를 도입하는 것은 먼저 일체형부터 만드는 접근법과 잘 맞다. 큰 기능 덩어리로 시작해서 도메인 경계가 분명해지고 시스템이 진화하면 기능을 분리해 나가는 것이 좋다. 시스템 앞에 API 게이트웨이를 놓으면 API 소비자가 항상 일체형을 보게 되므로, 뒤에서 아키텍처를 분산되고 확장 가능하도록 투명하게 진화시킬 수 있다. 이 패턴은 일체형을 마이크로서비스 아키텍처로 단계별로 분할하고 진화시키는 완벽한 방법이다.
모든 요청이 API 게이트웨이를 통과하기 때문에 인증과 같이 중앙에서 관리할 시능과 통합할 수 있다. 예를 들어, 스프링 시큐리티를 사용할 수 있고, 사용자 정의 주울 필터를 적용한 주울과 통합할 수 있다.
이러한 API 게이트웨이 패턴의 장점(내부 구조를 추상화하고 API 접근을 중앙 집중화)은 서비스 디스커버리나 로드 밸런싱을 필요로 하지 않는다는 점이다. 또한 주울의 URI 패턴에 따라 마이크로서비스 인스턴스를 직접 가리키도록 라우팅 테이블을 설정할 수도 있다. 하지만 고가용성을 제공하고 일부 마이크로서비스가 중단되더라도 시스템을 계속 동작시키려면 서비스 디스커버리나 로드 밸런싱은 꼭 필요하다.
이제 모든 것이 어떻게 결합되는지 거의 이해할 수 있다! 위의 그림은 모든 마이크로서비스 인스턴스 그룹 앞에 로드 밸런서를 배치한 논리적인 관점이다. 하지만 백엔드에 API 게이트웨이 마이크로서비스가 생겼으니 프론트엔드 요청을 로드 밸런싱하도록 하면 어떨까? 그러면 웹 클라이언트 측 로드밸런싱을 제공할 수 없다는 남은 문제를 해결할 수 있다. 다음 절에서 주울, 유레카, 리본을 통합하고 각기 다른 툴(그리고 아키텍처 패턴)이 어떻게 함께 동작하는지 충분히 이해하게 될 것이다.
실운영 준비: 에지 서비스
많은 사람들이 마이크로서비스 아키텍처에서 중앙 집중적인 부분이 있는 것을 두려워한다. 라우팅이나 필터링처럼 중요한 요소는 단일 장애 지점이 될 수 있기 때문이다. 특히 마이크로서비스의 문에 해당하는 API 게이트웨이는 에지 서비스라고도 알려져 있다. 이런 종류의 서비스를 마이크로서비스 인프라에서 잘 동작시키려면 적절한 로드 밸런싱이 핵심이다. 이를 위해 보통 계층을 나눠서 DNS 로드 밸런서 같은 솔루션을 사용한다. 예를 들어 http://gateway.ourwebapp.com에서 게이트웨이가 세 개의 다른 서버 인스턴스를 지원하는 식이다. 대부분의 클라우드 제공 업체는 이러한 서비스를 제공하고 있고, 엔진엑스 같은 도구를 이용해 직접 구현할 수도 있다.
함께 동작하는 주울, 유레카, 리본
이제 서비스 디스커버리와 로드 밸런싱을 API 게이트웨이에 포함시켜 활용할 수 있을지 알아보자. 즉, 주울, 유레카, 리본을 함께 사용해 어떻게 문제를 해결하는지 알아보자.
아래 그림은 API 게이트웨이 마이크로서비스를 적용한 모습이다. 서비스 디스커버리와 로드 밸런싱으로 서비스가 서로 찾을 수 있게 한다.
웹 클라이언트는 더 이상 마이크로서비스에 직접 연결하지 않고 모든 요청을 게이트웨이로 전송한다(8000 포트).
주울과 스프링 부트로 구현할 게이트웨이 마이크로서비스에는 물리적인 주소 대신 유레카에 등록된 마이크로서비스 별칭을 가리키는 라우팅 테이블이 포함되어 있다. 따라서 주울, 유레카, 리본이 완벽히 통합되고 API 게이트웨이, 서비스 디스커버리, 로드 밸런싱 기반의 솔루션이 된다. 주울이 요청을 받으면 URL을 분석하고 라우팅 테이블에서 패턴을 찾는다. 그 다음 리본이 로드 밸런싱 전략(기본적으로 라운드 로빈)을 기반으로 인스턴스를 고른다. 마지막으로 주울이 원래 요청을 일치 하는 마이크로서비스 인스턴스로 리다이렉트 한다.
게임화 마이크로서비스가 곱셈 마이크로서비스에서 결과를 검색하는 두 가지 옵션이 있다. 이전처럼 서비스 레지스트리로 자체 연결할 수도 있지만, 새로운 API 게이트웨이 서비스를 활용할 수도 있다. 이것은 마이크로서비스에서 가장 논란이 되는 주제 중 하나이다. 각 서비스가 게이트웨이를 활용해 서로 호출할 것인지, 아니면 유레카와 리본으로 내부 통신을 유지할지 선택해야 한다. 여기에는 옳고 그름은 없다. 이는 시나리오에 따라, 더 구체적으로는 인프라가 어떻게 구축되어 있느냐에 따라 달려있다. 하지만 라우팅과 로드 밸런싱 기능을 중앙 집중화하면 다음과 같은 장점이 있다.
- 마이크로서비스가 주어진 기능의 위치를 알지 못하게 할 수 있다. 일체형을 나누거나 몇 가지 기능을 다른 마이크로서비스로 옮기더라도 API 게이트웨이를 통과하면 다른 모든 마이크로서비스는 아무 영향도 받지 않고 동작한다. 따라서, 마이크로서비스 간에 더 느슨한 결합이 이뤄진다.
- 로드 밸런싱은 인프라에서 빈번하게 다루는 중요한 주제이다. 적절하게 구성하는 것이 복잡하고, 지리적 영역, 네트워크 지연, 마이크로서비스 부하 등에 영향을 받을 수 있다. 일반적으로 이러한 정책은 중앙 집중식으로 유지되는 것이 더 낫고 각 서비스의 구현에 의존하면 안 된다. 클라이언트 측 로드 밸런싱은 각 서비스가 인프라 문제를 가지고 있을 수 있어서 백엔드 서비스에게 좋은 접근 방식은 아니다. 하지만 전체적인 관점에서는 필요할 수도 있다.
반면 시스템 내 모든 요청을 API 게이트웨이로 처리한다면 게이트웨이가 더 중요한 에지 서비스가 된다. 따라서 내부적으로나 외부적으로 고가용성을 유지할 수 있는 좋은 중복 전략으로 지원해야 한다.
우리 애플리케이션은 요청을 라우팅 할 때 API 게이트웨이 마이크로서비스만 사용할 것이다. 따라서 게임화와 곱셈 마이크로서비스는 서로를 알지 못하게 유지한다. 즉, 게임화와 곱셈 서비스는 게이트웨이를 찾을 때만 유레카와 리본을 이용하고, 스스로 여러 인스턴스로 복제될 수 있다.
다음 절에서 논리적인 관점을 실제로 구현하기 위해 단계별로 시스템을 개선해 보자.