Skip to content

[FEATURE] AWS RDS 자원 관리 기능 구현 #179

@hyobin-yang

Description

@hyobin-yang

AWS RDS 자원 관리 기능 구현

개요

AWS RDS(Relational Database Service)를 헥사고날 아키텍처로 구현합니다.
도메인 추상화명은 RDBMS(Relational Database Management System)를 사용합니다.

추상화명

  • RDBMS
    • 관계형 데이터베이스 관리 시스템의 일반적인 용어
    • AWS RDS, Azure Database, GCP Cloud SQL 모두를 포괄하는 추상화명
    • 다른 CSP에서도 자연스럽게 매핑 가능

아키텍처 구조

헥사고날 아키텍처를 준수하여 다음과 같이 구성합니다:

domain/cloud/
├── adapter/outbound/aws/rds/          # AWS RDS 전용 어댑터
│   ├── AwsRdsManagementAdapter.java   # RDS 생명주기 관리
│   ├── AwsRdsDiscoveryAdapter.java    # RDS 조회
│   ├── AwsRdsMapper.java              # AWS SDK ↔ CloudResource 변환
│   └── ...
├── adapter/outbound/aws/config/
│   └── AwsRdsConfig.java              # RDS 클라이언트 설정
├── controller/
│   └── RdbmsController.java          # REST API (CSP 독립적)
├── dto/
│   ├── RdbmsCreateRequest.java
│   ├── RdbmsUpdateRequest.java
│   ├── RdbmsDeleteRequest.java
│   ├── RdbmsQueryRequest.java
│   └── RdbmsResponse.java
├── port/model/rdbms/                  # Command 객체
│   ├── RdbmsCreateCommand.java
│   ├── RdbmsUpdateCommand.java
│   ├── RdbmsDeleteCommand.java
│   ├── RdbmsQuery.java
│   └── GetRdbmsCommand.java
├── port/outbound/rdbms/               # 포트 인터페이스
│   ├── RdbmsManagementPort.java       # CRUD 작업
│   ├── RdbmsDiscoveryPort.java        # 조회 작업
│   └── RdbmsLifecyclePort.java        # 생명주기 관리 (시작/중지/재시작)
├── service/rdbms/
│   ├── RdbmsUseCaseService.java       # 유스케이스 서비스 (CSP 독립적)
│   └── RdbmsPortRouter.java           # 포트 라우터
└── repository/
    └── CloudResourceRepository.java   # 기존 리포지토리 활용

구현 단계

Phase 1: 포트 및 모델 정의

1.1 Command 모델 정의 (port/model/rdbms/)

RdbmsCreateCommand.java

/**
 * RDBMS 생성 도메인 커맨드 (CSP 중립적)
 * 
 * UseCase Service에서 Adapter로 전달되는 내부 명령 모델입니다.
 * CSP 중립적인 필드를 사용하며, 각 CSP Adapter의 Mapper에서 CSP 특화 요청으로 변환합니다.
 * 
 * 필드 매핑 예시:
 * - instanceName: AWS(dbInstanceIdentifier), Azure(serverName), GCP(instanceId)
 * - instanceSize: AWS(db.t3.micro), Azure(GP_Gen5_2), GCP(db-custom-2-7680)
 * - networkSecurityId: AWS(securityGroupId), Azure(firewallRule), GCP(authorizedNetworks)
 * - zone: AWS(availabilityZone), Azure(zone), GCP(zone)
 */
