Part 3. 실전 Kubernetes 프로젝트

Ch 2. Social Feed 서버 개발

Ch 2. Social Feed 서버 개발


01. 스프링 프로젝트 구성

06. Spring Boot 소셜 피드 서버 프로젝트 생성과 데이터베이스 설정

SNS 백엔드의 첫 번째 마이크로서비스로 소셜 피드 서버를 구성한다. 소셜 피드 서버는 사용자가 작성한 게시글을 저장하고 조회하는 역할을 담당한다.

이미지 파일 자체는 이후에 구현할 이미지 서버가 관리한다. 피드 서버는 게시글 작성자 ID, 본문, 이미지 ID와 같은 메타데이터만 저장한다. 서비스의 책임을 이렇게 분리하면 게시글 처리와 이미지 저장 방식을 독립적으로 변경하고 확장할 수 있다.

소셜 피드 서버의 책임

flowchart LR
    CLIENT["SNS Client"] --> FEED["Feed Server"]
    FEED --> DB["Amazon RDS for MySQL"]
    FEED --> IMAGE["Image Server"]
    DB --> META["작성자 ID, 본문, 이미지 ID"]
    IMAGE --> EFS["Amazon EFS 또는 Object Storage"]
데이터관리 주체
게시글 본문Feed Server
작성자 IDFeed Server가 참조
이미지 IDFeed Server가 참조
이미지 파일Image Server
사용자 상세 정보User Server

MSA에서는 다른 서비스의 데이터를 직접 수정하지 않는 것이 중요하다. 피드 서버가 보관하는 작성자 ID와 이미지 ID는 다른 서비스의 데이터를 가리키는 논리적 참조다. 각 서비스가 데이터베이스를 분리한다면 일반적인 외래 키로 서비스 간 정합성을 강제할 수 없으므로 API 호출이나 이벤트를 이용한 검증과 보상 처리가 필요하다.

전체 구성 흐름

flowchart TD
    A["Spring Initializr에서 프로젝트 생성"] --> B["Gradle과 Java 21 설정"]
    B --> C["Spring Web, Spring Data JPA, MySQL Driver 추가"]
    C --> D["Spring Profile 분리"]
    D --> E["Datasource 환경 변수 설정"]
    E --> F["Kubernetes ConfigMap 생성"]
    E --> G["Kubernetes Secret 생성"]
    F --> H["Feed Server Pod에 환경 변수 주입"]
    G --> H
    H --> I["mysql.infra.svc.cluster.local"]
    I --> J["Amazon RDS for MySQL"]

Spring Boot 프로젝트 생성

Spring Initializr에서 다음과 같이 프로젝트를 생성한다.

항목설정
ProjectGradle - Groovy
LanguageJava
Spring Boot유지보수 중인 안정 버전
Groupcom.sns
Artifactfeed-server
Namefeed-server
Package namecom.sns.feed
PackagingJar
Java21

새 프로젝트라면 유지보수 중인 Spring Boot 버전을 선택해야 한다. 기존 프로젝트가 Spring Boot 3.2.1처럼 특정 버전에 맞춰 작성되어 있다면 개별 서비스 하나만 임의로 업그레이드하지 말고 Spring Cloud, Gradle, 테스트 환경의 호환성을 함께 확인해야 한다. 현재 안정 버전 목록과 Java 요구사항은 Spring Boot 공식 문서에서 확인할 수 있다.

다음 의존성을 추가한다.

  • Spring Web
  • Spring Data JPA
  • MySQL Driver
  • Validation
  • Spring Boot Actuator

Gradle 설정

Spring Boot 3.x와 Java 21을 사용하는 예시는 다음과 같다.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.5.16'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.sns'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'

    runtimeOnly 'com.mysql:mysql-connector-j'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}
  • spring-boot-starter-web은 REST API와 내장 Tomcat을 제공한다.
  • spring-boot-starter-data-jpa는 JPA와 Hibernate 기반 데이터 접근을 제공한다.
  • mysql-connector-j는 MySQL JDBC Driver다.
  • spring-boot-starter-validation은 요청 객체의 입력값을 검증할 때 사용한다.
  • spring-boot-starter-actuator는 상태 확인과 Kubernetes Probe 구성에 활용할 수 있다.
  • Java Toolchain은 빌드 환경이 Java 21을 사용하도록 명시한다.

애플리케이션 진입점

package com.sns.feed;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FeedServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeedServerApplication.class, args);
    }
}

@SpringBootApplication은 자동 구성, Component Scan, Java Config 기능을 함께 활성화한다. 기본 패키지인 com.sns.feed 아래에 Controller, Service, Repository, Entity를 배치하면 별도의 Component Scan 설정 없이 Spring Bean으로 등록된다.

