이 미들 프록시는 클라이언트와 백엔드 서비스 사이에서 중간 매개체 역할을 하지만 단순한 프록시를 넘어서 인증/인가, 라우팅, 모니터링, 이벤트 처리 등 다양한 횡단 관심사를 처리하는 종합적인 게이트웨이 솔루션으로 설계되어 백엔드 서비스가 비즈니스 로직에만 집중할 수 있도록 하였습니다
주요 횡단 관심사 처리는 다음과 같습니다:
인증/인가
라우팅 & 필터링
모니터링 & 추적
이벤트 처리
서비스간 통신
기타 부가 기능
Spring Boot 2.6.3을 기반으로 구축되었으며, Java 17 버전을 채택하였습니다. build.gradle 파일을 분석한 결과, 다음과 같은 핵심 기술 스택을 확인할 수 있었습니다.
Spring Cloud Gateway를 사용하고 있으며, 이는 Spring WebFlux 기반으로 동작합니다. SecurityConfiguration.java에서 @EnableWebFluxSecurity 어노테이션을 확인했고, 모든 컨트롤러 메서드가 Mono나 Flux 타입을 반환하는 것을 볼 수 있었습니다.
인증/인가는 Keycloak 15.0.2와 Spring Security OAuth2를 연동하여 구현되어 있습니다. build.gradle의 224줄에서 keycloak-adapter-bom 의존성을, 238-239줄에서 oauth2-client와 oauth2-resource-server 의존성을 확인했습니다.
데이터 저장 및 메시징은 Redis와 Kafka를 사용합니다. build.gradle 246줄의 spring-boot-starter-data-redis와 248줄의 spring-session-data-redis로 세션 관리를 구현하고, 287줄의
spring-kafka로 이벤트 처리를 구현했습니다.
마이크로서비스 간 통신은 280줄의 spring-cloud-starter-openfeign 2.2.8.RELEASE를 사용하여 선언적 HTTP 클라이언트를 구현하고 있습니다.
프로젝트는 명확한 패키지 구조를 통해 관심사를 분리하고 있습니다. src/main/Java/com/arms 하위에 크게 api, Client, config, util 패키지로 구성되어 있으며, 각각의 역할이 명확히 구분되어 있습니다.
api 패키지는 비즈니스 로직의 핵심으로, keycloak(인증관리), mapping(ALM 상태 매핑), req(요구사항 처리), Scheduler(동적 스케줄링)등의 도메인별 하위 패키지로 구성됩니다. 각 도메인은 controller-service-repository의 전통적인 레이어드 아키텍처를 따르고 있습니다.
config 패키지에는 Spring Security, Kafka, Redis, OpenFeign 등 각종 설정 클래스들이 위치하며, 특히 SecurityConfiguration 클래스에서 WebFlux 기반의 보안 설정을 관리합니다. filter와 handler 하위 패키지를 통해 Gateway의 요청/응답 처리 로직을 커스터마이징하고 있습니다.
util 패키지에는 AES 암호화, Slack 알림, Redis 유틸리티 등 공통 기능들이 구현되어 있으며, 특히 external_communicate 패키지에서 한글 메서드명을 사용한 독특한 네이밍 컨벤션을 발견할 수 있었습니다. 백엔드코어통신기, 엔진통신기 등의 인터페이스를 통해 외부 서비스와의 통신을 추상화하고 있습니다.
라우팅 및 필터링
프로젝트를 분석하면서 Spring Cloud Gateway 기반의 라우팅 메커니즘을 확인했습니다. src/main/java/com/arms/config/filter/BlockRequestGatewayFilter.java에서 커스텀 필터링 로직을 발견했는데, 처음에는 이 필터의 역할이 명확하지 않았습니다.
코드를 자세히 살펴보니 AbstractGatewayFilterFactory<BlockRequestGatewayFilter.Config>를 상속받아 구현되어 있었습니다. 필터의 핵심 로직은 apply() 메서드에서 확인할 수 있었습니다.
PermitUrl.java는 @ConfigurationProperties(prefix = "permit")를 통해 외부 설정에서 허용 URL 목록을 주입받는 구조였습니다.
다만 실제 Gateway 라우팅 설정(routes 설정)은 application.yml 파일이나 별도의 RouteLocator 빈에서 관리되고 있을 것으로 예상되는데, 현재 코드에서는 명시적인 라우팅 규칙을 찾지 못했습니다.
BlockRequestGatewayFilter는 라우팅보다는 접근 제어에 가까운 역할을 하고 있어, 실제로는 보안 필터로 동작하는 것으로 이해했습니다. 이는 허용되지 않은 경로로의 요청을 조기에 차단함으로써 불필요한 백엔드 호출을 방지하고 시스템 보안을 강화하는 효과가 있습니다.
인증/인가 (Keycloak)
보안체계는 SecurityConfiguration 클래스에서 구현 되어 있습니다.
Auth2/OIDC 프로토콜을 통해 Keycloak과 연동되며 접근 제어를 구현하고 있습니다.
경로별 권한 설정을 살펴보면, /auth-admin/ 경로는 ADMIN 역할만 접근 가능하고, /auth-user/는 일반 사용자 이상, /auth-anon/는 인증 없이 접근 가능하도록 구성되어 있습니다. 특히 주목할 점은 oidcUserService 메서드에서 Keycloak의 realm_access 클레임을 파싱하여 Spring Security의 GrantedAuthority로 변환하는 로직입니다. 이를 통해 Keycloak에서 관리되는 역할이 자동으로 Spring Security 컨텍스트에 매핑됩니다.
KeycloakAdminController는 Keycloak 관리 기능을 RESTful API로 노출하여, 사용자 생성, 역할 할당, 그룹 관리, SMTP 설정 등 Keycloak의 거의 모든 관리 기능을 제어할 수 있도록 합니다. 특히 configureSmtp 메서드에서 SMTP 설정을 하드코딩하고 있는 점이 특이한데 이는 보안상의 이유로 의도적으로 코드 레벨에서 관리하는 것으로 보입니다.
세션 관리 (Redis)
분산 환경에서의 세션 관리를 위해 Redis를 활용하고 있습니다.
spring-session-data-redis 의존성을 통해 Spring Session이 자동으로 Redis와 통합되며 이를 통해 스케일 아웃 환경에서도 세션 일관성을 유지할 수 있습니다.
AuthSuccessHandler에서는 인증되지 않은 사용자가 보호된 페이지에 접근을 시도하면 authenticationEntryPoint에서 요청한 페이지를 세션에 저장한 후 로그인 페이지로 리다이렉트 합니다.
인증 성공 후에는 세션에 리다이렉트 페이지 정보를 저장하여 사용자가 원래 접근하려던 페이지로 자동 이동하는 UX를 제공합니다. AuthSuccessAfterDuplicateUserRemove 컴포넌트는 중복 로그인을 방지하는 로직을 담당하며, Redis에 저장된 세션 정보를 활용하여 동일 사용자의 이전 세션을 무효화합니다.
요구사항 이벤트 (Kafka + Redis)
ReqService와 ReqController를 통해 구현된 요구사항 관리 시스템은 이 프로젝트의 또 다른 핵심 기능입니다. 이 시스템은 Redis와 Kafka를 조합하여 요구사항 이벤트를 처리합니다.
요구사항이 생성되면 먼저 Redis에 저장되고, 동시에 Kafka 토픽으로 이벤트가 발행됩니다. 이때 ListenableFuture를 사용하여 Kafka 전송 결과에 따라 Redis의 상태를 업데이트하는 패턴을 사용합니다. 이는 이벤트 발행의 신뢰성을 보장하면서도 비동기 처리의 이점을 활용하도록 합니다.
주목할 점은, Kafka 토픽이 존재하지 않을 경우 자동으로 생성하는 로직입니다.
hasTopic, topicExists, createTopic 메소드를 통해 구현된 이 기능은 운영 중 새로운 요구사항 타입이 추가되더라도 시스템이 자동으로 적응할 수 있도록 합니다.
이는 운영 안정성과 유연성을 위한 설계인 것으로 이해됩니다:
운영 편의성: 새로운 요구사항 타입 추가시 수동 토픽 생성 불필요
장애 복구: 토픽이 삭제되거나 손실된 경우 자동 재생성
개발 효율성: 개발/스테이징 환경(@Profile({“dev”, “stg”})) 빠른 프로토타이핑 지원
self-healing(장애가 발생해도 시스템이 스스로 복원하고 회복하는 메커니즘, ex. Kafka 토픽이 없을 경우 시스템이 이를 자동으로 감지하고 생성하여 정상 동작을 유지)
ALM 도구 상태 매핑 (Application Lifecycle Management)
MappingController와 관련 서비스들은 다양한 ALM 도구(JIRA, Redmine 등)의 이슈 상태를 통합 관리하는 기능을 제공합니다. 각 ALM 도구마다 상이한 상태 체계를 ARMS(A-RMS) 시스템의 표준화된 상태로 매핑합니다.
MappingController.java의 34-59줄에서 ALM 이슈 상태를 초기화하는 엔드포인트를 발견했습니다:
@PostMapping("/alm/issuestatus")
public Mono<CommonResponse.ApiResult<String>>
almIssueStatusInit(@RequestBody AlmServerRequestDTO
almServerRequestDTO)
이 메서드는 AlmServerRequestDTO를 받아서 각 이슈 상태에 대해 복합 키를 생성합니다:
String key = this.generateKey(almServerRequestDTO.getServerId(),
almServerRequestDTO.getProjectKeyOrId(),
almServerRequestDTO.getIssueTypeId(),
almIssueStatusDTO.getId());
키 생성 로직은 generateKey() 메서드에 구현되어 있습니다.
이 메서드는 서버ID, 프로젝트키, 이슈타입ID, 상태ID를 받아서 대시(-)로 연결한 문자열을 생성하고, 연속된 대시나 끝의 대시를 정규표현식으로 제거합니다.
AlmIssueStatus.java를 보면:
@RedisHash("almIssueStatus")
@Getter
@Builder
public class AlmIssueStatus implements Serializable {
@Id
private String id;
private String name;
private String armsStateMappingId;
}
@RedisHash 어노테이션으로 Redis에 저장되며, id 필드에 앞서 생성한 복합키가 저장됩니다. armsStateMappingId 필드가 ARMS 시스템의 상태와 연결 지점입니다.
findStateCategory() 메서드에서는 이 매핑을 활용합니다:
String key = this.generateKey(serverId, projectKey, issueTypeId, issueStatusId);
AlmIssueStatus almIssueStatus = almIssueStatusService.getAlmIssueStatus(key);
String armsStateMappingId = almIssueStatus.getArmsStateMappingId();
먼저 동일한 방식으로 복합 키를 생성하고, Redis에서 해당 ALM 상태를 조회한 후, ARMS 상태 ID를 추출하여 최종적으로 카테고리를 찾습니다.
@RedisHash("category")
public class StateCategory {
@Id
private String id;
private String name;
}
이 전체 구조를 통해 JIRA의 "In Progress"나 Redmine의 "진행중" 같은 서로 다른 ALM 도구의 상태들이 ARMS 시스템의 통일된 상태 카테고리로 매핑됩니다.
OpenFeign 클라이언트
마이크로서비스 간 통신은 인터페이스에 메서드만 선언하면 자동으로 구현되는 선언적 HTTP 클라이언트인 OpenFeign을 활용하여 처리하고 있습니다. OpenFeignConfig 클래스에서 전역 설정을 관리하며 각 서비스별로 독립적인 인터페이스를 정의하여 타입 안전성을 보장합니다.
백엔드/엔진 서비스 연동
백엔드코어통신기와 엔진통신기 인터페이스는 각각 백엔드 코어 서비스와 엔진 서비스와의 통신을 담당합니다. @FeignClient 어노테이션을 통해 서비스 URL을 외부 설정(${arms.backend.url})에서 주입받아 환경별로 유연하게 대응할 수 있도록 구성되어 있습니다.
이들 인터페이스는 주로 스케줄러 관련 작업을 처리하는데, 요구사항 상태 업데이트, ElasticSearch 연동, ALM 이슈 재생성 등의 배치성 작업을 RESTful API로 트리거합니다. 이러한 설계는 각 서비스의 독립성을 유지하면서도 필요시 동기적 호출을 가능하게 합니다.
Spring Cloud Sleuth와 Zipkin 통합을 통해 이러한 서비스 간 호출이 자동으로 추적되며, 분산 트레이싱을 통해 전체 요청 흐름을 모니터링할 수 있습니다. 이는 복잡한 마이크로서비스 환경에서 디버깅과 성능 분석에 필수적인 기능입니다.
이 프로젝트는 Spring WebFlux를 사용하여 비동기 논블로킹 처리를 구현하고 있습니다. Application.java에서 @EnableAsync 어노테이션을 확인했고, 모든 컨트롤러가 Mono나 Flux를 반환하도록 구현되어 있습니다.
예를 들어, KeycloakAdminController의 모든 메서드는 동기적 작업을 수행한 후에도 Mono.just() 또는 Mono.empty()를 반환합니다. MappingController에서도 모든 엔드포인트가 Mono<CommonResponse.ApiResult> 형태로 반환값을 정의하고 있습니다.
AuthSuccessHandler.java의 27-45줄에서는 flatMap, map, then 같은 연산자를 체이닝하여 비동기 로직을 처리하고 있습니다. 이러한 구현은 적은 수의 스레드로 많은 동시 요청을 처리할 수 있게 합니다.
프로젝트는 API Gateway 역할을 수행하며 클라이언트에게 단일 진입점을 제공합니다.
BlockRequestGatewayFilter를 통한 요청 필터링과 SecurityConfiguration의 경로별 권한 설정으로 백엔드 서비스를 보호하고 있습니다. OpenFeign 클라이언트(백엔드코어통신기, 엔진통신기)를 통해 백엔드 서비스와 통신하며, Spring Cloud Sleuth와 Zipkin으로 서비스 간 호출을 추적합니다.
API Gateway는 횡단 관심사를 중앙에서 처리하고 있습니다.
SecurityConfiguration에서 모든 보안 정책을 중앙 관리하며, 백엔드 서비스는 이미 인증된 요청만 받게 됩니다. Keycloak 통합을 통해 토큰 검증, 역할 확인, 권한 부여가 게이트웨이 레벨에서 일괄 처리되므로 백엔드 서비스는 비즈니스 로직에만 집중할 수 있습니다.
프로젝트는 세 가지 환경 프로파일을 통해 개발부터 운영까지의 전체 라이프사이클을 관리하고 있습니다. src/main/resources 디렉토리를 확인한 결과, application-dev.yml, application-stg.yml, application-live.yml 파일로 환경별 설정이 분리되어 있었습니다.
각 프로파일의 특징을 분석해보니, 개발 환경(dev)과 스테이징 환경(stg)에서만 특정 기능이 활성화되는 것을 발견했습니다. 예를 들어, ReqService.java와 KafkaConfig.java에 적용된 @Profile({"dev", "stg"}) 어노테이션은 Kafka 관련 기능이 운영 환경에서는 비활성화됨을 나타냅니다. 이는 운영 환경의 안정성을 위해 별도의 브로커를 사용하거나 다른 설정을 적용하기 위한 것으로 이해했습니다.
Spring Actuator 엔드포인트 설정도 환경별로 다르게 구성되어 있습니다.
모든 환경에서 refresh, env, health, beans, httptrace가 노출되어 있는데, 이는 무중단 설정 변경과 모니터링을 위한 필수 엔드포인트들입니다. 특히 refresh 엔드포인트는 Spring Cloud Config와 연동되어 재시작 없이 설정을 갱신할 수 있게 합니다.
외부 설정 관리를 위해 Spring Cloud Config Server와 연동되어 있습니다.
application-dev.yml에서 확인한 설정은 다음과 같습니다:
spring:
config:
import: optional:configserver:http://www.313.co.kr:33133
@RefreshScope 어노테이션이 여러 클래스에 적용되어 있는 것을 확인했습니다. SecurityConfiguration.java, KafkaConfig.java 등 주요 설정 클래스들이 이 어노테이션을 사용하여, /actuator/refresh 엔드포인트 호출 시 설정이 동적으로 갱신됩니다.
build.gradle 파일을 분석하면서 발견한 자동 버전 관리 시스템:
이번 프로젝트를 분석하면서 처음 접하는 기술들이 많았고, 그만큼 많은 것을 배울 수 있는 기회였습니다.
특히 Spring WebFlux는 이번에 처음 접한 기술이었는데, 처음에는 왜 모든 컨트롤러가 Mono나 Flux를 반환하는지에 대해 많은 시간 고민한 것 같습니다. 하지만 코드를 분석하면서 이것이 비동기 논블로킹 처리를 위한 것이며 적은 수의 스레드로 많은 요청을 처리하기 위한 선택임을 알게 되었습니다.
또한, AuthSuccessHandler에서 flatMap, map, then 같은 연산자를 체이닝하는 방식 역시 처음 보는 방식이었지만, 이러한 흐름이 비동기 로직을 선언적으로 구성하는 방식임을 이해하게 되었습니다.
그리고 ReqService에서 사용된 ListenableFuture 패턴과, Kafka 메시지 전송 이후 콜백을 통해 Redis 상태를 업데이트하는 방식은 생소했지만, 이를 통해 이벤트 발행의 신뢰성을 확보하고 있다는 점을 배웠습니다.
개인적으로는 백엔드코어통신기와 같은 한글 메서드명이 인상 깊었으며 이러한 방식으로도 추상화가 이루어진다는 점이 독특하게 다가왔습니다. 아직 이해하지 못한 부분도 많지만, 앞으로도 이 프로젝트를 지속적으로 분석하고 확장하면서 더 깊은 이해를 쌓아가고 싶습니다.