@Builder
public record RdbmsCreateCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String serviceKey,           // "RDS", "AZURE_DATABASE", "CLOUD_SQL"
    String resourceType,         // "DATABASE"
    String instanceName,         // CSP 중립적: AWS(dbInstanceIdentifier), Azure(serverName), GCP(instanceId)
    String engine,               // "mysql", "postgresql", "mariadb", "oracle", "sqlserver"
    String engineVersion,       // CSP별 버전 형식 다를 수 있음
    String instanceSize,        // CSP 중립적: AWS(db.t3.micro), Azure(GP_Gen5_2), GCP(db-custom-2-7680)
    Integer allocatedStorage,    // GB (모든 CSP 공통)
    String masterUsername,       // 관리자 사용자명
    String masterPassword,       // 암호화 필요
    String dbName,               // 초기 데이터베이스 이름
    String networkSecurityId,   // CSP 중립적: AWS(securityGroupId), Azure(firewallRule), GCP(authorizedNetworks)
    String subnetId,            // 서브넷 식별자 (선택적, 일부 CSP만 사용)
    Integer port,               // 데이터베이스 포트 (기본값: engine별로 다름)
    String zone,                // CSP 중립적: AWS(availabilityZone), Azure(zone), GCP(zone)
    Boolean highAvailability,   // 고가용성 설정: AWS(multiAz), Azure(highAvailability), GCP(highAvailability)
    Boolean publiclyAccessible, // 공개 접근 허용 여부
    Map<String, String> tags,
    String tenantKey,
    Map<String, Object> providerSpecificConfig,  // CSP별 특화 설정
    CloudSessionCredential session
) {}

RdbmsUpdateCommand.java

/**
 * RDBMS 수정 도메인 커맨드 (CSP 중립적)
 */
@Builder
public record RdbmsUpdateCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String providerResourceId,   // RDBMS 인스턴스 ID (CSP별 형식 다를 수 있음)
    String instanceSize,         // CSP 중립적: 인스턴스 크기 변경
    Integer allocatedStorage,    // 스토리지 크기 변경 (GB)
    String masterPassword,       // 선택적: 마스터 패스워드 변경
    Boolean applyImmediately,    // 즉시 적용 여부 (일부 CSP만 지원)
    Map<String, String> tags,
    String tenantKey,
    Map<String, Object> providerSpecificConfig,  // CSP별 특화 설정
    CloudSessionCredential session
) {}

RdbmsDeleteCommand.java

/**
 * RDBMS 삭제 도메인 커맨드 (CSP 중립적)
 */
@Builder
public record RdbmsDeleteCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String providerResourceId,
    Boolean skipSnapshot,           // 최종 스냅샷 건너뛰기 (AWS 특화, 다른 CSP는 providerSpecificConfig로)
    String snapshotName,            // 최종 스냅샷 이름 (선택적)
    Boolean deleteAutomatedBackups,  // 자동 백업 삭제 여부
    String tenantKey,
    Map<String, Object> providerSpecificConfig,  // CSP별 특화 삭제 옵션
    CloudSessionCredential session
) {}

RdbmsQuery.java

/**
 * RDBMS 조회 쿼리 (CSP 중립적)
 */
@Builder
public record RdbmsQuery(
    ProviderType providerType,
    String accountScope,
    Set<String> regions,
    String instanceName,        // CSP 중립적: 인스턴스 이름으로 필터링
    String engine,              // 엔진 타입으로 필터링
    String instanceSize,        // 인스턴스 크기로 필터링
    String status,              // 상태로 필터링 (CSP별 상태 값 다를 수 있음)
    Map<String, String> tagsEquals,  // 태그로 필터링
    int page,
    int size
) {}

GetRdbmsCommand.java

@Builder
public record GetRdbmsCommand(
    ProviderType providerType,
    String accountScope,
    String region,
    String providerResourceId,
    String serviceKey,
    String resourceType,
    CloudSessionCredential session
) {}

1.2 포트 인터페이스 정의 (port/outbound/rdbms/)

RdbmsManagementPort.java

public interface RdbmsManagementPort {
    /**
     * RDBMS 인스턴스 생성
     */
    CloudResource createRdbms(RdbmsCreateCommand command);
    
    /**
     * RDBMS 인스턴스 수정
     */
    CloudResource updateRdbms(RdbmsUpdateCommand command);
    
    /**
     * RDBMS 인스턴스 삭제
     */
    void deleteRdbms(RdbmsDeleteCommand command);
}

RdbmsDiscoveryPort.java

public interface RdbmsDiscoveryPort {
    /**
     * RDBMS 인스턴스 목록 조회
     */
    Page<CloudResource> listRdbmsInstances(RdbmsQuery query, CloudSessionCredential session);
    
    /**
     * 특정 RDBMS 인스턴스 조회
     */
    Optional<CloudResource> getRdbmsInstance(String instanceId, CloudSessionCredential session);
    