Spring Profile 분리

로컬 환경과 EKS 개발 환경은 데이터베이스 주소와 자격 증명이 다르다. 설정값을 소스 코드에 고정하지 않고 Spring Profile과 환경 변수로 분리한다.

application.yml에는 공통 설정을 작성한다.

spring:
  application:
    name: feed-server
  profiles:
    default: local
  jpa:
    open-in-view: false
    properties:
      hibernate:
        jdbc:
          time_zone: UTC

server:
  port: 8080
  shutdown: graceful

management:
  endpoints:
    web:
      exposure:
        include:
          - health
          - info
  endpoint:
    health:
      probes:
        enabled: true
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true

open-in-view: false는 Controller까지 영속성 컨텍스트를 유지하지 않도록 한다. 필요한 연관 데이터는 Service의 트랜잭션 안에서 명확하게 조회해야 한다.

로컬 환경 설정

application-local.yml을 작성한다.

spring:
  config:
    activate:
      on-profile: local
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:sns}?serverTimezone=UTC&characterEncoding=UTF-8&sslMode=DISABLED
    username: ${MYSQL_USER:sns_server}
    password: ${MYSQL_PASSWORD:local-password}
    hikari:
      maximum-pool-size: 5
      minimum-idle: 1
      connection-timeout: 3000
  jpa:
    hibernate:
      ddl-auto: none
    show-sql: true

MySQL Connector/J의 올바른 Driver 클래스는 com.mysql.cj.jdbc.Driver다. 과거에 사용하던 com.mysql.jdbc.Driver는 더 이상 사용하지 않는다. Spring Boot가 JDBC URL을 통해 Driver를 자동으로 판단할 수 있으므로 driver-class-name을 생략하는 것도 가능하다.

EKS 개발 환경 설정

application-dev.yml을 작성한다.

spring:
  config:
    activate:
      on-profile: dev
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:sns}?serverTimezone=UTC&characterEncoding=UTF-8&sslMode=REQUIRED
    username: ${MYSQL_USER}
    password: ${MYSQL_PASSWORD}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2
      connection-timeout: 3000
      validation-timeout: 1000
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
  • MYSQL_HOSTMYSQL_PORT는 ConfigMap에서 주입한다.
  • MYSQL_USERMYSQL_PASSWORD는 Secret에서 주입한다.
  • sslMode=REQUIRED는 RDS와의 통신을 TLS로 암호화한다.
  • ddl-auto: validate는 애플리케이션 시작 시 Entity와 테이블 구조가 일치하는지 검사한다.
  • 운영 환경에서 ddl-auto: update를 사용하면 애플리케이션 시작 시 의도하지 않은 스키마 변경이 발생할 수 있으므로 Flyway나 Liquibase로 마이그레이션을 관리하는 것이 좋다.
  • HikariCP 크기는 Pod 수와 RDS의 최대 연결 수를 함께 고려해야 한다. Pod가 10개이고 각 Pod의 최대 Pool 크기가 10이면 애플리케이션에서 최대 100개의 연결을 생성할 수 있다.

설정 파일 우선순위

flowchart LR
    COMMON["application.yml"] --> PROFILE["application-dev.yml"]
    PROFILE --> ENV["Kubernetes 환경 변수"]
    ENV --> FINAL["최종 Datasource 설정"]

Spring Boot에서는 환경 변수로 전달된 값이 YAML의 기본값보다 높은 우선순위를 가진다. 따라서 동일한 컨테이너 이미지를 사용하더라도 Namespace와 배포 환경에 따라 다른 데이터베이스 설정을 주입할 수 있다.

Kubernetes Namespace 생성

애플리케이션 리소스는 sns Namespace에 배치한다.

sns-namespace.yaml을 작성한다.

apiVersion: v1
kind: Namespace
metadata:
  name: sns
  labels:
    app.kubernetes.io/part-of: sns
    environment: dev
kubectl apply -f sns-namespace.yaml
kubectl get namespace sns

Redis, Kafka, MySQL ExternalName Service는 infra Namespace에 있고 애플리케이션은 sns Namespace에 있다. Namespace가 달라도 Service의 전체 DNS 이름을 사용하면 접근할 수 있다. 네트워크 통신까지 차단하려면 별도의 NetworkPolicy가 필요하다.

MySQL ConfigMap 작성

mysql-configmap.yaml을 작성한다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
  namespace: sns
  labels:
    app.kubernetes.io/part-of: sns
    app.kubernetes.io/component: database-config
