[스프링 마이크로서비스 코딩 공작소] 교재를 통해 마이크로서비스를 처음 학습하게 되었다.

 

최근 팀 프로젝트에서 마이크로서비스 구조를 적용해보자는 논의가 있었지만, 프로젝트 일정이 지연되면서 그 아이디어는 자연히 사라지게 되었다. 그렇지만 마이크로서비스를 알지 못한 채 개발자로 성장하는 것은 원치 않아 학습을 시작하게 되었다.

 

학습을 시작하면서 크게 두 가지를 느꼈다. 첫째, 마이크로서비스를 너무 가볍게 생각했다는 것이다. 이전 프로젝트에서도 MSA 도입을 논의했지만, 단순히 서비스를 분리하고 통신하는 수준에서 그쳤던 것이다. 마이크로서비스 환경은 서비스 간 복잡한 통신으로 인해 예상보다 더 큰 복잡도를 동반하며, 이를 해결하기 위한 노력이 필요하다는 것을 깨달았다.

둘째, 마이크로서비스를 위해 이미 많은 기술들이 준비되어 있다는 것이다. 특히 Spring Cloud는 config server, gateway server 등 마이크로서비스에 필요한 다양한 기술들을 쉽게 제공하고 있어, 내가 그동안 경험하지 못한 부분이 많음을 느꼈다.

 

이번 글에서는 [스프링 마이크로서비스 코딩 공작소] 교재에서 다룬 마이크로서비스 환경의 기본 기술들이 각각 어떤 역할을 하는지 간단히 정리해보도록 하겠다.

 

* 첨언하자면 이번 교재에서는 Spring Cloud를 중심으로 다루었지만, 2025년 기준으로는 Spring Cloud와 K8S를 함께 활용하는 프로젝트가 많다고 한다. K8S를 함께 활용할 경우 이번 글에서 다루는 일부 기술을 K8S 쪽 기술로 대체할 수 있다.해당 내용은 별도로 다룰 예정이다.



Spring Cloud

Spring Cloud는 마이크로서비스 환경을 쉽게 구축하고 관리할 수 있도록 다양한 도구와 기능을 제공하는 프레임워크다.

마이크로서비스 간의 통신, 서비스 디스커버리, 보안, 로드 밸런싱 등 MSA의 필수 요소들을 쉽게 구현할 수 있게 해준다.

Spring Cloud는 Config Server, Eureka Server, Gateway Server 등의 기능을 제공하여 서비스 간의 연결과 데이터 교환을 쉽게 할 수 있도록 도와준다. 이를 통해 서비스 간의 결합도를 낮추고, 시스템의 확장성을 확보할 수 있다.

 

Config Server

Config Server는 중앙 집중식 설정 관리 서버로, Spring Cloud Config를 활용해 각 마이크로서비스의 설정을 중앙에서 관리할 수 있다. 각 서비스가 개별적으로 application.properties 또는 application.yml 파일을 작성하는 대신, 모든 설정을 Config Server에 작성하고 각 서비스가 이를 가져오는 방식이다. 또한, 설정 변경 사항을 실시간으로 반영할 수 있어 서비스 간 일관된 구성을 유지하는 데 도움이 된다. 보통 Git 저장소와 연동하여 설정 파일을 관리하며, 이를 통해 설정 관리의 효율성과 일관성을 높일 수 있다.

 

아래와 같이 설정을 통해 Config Server는 Git 저장소에서 설정 파일을 가져와 관리할 수 있다.

server:
  port: 8888
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: https://github.com/my-repo/config-repo

 

Gateway Server

Spring Cloud Gateway는 API Gateway 역할을 수행하는 서버다.

API Gateway는 클라이언트의 요청을 적절한 마이크로서비스로 라우팅하는 중앙 진입점 역할을 하며, 인증/인가, 로드 밸런싱, 로깅, 트래픽 제어 등 다양한 기능을 제공한다.

 