    /**
     * RDBMS 인스턴스 상태 조회
     */
    String getInstanceStatus(String instanceId, CloudSessionCredential session);
}

RdbmsLifecyclePort.java

public interface RdbmsLifecyclePort {
    /**
     * RDBMS 인스턴스 시작
     */
    void startInstance(String instanceId, CloudSessionCredential session);
    
    /**
     * RDBMS 인스턴스 중지
     */
    void stopInstance(String instanceId, CloudSessionCredential session);
    
    /**
     * RDBMS 인스턴스 재시작
     */
    void rebootInstance(String instanceId, CloudSessionCredential session);
}

Phase 2: AWS 어댑터 구현

2.1 Config 클래스 (adapter/outbound/aws/config/AwsRdsConfig.java)

@Slf4j
@Configuration
@ConditionalOnProperty(name = "aws.enabled", havingValue = "true", matchIfMissing = true)
public class AwsRdsConfig {
    
    @Value("${aws.rds.region:us-east-1}")
    private String region;
    
    @Value("${aws.rds.endpoint-override:}")
    private String endpointOverride;
    
    @Bean
    public RdsClient rdsClient() {
        log.info("[AwsRdsConfig] Creating RDS client for region: {}", region);
        
        var builder = RdsClient.builder()
            .region(Region.of(region))
            .credentialsProvider(createThreadLocalCredentialsProvider());
        
        if (!endpointOverride.isEmpty()) {
            builder.endpointOverride(URI.create(endpointOverride));
        }
        
        return builder.build();
    }
    
    // ThreadLocal 자격증명 제공자 구현 (AwsS3Config 참고)
    private AwsCredentialsProvider createThreadLocalCredentialsProvider() {
        // AwsS3Config의 구현 참고
    }
}

2.2 Mapper 클래스 (adapter/outbound/aws/rds/AwsRdsMapper.java)

@Component
public class AwsRdsMapper {
    
    /**
     * AWS DBInstance → CloudResource 변환
     */
    public CloudResource toCloudResource(DBInstance dbInstance, CloudProvider provider, CloudRegion region, CloudService service) {
        return CloudResource.builder()
            .resourceId(dbInstance.dbInstanceIdentifier())
            .resourceName(dbInstance.dbInstanceIdentifier())
            .displayName(dbInstance.dbInstanceIdentifier())
            .provider(provider)
            .region(region)
            .service(service)
            .resourceType(CloudResource.ResourceType.DATABASE)
            .lifecycleState(mapLifecycleState(dbInstance.dbInstanceStatus()))
            .instanceType(dbInstance.dbInstanceClass())
            .storageGb((long) dbInstance.allocatedStorage())
            .ipAddress(dbInstance.endpoint() != null ? dbInstance.endpoint().address() : null)
            .tags(convertTagsToJson(dbInstance.tagList()))
            .configuration(convertConfigurationToJson(dbInstance))
            .status(mapStatus(dbInstance.dbInstanceStatus()))
            .createdInCloud(dbInstance.instanceCreateTime())
            .lastSync(LocalDateTime.now())
            .build();
    }
    
    /**
     * RdbmsCreateCommand → CreateDBInstanceRequest 변환
     * CSP 중립적 Command를 AWS SDK 요청으로 변환
     */
    public CreateDBInstanceRequest toCreateRequest(RdbmsCreateCommand command) {
        return CreateDBInstanceRequest.builder()
            .dbInstanceIdentifier(command.instanceName())  // instanceName → dbInstanceIdentifier
            .engine(command.engine())
            .engineVersion(command.engineVersion())
            .dbInstanceClass(command.instanceSize())  // instanceSize → dbInstanceClass
            .allocatedStorage(command.allocatedStorage())
            .masterUsername(command.masterUsername())
            .masterUserPassword(command.masterPassword())
            .dbName(command.dbName())
            .vpcSecurityGroupIds(command.networkSecurityId() != null ? 
                List.of(command.networkSecurityId()) : null)  // networkSecurityId → vpcSecurityGroupIds
            .dbSubnetGroupName(getSubnetGroupName(command))  // providerSpecificConfig에서 추출
            .port(command.port())
            .availabilityZone(command.zone())  // zone → availabilityZone
            .multiAz(command.highAvailability() != null && command.highAvailability())  // highAvailability → multiAz
            .publiclyAccessible(command.publiclyAccessible())
            .tags(convertTags(command.tags()))
            .build();
    }
    