data:
  MYSQL_HOST: mysql.infra.svc.cluster.local
  MYSQL_PORT: "3306"
  MYSQL_DATABASE: sns
  • apiVersion: v1은 ConfigMap이 Kubernetes Core API 객체임을 의미한다.
  • metadata.name은 Deployment에서 참조할 ConfigMap 이름이다.
  • ConfigMap은 자신을 사용하는 Pod와 같은 sns Namespace에 있어야 한다.
  • MYSQL_HOSTinfra Namespace의 ExternalName Service를 가리킨다.
  • ConfigMap의 data 값은 문자열이어야 하므로 포트도 "3306"으로 작성한다.

Kubernetes Service의 전체 DNS 형식은 다음과 같다.

<SERVICE_NAME>.<NAMESPACE>.svc.cluster.local

따라서 mysql.infra.svc.cluster.localinfra Namespace에 있는 mysql Service를 의미한다.

MySQL Secret 작성

mysql-secret.yaml을 작성한다.

apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
  namespace: sns
  labels:
    app.kubernetes.io/part-of: sns
    app.kubernetes.io/component: database-credentials
type: Opaque
stringData:
  MYSQL_USER: sns_server
  MYSQL_PASSWORD: CHANGE_ME_STRONG_PASSWORD
  • type: Opaque는 일반적인 Key-Value 형식의 Secret을 의미한다.
  • stringData는 평문을 입력받아 Kubernetes API Server가 Base64 형식의 data로 변환한다.
  • MYSQL_PASSWORD는 RDS에서 생성한 sns_server 계정의 실제 비밀번호로 변경해야 한다.
  • 이 파일에 실제 비밀번호를 입력했다면 Git에 Commit해서는 안 된다.

Base64는 암호화가 아니라 인코딩이다. Secret을 Base64로 변환했다고 해서 안전하게 보호되는 것은 아니다. 실무에서는 RBAC으로 Secret 조회 권한을 제한하고 AWS Secrets Manager, External Secrets Operator, Secrets Store CSI Driver 같은 외부 비밀 관리 방식을 고려해야 한다.

명령어로 Secret을 직접 생성하면 평문 비밀번호가 포함된 YAML 파일을 만들지 않을 수 있다.

kubectl create secret generic mysql-secret \
  --namespace sns \
  --from-literal=MYSQL_USER=sns_server \
  --from-literal=MYSQL_PASSWORD='<MYSQL_PASSWORD>'

다만 명령 기록에 비밀번호가 남을 수 있으므로 운영 환경에서는 CI/CD Secret이나 외부 Secret Manager를 통해 생성하는 것이 좋다.

ConfigMap과 Secret 적용

kubectl apply -f mysql-configmap.yaml
kubectl apply -f mysql-secret.yaml

생성 결과를 확인한다.

kubectl get configmap mysql-config -n sns
kubectl describe configmap mysql-config -n sns
kubectl get secret mysql-secret -n sns
kubectl describe secret mysql-secret -n sns

kubectl describe secret은 일반적으로 실제 값을 출력하지 않고 Key와 바이트 크기만 보여준다. 확인을 위해 Secret 값을 터미널에 출력하는 방식은 로그와 화면 기록에 비밀번호가 남을 수 있으므로 피해야 한다.

ConfigMap과 Secret 주입 구조

flowchart TD
    CM["ConfigMap mysql-config"] --> HOST["MYSQL_HOST"]
    CM --> PORT["MYSQL_PORT"]
    CM --> DB["MYSQL_DATABASE"]
    SEC["Secret mysql-secret"] --> USER["MYSQL_USER"]
    SEC --> PASSWORD["MYSQL_PASSWORD"]
    HOST --> POD["Feed Server Pod"]
    PORT --> POD
    DB --> POD
    USER --> POD
    PASSWORD --> POD
    POD --> SPRING["Spring Boot Datasource"]

다음 단계에서 Deployment의 envFrom 또는 개별 env.valueFrom 설정을 이용해 ConfigMap과 Secret을 컨테이너 환경 변수로 주입할 수 있다.

환경 변수로 주입된 ConfigMap과 Secret은 원본 객체를 수정하더라도 실행 중인 컨테이너에서 자동으로 변경되지 않는다. 새로운 값을 적용하려면 Deployment의 Pod를 Rolling Update 방식으로 다시 생성해야 한다.

MySQL Service DNS 확인

sns Namespace에서 infra Namespace의 MySQL Service가 조회되는지 테스트한다.

kubectl run dns-test \
  --namespace sns \
  --rm \
  --interactive \
  --tty \
  --restart=Never \
  --image=busybox:1.36 \
  -- nslookup mysql.infra.svc.cluster.local

