Nimbus – OpenStack 통합 플랫폼
기간: 2025.01 ~ 2025.05
역할: Lead Architecture Designer & Core Developer
기술스택: Java, Spring Boot, OpenStack APIs, REST Client, Apache HttpClient
프로젝트 개요
OpenStack 클라우드 환경의 복잡성을 해결하고 운영 효율성을 극대화하는 오케스트레이션 플랫폼을 설계하고 개발. 단순한 API 래핑을 넘어서 비즈니스 로직이 포함된 통합 관리 시스템 구현
핵심 성과: 비즈니스 로직에 대응할 수 있는 OpenStack 플랫폼 자체 구축
전체 시스템 아키텍처

아키텍처 설계
1) API Layer (프론트엔드 인터페이스)
// REST Controller - inbound requests 처리
@RestController
public class InstancesController {
private final ApiInstancesService apiInstancesService;
@PostMapping("/projects/{projectId}/instances")
public ResponseEntity<NimbusBaseResponse<InstanceCreateResponse>> create(
@PathVariable String projectId,
@RequestBody @Valid InstanceCreateRequest request) {
// REST DTO를 APP DTO로 변환하여 API Service에 전달
AppInstancesCreateRequest appRequest = convertToAppDto(request);
InstanceCreateResponse response = apiInstancesService.createInstance(projectId, appRequest);
return ResponseEntity.ok(NimbusBaseResponse.success(response));
}
}
2) COMPONENT Layer (비즈니스 로직 & 오케스트레이션)
// API Service - 복잡한 비즈니스 로직 처리
@Service
public class ApiInstancesService {
private final InstancesService instancesService;
private final ImagesService imagesService;
private final FlavorsService flavorsService;
private final NetworksService networksService;
public InstanceCreateResponse createInstance(String projectId, AppInstancesCreateRequest request) {
// 1. 사전 검증 단계 - 여러 Component Service 조합
AppFlavorsRetrieveResponse flavor = flavorsService.retrieveFlavor(request.getFlavorId());
AppImagesDetailRetrieveResponse image = imagesService.detailRetrieve(projectId, request.getImageId());
// 2. 네트워크 구성 자동화
NetworkInterface networkConfig = configureNetwork(projectId, request);
// 3. 볼륨 구성 (부팅 볼륨 자동 처리)
Optional<BlockDeviceMapping> blockDevice = configureBootVolume(request, image, flavor);
// 4. 보안 그룹 검증 및 적용
List<String> securityGroups = validateSecurityGroups(projectId, request.getSecurityGroupIds());
// 5. Component Service에 통합 요청 전달
return instancesService.createInstance(projectId, buildRequest(request, networkConfig, blockDevice, securityGroups));
}
}
// Component Service - OpenStack 서비스별 도메인 로직
@Service
public class InstancesService {
private final InstancesAdapter instancesAdapter;
private final Cache cache;
private final Repository repository;
public InstanceCreateResponse createInstance(String projectId, ComponentInstanceCreateRequest request) {
// JPA Entity 조작, 캐시 관리, Adapter 호출
// 도메인 로직과 인프라 레이어 사이의 중재 역할
return instancesAdapter.createInstance(token, osRequest);
}
}
3) BATCH Layer (스케줄링 & 자동화)
// Job Manager - 백업, 대피 등 배치 작업 관리
@Component
public class JobManager {
private final Scheduler scheduler;
private final Repository repository;
// 스케줄러와 실행 서비스 간의 협업
public void scheduleBackupJob(JobBackupScheduleRequest request) {
// JOB DTO를 기반으로 스케줄 등록
scheduler.schedule(request);
}
}
// Execution Service - 실제 작업 실행
@Service
public class ExecutionService {
private final QueueWorker queueWorker;
private final OpenstackExecutor openstackExecutor;
public void triggerBackup(ExecBackupTaskRequest request) {
// Queue에 작업 등록
queueWorker.enqueue(request);
}
}
// Queue Worker - 비동기 작업 처리
@Component
public class QueueWorker {
private final BlockingQueue<ExecBackupTaskRequest> taskQueue = new LinkedBlockingQueue<>();
@PostConstruct
public void startWorker() {
executor.submit(() -> {
while (true) {
ExecBackupTaskRequest task = taskQueue.take();
openstackExecutor.executeBackup(task);
}
});
}
}
4) EVENT Layer (이벤트 기반 통신)
// Event Service - 비동기 이벤트 발행
@Service
public class EventService {
private final Publisher publisher;
@EventListener
public void handleInstanceCreated(InstanceCreatedEvent event) {
// MQ DTO로 변환하여 메시지 큐에 발행
InstanceCreatedMessage message = convertToMqDto(event);
publisher.publish("instance.created", message);
}
}
// Publisher - 메시지 큐 연동
@Component
public class Publisher {
private final MessageQueue messageQueue;
public void publish(String topic, Object message) {
messageQueue.send(topic, message);
}
}
핵심 설계 결정사항
1) DTO 변환 전략 (계층별 데이터 모델 분리)
// 계층별 DTO 변환으로 관심사 분리
public class InstancesController {
private AppInstancesCreateRequest convertToAppDto(InstanceCreateRequest restDto) {
// REST DTO → APP DTO 변환
return AppInstancesCreateRequest.builder()
.instanceName(restDto.getInstanceName())
.flavorId(restDto.getFlavorId())
.imageId(restDto.getImageId())
// ... REST 레벨의 검증 및 변환
.build();
}
}
public class InstancesService {
private OSInstancesCreateRequest convertToOsDto(AppInstancesCreateRequest appDto) {
// APP DTO → OS DTO 변환
return OSInstancesCreateRequest.builder()
.name(appDto.getInstanceName())
.flavor(appDto.getFlavorId())
.image(appDto.getImageId())
// ... OpenStack API 호출에 맞는 구조로 변환
.build();
}
}
2) 토큰 관리 시스템 (Component Layer 내 Cache 활용)
// 지능형 토큰 캐싱 시스템
@Service
public class AuthenticateService {
private final Cache tokenCache;
private final AuthenticateAdapter authenticateAdapter;
public KeystoneToken getTokenByProjectId(String projectId) {
// Cache Layer를 통한 토큰 조회
KeystoneToken cachedToken = tokenCache.get(projectId);
if (cachedToken != null && !isExpired(cachedToken.getOpenstackKeystoneToken())) {
return cachedToken;
}
// 만료된 경우 Adapter를 통해 새 토큰 발급
KeystoneToken newToken = authenticateAdapter.authenticate(projectId);
tokenCache.put(projectId, newToken);
return newToken;
}
}
3) OpenStack 서비스별 Adapter 분리
// Component Service → Adapter → OpenStack API 흐름
@Component
public class InstancesAdapter {
private final OpenstackClientManager clientManager;
public OSInstancesCreateResponse createInstance(KeystoneToken token,
OSInstancesCreateRequest request) {
RestClient novaClient = clientManager.getRestClient(ComponentsType.COMPUTE);
return novaClient.post()
.uri("/v2.1/servers")
.header("X-Auth-Token", token.getAuthToken())
.header("X-OpenStack-Nova-API-Version", NovaMicroVersion)
.contentType(MediaType.APPLICATION_JSON)
.body(dtoToJson(request))
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, this::handleClientError)
.onStatus(HttpStatusCode::is5xxServerError, this::handleServerError)
.body(OSInstancesCreateResponse.class);
}
}
자체 스케줄러 시스템 (BATCH Layer)
1) 호스트 장애 감지 및 자동 대피
@Component
@ConditionalOnProperty(name = "nimbus.scheduler.evacuate.enabled", havingValue = "true")
public class HostFailoverScheduler {
@Scheduled(cron = "0 */5 * * * *") // 매 5분마다 실행
public void executeEvacuateScheduler() {
List<HostEvacuateScheduleResponse> evacuateList =
jobManager.retrieveEvacuateHostSchedule();
for (HostEvacuateScheduleResponse targetHost : evacuateList) {
if (isHostDown(targetHost.getHostname())) {
// Execution Service를 통해 대피 작업 실행
executionService.triggerHostEvacuate(
new HostEvacuateRequest(targetHost.getHostname())
);
log.info("Triggered evacuation for failed host: {}",
targetHost.getHostname());
}
}
}
}
2) 인스턴스 백업 오케스트레이션
@Component
public class BackupScheduler {
@Scheduled(cron = "0 */10 * * * *") // 매 10분마다 실행
public void executeBackupScheduler() {
List<JobBackupScheduleResponse> schedules = jobManager.retrieveSchedule();
for (JobBackupScheduleResponse schedule : schedules) {
if (isScheduleMatched(schedule)) {
List<JobInstanceTargetResponse> targets = schedule.getInstanceTargets();
for (JobInstanceTargetResponse target : targets) {
// Queue Worker를 통한 비동기 백업 처리
ExecBackupTaskRequest task = ExecBackupTaskRequest.builder()
.projectId(schedule.getProjectId())
.jobId(schedule.getJobId())
.instanceId(target.getInstanceOpenstackId())
.backupType(schedule.getBackupType())
.build();
executionService.triggerBackup(task);
}
}
}
}
}
HTTP 클라이언트 관리 시스템
1) 컴포넌트별 클라이언트 분리 전략
@Component
public class OpenstackClientManager {
// 컴포넌트별 RestClient 관리
private final ConcurrentHashMap<ComponentsType, RestClient> restClientRepository = new ConcurrentHashMap<>();
// 대용량 파일 처리용 Apache HttpClient 관리
private final ConcurrentHashMap<ComponentsType, CloseableHttpClient> httpClientRepository = new ConcurrentHashMap<>();
public void setAllClients(Map<ComponentsType, String> componentMap) {
for (Map.Entry<ComponentsType, String> entry : componentMap.entrySet()) {
ComponentsType type = entry.getKey();
String endpoint = entry.getValue();
// 모든 컴포넌트에 RestClient 설정
setRestClient(type, endpoint);
// Object Store와 Image 서비스는 대용량 파일 처리를 위해 Apache HttpClient 추가
if (type.equals(ComponentsType.OBJECT_STORE) || type.equals(ComponentsType.IMAGE)) {
setHttpClient(type, endpoint);
}
}
}
public RestClient getRestClient(ComponentsType type) {
RestClient client = restClientRepository.get(type);
if (client == null) {
throw new IllegalStateException("RestClient not configured for: " + type);
}
return client;
}
}
2) 연결 풀 및 타임아웃 최적화
private RestClient createRestClient(String baseUrl) {
return RestClient.builder()
.baseUrl(baseUrl)
.requestFactory(new HttpComponentsClientHttpRequestFactory(
HttpClients.custom()
.setMaxConnTotal(200) // 전체 연결 풀 크기
.setMaxConnPerRoute(50) // 라우트별 최대 연결 수
.setConnectionTimeToLive(30, TimeUnit.SECONDS) // 연결 생존 시간
.evictIdleConnections(10, TimeUnit.SECONDS) // 유휴 연결 제거
.build()
))
.requestInterceptor((request, body, execution) -> {
// 요청/응답 로깅, 인증 헤더 추가 등
request.getHeaders().add("User-Agent", "Nimbus-Platform/1.0");
return execution.execute(request, body);
})
.build();
}
예외 처리 및 안정성
1) 계층별 예외 매핑
// Adapter Level - OpenStack 에러를 플랫폼 에러로 변환
public class InstancesAdapter {
private ClientResponse.ErrorHandler exceptionHandler = (request, response) -> {
String responseBody = response.getBodyAsText();
if (response.getStatusCode() == HttpStatus.CONFLICT) {
// Nova의 409 에러를 의미있는 메시지로 변환
if (responseBody.contains("Instance name already exists")) {
throw new InstanceNameConflictException("Instance name already in use");
}
}
if (response.getStatusCode() == HttpStatus.BAD_REQUEST) {
if (responseBody.contains("Invalid flavor")) {
throw new InvalidFlavorException("Specified flavor is not available");
}
}
// 기본 OpenStack 에러
throw new OpenStackApiException("OpenStack API error: " + responseBody);
};
}
// Service Level - 비즈니스 로직 예외
@Service
public class ApiInstancesService {
public InstanceCreateResponse createInstance(String projectId, AppInstancesCreateRequest request) {
try {
// 사전 검증
validateInstanceCreationRequest(request);
// 실제 생성
return instancesService.createInstance(projectId, request);
} catch (InvalidFlavorException e) {
log.error("Flavor validation failed for project {}: {}", projectId, e.getMessage());
throw new ValidationException("Invalid flavor specified: " + request.getFlavorId());
} catch (NetworkConfigurationException e) {
log.error("Network configuration failed: {}", e.getMessage());
throw new ServiceException("Failed to configure network for instance");
}
}
}
2) 회복력 있는 토큰 관리
@Service
public class AuthenticateService {
private final RetryTemplate retryTemplate = RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(Duration.ofSeconds(2))
.retryOn(TokenExpiredException.class, ConnectTimeoutException.class)
.build();
public KeystoneToken getTokenWithRetry(String projectId) {
return retryTemplate.execute(context -> {
try {
return authenticateAdapter.authenticate(projectId);
} catch (UnauthorizedException e) {
log.warn("Authentication failed for project {}, attempt {}",
projectId, context.getRetryCount() + 1);
throw new TokenExpiredException("Token authentication failed", e);
}
});
}
}
Repository & Cache 전략
1) JPA Entity 기반 데이터 관리
// JPA Entity - Database Layer
@Entity
@Table(name = "backup_schedules")
public class JobBackupSchedule {
@Id
private String jobId;
private String scheduleName;
private String projectId;
@Enumerated(EnumType.STRING)
private BackupType backupType;
@Enumerated(EnumType.STRING)
private ScheduleType scheduleType;
// JPA 연관관계 매핑
@OneToMany(mappedBy = "schedule", cascade = CascadeType.ALL)
private List<BackupInstanceTarget> instanceTargets;
}
// Repository - Data Access Layer
@Repository
public interface JobBackupScheduleRepository extends JpaRepository<JobBackupSchedule, String> {
@Query("SELECT s FROM JobBackupSchedule s WHERE s.isEnabled = true")
List<JobBackupSchedule> findActiveSchedules();
@Query("SELECT s FROM JobBackupSchedule s JOIN FETCH s.instanceTargets WHERE s.projectId = :projectId")
List<JobBackupSchedule> findByProjectIdWithTargets(@Param("projectId") String projectId);
}
2) 다층 캐시 전략
@Service
public class CacheService {
// L1 Cache - 토큰 캐시 (빠른 접근)
private final CaffeineCache tokenCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(55)) // 토큰 만료 5분 전 캐시 만료
.build();
// L2 Cache - 메타데이터 캐시 (중간 지속성)
private final CaffeineCache metadataCache = Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
public <T> T getFromCache(String key, Class<T> type) {
// 캐시 계층별 조회 로직
return (T) tokenCache.getIfPresent(key);
}
}
운영 성과 및 확장성
정량적 성과
- API 응답 시간 단축: 토큰 캐싱과 연결 풀 최적화
- 운영 효율성 향상: 자동화된 스케줄러 시스템
- 장애 복구 시간 단축: 자동 감지 및 대응 메커니즘
- 동시 처리 성능 향상: 컴포넌트별 클라이언트 분리로 병렬 처리 가능
아키텍처의 핵심 가치
- 계층별 관심사 분리: API-COMPONENT-BATCH-EVENT 레이어의 명확한 책임 분담
- DTO 변환 전략: 계층 간 데이터 모델 분리로 유지보수성 향상
- 확장 가능한 설계: Queue/Worker 패턴으로 수평적 확장 용이
- 운영 중심 설계: 토큰 관리, 예외 처리, 로깅 등 실제 운영 환경 고려
기술적 학습 효과
- 대규모 시스템 아키텍처: 복잡한 도메인을 체계적으로 분리하고 설계하는 능력
- OpenStack 생태계 이해: 각 서비스의 특성과 상호작용을 깊이 있게 파악
- 성능 최적화: HTTP 클라이언트, 연결 풀, 캐시 등 시스템 레벨 튜닝
- 운영 자동화: 스케줄링, 모니터링, 장애 대응의 체계적 구현