    /**
     * providerSpecificConfig에서 AWS 특화 설정 추출
     */
    private String getSubnetGroupName(RdbmsCreateCommand command) {
        if (command.providerSpecificConfig() != null) {
            return (String) command.providerSpecificConfig().get("subnetGroupName");
        }
        return null;
    }
    
    // 기타 변환 메서드들...
}

2.3 Management Adapter (adapter/outbound/aws/rds/AwsRdsManagementAdapter.java)

@Component
@RequiredArgsConstructor
public class AwsRdsManagementAdapter implements RdbmsManagementPort, ProviderScoped {
    
    private final RdsClient rdsClient;
    private final AwsRdsMapper mapper;
    private final CloudErrorTranslator errorTranslator;
    
    @Override
    public CloudResource createRdbms(RdbmsCreateCommand command) {
        try {
            CreateDBInstanceRequest request = mapper.toCreateRequest(command);
            CreateDBInstanceResponse response = rdsClient.createDBInstance(request);
            
            // 생성된 인스턴스 조회
            DBInstance dbInstance = waitForInstanceAvailable(response.dbInstance().dbInstanceIdentifier());
            
            return mapper.toCloudResource(dbInstance, ...);
        } catch (Throwable t) {
            throw errorTranslator.translate(t);
        }
    }
    
    @Override
    public ProviderType getProviderType() {
        return ProviderType.AWS;
    }
}

2.4 Discovery Adapter (adapter/outbound/aws/rds/AwsRdsDiscoveryAdapter.java)

@Component
@RequiredArgsConstructor
public class AwsRdsDiscoveryAdapter implements RdbmsDiscoveryPort, ProviderScoped {
    
    private final RdsClient rdsClient;
    private final AwsRdsMapper mapper;
    
    @Override
    public Page<CloudResource> listRdbmsInstances(RdbmsQuery query, CloudSessionCredential session) {
        // AWS SDK DescribeDBInstances 호출
        // 페이징 처리
        // CloudResource로 변환
    }
    
    @Override
    public ProviderType getProviderType() {
        return ProviderType.AWS;
    }
}

2.5 Lifecycle Adapter (adapter/outbound/aws/rds/AwsRdsLifecycleAdapter.java)

@Component
@RequiredArgsConstructor
public class AwsRdsLifecycleAdapter implements RdbmsLifecyclePort, ProviderScoped {
    
    private final RdsClient rdsClient;
    
    @Override
    public void startInstance(String instanceId, CloudSessionCredential session) {
        // AWS SDK StartDBInstance 호출
    }
    
    @Override
    public ProviderType getProviderType() {
        return ProviderType.AWS;
    }
}

Phase 3: Service 계층 구현

3.1 Port Router (service/rdbms/RdbmsPortRouter.java)

@Component
@RequiredArgsConstructor
public class RdbmsPortRouter {
    
    private final Map<ProviderType, RdbmsManagementPort> managementMap;
    private final Map<ProviderType, RdbmsDiscoveryPort> discoveryMap;
    private final Map<ProviderType, RdbmsLifecyclePort> lifecycleMap;
    
    public RdbmsManagementPort management(ProviderType type) {
        return require(managementMap, type);
    }
    
    public RdbmsDiscoveryPort discovery(ProviderType type) {
        return require(discoveryMap, type);
    }
    
    public RdbmsLifecyclePort lifecycle(ProviderType type) {
        return require(lifecycleMap, type);
    }
}

3.2 UseCase Service (service/rdbms/RdbmsUseCaseService.java)

@Slf4j
@Service
@RequiredArgsConstructor
public class RdbmsUseCaseService {
    
    private static final String SERVICE_KEY = "RDS";
    private static final String RESOURCE_TYPE = "DATABASE";
    