정상적인 경우 ExternalName Service가 가리키는 RDS Endpoint가 출력된다.

DNS 조회가 성공했다고 데이터베이스 연결까지 성공한 것은 아니다. MySQL TCP 3306 연결, RDS Security Group, 사용자 인증, TLS 설정은 별도로 검증해야 한다.

로컬 실행

로컬 MySQL 또는 접근 가능한 개발 데이터베이스를 준비한 뒤 환경 변수를 설정한다.

$env:MYSQL_HOST = "localhost"
$env:MYSQL_PORT = "3306"
$env:MYSQL_DATABASE = "sns"
$env:MYSQL_USER = "sns_server"
$env:MYSQL_PASSWORD = "local-password"

.\gradlew.bat bootRun --args="--spring.profiles.active=local"

애플리케이션이 기동되면 Actuator Health Endpoint를 확인한다.

curl http://localhost:8080/actuator/health

정상적인 경우 다음과 같은 응답을 확인할 수 있다.

{
  "status": "UP"
}

UP은 애플리케이션과 등록된 HealthIndicator가 정상이라는 의미다. 비즈니스 API 전체가 정상 동작하거나 실제 운영 트래픽을 처리할 수 있다는 의미까지 보장하지는 않는다.

빌드와 테스트

.\gradlew.bat clean test
.\gradlew.bat bootJar

빌드가 성공하면 다음 위치에 실행 가능한 Jar가 생성된다.

build/libs/feed-server-0.0.1-SNAPSHOT.jar

직접 실행할 수도 있다.

java -jar build/libs/feed-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=local

자주 발생하는 문제

현상주요 원인해결 방법
Failed to determine a suitable driver classMySQL Driver 누락mysql-connector-j 의존성 확인
Driver 클래스를 찾지 못함이전 Driver 이름 사용com.mysql.cj.jdbc.Driver 사용
Communications link failureHost, Port, Security Group 오류RDS Endpoint와 TCP 3306 확인
Access denied for user사용자명, 비밀번호, 권한 오류SHOW GRANTS와 Secret 값 확인
UnknownHostExceptionService 이름 또는 Namespace 오타mysql.infra.svc.cluster.local 확인
애플리케이션 시작 실패환경 변수 누락ConfigMap과 Secret Key 이름 확인
Hibernate 검증 실패Entity와 DB Schema 불일치마이그레이션 상태와 ddl-auto 확인
Pod가 CreateContainerConfigErrorConfigMap 또는 Secret이 같은 Namespace에 없음sns Namespace의 객체 확인

실무적인 구성 기준

ConfigMap과 Secret은 피드 서버만의 리소스처럼 보이지만 실제로는 여러 마이크로서비스가 공유할 수 있는 인프라 설정이다. 초기 실습에서는 피드 서버 프로젝트에 둘 수 있지만, 프로젝트가 커지면 공통 Helm Chart나 별도의 인프라 Repository에서 관리하는 편이 적절하다.

또한 모든 마이크로서비스가 하나의 데이터베이스 계정을 공유하면 특정 서비스의 권한이 전체 테이블로 확장된다. 운영 환경에서는 서비스별 Database 또는 Schema와 전용 계정을 생성하고 필요한 테이블에만 최소 권한을 부여해야 한다.

정리

소셜 피드 서버는 게시글 작성자 ID, 본문, 이미지 ID를 관리하며 이미지 파일 자체는 이미지 서버에 위임한다. 이러한 책임 분리는 서비스를 독립적으로 개발하고 확장하기 위한 MSA의 기본 구조다.

Spring Boot 프로젝트는 Java 21, Spring Web, Spring Data JPA, MySQL Driver를 기반으로 생성하고 로컬 환경과 EKS 개발 환경을 Spring Profile로 분리했다. 데이터베이스 주소와 포트는 ConfigMap으로, 사용자명과 비밀번호는 Secret으로 관리하도록 구성했다.

애플리케이션은 mysql.infra.svc.cluster.local을 통해 infra Namespace의 ExternalName Service를 조회하고 최종적으로 Amazon RDS에 연결한다. ConfigMap과 Secret은 같은 Namespace의 Pod에서만 직접 참조할 수 있지만 Service DNS를 통한 네트워크 호출은 Namespace를 넘어서 수행할 수 있다.

다음 단계에서는 이 설정을 Feed Server Deployment에 주입하고, 컨테이너 이미지와 Resource, Probe, Service를 구성하여 EKS 클러스터에 실제 애플리케이션을 배포할 수 있다.


© 2020. All rights reserved.

SIKSIK