모놀리식 기반 프로젝트를 진행하면서 위와 같이 MSA 의 필요성을 자주 느꼈고 러닝커브가 다소 있었지만 이후 쿠버네티스를 적용해서 배포까지 할 계획을 생각해뒀기 때문에 MSA 을 적용해보기로 했다.
우리 프로젝트는 콘서트 티켓을 예매하는 사이트 이다. 유저를 관리할 서버, 결제를 담당할 서버, 콘서트 정보를 가지는 서버, 관리자 서버 이렇게 4가지로 구성되어 있고 기능에 따라 DB는 3가지로 나눠줬다.
그리고 MSA로 분리된 서버들의 주소를 모두 알고 있어야 하거나 모든 서버에 대한 인증 및 인가를 설정하기엔 번거롭기 때문에 이를 해결하기 위해 API Gateway와 Eureka Server를 추가해주었다. Gateway 서버는 API 서버 앞단에서 모든 API 서버들의 엔드포인트를 단일화하여 묶어주고 API에 대한 인증과 인가 기능에서 메시지에 따라서 여러 서버로 라우팅하는 고급기능까지 많은 기능을 담당한다.
Eureka 서버는 Netflix에서 개발한 Service Discovery 인데 이는 흩어져 있는 서버들의 주소 정보를 가지고 있어 Gateway 서버에서 라우팅할 때 이 Eureka 서버를 통해 경로를 얻고 라우팅 해주게 된다.
그렇게 작성된 설계도는 다음과 같다.

MSA로 개발하는 것은 하나의 서버를 여러 개로 나뉘어 개발하는 것이다. 기존에 Github에서 개발 브랜치를 하나만 사용했다면 다음과 같이 서버별로 브랜치를 생성해야한다. 왜냐하면 브랜치별로 배포를 하고 같은 브랜치에 있으면 다른 서버 수정사항이 계속 적용돼서 Pull/Push에 의도치 않은 파일을 받고 충돌이 발생할 수 있기 때문이다.

이처럼 개발할 서버별로 브랜치를 생성하고 각자 맡은 서버를 내려받아 개발하면 된다. 하지만 로컬에서 테스트할 때 Eureka, Gateway 서버를 항시 켜줘야 하는 번거로움이 있다. 또한 로컬로 Intellij IDE를 여러개 가동시키면 메모리를 많이 차지한다. 이런 경우 변경사항이 적은 서버 eureka, gateway 서버는 Build 해서 jar 파일로 만들어서 실행시키면 다소 자원을 적게 소모하고 개발 할 수 있다.
기존 모놀리식 아키텍처에서는 Service를 호출해서 구현된 기능을 썼을 것이다. MSA로 서버로 나뉘었다면 다른 서버로 접근해 데이터를 요청해야한다. 여기서는 보통 RestTemplate를 많이 쓰지만 우리 팀은 Netflix에서 만든 API Client 라이브러리인 Feign Client를 사용했다. 어노테이션 기반으로 클래스 분리가 깔끔하고 사용성도 좋아서 선택하게 되었다.
// Feign Client
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.0.2'
@EnableFeignClients
...
@SpringBootApplication
public class CygiConcertApplication {
public static void main(String[] args) {
SpringApplication.run(CygiConcertApplication.class, args);
}
}
concert:
server:
prefix: /api/v1/server-concert
@FeignClient(name = "payment-service", path = "${concert.server.prefix}")
public interface PaymentServerClient {
@GetMapping("/seat/{concertId}")
ResponseEntity<List<String>> getSeatInfo(@PathVariable("concertId") long concertId);
}
만약 payment로 요청을 보냈는데 payment 서버가 닫혀있거나 에러가 발생하면 어떻게 해야할까?
서버 간 통신에서 장애가 발생했을 때 서킷브레이커를 사용해서 적절한 예외 처리를 할 수 있다.
Service 1 <-> **Circuit Breaker** <-> Service2
Payment 서버에서 Concert 서버로 공연 제목 정보를 요청했는데 Concert 서버와의 통신에 장애가 발생해서 임시 데이터를 반환하는 예시를 적용해보겠다. 서킷브레이커 종류에는 Hystrix, Resilience4j 가 있는데 Hystrix는 지원 중단된 관계로 Resilience4j 를 사용했다.
// Circuit breaker
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
@Configuration
public class Resilience4JConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfiguration() {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(4) // 실패율 임계값으로, 지정된 실패율보다 높으면 회로 차단 상태로 전환
.waitDurationInOpenState(Duration.ofMillis(1000)) // 오픈 상태에서 대기하는 시간으로, 회로가 닫혔다가 다시 열린 후에 요청이 허용되기 전까지 기다리는 시간
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(2)
.build();
// 요청의 최대 실행 시간
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(4))
.build();
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build()
);
}
}
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitbreaker");
data = reservationPage.stream()
.map(reservation -> {
String concert = circuitBreaker.run(() -> concertServerClient.getConcertTitle(reservation.getConcertId()).getBody(),
throwable -> "Wait..");
...
개발이 완료되면 배포를 해줘야 하는데 Jenkins 기준으로 모놀리식은 하나만 작성하면 되지만 MSA에서는 서버마다 Jenkinsfile을 작성해서 각각 Item을 생성해서 배포해야한다. MSA 단점이라고도 할 수 있는 관리해야할게 많아지는 단점이긴 하지만 한번 해두면 잘 안 바뀌고 분리해서 관리할 수 있다는 장점이 있다. Jenkinsfile 코드는 깃허브에 있습니다.