    private final RdbmsPortRouter portRouter;
    private final CapabilityGuard capabilityGuard;
    private final AccountCredentialManagementPort credentialPort;
    private final CloudResourceRepository resourceRepository;
    
    /**
     * RDBMS 인스턴스 생성
     */
    @Transactional
    public CloudResource createRdbms(RdbmsCreateRequest request) {
        ProviderType providerType = request.getProviderType();
        String accountScope = request.getAccountScope();
        
        // Capability 검증
        capabilityGuard.ensureSupported(providerType, SERVICE_KEY, RESOURCE_TYPE, Operation.CREATE);
        
        // 세션 획득 (JIT 패턴)
        CloudSessionCredential session = getSession(providerType, accountScope);
        
        // Command 변환
        RdbmsCreateCommand command = toCreateCommand(request, session);
        
        // 어댑터 호출
        CloudResource resource = portRouter.management(providerType).createRdbms(command);
        
        // 리소스 저장
        CloudResource saved = resourceRepository.save(resource);
        
        return saved;
    }
    
    /**
     * RDBMS 인스턴스 목록 조회
     */
    @Transactional(readOnly = true)
    public Page<CloudResource> listRdbmsInstances(ProviderType providerType, String accountScope, RdbmsQueryRequest request) {
        CloudSessionCredential session = getSession(providerType, accountScope);
        RdbmsQuery query = toQuery(request);
        
        return portRouter.discovery(providerType).listRdbmsInstances(query, session);
    }
    
    // 기타 메서드들...
    
    private CloudSessionCredential getSession(ProviderType providerType, String accountScope) {
        String tenantKey = TenantContextHolder.getCurrentTenantKeyOrThrow();
        return credentialPort.getSession(tenantKey, accountScope, providerType);
    }
}

Phase 4: Controller 및 DTO 구현

4.1 DTO 클래스 (dto/)

RdbmsCreateRequest.java

/**
 * RDBMS 생성 요청 DTO (CSP 중립적)
 * 
 * Controller에서 받는 요청 객체로, CSP 중립적인 필드만 포함합니다.
 * CSP 특화 설정은 providerSpecificConfig에 포함됩니다.
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RdbmsCreateRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String region;
    
    @NotBlank
    @Size(min = 1, max = 100)
    private String instanceName;  // CSP 중립적
    
    @NotBlank
    private String engine;  // mysql, postgresql, mariadb, oracle, sqlserver
    
    private String engineVersion;
    
    @NotBlank
    private String instanceSize;  // CSP 중립적: AWS(db.t3.micro), Azure(GP_Gen5_2), GCP(db-custom-2-7680)
    
    @Min(20)
    private Integer allocatedStorage;  // GB
    
    @NotBlank
    private String masterUsername;
    
    @NotBlank
    @Size(min = 8)
    private String masterPassword;
    
    private String dbName;
    
    private String networkSecurityId;  // CSP 중립적: AWS(securityGroupId), Azure(firewallRule), GCP(authorizedNetworks)
    
    private String subnetId;  // 선택적
    
    private Integer port;  // 기본값: engine별로 다름
    
    private String zone;  // CSP 중립적: 가용 영역
    
    private Boolean highAvailability = false;  // CSP 중립적: 고가용성 설정
    
    private Boolean publiclyAccessible = false;
    
    private Map<String, String> tags;
    
    /**
     * CSP별 특화 설정
     * 
     * AWS 예시:
     *   - subnetGroupName: "my-db-subnet-group"
     *   - parameterGroupName: "default.mysql8.0"
     * 
     * Azure 예시:
     *   - resourceGroupName: "my-resource-group"
     *   - sku: { "name": "GP_Gen5_2", "tier": "GeneralPurpose", "capacity": 2 }
     * 
     * GCP 예시:
     *   - databaseFlags: [{"name": "max_connections", "value": "100"}]
     *   - backupConfiguration: {"enabled": true, "startTime": "23:00"}
     */
    private Map<String, Object> providerSpecificConfig;
}

RdbmsUpdateRequest.java