API Gateway를 사용하면 여러 마이크로서비스가 각기 다른 포트를 사용할 때 클라이언트는 하나의 단일 진입점을 통해 모든 요청을 처리할 수 있다. 즉, 여러 서비스가 있을 때 service1.com과 service2.com으로 개별적으로 접근하는 것이 아니라, gateway.com/service1과 gateway.com/service2로 접근하게 되어 gateway.com이라는 단일 진입점을 제공한다. 이를 통해 API Gateway는 서비스 간의 결합도를 낮추고, 다양한 부가 기능을 중앙에서 관리할 수 있도록 도와준다.

 

아래 설정은 /users/** 경로의 요청을 user-service로 전달하도록 하는 설정을 보여준다.

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: http://localhost:8081
          predicates:
            - Path=/users/**

 

Eureka Server

Eureka Server는 서비스 디스커버리를 위한 서버다.

서비스 디스커버리란, 마이크로서비스들이 서로의 위치를 동적으로 찾을 수 있도록 지원하는 메커니즘을 말한다. Eureka 서버에 각 마이크로서비스들이 등록되면, 다른 서비스들은 Eureka를 통해 필요한 서비스의 위치를 조회할 수 있다. 이를 통해 부하 분산, 장애 대응, 동적 서비스 인스턴스 관리 등을 보다 쉽게 구현할 수 있다.

 

서비스 디스커버리는 MSA에서 중요한 요소로, 서비스 간의 통신을 효율적으로 처리하고, 서비스가 추가되거나 제거될 때도 다른 서비스에 미치는 영향을 최소화할 수 있다.

 

아래처럼 Eureka Server를 설정하고, 다른 마이크로서비스들이 이를 사용하도록 할 수 있다.

server:
  port: 8761
spring:
  application:
    name: eureka-server
  eureka:
    client:
      register-with-eureka: false
      fetch-registry: false

 

Kafka / Zookeeper

Kafka는 대량의 데이터를 빠르게 처리하고, 서비스 간의 비동기 이벤트를 전달하는 메시지 브로커다.

Kafka는 MSA 환경에서 서비스 간의 비동기적이고 효율적인 통신을 위해 사용된다. 각 서비스는 Kafka를 통해 이벤트를 발행하고, 다른 서비스들은 이를 소비하여 처리할 수 있다. 이를 통해 서비스 간의 결합도를 낮추고, 시스템의 확장성을 높일 수 있다.

 

또한 Kafka는 로그, 이벤트 스트림, 데이터 파이프라인 등 다양한 용도로 활용된다. 예를 들어, Kafka는 마이크로서비스들 간의 실시간 이벤트를 처리하거나, 데이터 흐름을 분리하여 처리하는 데 적합하다.

Zookeeper는 Kafka의 클러스터 노드 관리 및 리더 선출 등의 역할을 수행하는 코디네이션 서비스다. Kafka와 함께 사용하면 클러스터 관리가 용이해지고, 서비스의 장애 대응 및 복구가 간편해진다.

 

아래는 my-topic라는 kafka topic에 메시지를 발행하는 java 코드 예시이다.

@KafkaTemplate("my-topic")
public void sendMessage(String message) {
    kafkaTemplate.send("my-topic", message);
}

 

Keycloak

Keycloak은 오픈소스 인증 및 권한 부여 솔루션으로, OAuth 2.0, OpenID Connect, JWT 등의 표준을 지원한다. MSA 환경에서는 각 서비스가 독립적으로 보안 인증을 처리하는 대신, Keycloak을 통해 중앙에서 모든 인증과 권한 부여를 관리할 수 있다.

 

Keycloak을 활용하면 사용자 로그인, 액세스 토큰 발급 등을 쉽게 처리할 수 있다. 또한, Keycloak은 사용자 인증뿐만 아니라, 역할 기반 접근 제어(RBAC)와 같은 기능을 제공하여 서비스 간의 보안을 효과적으로 관리할 수 있다.

 

ELK 스택

ELK 스택은 Elasticsearch, Logstash, Kibana의 세 가지 오픈소스 도구로 구성된 시스템으로, 대량의 로그와 데이터를 수집, 처리, 검색, 분석 및 시각화하는 데 유용하다. 마이크로서비스 아키텍처(MSA)에서 각 서비스가 로그를 별도로 처리하고 관리하는 것은 매우 복잡할 수 있기 때문에, ELK 스택을 활용하면 모든 로그를 중앙에서 수집하고 분석할 수 있어 효율적인 모니터링과 디버깅을 할 수 있다.

 

Elasticsearch
Elasticsearch는 분산형 검색 엔진으로, 데이터를 빠르게 검색하고 분석할 수 있다. 로그 데이터를 저장하고, 인덱싱하여 검색 성능을 극대화한다. Elasticsearch는 RESTful API를 통해 다른 시스템과 쉽게 통합될 수 있어 MSA 환경에서 로그 데이터를 빠르게 검색하고 실시간으로 분석할 수 있는 강력한 도구로 사용된다.

예를 들어, 서비스 로그 데이터를 Elasticsearch에 저장한 후, Kibana를 사용해 시각적으로 대시보드를 구성하거나, 로그를 검색해 문제를 해결할 수 있다.


Logstash
Logstash는 데이터를 수집, 변환하고 저장소로 전송하는 데이터 파이프라인 도구다. 여러 다양한 소스(파일, DB, 메시지 큐 등)로부터 데이터를 수집하여 변환하고 Elasticsearch로 전송한다. Logstash는 로그 데이터를 수집하면서 로그의 형식을 변환하거나, 데이터를 필터링하고, 정규화하는 작업을 처리한다. 예를 들어, 다양한 포맷의 로그를 하나의 일관된 포맷으로 변환하고, 불필요한 로그를 제외하는 등의 처리가 가능하다.

 

Kibana
Kibana는 Elasticsearch 데이터를 시각화하는 도구로, 로그 데이터를 대시보드 형태로 시각화하여 직관적으로 모니터링할 수 있도록 돕는다. Kibana를 사용하면 실시간으로 서비스의 상태를 모니터링하고, 로그를 쉽게 검색하고 분석할 수 있다. 대시보드에서 데이터를 그래프나 차트로 시각화하여 문제가 발생한 지점을 빠르게 찾아낼 수 있다.

 

Zipkin

Zipkin은 분산 트레이싱 시스템으로, 마이크로서비스 간의 요청 흐름을 추적하여 성능 병목 현상 및 지연 문제를 분석하는 데 사용된다. Zipkin을 활용하면 서비스 간의 요청이 어떤 경로를 통해 전달되는지, 어느 지점에서 문제가 발생하는지 명확하게 파악할 수 있다.

 

Resilience4j

Resilience4j는 마이크로서비스의 장애 대응을 위한 라이브러리다. 마이크로서비스는 서로 의존성이 있기 때문에, 한 서비스의 장애가 다른 서비스로 전파될 수 있다. Resilience4j는 Circuit Breaker, Retry, Rate Limiter, Timeout 등 다양한 패턴을 통해 이러한 장애를 예방하고 복구하는 데 도움을 주며, 마이크로서비스의 신뢰성을 높여준다.

 

대표적으로 Circuit Breaker 패턴만 다뤄보도록 하겠다.

 

Circuit Breaker 패턴

Circuit Breaker(서킷 브레이커) 패턴은 특정 서비스가 장애를 겪을 경우, 다른 서비스로 영향을 전파하지 않도록 차단하는 기법이다. Resilience4j와 같은 라이브러리를 활용하여 이 패턴을 구현할 수 있다. 서킷 브레이커는 서비스가 실패한 경우, 잠시 동안 해당 서비스를 호출하지 않도록 하여, 시스템이 복구될 수 있는 시간을 준다.

 

아래 코드는 Circuit Breaker 예시로, getData() 실행 중 오류 발생 시 별도 작성된 fallbackMethod()를 수행하도록 한다.

@CircuitBreaker(name = "backendA", fallbackMethod = "fallbackMethod")
public String getData() {
    // 외부 API 호출
}

public String fallbackMethod(Exception e) {
    return "서비스가 현재 이용할 수 없습니다.";
}

 

끝!

 

Fast Refresh 무한루프

Next JS 사용 중 위와 같은 현상을 접했다.

클라이언트 컴포넌트에서 api 경로에 접근해서 데이터를 가져오는 테스트를 하던 중이었는데, 화면은 이상 없이 로딩되었지만 콘솔을 보니 저렇게 Fast Refresh라는 로그가 계속 반복되고 있었다.

 

Fast Refresh는 웹소켓이 연결을 유지하는 것처럼 특정한 상황이라면 정상적일 수도 있으나, 이번 경우는 단순히 1회성으로 데이터를 가져오는 화면이었고 refresh 속도도 매우 빨랐기에 불필요한 현상이었다.

당장은 문제가 없을지라도 향후 리렌더링이 무한으로 발생하는 등 이슈가 될 수 있기에 코드를 검토해보았다.

 

'use client';

import { useEffect, useState } from 'react';

export default function Fetch() {
  const [user, setUser] = useState({id: null});
  useEffect(() => {
    fetch(process.env.NEXT_PUBLIC_API_URL + '/api/1')
      .then(type => type.json())
      .then(result => {
        setUser(result);
      })
  });

  return (
    <>
      <h1>/app/sub/fetch/page.js</h1>
      <p>{user.id}</p>
      <a href="/">/app/page.js</a>
    </>
  )
}

 

검토 결과, useEffect 내부에서 setUser를 사용한 것이 무한루프의 원인이었다.

useEffect 내부에서 setUser를 실행하면서 상태가 변경되게 되는데, 상태 변경에 따라 useEffect가 재실행되면서 무한 루프가 발생했다.

useEffect() 실행 set User 사용
컴포넌트 리렌더링 user 상태 변경

(시계방향으로 순회)

 

그래서 useEffect에 의존성 배열을 추가함으로써 무한루프 현상을 해결하였다.

의존성 배열을 추가하게 되면 컴포넌트가 처음 렌더링될 때에만 useEffect가 실행되고 되고, 이후에는 상태가 변경되더라도 실행되지 않아 무한루프를 방지할 수 있다.

  useEffect(() => {
    fetch(process.env.NEXT_PUBLIC_API_URL + '/api/1')
      .then(type => type.json())
      .then(result => {
        setUser(result);
      })
  }, []);

 

* 2025.03.14 추가

의존성 배열(Dependency Array)이란, useEffect가 언제 실행될지를 결정하는 배열이다.

예를 들어 user 상태가 변경될 때마다 useEffect를 실행하고 싶다면, useEffect의 두 번째 인자로 [user]를 전달하면 된다.

 

의존성 배열이 없는 경우 (useEffect(() => {...}))
→ 모든 렌더링마다 실행되므로 성능 이슈가 발생할 수 있다.

 

빈 배열 []을 전달하는 경우 (useEffect(() => {...}, []))
→ 처음 렌더링(마운트) 시에만 실행되고, 이후에는 다시 실행되지 않는다.

 

 

끝!

You have a Server Component that imports next/router.

 

Next JS 학습 중 위 에러가 발생했다.

id라는 이름의 path parameter를 사용하려던 중 발생했는데, 그 해결과정을 간략히 정리해보고자 한다.

 



1. 에러 발생 및 설명

먼저, 문제가 된 코드는 아래와 같다.

import { useRouter } from 'next/router';

export default function Id() {
  const router = useRouter();
  const id = Number(router.query.id);

  return (
    <>
      <h1>/pages/sub/[id]/page.js</h1>
      <p>Parameter id: {id}</p>
      <a href="/">/pages/page.js</a>
    </>
  );
}

/sub/[id] 라는 경로로 접근했을 때, next/router의 useRouter를 활용하여 query 값에서 id를 찾아내는 구조이다.

하지만 위 코드를 컴파일하던 중 You have a Server Component that imports next/router, 즉  서버 컴포넌트에서 next/router를 사용했다는 에러가 발생했다.

 

알아보니 next.js 13 버전부터 기존 pages 폴더에서 app 폴더 기반 라우팅 시스템으로 바뀌었는데, 이와 함께 서버 컴포넌트와 클라이언트 컴포넌트를 구분하기 시작했다. next/router의 useRouter는 클라이언트 전용 훅으로서, 클라이언트 컴포넌트에서만 사용이 가능하기 때문에 서버 컴포넌트에서 호출 시 위 에러가 발생한다.


2. 해결

다른 블로그를 참고했을 때 서버 컴포넌트에서는 next/router가 대신 next/navigation을 사용해야 한다고 하여 적용해보았다. 하지만 next/navigation의 useRouter는 query를 포함하고 있지 않아 고민하던 중, 아래와 같이 pathname을 활용하는 방식으로 먼저 접근해보았다.

'use client';

import { usePathname } from 'next/navigation';

export default function IdPage() {
  const pathname = usePathname();
  const id = pathname.split("/")[2];

  return (
    <div>
      <h1>/pages/sub/[id]/page.js</h1>
       <p>Parameter id: {id}</p>
       <a href="/">/pages/page.js</a>
    </div>
  );
}

접근경로인 /sub/[id] 을 pathname으로 받은 후 split()하여 id를 추출하는 방식이다.

 

하지만 위와 같은 방식은 url이 변경될 때마다 코드의 수정이 필요하다.

그래서 좀 더 알아보니, 간단한 해결책이 나왔다. 알고보니 app 폴더 기반 라우팅 시스템에서는 params 객체를 통해 path parameter 값을 받아올 수 있었다.

따라 아래와 같이 params 객체에서 id를 추출하는 방식을 채택했다.

export default function Id({ params }) {
  const { id } = params;

  return (
    <>
      <h1>/pages/sub/[id]/page.js</h1>
      <p>Parameter id: {id}</p>
      <a href="/">/pages/page.js</a>
    </>
  );
}

3. 서버 컴포넌트 & 클라이언트 컴포넌트

서버 컴포넌트

서버 컴포넌트는 서버에서만 실행되는 컴포넌트로 클라이언트 측에서 렌더링되는 JavaScript 코드를 보내지 않고, 서버에서 데이터를 처리하거나 렌더링을 한 뒤 HTML을 클라이언트로 전송한다. Next JS에서는 별다른 선언을 하지 않으면 서버 컴포넌트로 간주한다.

  • 서버에서 동적으로 데이터를 처리하고 렌더링
  • 클라이언트에서 JavaScript를 실행하지 않으며 필요한 경우 React의 상태 업데이트나 이벤트 핸들러 없이 서버에서 완료된 HTML을 클라이언트로 전송
  • 서버 컴포넌트는 클라이언트 사이드 라이브러리나 API 호출과 같은 동적 JavaScript 동작을 할 수 없으며, 대신 서버에서 데이터를 가져와서 HTML을 렌더링

 

클라이언트 컴포넌트

클라이언트 컴포넌트는 클라이언트 브라우저에서 실행되는 컴포넌트이며, 상태 관리, 이벤트 핸들링, 클라이언트 측 API 요청, 동적 렌더링 등을 클라이언트에서 처리할 수 있다. 최상단에 'use client'를 적음으로써 클라이언트 컴포넌트를 선언할 수 있다.

  • 클라이언트에서 JavaScript를 실행하여 동적인 콘텐츠를 표시
  • 상태(state)를 관리하거나 이벤트 핸들러를 통해 동작을 처리
  • 클라이언트에서 useState, useEffect, useRouter와 같은 React 훅을 사용하여 UI를 동적으로 업데이트

 

서버 컴포넌트와 클라이언트 컴포넌트의 구분 이유

  • 서버 컴포넌트는 최적화된 서버 사이드 렌더링(SSR)을 통해 페이지를 클라이언트에게 전달하고, 불필요한 JavaScript 번들을 줄여 페이지 로딩 속도를 개선
  • 클라이언트 컴포넌트는 상태 관리, 이벤트 처리, 동적 UI 업데이트를 클라이언트에서 처리하게 함으로써 사용자 경험 향상

끝!

'Languages > Next.js' 카테고리의 다른 글

[NextJS] Fast Refresh 무한루프 해결  (0) 2025.02.18

+ Recent posts