/**
 * RDBMS DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RdbmsUpdateRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String instanceId;  // 수정할 인스턴스 ID
    
    private String instanceSize;  // CSP 중립적: 인스턴스 크기 변경
    
    @Min(20)
    private Integer allocatedStorage;  // 스토리지 크기 변경 (GB)
    
    @Size(min = 8)
    private String masterPassword;  // 선택적: 마스터 패스워드 변경
    
    private Boolean applyImmediately;  // 즉시 적용 여부 (일부 CSP만 지원)
    
    private Map<String, String> tagsToAdd;  // 추가할 태그
    
    private Map<String, String> tagsToRemove;  // 제거할 태그
    
    /**
     * CSP별 특화 설정
     */
    private Map<String, Object> providerSpecificConfig;
}

RdbmsDeleteRequest.java

/**
 * RDBMS 삭제 요청 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RdbmsDeleteRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    @NotBlank
    private String instanceId;  // 삭제할 인스턴스 ID
    
    @Builder.Default
    private Boolean skipSnapshot = false;  // 최종 스냅샷 건너뛰기
    
    private String snapshotName;  // 최종 스냅샷 이름 (선택적)
    
    @Builder.Default
    private Boolean deleteAutomatedBackups = false;  // 자동 백업 삭제 여부
    
    private String reason;  // 삭제 이유 (감사 로그용)
    
    /**
     * CSP별 특화 삭제 옵션
     */
    private Map<String, Object> providerSpecificConfig;
    
    /**
     * 기본 삭제 요청 생성
     */
    public static RdbmsDeleteRequest basic(String instanceId) {
        return RdbmsDeleteRequest.builder()
            .instanceId(instanceId)
            .skipSnapshot(false)
            .deleteAutomatedBackups(false)
            .build();
    }
}

RdbmsQueryRequest.java

/**
 * RDBMS 조회 요청 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RdbmsQueryRequest {
    
    @NotNull
    private ProviderType providerType;
    
    @NotBlank
    private String accountScope;
    
    private Set<String> regions;  // 조회할 리전 목록
    
    private String instanceName;  // 인스턴스 이름으로 필터링
    
    private String engine;  // 엔진 타입으로 필터링
    
    private String instanceSize;  // 인스턴스 크기로 필터링
    
    private String status;  // 상태로 필터링
    
    private Map<String, String> tags;  // 태그로 필터링
    
    @Min(0)
    @Builder.Default
    private int page = 0;
    
    @Min(1)
    @Max(100)
    @Builder.Default
    private int size = 20;
}

RdbmsResponse.java

/**
 * RDBMS 응답 DTO (CSP 중립적)
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RdbmsResponse {
    
    private Long id;
    private String resourceId;
    private String resourceName;
    private ProviderType providerType;
    private String region;
    private String engine;
    private String engineVersion;
    private String instanceSize;  // CSP 중립적: AWS(db.t3.micro), Azure(GP_Gen5_2), GCP(db-custom-2-7680)
    private Long storageGb;
    private String status;
    private String lifecycleState;
    private String endpoint;  // 데이터베이스 엔드포인트
    private Integer port;
    private Boolean highAvailability;  // 고가용성 설정 여부
    private Boolean publiclyAccessible;  // 공개 접근 허용 여부
    private Map<String, String> tags;
    private LocalDateTime createdAt;
    private LocalDateTime lastSync;
    
    /**
     * CloudResource → RdbmsResponse 변환
     */
    public static RdbmsResponse from(CloudResource resource) {
        // CloudResource의 configuration JSON에서 추가 정보 추출
        // instanceSize는 resource.getInstanceType() 또는 configuration에서 추출
        return RdbmsResponse.builder()
            .id(resource.getId())
            .resourceId(resource.getResourceId())
            .resourceName(resource.getResourceName())
            .providerType(resource.getProvider().getProviderType())
            .region(resource.getRegion() != null ? resource.getRegion().getRegionKey() : null)
            .instanceSize(resource.getInstanceType())  // 또는 configuration에서 추출
            .storageGb(resource.getStorageGb())
            .status(resource.getStatus() != null ? resource.getStatus().name() : null)
            .lifecycleState(resource.getLifecycleState() != null ? resource.getLifecycleState().name() : null)
            .endpoint(resource.getIpAddress())  // 또는 configuration에서 추출
            .tags(parseTags(resource.getTags()))
            .createdAt(resource.getCreatedAt())
            .lastSync(resource.getLastSync())
            .build();
    }
    
    private static Map<String, String> parseTags(String tagsJson) {
        // JSON 문자열을 Map으로 변환
        // 구현 생략
        return Map.of();
    }
}

4.2 Controller (controller/RdbmsController.java)

@Slf4j
@RestController
@RequestMapping("/api/v1/cloud/providers/{provider}/accounts/{accountScope}/rdbms/instances")
@RequiredArgsConstructor
@Tag(name = "RDBMS Management", description = "RDBMS 인스턴스 관리 API (멀티 클라우드 지원)")
public class RdbmsController {
    
    private final RdbmsUseCaseService rdbmsUseCaseService;
    
    @PostMapping
    @Operation(summary = "RDBMS 인스턴스 생성")
    public ResponseEntity<ApiResponse<RdbmsResponse>> createRdbms(
            @PathVariable ProviderType provider,
            @PathVariable String accountScope,
            @Valid @RequestBody RdbmsCreateRequest request) {
        
        request.setProviderType(provider);
        request.setAccountScope(accountScope);
        
        CloudResource resource = rdbmsUseCaseService.createRdbms(request);
        RdbmsResponse response = RdbmsResponse.from(resource);
        
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(ApiResponse.success(response, "RDBMS 인스턴스 생성에 성공했습니다."));
    }
    
    @GetMapping
    @Operation(summary = "RDBMS 인스턴스 목록 조회")
    public ResponseEntity<ApiResponse<Page<RdbmsResponse>>> listRdbmsInstances(
            @PathVariable ProviderType provider,
            @PathVariable String accountScope,
            @Valid RdbmsQueryRequest request) {
        
        // 구현...
    }
    
    // 기타 엔드포인트들...
}

Phase 5: Capability 등록

CapabilityRegistry에 RDS 기능 등록

// AwsCapabilityConfig.java에 추가
@Bean
public CspCapability awsRdsCapability() {
    return CspCapability.builder()
        .providerType(ProviderType.AWS)
        .serviceKey("RDS")
        .resourceType("DATABASE")
        .supportsCreate(true)
        .supportsRead(true)
        .supportsUpdate(true)
        .supportsDelete(true)
        .supportsStart(true)
        .supportsStop(true)
        .supportsReboot(true)
        .build();
}

고려사항

1. 헥사고날 아키텍처 준수

  • 포트 인터페이스: CSP 독립적인 비즈니스 계약 정의
  • 어댑터 분리: AWS 전용 로직은 adapter/outbound/aws/rds/에만 존재
  • 의존성 방향: Service → Port → Adapter (단방향)
  • 도메인 모델: CloudResource 엔티티 활용

2. CSP 독립성 보장

  • Controller: 특정 CSP에 종속되지 않는 공통 API만 제공
  • Service: CSP별 차이는 Port 인터페이스로 추상화
  • Command: CSP 중립적인 필드만 포함, providerSpecificConfig로 확장 가능

3. 보안 및 자격증명 관리

  • JIT 세션 관리: Service 레벨에서 세션 획득 및 Port에 전달
  • 암호화: 마스터 패스워드는 암호화하여 전달 (Command에서 처리)
  • ThreadLocal 자격증명: 멀티 테넌트 환경 지원

4. 리소스 동기화

  • CloudResource 저장: 생성/수정 시 CloudResourceRepository에 저장
  • 동기화 전략:
    • 생성/수정 시 즉시 저장
    • 조회 시 최신 상태 반영 (lastSync 필드 활용)
    • 주기적 동기화 작업 고려 (별도 스케줄러)

5. 에러 처리

  • CloudErrorTranslator: AWS SDK 예외를 도메인 예외로 변환
  • BusinessException: 비즈니스 로직 예외 처리
  • 에러 코드: CloudErrorCode에 RDBMS 관련 에러 코드 추가

6. 트랜잭션 관리

  • @transactional: Service 메서드에 적절한 트랜잭션 설정
  • 읽기 전용: 조회 메서드는 @Transactional(readOnly = true)

7. Capability 검증

  • CapabilityGuard: 작업 전 CSP 지원 여부 검증
  • 동적 검증: 런타임에 Capability 확인

8. 데이터베이스 엔진별 차이

  • 엔진 추상화: engine 필드로 엔진 타입 지정
  • 엔진별 설정: providerSpecificConfig로 엔진별 특화 설정 지원
  • 매퍼에서 처리: 각 CSP Adapter의 Mapper에서 엔진별 차이 흡수

8-1. CSP 중립성 보장

  • 필드명 중립화:

    • dbInstanceIdentifierinstanceName (모든 CSP 공통)
    • instanceClassinstanceSize (VM 패턴과 일치)
    • vpcSecurityGroupIdnetworkSecurityId (VM 패턴과 일치)
    • availabilityZonezone (VM 패턴과 일치)
    • multiAzhighAvailability (모든 CSP 공통 개념)
  • CSP 특화 필드 제거:

    • subnetGroupName (AWS 전용) → providerSpecificConfig로 이동
    • skipFinalSnapshot (AWS 전용) → skipSnapshot으로 일반화하거나 providerSpecificConfig로 이동
  • 확장성: providerSpecificConfig로 CSP별 특화 설정 지원

    • AWS: subnetGroupName, parameterGroupName, optionGroupName
    • Azure: resourceGroupName, sku, storageProfile
    • GCP: databaseFlags, backupConfiguration, ipConfiguration

9. RDS 특화 기능

  • 스냅샷 관리: 삭제 시 최종 스냅샷 옵션
  • Multi-AZ: 고가용성 설정
  • 자동 백업: 백업 정책 설정
  • 파라미터 그룹: 데이터베이스 파라미터 설정 (향후 확장)

10. 테스트 전략

  • 단위 테스트: 각 Adapter, Service, Controller 테스트
  • 통합 테스트: LocalStack을 활용한 AWS 통합 테스트
  • Mock 테스트: Port 인터페이스 Mock으로 Service 테스트

11. 성능 고려사항

  • 페이징: 대량 조회 시 페이징 처리
  • 비동기 처리: 장시간 작업(생성, 수정)은 비동기 고려
  • 캐싱: 자주 조회되는 인스턴스 정보 캐싱 고려

12. 모니터링 및 로깅

  • 구조화된 로깅: 모든 레이어에서 일관된 로깅
  • 마스킹: 민감 정보(패스워드 등) 마스킹 처리
  • 메트릭: RDS 작업 성공/실패 메트릭 수집

구현 체크리스트

Phase 1: 포트 및 모델

  • RdbmsCreateCommand 정의
  • RdbmsUpdateCommand 정의
  • RdbmsDeleteCommand 정의
  • RdbmsQuery 정의
  • GetRdbmsCommand 정의
  • RdbmsManagementPort 인터페이스 정의
  • RdbmsDiscoveryPort 인터페이스 정의
  • RdbmsLifecyclePort 인터페이스 정의

Phase 2: AWS 어댑터

  • AwsRdsConfig 클래스 생성
  • AwsRdsMapper 클래스 생성
  • AwsRdsManagementAdapter 구현
  • AwsRdsDiscoveryAdapter 구현
  • AwsRdsLifecycleAdapter 구현
  • ProviderScoped 인터페이스 구현

Phase 3: Service 계층

  • RdbmsPortRouter 생성
  • RdbmsUseCaseService 구현
  • Capability 검증 로직 추가
  • JIT 세션 관리 구현
  • 리소스 저장 로직 구현

Phase 4: Controller 및 DTO

  • RdbmsCreateRequest DTO 생성
  • RdbmsUpdateRequest DTO 생성
  • RdbmsDeleteRequest DTO 생성
  • RdbmsQueryRequest DTO 생성
  • RdbmsResponse DTO 생성
  • RdbmsController 구현
  • API 문서화 (Swagger)

Phase 5: Capability 및 설정

  • CapabilityRegistry에 RDS 등록
  • 설정 파일 업데이트
  • Bean 등록 확인

Phase 6: 테스트

  • 단위 테스트 작성
  • 통합 테스트 작성
  • API 테스트

참고 자료

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions